前端,你是文艺界的程序员
我为什么要用Riot
优点明显,体积小,加载快,继承了react,Polymer等框架的优势,并简化他们,主要从以下几个方面考虑:
1. 自定义标签。
Riot 在所有浏览器上支持自定义标签,我们能将页面组件化,一个自定义标签结构如下所示:
{ item }
2. 对阅读友好
有了自定义标签功能后,我们可以用很简洁语言‘拼凑’出复杂的用户界面,加上语义化标签定义,在阅读的时候,很容易看清楚哪个标签是给html加入了什么组件,一共有多少个组件,这样一个页面大致实现什么功能,甚至不用看浏览器展示都能明白。你的代码可能是这样的
Acme community
很清楚,页面被分成四三个模块,一个标题h1
,一个header
,一个content
和一个footer
,这样就在我们脑海构成一个基础的网页模型
Riot 标签首先被 编译 成标准 JavaScript,然后在浏览器中运行。
3. 虚拟 DOM
- 保证最少量的DOM 更新和数据流动
- 单向数据流: 更新和删除操作由父组件向子组件传播
- 表达式会进行预编译和缓存以保证性能
- 为更精细的控制提供生命周期事件
- 支持自定义标签的服务端渲染,支持单语言应用
4. 与标准保持一致
- 没有专有的事件系统
- 渲染出的 DOM 节点可以放心地用其它工具(库)进行操作
- 不要求额外的 HTML 根元素或
data-
属性 - 与 jQuery 友好共存
5. 友好的语法
(1).强大的简写语法
class={ enabled: is_enabled, hidden: hasError() }
(2).语义化强,不需要费脑记忆
render, state, constructor 或 shouldComponentUpdate
(3).直接插值
Add #{ items.length + 1 } 或 class="item { selected: flag }"
(4).用 标签来包含逻辑代码不是必需的
(5).紧凑的 ES6
方法定义语法
6. 麻雀小,五脏全
最小化是 Riot 区别于其它库的重要特点,它所提供的 API 方法比其他库要少 10 至 100倍。
react.min.js – 34.89KB
polymer.min.js – 49.38KB
riot.min.js – 10.38KB
Riot 拥有创建现代客户端应用的所有必需的成分:
- “响应式” 视图层用来创建用户界面
- 用来在各独立模块之间进行通信的事件库
- 用来管理
URL
和浏览器回退按钮的路由器(Router
)
学前三两个FAQ
1. 标签名中必须使用横线 (-) 吗?
W3C 规范要求在标签名中使用横线。所以
需要写成
. 如果你关心 W3C 的话就遵循这个规则. 其实两者都能跑
2. 为什么源码中没有分号?
不写分号使代码更整洁。这与我们的整体的最小化哲学是一致的。同样的原因,我们使用单引号,也建议你在使用riot
时不要使用分号和双引号。
3. 为什么使用 == 运算符?
运算符本身没有好坏之分,如果你知道它的工作原理,巧妙的使用能简化你的代码,如node.nodeValue = value == null ? '' : value
这将导致 0
和 false
被显示而 null
和 undefined
显示为空字符串。这正是我们想要的!
4. 使用 onclick?
onclick只是比较‘过时’。将JS和HTML放在同一个模块里比美学重要。Riot的最小化语法使事件处理器看起来很象样儿。
开始学习 Riot
一、自定义标签
1. 直观感受
Riot
自定义标签是构建用户界面的单元。它们构成了应用的”视图”部分。我们先从一个实现TODO应用的例子来感受一下Riot的各个功能:(自定义标签会被 编译 成 JavaScript.)
{ opts.title }
-
2. 标签语法
Riot标签是布局(HTML
)与逻辑(JavaScript
)的组合。以下是基本的规则:
- 先定义HTML,然后将逻辑包含在一个可选的
标签里. 注意: 如果在
document body
里包含标签定义,则不能使用script
标签,它只能用于外部标签文件中 - 如果不写
标签,则最后一个
HTML
标签结束的位置被认为是JavaScript
代码是开始。 - 自定义标签可以是空的,可以只有
HTML
,也可以只有JavaScript
- 标签属性值的引号是可选的:
会被理解成
. - 支持ES6 方法语法:
methodName()
会被理解成this.methodName = function()
,this
变量总是指向当前标签实例。 - 可以使用针对 class 名的简化语法: 当
done
的值是true
时,class={ completed: done }
将被渲染成class="completed"
。 - 如果表达式的值不为真值,则布尔型的属性(
checked
,selected
等等..)将不被渲染:的渲染结果是
.
- 所有的属性名必须是小写. 这是由于浏览器的限制。
- 支持自关闭标签:
等价于
. 那些众所周知的 “不关闭标签” 如
在编译后总是不关闭的。
,
, or - 自定义标签需要被关闭(正常关闭,或自关闭)。
- 标准
HTML tags
(label, table, a
等..) 也可以被自定义,但并不建议这么做。
注意: 自定义标签文件里的标签定义总是从某一行的行首开始,前面不能放空格。内置标签定义(定义在 document body
中) 必须正确缩进,所有的自定义标签拥有相同的最低级的缩进级别, 不建议将tab与空格混合使用
3. 省略 script 标签
可以省略
5. 标签 css
在标签内部可以放置一个 style
标签,Riot.js 会自动将它提取出来并放到 部分,因为放到了head部分,所以其他文件也能调用到该样式
{ opts.title }
6. 局部 CSS
支持局部 CSS 。
{ opts.title }
css的提取和移动只执行一次,无论此自定义标签被初始化多少次。 为了能够方便地覆盖CSS,你可以指定Riot在中的哪个位置插入标签所定义的css:
例如,在某些场景下可以指定将riot组件库的标签css放在normalize.css后面,而放在网站的整体主题CSS之前,这样可以覆盖组件库的默认风格。
二、自定义标签的加载
1. 直观体验
自定义标签实例被创建后,就可以象这样将其加载到页面上:
放置在页面 body
中的自定义标签必须使用正常关闭方式:
,自关闭的写法:
不支持。
2. mount 方法的使用方法
将自定义标签放在 后,我们还需要调用
riot.mount()
才能将其加载进来。一个html文档中可以包含一个自定义标签的多个实例。
// mount 页面中所有的自定义标签
riot.mount('*')
// mount 自定义标签到指定id的html元素
riot.mount('#my-element')
// mount 自定义标签到选择器选中的html元素
riot.mount('todo, forum, comments')
3. 标签生命周期
自定义标签的创建过程是这样的:
- 创建标签实例
- 标签定义中的
JavaScript
被执行 - HTML 中的表达式被首次计算并首次触发
“update”
事件 - 标签被加载 (
mount
) 到页面上,触发“mount”
事件
加载完成后,表达式会在以下时机被更新:
- 当一个事件处理器被调用(如上面ToDo示例中的
toggle
方法)后自动更新。你也可以在事件处理器中设置e.preventUpdate = true
来禁止这种行为。 - 当前标签实例的
this.update()
方法被调用时 - 当前标签的任何一个祖先的
this.update()
被调用时. 更新从父亲到儿子单向传播。 - 当
riot.update()
方法被调用时, 会更新页面上所有的表达式。
每次标签实例被更新,都会触发“update”
事件。
由于表达式的首次计算发生在加载之前,所以不会有类似 计算失败之类的意外问题。
4. 监听生命周期事件
在标签定义内部可以这样监听各种生命周期事件:
this.on('before-mount', function() {
// 标签被加载之前
})
this.on('mount', function() {
// 标签实例被加载到页面上以后
})
this.on('update', function() {
// 允许在更新之前重新计算上下文数据
})
this.on('updated', function() {
// 标签模板更新后
})
this.on('before-unmount', function() {
// 标签实例被删除之前
})
this.on('unmount', function() {
// 标签实例被从页面上删除后
})
// 想监听所有事件?
this.on('all', function(eventName) {
console.info(eventName)
})
5. 访问 DOM 元素
Riot 允许开发人员通过 this
实例直接访问设置了 name
属性的元素,也提供了各种简化的属性方法如 if="{...}"
,但偶尔你还是需要直接完成这些内置手段所不支持的DOM操作。
6. 如何使用 jQuery, Zepto, querySelector, 等等
如果需要在Riot中访问DOM,要注意 DOM 元素的初始化发生在第一个 update()
事件被触发之后,这意味着在这之前试图选择这个元素将都失败。
Do I even Exist?
你可能并不打算在每次update
时都去取一下你想要的元素,而更倾向于在 mount
事件中做这件事。
Do I even Exist?
7. 基于上下文的 DOM 查询
现在我们知道了如何在处理 update
或 mount
事件时获取 DOM 元素,现在我们可以利用这一点,将 根元素 (我们所创建的 riot
标签实例) 作为DOM元素查询的上下文。
Do I even Exist?
Is this real life?
Or just fantasy?
8. 标签选项(参数)
mount
方法的第二个参数用来传递标签选项
在标签内部,通过 opts
变量来访问这些参数,如下:
{ opts.title }
// 在 JavaScript 中访问参数
var title = opts.title
9. Mixin
Mixin 可以将公共代码在不同标签之间方便地共享。
var OptsMixin = {
init: function() {
this.on('updated', function() { console.log('Updated!') })
},
getOpts: function() {
return this.opts
},
setOpts: function(opts, update) {
this.opts = opts
if(!update) {
this.update()
}
return this
}
}
{ opts.title }
this.mixin(OptsMixin) // 用 mixin() 加上mixin名字来将mixin混入标签.
上例中,我们为所有 my-tag
标签实例混入了 OptsMixin
,它提供 getOpts
和 setOpts
方法. init
是个特殊方法,用来在标签载入时对mixin
进行初始化。 (init
方法不能混入此mixin
的标签中访问)
var my_tag_instance = riot.mount('my-tag')[0]
console.log(my_tag_instance.getOpts()) // 输出的所有的标签选项
标签的mixin可以是 object – {'key': 'val'} var mix = new function(...)
– 混入任何其它类型的东西将报错.
现在:my-tag
定义又加入了一个 getId
方法,以及OptMixin
中除init以外的所有其它方法
function IdMixin() {
this.getId = function() {
return this._id
}
}
var id_mixin_instance = new IdMixin()
{ opts.title }
this.mixin(OptsMixin, id_mixin_instance)
由于定义在标签这个级别,mixin
不仅仅扩展了你的标签的功能, 也能够在重复的界面中使用. 每次标签被加载时,即使是子标签, 标签实例也获得了mixin
中的代码功能.
10. 共享 mixin
为了能够在文件之间和项目之间共享mixin,提供了 riot.mixin
API. 你可以像这样全局性地注册mixin :
riot.mixin('mixinName', mixinObject)
用 mixin() 加上mixin名字来将mixin混入标签.
{ opts.title }
this.mixin('mixinName')
表达式
1, 直观理解
在 HTML 中可以混合写入表达式,用花括号括起来。[ style 标签中的表达式将被忽略.]
{ /* 某个表达式 */ }
表达式可以放在html属性里,也可以作为文本节点嵌入:
{ /* 嵌入表达式 */ } // 文本节点
当然,并不是什么表达式都是能嵌入,因为Riot只支持属性(值)表达式和嵌入的文本表达式,以下将会执行失败。
表达式是 100% 纯 JavaScript. 一些例子:
{ title || 'Untitled' }
{ results ? 'ready' : 'loading' }
{ new Date() }
{ message.length > 140 && 'Message is too long' }
{ Math.round(rating) }
建议的设计方法是使表达式保持最简从而使你的HTML尽量干净。如果你的表达式变得太复杂,建议你考虑将它的逻辑转移到 “update”
事件的处理逻辑中. 例如:
{ val }
// 每次更新时计算
this.on('update', function() {
this.val = some / complex * expression ^ here
})
2. 布尔属性
如果表达式的值为非真,则布尔属性 (checked, selected
等..) 将不被渲染:
渲染为
这与 W3C 有很大区别,W3C规范是只要布尔属性存在即为true
,即使他的值为空
或者false
3. class 属性简化写法
Riot 为 CSS class 名称提供了特殊语法. 看一个例子
该表达式最终被计算为 “foo baz zorro”.
,只有表达式中为真值的属性名会被加入到class名称列表中. 这种用法并不限于用在计算class名称的场合。
4. 转义
用以下的写法来对花括号进行转义:
\\{ this is not evaluated \\} 输出为 { this is not evaluated }
5. 渲染原始HTML
Riot 表达式只能渲染不带HTML格式的文本值。如果真的需要,可以写一个自定义标签来做这件事. 例如:
this.root.innerHTML = opts.content
这个标签定义以后,可以被用在其它的标签里. 例如
原始HTML:
this.html = 'Hello, world!'
嵌套标签
我们来定义一个父标签
,其中嵌套一个子标签
:
// 子标签
{ opts.plan.name }
// 取得标签选项
var plan = opts.plan,
show_details = opts.show_details // 取出子标签的标签属性
// 访问父标签实例
var parent = this.parent // 获取父标签的标签实例
注意: 我们使用下划线的风格(而不是驼峰风格)对 show_details 进行命名,由于浏览器的约定,驼峰风格的命名会被自动转换成小写.
如果在页面上加载 account
标签,带 plan
选项,调用riot.mount()
方法。
注意: 嵌套的标签只能定义在自定义父标签里,如果定义在页面上,将不会被初始化。
嵌套 HTML
“HTML transclusion”
是处理页面上标签内部 HTML 的方法. 通过内置的
标签来实现.
Hello
this.text = 'world'
页面上放置自定义标签,并包含嵌套的 HTML
{ text }
结果得到
Hello world
DOM元素与name自动绑定
我感觉这个功能真的是帅炸了。当html被定义好了之后,带有 ref
属性的DOM元素将自动被绑定到上下文中,这样就可以从JavaScript中方便地访问它们:
// 获取 HTML 元素
var form = this.refs.login,
username = this.refs.username.value,
password = this.refs.password.value,
button = this.refs.submit
当然,因为DOM已经被绑定到上下文中,所以我们也可以直接在HTML中以表达式形式引用:
{ username.value }
事件处理器
1. 一般处理
响应DOM事件的函数称为 “事件处理器”.
// 上面的表单提交时调用此方法
submit(e) {
}
以”on”
(onclick, onsubmit, oninput
等…)开头的属性的值是一个函数名,当相应的事件发生时,此函数被调用. 函数也可以通过表达式来动态定义:
在此函数中,this
指向当前标签实例。当处理器被调用之后, this.update()
将被自动调用,将所有可能的变化体现到 UI 上。
2. 阻止默认行为
如果事件的目标元素不是checkbox
或radio
按钮,默认的事件处理器行为是 自动取消事件 . 意思是它总会自动调用 e.preventDefault()
, 因为通常都需要调用它,而且容易被遗忘。如果要让浏览器执行默认的操作,在处理器中返回 true
就可以了.
submit() {
return true
}
3. 事件对象
事件处理器的第一个参数是标准的事件对象。事件对象的以下属性已经被Riot进行了跨浏览器兼容
-
e.currentTarget
指向事件处理器的所属元素. -
e.target
是发起事件的元素,与currentTarget
不一定相同. -
e.which
是键盘事件(keypress, keyup
, 等…)中的键值 . -
e.item
是 循环 中的当前元素.
渲染条件 - show / hide / if
可以基于条件来决定显示或隐藏元素。例如:
This is for premium users only
同样, 渲染条件中的表达式也可以是一个简单属性,或一个完整的 JavaScript 表达式. 有以下选择:
- show – 当值为真时用 style="display: ''" 显示元素
- hide – 当值为真时用 style="display: none" 隐藏元素
- if – 在 document 中添加 (真值) 或删除 (假值) 元素
判断用的操作符是 ==
而非 ===
. 例如: 'a string' == true
.
循环
1. 循环是用 each 属性来实现:
-
{ title }
this.items = [
{ title: 'First item', done: true },
{ title: 'Second item' },
{ title: 'Third item' }
]
定义 each
属性的html元素根据对数组中的所有项进行重复。 当数组使用 push()
, slice()
或 splice
方法进行操作后,新的元素将被自动添加或删除。
2. 上下文
循环中的每一项将创建一个新的上下文(标签实例);如果有嵌套的循环,循环中的子标签都会继承父循环中定义了而自己未定义的属性和方法。Riot通过这种方法来避免重写不应在父标签中重写的东西。
从子上下文中可以通过显式地调用 parent
变量来访问上级上下文.
{ title }
Remove
this.items = [ { title: 'First' }, { title: 'Second' } ]
remove(event) {
}
该例中,除了 each 属性外,其它都属于子上下文, 因此 title
可以被直接访问而 remove
需要从 parent.
中访问,因为remove
方法并不是循环元素的属性.
每一个循环项都是一个标签实例. Riot 不会修改原始数据项,因此不会为其添加新的属性。
3. 循环项的事件处理器
事件处理器中可以通过 event.item
来访问单个集合项。这种办法采用了事件委托机制,极大减少了对DOM的访问。
下面我们来实现上方的 remove
函数:
{ title }
Remove
this.items = [ { title: 'First' }, { title: 'Second' } ]
remove(event) {
// 循环项
var item = event.item
// 在集合中的索引
var index = this.items.indexOf(item)
// 从集合中删除
this.items.splice(index, 1)
}
事件处理器被执行后,当前标签实例会自动调用 this.update()
(你也可以在事件处理器中设置 e.preventUpdate = true
来禁止这种行为)从而导致所有循环项也被更新. 父亲会发现集合中被删除了一项,从而将对应的DOM结点从document
中删除。
4. 循环自定义标签
自定义标签也可以被循环
当前循环项可以用 this
来引用,你可以用它来将循环项作为一个参数传递给循环标签。
5. 非对象数组
数组元素不要求是对象. 也可以是字符串或数字. 这时可以用 { name, i in items }
写法
{ i }: { name }
this.arr = [ true, 110, Math.random(), 'fourth']
name
是元素的名字,i
是索引. 这两个变量的变量名可以自由选择。
6. 对象循环
也可以对普通对象做循环. 例如:
{ name } = { value }
this.obj = {
key1: 'value1',
key2: 1110.8900,
key3: Math.random()
}
不太建议使用对象循环,因为在内部实现中,Riot使用 JSON.stringify
来探测对象内容的改变. 整个 对象都会被检查,只要有一处改变,整个循环将会被重新渲染. 会很慢. 普通的数组要快得多,而且只有变化的部分会在页面上体现。
7. 循环的高级技巧
在 riot v2.3 中,为了使循环渲染更可靠,DOM 结点的移动,插入和删除总是与数据集合同步的: 这种策略会导致渲染过程比之前的版本慢一些。要使用更快的渲染算法,可以在循环结点上加上 no-reorder 属性。
{ item }
使用标准 HTML 元素作为标签 | #riot-tag
页面body
中的标准 HTML 元素也可以作为riot标签来使用,只要加上 riot-tag
属性.
这为用户提供了一种选择,与css框架的兼容性更好. 这些标签将被与其它自定义标签一样进行处理。
riot.mount('my-tag')
会将 my-tag
标签 加载到 ul
元素上
服务端渲染 | #server-side
Riot 支持服务端渲染,使用 Node/io.js 可以方便地引用标签定义并渲染成 html:
var riot = require('riot')
var timer = require('timer.tag')
var html = riot.render(timer, { start: 42 })
console.log(html) // Seconds Elapsed: 42
循环和条件渲染都支持.