TodoMVC × Lit:用 Web Components 构建 TodoMVC 的完整实现与事件驱动状态管理

发布时间:2026/9/19 16:19:36
TodoMVC × Lit:用 Web Components 构建 TodoMVC 的完整实现与事件驱动状态管理
TodoMVC × Lit用 Web Components 构建 TodoMVC 的完整实现与事件驱动状态管理【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvc本篇以仓库中的 Lit 示例为主体拆解 TodoMVC 官方 Lit 实现的完整技术脉络它如何用自定义元素与 Shadow DOM 重构了其他实现中常见的组件树如何在不引入任何状态管理库的前提下用一个继承EventTarget的类 自定义 DOM 事件完成集中式变更、订阅式更新以及配套的 wireit/Rollup 构建与本地运行流程。读完后你将能够独立复刻这套事件驱动 属性装饰器的 Web Components 状态管理模式并理解每个 Todo 功能背后的具体调用链。一、为什么 Lit 实现的 DOM 与其他实现不同README 对 Lit 的定位是一个用于构建快速、轻量 Web Components 的简单库。与其他框架版 TodoMVC 最大的结构差异在于它基于 Web Components 技术创建可互操作、可封装的新 HTML 元素。这带来两个直接后果文档对象模型DOM形态不同。页面上出现的是todo-app、todo-list、todo-form、todo-item、todo-footer这类自定义元素各组件的内部 DOM 被隔离在自己的 Shadow Root 中互不干扰。CSS 采用 Shadow DOM 的样式作用域style scoping。由于 Shadow DOM 天然隔离样式README 特别指出 CSS 被拆分成了独立的模块每个组件只包含自己所需的部分。这一点可以在源码中直接验证src/lib/todo.css.ts 提供各组件共享的样式基座导出为todoStyles而 todo-app.ts、todo-form.ts 等每个组件再把自己的专属css\片段叠加进去例如 [todo-app.ts](https://link.gitcode.com/i/3253abd719d01a014f5f28f53c250d13#L17-L51) 中的static styles [todoStyles, css...]组件级样式:host布局、标题、.hidden 等与全局基座样式分离维护。二、状态管理继承 EventTarget 的 Todos 类这是本实现最有特色的部分。README 明确说明本实现不使用任何状态管理库而是把 Todo 数据建模为一个继承EventTarget的类该类实例被传递给各组件组件通过监听其change事件感知数据变化README 同时指出Lit 完全可以搭配 Redux、MobX、各种 signals 库等状态管理方案使用一个普通类只是最简单的选项之一是最接近原生vanilla的做法所有变更mutations由 app 组件集中发起其他组件只负责通过事件通知 app有变更请求。对应源码是 src/lib/todos.ts核心结构如下export interface Todo { id: string; text: string; completed: boolean; } export class Todos extends EventTarget { #todos: ArrayTodo []; #filter: TodoFilter this.#filterFromUrl(); // 只读视图all / active / completed / allCompleted // 变更入口add / delete / update / toggle / toggleAll / clearCompleted // 每次变更后统一触发 #notifyChange() { this.dispatchEvent(new Event(change)); } }结合源码可以补充几个 README 未展开的实现细节数据与筛选全部私有化#todos与#filter是私有字段外部只能通过只读 getterall、active、completed、allCompleted访问filter的 setter 在写入后同样触发change事件见 todos.ts。筛选状态持久化在 URL hash 中#filterFromUrl()用正则/#\/(.*)/解析window.location.hash非法值回退为allTodos实例通过connect()/disconnect()注册与移除hashchange监听因此切换Active / Completed过滤器链接形如#/active刷新页面后依然生效。ID 生成add()中的nanoid()是从 nanoID 项目借鉴的 21 位短 ID 生成器todos.ts使用Math.random()非安全版本适合前端 UI 场景。防御式删除delete()中findIndex未命中时返回-1代码利用index 0将负数翻转成极大数使splice变为无操作no-op——这是一个避免负索引误删末位元素的位运算技巧todos.ts。三、集中式变更todo-app 组件与事件协议变更集中在 app 组件这一架构落在 src/lib/todo-app.ts 上。它维护唯一的Todos实例并在构造函数中把五类自定义事件绑定到自己的私有处理器customElement(todo-app) export class TodoApp extends LitElement { updateOnEvent(change) state() readonly todoList new Todos(); constructor() { super(); this.addEventListener(AddTodoEvent.eventName, this.#onAddTodo); this.addEventListener(DeleteTodoEvent.eventName, this.#onDeleteTodo); this.addEventListener(EditTodoEvent.eventName, this.#onEditTodo); this.addEventListener(ToggleAllTodoEvent.eventName, this.#onToggleAll); this.addEventListener(ClearCompletedEvent.eventName, this.#onClearCompleted); } }事件协议定义在 src/lib/events.ts。五个事件类均继承Event且构造参数统一为{ bubbles: true, composed: true }事件名事件类负载触发组件todo-addAddTodoEventtexttodo-form输入新 Todotodo-deleteDeleteTodoEventidtodo-item点删除按钮/编辑后留空todo-editEditTodoEventedit: TodoEdittodo-item勾选完成/提交编辑todo-toggle-allToggleAllTodoEvent无todo-list全选框clear-completedClearCompletedEvent无todo-footer这里有一个 Web Components 的关键细节composed: true使事件能够穿透 Shadow DOM 边界。子组件渲染在自己的 Shadow Root 内事件若不带composed冒泡到宿主元素就会停止、无法到达todo-app声明bubbles: true, composed: true后事件才能一路冒泡到 app 组件被集中处理。文件末尾的HTMLElementEventMap全局类型扩展events.ts则让 TypeScript 对todo-add等事件名提供类型提示。updateOnEvent把 change 事件接进 Lit 更新机制各展示组件如何订阅change答案是一个自研属性装饰器 src/lib/utils.tsexport const updateOnEvent (eventName: string) (target, propertyKey) { const descriptor Object.getOwnPropertyDescriptor(target, propertyKey)!; const { get, set } descriptor; const newDescriptor { ...descriptor, set(this, v: EventTarget) { const listener this.__updateOnEventListener ?? () this.requestUpdate(); const oldValue get!.call(this); oldValue?.removeEventListener?.(eventName, listener); v?.addEventListener?.(eventName, listener); return set!.call(this, v); }, }; Object.defineProperty(target, propertyKey, newDescriptor); };它劫持属性的 setter每当todoList属性被赋值为新的EventTarget实例时自动对旧实例解绑、对新实例绑定change监听监听器只调用 Lit 的requestUpdate()。这样updateOnEvent(change) property({ attribute: false }) todoList?: Todos;出现在 todo-form.ts、todo-list.ts、todo-footer.ts就等价于该属性一变/数据一变组件自动重渲染。源码注释也坦诚若要在其他项目里复用应使用类型系统强制属性值必须是EventTarget。另外TodoApp在connectedCallback中调用this.todoList.connect()、在disconnectedCallback中调用disconnect()todo-app.ts把 hashchange 监听的生死与组件挂载周期绑定避免内存泄漏。四、各组件的数据流组件间数据流可以概括为属性向下、事件向上app 把Todos实例通过属性.todoList${this.todoList}传给子组件见 todo-app.ts 的render()子组件交互后 dispatch 事件app 收到后统一调用Todos的变更方法。todo-form渲染一个输入框change或按 Enter 时若输入非空则dispatchEvent(new AddTodoEvent(value))并清空输入框todo-form.ts。它从不直接改动数据。todo-list用 Lit 的repeat指令按todo.id作为 key 渲染todo-item列表数据源是this.todoList.filtered()——即受当前 hash 过滤器影响的视图todo-list.ts左侧的Mark all as complete复选框变化时发出ToggleAllTodoEvent。todo-item只接收todoId、text、completed三个普通属性是纯粹的表现组件。勾选时发出EditTodoEvent({ id, completed: !completed })双击进入行内编辑state() isEditing为组件私有状态Enter 通过blur()触发提交、Escape 取消先把输入框值重置回原文使随后的 blur 提交变成无操作、提交时若文本为空则改发DeleteTodoEventtodo-item.ts。todo-footer渲染剩余项计数、All/Active/Completed 三个过滤器链接href#/${filter}与Todos的 hash 路由相呼应以及Clear completed按钮todo-footer.ts。列表为空时直接return nothing不渲染。五、构建与运行README 给出的运行步骤非常直接在 examples/lit 目录下执行npm cinpm run serve --watch浏览器访问http://localhost:8000/查看 debug 构建或http://localhost:8000/dist/查看优化构建如果只想重新构建代码直接运行npm run build即可运行npm run serve --watch时构建会在需要时自动触发。结合 package.json 可以进一步理解这套命令背后的任务图依赖运行时仅依赖lit^3.3.2开发侧为rollup、web/dev-server即wds命令、typescript与任务编排器wireit。engines要求 Node20.19.0、npm10.0.0。wireit 任务依赖链package.jsonserve依赖build命令是wds并以service: true声明为长驻服务进程dev是serve的轻量版——README 注释写明与 serve 类似只是不做较慢的 rollup 构建它只依赖tsc后启动wdsbuild依赖五个子任务tsc产出index.js、lib/等编译结果、rollup产出dist/index.js、copy-index-html、copy-base-js、copy-base-css把 index.html 与todomvc-common包中的base.js/base.css拷入dist/即优化构建页面所引用的公共资源。Rollup 优化管线rollup.config.mjs依次使用rollup/plugin-typescriptoutputToFilesystem: true、rollup/plugin-node-resolve把裸模块说明符解析为相对路径、rollup-plugin-html-literals压缩 HTML 模板字符串和rollup/plugin-terserecma: 2022压缩 JS入口为src/index.ts以 ES 模块格式输出到dist/index.js。开发服务器配置web-dev-server.config.jsnodeResolve指定exportConditions: [development, browser]让wds在开发模式下解析到 Lit 的 development 导出条件。入口src/index.ts 仅一行export * from ./lib/todo-app.js即整个应用只需注册todo-app自定义元素其余元素由其内部import链自动注册。六、小结examples/lit 示例展示了一种与框架版 TodoMVC 截然不同的技术路径以 Web Components自定义元素 Shadow DOM为封装单位以继承EventTarget的数据类 change事件为状态中枢以bubbles: true, composed: true的自定义事件为组件间通信协议再辅以updateOnEvent属性装饰器把事件订阅无缝接入 Lit 的requestUpdate()更新机制。它验证了 README 的判断——不依赖 Redux/MobX 等状态库用最接近原生 Web API 的方式即可完成一个完整的 TodoMVC 应用包括 hash 路由过滤器、行内编辑与集中式数据变更而 wireit Rollup Web Dev Server 的工具链则同时支撑了根路径的调试构建与dist/下的优化构建。【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考