WebCell

July 22, 2025 · View on GitHub

WebCell logo

简体中文 | English

基于 VDOM、JSXMobXTypeScriptWeb 组件 引擎

NPM 依赖性 CI 和 CD

反 996 许可证 UI 库推荐榜单

幻灯片 Gitter

编辑 WebCell 示例

NPM

特性

引擎比较

特性WebCell 3WebCell 2ReactVue
JS 语言TypeScript 5TypeScript 4ECMAScript 或 TypeScriptECMAScript 或 TypeScript
JS 语法ES 装饰器 stage-3ES 装饰器 stage-2
XML 语法JSX importJSX factoryJSX factory/importHTML/Vue 模板或 JSX(可选)
DOM APIWeb 组件Web 组件HTML 5+HTML 5+
视图渲染器DOM Renderer 2SnabbDOM(内置)SnabbDOM(分叉)
state APIMobX @observablethis.statethis.stateuseState()this.$dataref()
props APIMobX @observable@watchthis.propsprops => {}this.$propsdefineProps()
状态管理MobX 6+MobX 4/5ReduxVueX
页面路由器JSX 标签JSX 标签 + JSON 数据JSX 标签JSON 数据
资源打包工具Parcel 2Parcel 1webpackVite

安装

npm install dom-renderer mobx web-cell

Web 浏览器用法

演示和 GitHub 模板

项目引导

工具链

npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D

package.json

{
    "scripts": {
        "start": "parcel source/index.html --open",
        "build": "parcel build source/index.html --public-url ."
    }
}

tsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "ES2020",
        "moduleResolution": "Node",
        "useDefineForClassFields": true,
        "jsx": "react-jsx",
        "jsxImportSource": "dom-renderer"
    }
}

.parcelrc

{
    "extends": "@parcel/config-default",
    "transformers": {
        "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
    }
}

source/index.html

<script src="https://polyfill.web-cell.dev/feature/ECMAScript.js"></script>
<script src="https://polyfill.web-cell.dev/feature/WebComponents.js"></script>
<script src="https://polyfill.web-cell.dev/feature/ElementInternals.js"></script>

<script src="source/MyTag.tsx"></script>

<my-tag></my-tag>

函数组件

import { DOMRenderer } from 'dom-renderer';
import { FC, PropsWithChildren } from 'web-cell';

const Hello: FC<PropsWithChildren> = ({ children = '世界' }) => <h1>你好,{children}!</h1>;

new DOMRenderer().render(<Hello>WebCell</Hello>);

类组件

子元素插槽

import { DOMRenderer } from 'dom-renderer';
import { component } from 'web-cell';

@component({
    tagName: 'hello-world',
    mode: 'open'
})
class Hello extends HTMLElement {
    render() {
        return (
            <h1>
                你好, <slot />!
            </h1>
        );
    }
}

new DOMRenderer().render(
    <>
        <Hello>WebCell</Hello>
        {/* 或 */}
        <hello-world>WebCell</hello-world>
    </>
);

DOM 属性

import { DOMRenderer } from 'dom-renderer';
import { observable } from 'mobx';
import { WebCell, component, attribute, observer } from 'web-cell';

interface HelloProps {
    name?: string;
}

interface Hello extends WebCell<HelloProps> {}

@component({ tagName: 'hello-world' })
@observer
class Hello extends HTMLElement implements WebCell<HelloProps> {
    @attribute
    @observable
    accessor name = '';

    render() {
        return <h1>你好,{this.name}!</h1>;
    }
}

new DOMRenderer().render(<Hello name="WebCell" />);

// 或在 TypeScript 中提示 HTML 标签属性

declare global {
    namespace JSX {
        interface IntrinsicElements {
            'hello-world': HelloProps;
        }
    }
}
new DOMRenderer().render(<hello-world name="WebCell" />);

内部状态

函数组件

import { DOMRenderer } from 'dom-renderer';
import { observable } from 'mobx';
import { FC, observer } from 'web-cell';

class CounterModel {
    @observable
    accessor times = 0;
}

const couterStore = new CounterModel();

const Counter: FC = observer(() => (
    <button onClick={() => (couterStore.times += 1)}>计数:{couterStore.times}</button>
));

new DOMRenderer().render(<Counter />);

类组件

import { DOMRenderer } from 'dom-renderer';
import { observable } from 'mobx';
import { component, observer } from 'web-cell';

@component({ tagName: 'my-counter' })
@observer
class Counter extends HTMLElement {
    @observable
    accessor times = 0;

    handleClick = () => (this.times += 1);

    render() {
        return <button onClick={this.handleClick}>计数:{this.times}</button>;
    }
}

new DOMRenderer().render(<Counter />);

CSS 作用域

内联样式

import { component } from 'web-cell';
import { stringifyCSS } from 'web-utility';

@component({
    tagName: 'my-button',
    mode: 'open'
})
export class MyButton extends HTMLElement {
    style = stringifyCSS({
        '.btn': {
            color: 'white',
            background: 'lightblue'
        }
    });

    render() {
        return (
            <>
                <style>{this.style}</style>

                <a className="btn">
                    <slot />
                </a>
            </>
        );
    }
}

链接样式表

import { component } from 'web-cell';

