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

发布时间:2026/9/19 16:19:23

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),仅供参考
延伸阅读

更多相关文章

2026/9/19 16:14:23

AI工作流搭建实战:从LangChain到LangGraph的完整指南

AI 工具这两年最大的变化,不是模型本身又强了多少,而是大家开始认真琢磨"怎么把模型塞进一条能稳定跑起来的流水线里"。我身边不少朋友一开始都是打开对话框,问一句答一句,用得很开心;等到想把这件事变成每天…

2026/9/19 17:29:26

BrewUI实战:把Homebrew包管理变成可视化掌控

BrewUI这个项目名字一出来,老 mac 用户应该立刻能反应过来——这不就是给 Homebrew 套了个图形界面嘛。作为一个在终端里泡了十几年、用 Homebrew 装了上千个包的人,我最初对这种 GUI 封装是有点不屑的:brew install 一行命令的事&#xff0c…

2026/9/19 17:29:26

BrewUI:Homebrew 的图形界面客户端,macOS 包管理更直观

先说结论:BrewUI 是给 Homebrew 做的一个图形界面客户端,让你不用整天泡在终端里敲 brew 命令,也能完成软件的搜索、安装、升级、清理和服务管理。如果你平时用 macOS 做开发,电脑里的开发工具、数据库、命令行软件全靠 Homebrew …

2026/9/19 17:29:26

三一集团SCM计划管理指标体系与实操落地解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 17:24:26

生成对抗网络心电降噪全攻略:从原理到工程实践

简介:面向生物医学信号处理与深度学习方向的本硕毕业生及研究者,这份PDF全文详细阐述了一种基于生成对抗网络(GAN)的心电信号降噪研究方案。针对心电信号易受噪声干扰、传统方法泛化能力弱且降噪后波形易失真等痛点,论…

2026/9/18 14:13:01

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/19 0:03:10

验证 OpenSpec 兼容性,Cursor 的 Token 从 TaoToken 出

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 0:03:10

书桌角落的 Mac mini,OpenClaw 通过 TaoToken 跑任务。

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 0:03:10

oh-my-hermes:打造跨工具的命令编排与插件化工作流

1. 项目概述与设计初衷1.1 它到底是什么先说结论:oh-my-hermes 是一个面向开发者日常终端操作的效率工具套件,核心定位是“把分散在各类命令行工具里的高频操作,统一收拢成一套插件化、可编排的工作流”。项目灵感来源很明显——oh-my-zsh 重…

2026/9/18 14:13:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/18 14:13:02

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/18 14:13:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码