ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Riot.js TodoMVC 实战解析:用微型 UI 库与页面内编译器构建 Todo 应用

2026/9/20 4:41:18 拓冰建站 浏览量
Riot.js TodoMVC 实战解析:用微型 UI 库与页面内编译器构建 Todo 应用 Riot.js TodoMVC 实战解析用微型 UI 库与页面内编译器构建 Todo 应用【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvcRiot.js 是一款 React 风格的极简用户界面微库micro-library它反对当下流行的样板代码boilerplate与不必要的复杂度主张用小巧而强大的 API 与简洁的语法完成界面开发。本仓库的 examples/riotjs 是 TodoMVC 官方应用集中的 Riot.js 实现本文将以该示例为主线深入讲解 Riot.js 标签tag的声明式写法、页面内编译on-page compile的加载流程、父子标签通信、路由与 localStorage 持久化帮助你掌握用 Riot.js 2.x 快速搭建一个完整可运行的 Todo 应用的完整思路与具体写法。示例定位小而灵活的 Riot.jsRiot.js 的核心设计理念是反对样板代码与不必要的复杂度详见 examples/riotjs/readme.md团队认为一个客户端库应该拥有小而强大的 API 和简洁的语法。正因为 Riot 体积小、足够灵活它提供了多种使用方式使用编译器compiler编译标签文件使用 Browserify 等工具打包 JavaScript直接在页面引入微型 Riot 编译器把标签文件飞行编译compile tags on the fly。本示例采用的是第三种方式在页面上使用 Riot 编译器动态编译标签文件js/todo.html无需任何构建步骤打开浏览器即可运行这恰好是 Riot 去复杂度理念的直观体现。需要说明的是本示例严格遵循 TodoMVC 的 编码风格规范Tab 缩进、单引号、分号、严格模式等而 Riot 官方社区另有自己推荐的编码风格见 Riot 仓库的 CONTRIBUTING两者存在差异读者在阅读源码时可留意这一点。目录与依赖一览examples/riotjs/ ├── index.html # 页面入口按顺序加载依赖与标签 ├── package.json # npm 依赖声明 ├── readme.md # 官方示例说明文档 └── js/ ├── todo.html # Riot 标签定义todo 与 todoitem含模板与逻辑 ├── app.js # 应用启动挂载 todo 标签并注入初始数据 └── store.js # localStorage 封装负责数据读写依赖声明见 examples/riotjs/package.json{ private: true, dependencies: { todomvc-app-css: ^2.0.0, todomvc-common: ^1.0.1, riot: ^2.2.4 } }其中riot^2.2.4是本例使用的 Riot.js 2.x 版本todomvc-app-css与todomvc-common提供 TodoMVC 标准外观与公共脚本base.css、base.js。安装依赖后在 examples/riotjs 目录下执行npm install即可获得本地运行所需文件。页面加载与飞行编译流程先看入口 examples/riotjs/index.html!doctype html html langen>/*global riot, todoStorage */ (function () { use strict; riot.mount(todo, { data: todoStorage.fetch() }); }());riot.mount(todo, opts)会把页面中的todo节点替换为编译后的标签实例并将第二个参数{ data: todoStorage.fetch() }作为opts传入——todoStorage.fetch()从 localStorage 读出的历史数据就这样注入到了标签内部在todo.html中以opts.data接收。核心标签todo数据、筛选与事件标签定义集中在 examples/riotjs/js/todo.html一个文件里声明了两个标签外层todo与内层todoitem。Riot 标签的特点是模板 逻辑写在同一份标签内HTML 结构使用花括号{ }绑定表达式script块内则是标签的方法与生命周期逻辑。todo标签的模板部分todo section classtodoapp header classheader h1todos/h1 input classnew-todo autofocus autocompleteoff placeholderWhat needs to be done? onkeyup{ addTodo } /header section classmain show{ todos.length } input idtoggle-all classtoggle-all typecheckbox checked{ allDone } onclick{ toggleAll } label fortoggle-allMark all as complete/label ul classtodo-list li riot-tagtodoitem classtodo { completed: t.completed, editing: t.editing } each{ t, i in filteredTodos() } todo{ t } parentview{ parent }/li /ul /section footer classfooter show{ todos.length } span classtodo-count strong{ remaining }/strong { remaining 1 ? item : items } left /span ul classfilters lia class{ selected: activeFilterall } href#/allAll/a/li lia class{ selected: activeFilteractive } href#/activeActive/a/li lia class{ selected: activeFiltercompleted } href#/completedCompleted/a/li /ul button classclear-completed onclick{ removeCompleted } show{ todos.length remaining } Clear completed/button /footer /section ...几个值得展开的 Riot 2.x 语法点show{ todos.length }Riot 的条件显示指令。当列表为空时show表达式为 0falsymain与footer区域整体隐藏each{ t, i in filteredTodos() }循环指令。遍历filteredTodos()的返回值当前项绑定为t索引为itodos数组为空时each不渲染任何liriot-tagtodoitem在li上声明内嵌的子标签并把当前 todotodo{ t }与父标签实例parentview{ parent }传给子标签——这是 Riot 2.x 中父子标签通信的关键写法class{ completed: t.completed, editing: t.editing }条件 class 绑定根据completed/editing布尔值动态切换completed、editing两个 class配合 TodoMVC 样式实现勾选完成与双击编辑的视觉状态onkeyup{ addTodo }、onclick{ toggleAll }Riot 的事件绑定直接指向标签内部方法{ remaining 1 ? item : items }模板内支持 JavaScript 表达式实现单复数文案切换。标签内的逻辑部分如下省略了模板中重复的footer.info部分script use strict; var ENTER_KEY 13; var self this; self.todos opts.data || []; riot.route.exec(function(base, filter) { self.activeFilter filter || all; }); self.on(update, function() { self.remaining self.todos.filter(function(t) { return !t.completed; }).length; self.allDone self.remaining 0; self.saveTodos(); }); saveTodos() { todoStorage.save(self.todos); }; filteredTodos() { if (self.activeFilter active) { return self.todos.filter(function(t) { return !t.completed; }); } else if (self.activeFilter completed) { return self.todos.filter(function(t) { return t.completed; }); } else { return self.todos; } }; addTodo(e) { if (e.which ENTER_KEY) { var value e.target.value e.target.value.trim(); if (!value) { return; } self.todos.push({ title: value, completed: false }); e.target.value ; } }; removeTodo(todo) { self.todos.some(function (t) { if (todo t) { self.todos.splice(self.todos.indexOf(t), 1); } }); }; toggleAll(e) { self.todos.forEach(function (t) { t.completed e.target.checked; }); return true; }; removeCompleted() { self.todos self.todos.filter(function(t) { return !t.completed; }); }; riot.route(function(base, filter) { self.activeFilter filter; self.update(); }); /script这段逻辑体现了 Riot 2.x 的几个核心机制数据模型self.todos opts.data || []把挂载时传入的历史数据存入标签实例self即当前标签实例模板表达式{ remaining }、{ activeFilter }等都会读取self上的同名属性。更新钩子update 事件self.on(update, ...)注册了标签每次更新前的回调——Riot 在重新渲染模板前会自动触发update事件。这里统一计算派生状态remaining未完成数与allDone是否全部完成并调用saveTodos()把最新数据写回 localStorage实现每次界面变化自动持久化无需手动同步。路由riot.routeriot.route是 Riot 内置的 hash 路由。文件里注册了两次回调先通过riot.route.exec(function(base, filter) { ... })在初始化时读取当前 hash如#/all设置activeFilter再通过riot.route(function(base, filter) { ... })监听 hash 变化更新activeFilter并调用self.update()触发重渲染。模板中三个过滤器链接href#/all、#/active、#/completed就是路由的触发点配合class{ selected: activeFilterall }高亮当前筛选。事件方法addTodo在输入框按回车ENTER_KEY即 13时读取并 trim 输入非空则 push 一条{ title, completed: false }记录并清空输入框toggleAll根据全选框状态批量设置每条 todo 的completedremoveCompleted过滤掉已完成项removeTodo通过splice从数组中移除指定项。这些方法在模板中通过onclick、onkeyup等事件属性直接绑定。子标签todoitem单项编辑与父子通信列表项由子标签todoitem渲染它演示了 Riot 父子标签的典型协作模式todoitem div classview input classtoggle typecheckbox checked{ opts.todo.completed } onclick{ toggleTodo } label ondblclick{ editTodo }{ opts.todo.title }/label button classdestroy onclick{ removeTodo }/button /div input nametodoeditbox classedit typetext onblur{ doneEdit } onkeyup{ editKeyUp } script use strict; var ENTER_KEY 13; var ESC_KEY 27; var self this; opts.todo.editing false; toggleTodo() { opts.todo.completed !opts.todo.completed; opts.parentview.saveTodos(); return true; }; editTodo() { opts.todo.editing true; self.todoeditbox.value opts.todo.title; }; removeTodo() { opts.parentview.removeTodo(opts.todo); }; doneEdit() { if (!opts.todo.editing) { return; } opts.todo.editing false; var enteredText self.todoeditbox.value self.todoeditbox.value.trim(); if (enteredText) { opts.todo.title enteredText; opts.parentview.saveTodos(); } else { self.removeTodo(); } }; editKeyUp(e) { if (e.which ENTER_KEY) { self.doneEdit(); } else if (e.which ESC_KEY) { self.todoeditbox.value opts.todo.title; self.doneEdit(); } }; self.on(update, function() { if (opts.todo.editing) { opts.parentview.update(); self.todoeditbox.focus(); } }); /script /todoitem关键机制拆解opts传递父标签通过li riot-tagtodoitem ... todo{ t } parentview{ parent }传入两个opts——opts.todo指向当前这条 todo 数据对象opts.parentview指向父标签实例。由于opts.todo是对象引用子标签对它的修改如opts.todo.completed !opts.todo.completed会直接反映到父标签的todos数组中这是本示例不依赖事件冒泡而实现数据同步的关键勾选与删除toggleTodo翻转completed后调用opts.parentview.saveTodos()持久化removeTodo直接调用父标签的removeTodo(opts.todo)方法完成移除双击编辑ondblclick{ editTodo }进入编辑态editing true并把当前标题填入输入框输入框nametodoeditbox使 Riot 自动把它挂到self.todoeditbox上回车/失焦提交、Esc 取消editKeyUp中ENTER_KEY13提交编辑ESC_KEY27恢复原标题后提交doneEdit会先校验editing状态防止误触发输入非空则更新title并保存输入为空则删除该条 todo自动聚焦self.on(update, ...)中若当前处于编辑态则让父标签同步更新并调用todoeditbox.focus()保证双击后输入框立即获得焦点。数据持久化localStorage 封装examples/riotjs/js/store.js 是全部持久化逻辑只有 15 行(function (exports) { use strict; var STORAGE_KEY todos-riotjs; exports.todoStorage { fetch: function () { return JSON.parse(localStorage.getItem(STORAGE_KEY) || []); }, save: function (todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); } }; })(window);存储键为todos-riotjs通过exports参数把todoStorage挂到window全局IIFE 写法避免污染作用域符合 TodoMVC 编码规范中的严格模式使用方式fetch()读取并JSON.parse无数据时返回[]save()将数组序列化后写回与todo.html的调用关系是app.js挂载时用todoStorage.fetch()注入初始数据标签每次update时由saveTodos()调用todoStorage.save(self.todos)从而实现改一处、存一次、刷新不丢。如何本地运行与测试本示例无需构建安装依赖后即可直接访问。仓库根目录提供了统一的静态服务与测试链路在 examples/riotjs 下执行npm install安装riot、todomvc-app-css、todomvc-common启动仓库根目录的静态服务器npm run server对应根目录 package.json 中server: node tests/server.js由 tests/server.js 基于 Express 提供express.static服务然后浏览器打开http://localhost:8000/examples/riotjs/端口依据根目录 package.json 中start-server-and-test server http://localhost:8000的约定为 8000若要跑完整 TodoMVC 验收测试可在仓库根目录执行npm test启动服务器 Cypress 端到端测试或npm run test:all服务器 全量示例检查Cypress 配置见 cypress.config.js。由于本示例采用页面内编译本地调试无需 webpack、browserify 等构建链——修改js/todo.html后刷新浏览器即可生效这正是 readme 强调的 Riot 灵活性在实际工程中的直接收益。小结通过 examples/riotjs 这一 TodoMVC 实现可以看到 Riot.js 2.x 的核心能力全景标签tag把模板与逻辑收敛于一个文件each/show/class等指令完成声明式渲染opts对象引用实现父子标签间的直接数据协作riot.route提供内置 hash 路由update事件钩子把派生状态计算与自动持久化统一起来。整个应用没有框架层样板代码一个标签文件、一个启动脚本、一个存储封装就完成了 TodoMVC 的全部交互要求——这恰恰呼应了 readme 开篇对 Riot 的定位小 API、简洁语法、拒绝不必要的复杂度。【免费下载链接】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),仅供参考