@component({
    tagName: 'my-button',
    mode: 'open'
})
export class MyButton extends HTMLElement {
    render() {
        return (
            <>
                <link
                    rel="stylesheet"
                    href="https://unpkg.com/bootstrap@5.3.6/dist/css/bootstrap.min.css"
                />
                <a className="btn">
                    <slot />
                </a>
            </>
        );
    }
}

CSS 模块

scoped.css
.btn {
    color: white;
    background: lightblue;
}
MyButton.tsx
import { WebCell, component } from 'web-cell';

import styles from './scoped.css' assert { type: 'css' };

interface MyButton extends WebCell {}

@component({
    tagName: 'my-button',
    mode: 'open'
})
export class MyButton extends HTMLElement implements WebCell {
    connectedCallback() {
        this.root.adoptedStyleSheets = [styles];
    }

    render() {
        return (
            <a className="btn">
                <slot />
            </a>
        );
    }
}

事件委托

import { component, on } from 'web-cell';

@component({ tagName: 'my-table' })
export class MyTable extends HTMLElement {
    @on('click', ':host td > button')
    handleEdit(event: MouseEvent, { dataset: { id } }: HTMLButtonElement) {
        console.log(`编辑行:${id}`);
    }

    render() {
        return (
            <table>
                <tr>
                    <td>1</td>
                    <td>A</td>
                    <td>
                        <button data-id="1">编辑</button>
                    </td>
                </tr>
                <tr>
                    <td>2</td>
                    <td>B</td>
                    <td>
                        <button data-id="2">编辑</button>
                    </td>
                </tr>
                <tr>
                    <td>3</td>
                    <td>C</td>
                    <td>
                        <button data-id="3">编辑</button>
                    </td>
                </tr>
            </table>
        );
    }
}

MobX reaction

import { observable } from 'mobx';
import { component, observer, reaction } from 'web-cell';

@component({ tagName: 'my-counter' })
@observer
export class Counter extends HTMLElement {
    @observable
    accessor times = 0;

    handleClick = () => (this.times += 1);

    @reaction(({ times }) => times)
    echoTimes(newValue: number, oldValue: number) {
        console.log(`新值:${newValue},旧值:${oldValue}`);
    }

    render() {
        return <button onClick={this.handleClick}>计数:{this.times}</button>;
    }
}

表单关联

import { DOMRenderer } from 'dom-renderer';
import { WebField, component, formField, observer } from 'web-cell';

interface MyField extends WebField {}

@component({
    tagName: 'my-field',
    mode: 'open'
})
@formField
@observer
class MyField extends HTMLElement implements WebField {
    render() {
        const { name } = this;

        return (
            <input name={name} onChange={({ currentTarget: { value } }) => (this.value = value)} />
        );
    }
}

new DOMRenderer().render(
    <form method="POST" action="/api/data">
        <MyField name="test" />

        <button>提交</button>
    </form>
);

异步组件

import { DOMRenderer } from 'dom-renderer';
import { observer, PropsWithChildren } from 'web-cell';
import { sleep } from 'web-utility';

const AsyncComponent = observer(async ({ children }: PropsWithChildren) => {
    await sleep(1);

    return <p>{children} 中的异步组件</p>;
});

new DOMRenderer().render(<AsyncComponent>WebCell</AsyncComponent>);

异步加载

AsyncTag.tsx

import { FC } from 'web-cell';

const AsyncTag: FC = () => <div>异步</div>;

export default AsyncTag;

index.tsx

import { DOMRenderer } from 'dom-renderer';
import { lazy } from 'web-cell';

const AsyncTag = lazy(() => import('./AsyncTag'));

new DOMRenderer().render(<AsyncTag />);

异步渲染(试验)

DOM 树

import { DOMRenderer } from 'dom-renderer';

new DOMRenderer().render(
    <a>
        <b>异步渲染</b>
    </a>,
    document.body,
    'async'
);

类组件

import { component } from 'web-cell';

@component({
    tagName: 'async-renderer',
    renderMode: 'async'
})
export class AsyncRenderer extends HTMLElement {
    render() {
        return (
            <a>
                <b>异步渲染</b>
            </a>
        );
    }
}

Animate CSS 组件

import { DOMRenderer } from 'dom-renderer';
import { AnimateCSS } from 'web-cell';

new DOMRenderer().render(
    <AnimateCSS type="fadeIn" component={props => <h1 {...props}>淡入</h1>} />
);

Node.js 用法

工具链

npm install jsdom

Polyfill

import 'web-cell/polyfill';

服务端渲染

https://github.com/EasyWebApp/DOM-Renderer?tab=readme-ov-file#nodejs--bun

基础知识

生命周期钩子

  1. connectedCallback
  2. disconnectedCallback
  3. attributeChangedCallback
  4. adoptedCallback
  5. updatedCallback
  6. mountedCallback
  7. formAssociatedCallback
  8. formDisabledCallback
  9. formResetCallback
  10. formStateRestoreCallback

脚手架

  1. 基础
  2. 仪表盘
  3. 移动端
  4. 静态网站

生态系统

我们建议将这些库与 WebCell 一起使用:

路线图

v2 到 v3 迁移

更多指南

  1. 开发贡献