PPxu

使用 Riot、ES6 和 Webpack 构建应用

原文链接: blog.srackham.com

使用 Riot、ES6 和 Webpack 构建应用

Tue Feb 10, 2015

我第一次接触 Riot 是在看到 Muut 的博文 轻框架的 JavaScript 之后 —— 请一定要先看看这个!Muut 的程序员没有吹牛,他们创建了 Riot,一个用来构建响应式 UI 组件的极简的 类 Reactjs 库

在阅读 Riot 文档的时候,让我震惊的是 Riot 理解起来是如此的简单 —— 相比 React,只需要学习非常少的新术语和概念 (讲道理,React 比起 Polymer 和 Angular 这些已经算是非常直接明了了)。

为了学习 Riot,我把我的 React Flux Backbone Todos 示例 改写成了一个 Riot Todo 应用。这篇文章就汇集了我在改造过程中记录的笔记。

如果不想看文章,直接看如下结论:

  1. Riot 确实做到了它宣称的极简。(未压缩) 的 Riot 版 Todo 仅仅只有 32KB,相比之下,React 的版本达到了 964KB (在去掉了 Riot 版本没有用到的 Backbone 和 jQuery 之后还有 600KB)。无论怎么看这都是一个巨大的差别。

  2. Riot 有一种少见的像金发美女一样那种 “刚刚好” 的感觉,用起来是一种享受。

  3. Riot 是一个相对较新的项目,我还不能找到有关它在大型项目中的扩展性的统计信息。它和强大的 React 的对比表现还不得而知 —— 我希望它干的不错。

如果你对 Riot 还不熟悉,请先看一下 Riot 官网 —— 官方文档是最靠谱的。因此与其讨论 Riot 是如何运作的,我更愿意讲讲我学到的一些不太容易从文档中看到的东西。

在 Riot 中使用 ES6

示例应用 是用 ES6 写的,我用了 6to5 这个转换器将它转换成 ES5,并用 Webpack 将编译后的代码和依赖的库打包到了一起。这个方法必须要结合 JavaScript 模块 —— 在 ES6 下开发非常美妙,因为你可以利用 ES6 中新增的 import 和 export 语句 (参见 这个例子)。

我在 Webpack 中 配置了 使用 6to5-loader 自动将 ES6 源码转换成 CommonJS 风格的 ES5 模块,然后打包到一个单独的 bundle.js 分发文件。

为什么我不使用 Riot 的 .tag 文件

Riot 的 tag 文件是包含了 HTML 标记和 JavaScript UI 逻辑的 HTML 模板。如果你看过了 Todo 应用,你可能会疑惑这些 tag 文件在哪里 —— 答案是我根本没有用到它们,我选择用 JavaScript 来代替。通过去除 .tag 文件,我简化了我的代码、工具和流程。对我来说,tag 文件带来的复杂性和限制比优点要多。

这不是批评 Riot,tag 文件是完全可选的,我下面会说明为什么我不使用它们。

当你检查编译好的 JavaScript 文件,你会看到 Riot 的 tag 文件只是一层薄薄的语法糖。

  • 它们添加了额外的一层概念 —— 因此必须要学习新的语法和语义。

  • 它们添加了一步额外的编译过程。

  • tag 文件的编译器指定了你能使用的语言和模板 (CoffeeScript, ES6, Jade),这一点和它宣称的 “使用你喜爱的工具” 相矛盾。

  • tag 中构造体的逻辑在上下文之外:

    • 它对编译器或者 IDE 的代码检查等功能不太友好。

    • 在上下文之外引用 this 意味着这不是合法的 JavaScript 代码,而且在编译器或者 IDE 中会产生错误。

  • 目前还不支持这些模块风格 (CommonJS,AMD) 的 tag 文件转换成普通的 JavaScript。

  • tag 文件需要在 Webpack 或者 Broswerify 这些构建工具中直接使用 tag 加载器。

  • “类 ES6” 的构建方法很好,但是它们不是合法的 JavaScript,很可能会导致更多的困惑 (语法和语义)。举个例子,你可以使用 ES6 箭头函数 来实现相同的语义 (词法绑定 this),简洁程度上也差不多。

this.add = (e) => {
  var input = e.target[0]
  this.items.push(input.value)
  input.value = ''
};

这里是一个使用了 ES6 模板字符串和箭头函数的 ES6 版 tag 文件的示例

