
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
@vanilla-dom/widget
Advanced tools
基于 @vanilla-dom/core 的组件开发编码范式,属于增强层,为复杂应用提供结构化的组件开发模式和更好的开发体验。
graph LR
A["组件创建"] --> B["DOM 挂载"]
B --> C["onMounted 调用"]
C --> D["用户交互"]
D --> E["组件销毁"]
E --> F["onUnmounting 调用"]
两种使用方式:
graph TD
subgraph "JSX 方式 (推荐)"
A1["<Widget />"] --> A2["自动渲染"]
A2 --> A3["onMounted 回调"]
end
subgraph "手动方式"
B1["new Widget()"] --> B2["instance.mount()"]
B2 --> B3["onMounted 生命周期"]
end
生命周期特性:
onMounted 在任何渲染方式下都只调用一次onMounted 回调获取组件实例onUnmounting 负责清理资源(事件监听器、定时器等)📋 详细技术流程请参考 LIFECYCLE.md
graph LR
subgraph "Widget Class Component"
A1["class MyWidget extends Widget"] --> A2["constructor(props)"]
A2 --> A3["render(): VNode"]
A3 --> A4["onMounted(): void"]
A4 --> A5["$() / $$()] DOM 查询"]
A5 --> A6["onUnmounting(): void"]
end
subgraph "Function Component"
B1["createWidget((props) => VNode)"] --> B2["factory function"]
B2 --> B3["renderWithLifecycle"]
B3 --> B4["onMounted callback"]
B4 --> B5["instance methods"]
end
subgraph "Domain + UI 分层"
C1["TodoListDomain"] --> C2["业务逻辑"]
C2 --> C3["数据管理"]
C4["TodoListUI extends Widget"] --> C5["UI 渲染"]
C5 --> C6["用户交互"]
C1 --> C4
end
A6 --> D["DOM 销毁"]
B5 --> D
C6 --> D
| 特性 | Widget Class | Function Component | Domain + UI |
|---|---|---|---|
| 适用场景 | 复杂组件状态管理 | 简单 UI 渲染 | 复杂业务逻辑 |
| 状态管理 | 实例属性 | props 传递 | Domain 层 |
| DOM 查询 | $() / $$() | 手动 ref | 分离关注点 |
| 生命周期 | onMounted / onUnmounting | 外部回调 | Domain + UI 各自管理 |
| 代码组织 | 单文件 | 单函数 | 多文件分层 |
npm install @vanilla-dom/widget @vanilla-dom/babel-plugin
# 推荐使用 pnpm(更快的包管理)
pnpm add @vanilla-dom/widget @vanilla-dom/babel-plugin
# 或者使用预设,更加简单
npm install @vanilla-dom/widget @vanilla-dom/babel-preset-widget
pnpm add @vanilla-dom/widget @vanilla-dom/babel-preset-widget
import { Widget } from '@vanilla-dom/widget';
interface CounterProps {
initialCount?: number;
}
export class Counter extends Widget<CounterProps> {
private count: number;
constructor(props: CounterProps) {
super(props);
this.count = props.initialCount || 0;
}
private increment() {
this.count++;
this.updateDisplay();
}
private updateDisplay() {
const display = this.$('.count-display');
if (display?.element) {
display.element.textContent = this.count.toString();
}
}
protected render() {
return (
<div className="counter">
<span className="count-display">{this.count}</span>
<button on:click={this.increment.bind(this)}>+1</button>
</div>
);
}
}
import { createWidget } from '@vanilla-dom/widget';
interface GreetingProps {
name: string;
message?: string;
}
export const Greeting = createWidget((props: GreetingProps) => {
return (
<div className="greeting">
<h1>Hello, {props.name}!</h1>
{props.message && <p>{props.message}</p>}
</div>
);
});
对于复杂的业务组件,推荐使用 Domain + UI 分层架构:
// TodoListDomain.ts - 业务逻辑层
export class TodoListDomain {
protected todos: TodoItem[] = [];
protected onTodosChange?: (todos: TodoItem[]) => void;
addTodo(text: string): boolean {
if (!text.trim()) {
this.notifyError('待办事项不能为空');
return false;
}
const newTodo = { id: Date.now().toString(), text, completed: false };
this.todos.push(newTodo);
this.notifyDataChange();
return true;
}
getTodos(): TodoItem[] {
return [...this.todos];
}
setTodosChangeHandler(callback: (todos: TodoItem[]) => void) {
this.onTodosChange = callback;
}
private notifyDataChange() {
this.onTodosChange?.(this.getTodos());
}
}
// TodoListUI.tsx - UI 层
import { Widget } from '@vanilla-dom/widget';
import { TodoListDomain } from './TodoListDomain';
export class TodoListUI extends Widget<TodoListProps> {
private domain: TodoListDomain;
constructor(props: TodoListProps) {
super(props);
// 组合:创建业务逻辑实例
this.domain = new TodoListDomain(props);
this.domain.setTodosChangeHandler(this.handleTodosChange.bind(this));
}
private handleAddTodo() {
const input = this.$('.todo-input');
if (input?.element) {
const text = (input.element as HTMLInputElement).value;
if (this.domain.addTodo(text)) {
(input.element as HTMLInputElement).value = '';
}
}
}
private handleTodosChange(todos: TodoItem[]) {
this.updateTodosList(todos);
}
public render() {
return (
<div className="todo-list">
<input className="todo-input" placeholder="添加待办事项..." />
<button on:click={this.handleAddTodo.bind(this)}>添加</button>
<ul className="todo-items"></ul>
</div>
);
}
}
配置 babel-plugin 后,可以直接在 JSX 中使用组件:
function App() {
return (
<div>
<Counter initialCount={0} />
<Greeting name="World" message="欢迎使用 Vanilla DOM!" />
<TodoListUI maxItems={20} />
</div>
);
}
// 创建组件实例
const counter = new Counter({ initialCount: 5 });
const greeting = Greeting({ name: 'User', message: 'Hello!' });
// 挂载到 DOM
counter.mount(document.getElementById('counter-container'));
greeting.mount(document.getElementById('greeting-container'));
// 销毁组件
counter.destroy();
greeting.destroy();
在项目根目录创建 .babelrc.js:
module.exports = {
plugins: ['@babel/plugin-syntax-jsx', '@vanilla-dom/babel-plugin'],
presets: [
'@babel/preset-env',
[
'@babel/preset-typescript',
{
isTSX: true,
allExtensions: true,
onlyRemoveTypeImports: true,
},
],
],
};
import * as babel from '@babel/core';
import { defineConfig } from 'vite';
export default defineConfig({
esbuild: {
jsx: 'preserve', // 让 babel 处理 JSX
},
plugins: [
{
name: 'vanilla-dom-babel',
async transform(code, id) {
if (!/\.(tsx?|jsx?)$/.test(id)) return;
if (id.includes('node_modules')) return;
if (!/<[A-Za-z]/.test(code)) return;
const result = await babel.transformAsync(code, {
filename: id,
plugins: ['@babel/plugin-syntax-jsx', '@vanilla-dom/babel-plugin'],
presets: [
[
'@babel/preset-typescript',
{
isTSX: true,
allExtensions: true,
onlyRemoveTypeImports: true,
},
],
],
sourceMaps: true,
});
return {
code: result?.code || code,
map: result?.map,
};
},
},
],
});
对于复杂组件的开发,建议阅读我们的组件架构指南,其中包含:
class Widget<T = any> {
constructor(props: T);
// DOM 查询
$(selector: string): DOMQuery | null;
$$(selector: string): DOMBatchQuery;
// 生命周期
mount(container: Element): void;
destroy(): void;
protected onMounted(): void;
protected onDestroyed(): void;
// 渲染
public render(): VNode;
}
function createWidget<T>(render: (props: T) => VNode): SimpleWidgetFactory<T>;
interface WidgetProps {
[key: string]: any;
}
interface ComponentMountCallback<T> {
(instance: T): void;
}
interface SimpleWidgetInstance {
mount(container: Element): void;
destroy(): void;
element: Element | null;
}
注意: @vanilla-dom/widget 不是一个框架,而是一套组件开发编码范式。它提供了基于 @vanilla-dom/core 的结构化组件开发方式,包括 Widget 基类、createWidget 工厂函数和分层架构模式,帮助开发者以一致的方式构建可维护的组件。
MIT License
FAQs
Component abstraction layer for vanilla-dom
We found that @vanilla-dom/widget demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.