news 2026/9/20 4:41:16

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/todomvc

Riot.js 是一款 React 风格的极简用户界面微库(micro-library),它反对当下流行的样板代码(boilerplate)与不必要的复杂度,主张用小巧而强大的 API 与简洁的语法完成界面开发。本仓库的 examples/riotjs 是 TodoMVC 官方应用集中的 Riot.js 实现,本文将以该示例为主线,深入讲解 Riot.js 标签(tag)的声明式写法、页面内编译(on-page compile)的加载流程、父子标签通信、路由与 localStorage 持久化,帮助你掌握用 Riot.js 2.x 快速搭建一个完整可运行的 Todo 应用的完整思路与具体写法。

示例定位:小而灵活的 Riot.js

Riot.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-csstodomvc-common提供 TodoMVC 标准外观与公共脚本(base.cssbase.js)。安装依赖后在 examples/riotjs 目录下执行npm install即可获得本地运行所需文件。

页面加载与"飞行编译"流程

先看入口 examples/riotjs/index.html:

<!doctype html> <html lang="en">/*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 class="todoapp"> <header class="header"> <h1>todos</h1> <input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" onkeyup={ addTodo }> </header> <section class="main" show={ todos.length }> <input id="toggle-all" class="toggle-all" type="checkbox" checked={ allDone } onclick={ toggleAll }> <label for="toggle-all">Mark all as complete</label> <ul class="todo-list"> <li riot-tag="todoitem" class="todo { completed: t.completed, editing: t.editing }" each={ t, i in filteredTodos() } todo={ t } parentview={ parent }></li> </ul> </section> <footer class="footer" show={ todos.length }> <span class="todo-count"> <strong>{ remaining }</strong> { remaining === 1 ? 'item' : 'items' } left </span> <ul class="filters"> <li><a class={ selected: activeFilter=='all' } href="#/all">All</a></li> <li><a class={ selected: activeFilter=='active' } href="#/active">Active</a></li> <li><a class={ selected: activeFilter=='completed' } href="#/completed">Completed</a></li> </ul> <button class="clear-completed" onclick={ removeCompleted } show={ todos.length > remaining }> Clear completed</button> </footer> </section> ...

几个值得展开的 Riot 2.x 语法点:

  • show={ todos.length }:Riot 的条件显示指令。当列表为空时,show表达式为 0(falsy),mainfooter区域整体隐藏;
  • each={ t, i in filteredTodos() }:循环指令。遍历filteredTodos()的返回值,当前项绑定为t,索引为itodos数组为空时each不渲染任何<li>
  • riot-tag="todoitem":在<li>上声明内嵌的子标签,并把当前 todo(todo={ t })与父标签实例(parentview={ parent })传给子标签——这是 Riot 2.x 中父子标签通信的关键写法;
  • class={ completed: t.completed, editing: t.editing }:条件 class 绑定,根据completed/editing布尔值动态切换completedediting两个 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.route)riot.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: activeFilter=='all' }高亮当前筛选。

事件方法addTodo在输入框按回车(ENTER_KEY即 13)时读取并 trim 输入,非空则 push 一条{ title, completed: false }记录并清空输入框;toggleAll根据全选框状态批量设置每条 todo 的completedremoveCompleted过滤掉已完成项;removeTodo通过splice从数组中移除指定项。这些方法在模板中通过onclickonkeyup等事件属性直接绑定。

子标签<todoitem>:单项编辑与父子通信

列表项由子标签<todoitem>渲染,它演示了 Riot 父子标签的典型协作模式:

<todoitem> <div class="view"> <input class="toggle" type="checkbox" checked={ opts.todo.completed } onclick={ toggleTodo }> <label ondblclick={ editTodo }>{ opts.todo.title }</label> <button class="destroy" onclick={ removeTodo }></button> </div> <input name="todoeditbox" class="edit" type="text" 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-tag="todoitem" ... 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),并把当前标题填入输入框;输入框name="todoeditbox"使 Riot 自动把它挂到self.todoeditbox上;
  • 回车/失焦提交、Esc 取消editKeyUpENTER_KEY(13)提交编辑,ESC_KEY(27)恢复原标题后提交;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),从而实现"改一处、存一次、刷新不丢"。

如何本地运行与测试

本示例无需构建,安装依赖后即可直接访问。仓库根目录提供了统一的静态服务与测试链路:

  1. 在 examples/riotjs 下执行npm install,安装riottodomvc-app-csstodomvc-common
  2. 启动仓库根目录的静态服务器: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);
  3. 若要跑完整 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 4:40:55

Python列表与元组详解:可变性、性能与选型

列表和元组——Python 里的“爱情”&#xff1a;列表善变&#xff0c;元组长情我刚学 Python 那会儿&#xff0c;总有人跟我说&#xff1a;“列表和元组差不多&#xff0c;差一个能不能改而已。”真到了写项目的时候才发现&#xff0c;这个“能不能改”背后藏着 Python 里一套非…

作者头像 李华
网站建设 2026/9/20 4:40:14

MOSFET从入门到实战:选型、驱动与散热设计指南

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

作者头像 李华
网站建设 2026/9/20 4:39:52

LibreChat自托管部署指南:多模型AI对话聚合与知识库搭建

1. 为什么我最终把日常AI对话工作流迁到了LibreChat第一次接触LibreChat是在一个技术群里&#xff0c;有人丢了一张截图&#xff0c;界面左边是会话列表&#xff0c;右边是对话框&#xff0c;顶部可以随时切换模型&#xff0c;底下还挂着知识库和插件入口。当时我的第一反应是&…

作者头像 李华
网站建设 2026/9/20 4:39:48

后缀数组+二分答案 解洛谷P2852:height数组与最长重复子串

昨天把洛谷上的 P2852 [USACO06DEC] Milk Patterns G 这道信奥题过了&#xff0c;趁着打卡系列整理一下思路。这道题在 USACO 2006 年 12 月的 Gold 组里算是很经典的“后缀数组 二分答案”入门题&#xff0c;题面本身很朴素&#xff1a;给你一个长度为 N 的整数序列&#xff…

作者头像 李华
网站建设 2026/9/20 4:36:46

GB/T 34986与ASTM F1980:有源与无源医疗器械加速老化的标准选择

先问一个我经常在实验室面试里提的问题&#xff1a;当任务单写着“某有源监护仪需要做加速老化&#xff0c;请按GB/T 34986出方案”&#xff0c;结果方案工程师却拿着ASTM F1980的模板&#xff0c;把样品往60℃烘箱里一塞&#xff0c;算了个“2周等效2年”就交差——你会不会觉…

作者头像 李华
网站建设 2026/9/20 4:36:01

OpenCode命令与模型配置深度解析:从底层调度到实战排障

1. 这不是一份普通速查表&#xff1a;OpenCode 最新版的底层逻辑与真实使用场景 OpenCode 不是另一个“带AI按钮的编辑器”&#xff0c;它是一套把开发者工作流重新焊接起来的工具链。我从 v0.8.2 跟到 v1.4.3&#xff0c;亲手部署过 7 种模型接入方式&#xff0c;踩过控制台报…

作者头像 李华