ES6 的模板字符串增加了 tag 中 HTML 模板的可读性。类似的,如果你使用的是 CoffeeScript,你也可以使用 CoffeeScript 的块级字符串。JSX 也是一种选择 —— React 的 JSX 转换器可以稍做修改,输出一个字符串变量,这这样你就可以利用现有的 JSX 支持工具。

Riot 和 React 的本质区别

最主要的区别在于 UI 标记模板是如何声明的:

  • 在 React 中,UI 标记模板是写在 JavaScript 代码中 (使用 JSX 语言扩展)。

  • Riot 和 React 的模式相反,把标记和逻辑放在 HTML (tag) 文件中。

这个差异概括起来就是 React 的模板 DSL 语言是 JavaScript,而 Riot 基于一种自定义的模板 DSL (引入了一些自定义标签)。这里有两个简化的例子,用来从一个 todo 元素数组生成一个列表:第一个是 React JavaScript,第二个是等价的 Riot 的 tag 标记:

<ul>
  todos.map(todo =>
    <li><TodoItemComponent todo={todo} /></li>)
</ul>

<ul>
  <li each="{todo in todos}">
    <todo-item todo="{todo}">
  </li>
</ul>

第一个例子使用了 JavaScript 的 map 函数来生成一个列表,第二个使用了 Riot 自定义的 each 模板属性。

Steve Luscher 在 这个视频的最后 解释了为什么他认为 JavaScript 比自定义的模板 DSL 要好 —— 你不止要学一门自定义 DSL,你还被限制在这套 DSL 提供的功能之内。对于一些像上面这个一样小型的常用用例,这两个方式都可以选择,但是如果是一个大型高度动态化的 UI 组件化项目,React 的 JavaScript 方式的能力和灵活性就明显要好的多。

提示

避免自闭合的 XHTML 类标签

不要使用 /> 来闭合标签,因为它不总是会立即闭合标签。当处理 HTML5 元素 <foo /> 等同于 <foo> (而在 XHTML 中 <foo /> 等同于 <foo></foo>),即 HTML5 会忽略 / 记号。关于这个问题可以查看 Stackoverflow 上的这篇文章,以及:

绑定 this 到标签的事件处理器

绑定 this 到标签的事件处理器可以确保事件总是在标签的上下文中被触发 (或者使用常用的 var self = this)。举个例子:

this.clear = function(e) {
  dispatcher.trigger(dispatcher.CLEAR_TODOS);
}.bind(this);

在 ES6 中你可以用支持词法绑定的 箭头函数 获得相同的效果:

this.clear = (e) => {
  dispatcher.trigger(dispatcher.CLEAR_TODOS);
};

引用循环元素

使用 each="{item in items}" 结构将当前循环的元素传给一个子级自定义标签。下面这个例子中,自定义的 todo-item 标签中的代码可以通过 opts.todo 引用当前的 todo 元素:

<ul>
 <li each="{todo in opts.store.todos}">
   <todo-item store="{parent.opts.store}" todo="{todo}">
 </li>
</ul>

事件名的命名空间

约定使用带冒号的命名空间来对应用的事件名进行分组,比如 admin:edit, admin:delete, admin:new

你可以在 CSS 文件中使用自定义的标签名

自定义的标签会被渲染成 DOM,因此它们可以使用 CSS 选择器和 DOM 查询,这儿有 一个例子

调试

如果使用 Webpack 打包,你需要开启 devtool 的 source-map 配置项 来生成一个对应打包文件的 source map 文件。这样你就可以在 ES6 源码文件中进行调试。

打开浏览器的 Sources 面板 并定位到文件夹 webpack:///. 就可以浏览和调试源码。

  • 在 Firefox 中:打开 Debugger (Ctrl+Shift+S)。

  • 在 Google Chrome 中:打开 Console (Ctrl+Shift+J) 然后点击 Source 标签来查看 Source 面板。

我不会放重点在调试器和打断点上 —— 大部分时候我只是有选择的在代码中添加一些临时的 console.log() 语句。

总结

Riot (类似 React) 是一个 UI 库,而不是框架。这点很好 (我喜欢用一些小的针对性的库而不是一个大而全的框架),不过对于较重的应用还需要一个一致的高阶结构 (或者架构) 来便于扩展、升级和维护。Flux 就是一个为 React 类应用完善架构的库。我喜欢 FLux 因为它很容易理解而且非常直观 (没有超出任何理论基础)。Riot Todo 应用 使用了类 flux 的分发器 RiotControl (轻微改动) 来实现 Flux 架构。