简体中文 | English
基于 VDOM、JSX、MobX 和 TypeScript 的 Web 组件 引擎
特性 | WebCell 3 | WebCell 2 | React | Vue |
---|---|---|---|---|
JS 语言 | TypeScript 5 | TypeScript 4 | ECMAScript 或 TypeScript | ECMAScript 或 TypeScript |
JS 语法 | ES 装饰器 stage-3 | ES 装饰器 stage-2 | ||
XML 语法 | JSX import | JSX factory | JSX factory/import | HTML/Vue 模板或 JSX(可选) |
DOM API | Web 组件 | Web 组件 | HTML 5+ | HTML 5+ |
视图渲染器 | DOM Renderer 2 | SnabbDOM | (内置) | SnabbDOM(分叉) |
state API | MobX @observable |
this.state |
this.state 或 useState() |
this.$data 或 ref() |
props API | MobX @observable |
@watch |
this.props 或 props => {} |
this.$props 或 defineProps() |
状态管理 | MobX 6+ | MobX 4/5 | Redux | VueX |
页面路由器 | JSX 标签 | JSX 标签 + JSON 数据 | JSX 标签 | JSON 数据 |
资源打包工具 | Parcel 2 | Parcel 1 | webpack | Vite |
npm install dom-renderer mobx web-cell
npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D
{
"scripts": {
"start": "parcel source/index.html --open",
"build": "parcel build source/index.html --public-url ."
}
}
{
"compilerOptions": {
"target": "ES6",
"module": "ES2020",
"moduleResolution": "Node",
"useDefineForClassFields": true,
"jsx": "react-jsx",
"jsxImportSource": "dom-renderer"
}
}
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
}
}
<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>
</>
);
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 />);
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/[email protected]/dist/css/bootstrap.min.css"
/>
<a className="btn">
<slot />
</a>
</>
);
}
}
.btn {
color: white;
background: lightblue;
}
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>
);
}
}
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 { FC } from 'web-cell';
const AsyncTag: FC = () => <div>异步</div>;
export default AsyncTag;
import { DOMRenderer } from 'dom-renderer';
import { lazy } from 'web-cell';
const AsyncTag = lazy(() => import('./AsyncTag'));
new DOMRenderer().render(<AsyncTag />);
import { DOMRenderer } from 'dom-renderer';
import { AnimateCSS } from 'web-cell';
new DOMRenderer().render(
<AnimateCSS type="fadeIn" component={props => <h1 {...props}>淡入</h1>} />
);
npm install jsdom
import 'web-cell/polyfill';
https://github.com/EasyWebApp/DOM-Renderer?tab=readme-ov-file#nodejs--bun
connectedCallback
disconnectedCallback
attributeChangedCallback
adoptedCallback
updatedCallback
mountedCallback
formAssociatedCallback
formDisabledCallback
formResetCallback
formStateRestoreCallback
我们建议将这些库与 WebCell 一起使用:
-
状态管理:MobX(也由 TypeScript 和 Decorator 提供支持)
-
路由:Cell Router
-
UI 组件
- BootCell(基于 BootStrap v5)
- MDUI(基于 Material Design v3)
- GitHub Web Widget
-
HTTP 请求:KoAJAX(基于类 Koa 中间件)
-
实用程序:Web utility 方法和类型
-
事件流:Iterable Observer(
Observable
提案) -
MarkDown 集成:Parcel MDX transformer(MDX 编译器插件)