关于 Riot 的 compiler,具体请见:Riot compiler
之前在 Riot 源码阅读笔记 中提到另挖坑来聊聊 Riot 提供的 template + logic
compiler 是怎么干活的,于是有了这一篇文章... (其实大部分都是正则,正则)
Riot 提供了两种方式,分别是 pre-compile
和直接在页面使用 script[type="riot/tag"]
,都是用的同一个 compiler 来处理代码的解析,在真实环境时推荐使用 pre-compile
的方式。
在页面使用 script[type="riot/tag", src="path"] 和 React 在页面使用 script[type="text/jsx", src="path"] 一样,都是用了 ajax 来请求文件内容,跨域就拜拜了,请知悉。
npm install riot -g
之后便可以通过命令行的方式来启动 compiler 来处理。
源码中入口是:lib/cli.js,主要就是做命令参数解析和调用 lib/compiler.js 的工作,有兴趣请自己查阅。
/^<([\w\-]+)>([^\x00]*[\w\-\/]>$)?([^\x00]*?)^<\/\1>/gim
// [1 ][2 ][3 ][4 ]
// 1. 匹配 <custom-tag>, 即自定义标签
// 2. 匹配 html 内容,以一个标签结尾,如 `</h3>, <br />, </custom-tag>`,这里的 `$` 限制了 html 和 js 要隔行写
// 3. 匹配 js 内容
// 4. 以 `^` 限制了结束标签需要另起一行写,然后就是结对出现。
上述的这个正则已经将模板内容解析成三部分,标签名称,template 内容,logic 内容,相对简单吧,比起 JSX 需要的处理要少得多,也容易理解得多。
解析成三部分之后,分别对每一个部分做对应的处理后拼接成最后的 js 代码,即是类似这样的代码:
riot.tag('custom-tag', '...', function () {
// [1 ] [2 ]
this.prop = ...
// [3 ]
})
// 1. 标签名称
// 2. 处理后的 template 字符串
// 3. 处理后的可执行的 javascript 代码
Riot 支持使用 jade 的,通过参数判断,如果是 jade,则调用 jade 来编译
把所有多个空格连续的都换成一个空格,清理 comment
var HTML_COMMENT = /<!--.*?-->/g
html = html.replace(/\s+/g, ' ')
html = html.trim().replace(HTML_COMMENT, '')
// 如果带了 compact 参数,则压缩标签间空格
if (opts.compact) html = html.replace(/> </g, '><')
这里做的事情比较多,请允许我给一个列表:
-
在 Riot 中,标签属性的表达式可以不用加引号,所以 compiler 要加引号...
// foo={ bar } --> foo="{ bar }" html = html.replace(/=(\{[^\}]+\})([\s\/\>])/g, '="$1"$2')
-
在 IE8 下,boolean 类型的元素属性插入后会丢失其值,所以需要 hack 来处理一下,boolean 类型的转换一下以保留其值
// checked="{ foo }" --> __checked="{ foo }" html = html.replace(/([\w\-]+)=["'](\{[^\}]+\})["']/g, function(full, name, expr) { // [1 ][2 ] // 1. 匹配属性名称,后边跟一个 `=` // 2. 匹配属性值,引号开始和结束,中间是被 `{}` 包裹起来的表达式 // 这里的 BOOL_ATTR 是一个包括了所有 boolean 属性字符串的数组 if (BOOL_ATTR.indexOf(name.toLowerCase()) >= 0) name = '__' + name return name + '="' + expr + '"' })
关于这个,不用 compiler 直接写模板的话,要这样:
__checked
,有一个 issue 就是讨论这个问题的。 -
由于兼容了多个 transpiler ,所以表达式里边的其他语法也要调用其他 transpiler 来 compile,当然,如果没用 transpiler 就不用处理了,通过参数来控制
// { expr } 调用 `compileJS` 方法处理 expr 后放回来 html = html.replace(/\{\s*([^\}]+)\s*\}/g, function(_, expr) { return '{' + compileJS(expr, opts, type).trim() + '}' })
-
处理标签闭合,这个相对好理解,其中主要就是不处理 H5 中自闭合的标签
var CLOSED_TAG = /<([\w\-]+)([^>]*)\/\s*>/g // <foo/> -> <foo></foo> html = html.replace(CLOSED_TAG, function(_, name, attr) { var tag = '<' + name + (attr ? ' ' + attr.trim() : '') + '>' // Do not self-close HTML5 void tags // 这里的 VOID_TAGS 是一个包括了所有的 h5 自闭合标签的数组 if (VOID_TAGS.indexOf(name.toLowerCase()) == -1) tag += '</' + name + '>' return tag })
做一些必要的转义处理,如引号,转义了的 {}
// escape single quotes
html = html.replace(/'/g, "\\'")
// \{ jotain \} --> \\{ jotain \\}
html = html.replace(/\\[{}]/g, '\\$&')
首先是根据参数调用对应的 transplier 来干活,编译代码,如果是 js 代码就不用编译了。
logic 部分其实 Riot 也提供了一种简写的 js 语法的,就像是 ES6 提供的 class method 写法一样,如
edit(e) {
this.text = e.target.value
}
处理成:
this.edit = function (e) {
this.text = e.target.value
}.bind(this)
好吧,其他暂时不重要,我们看一下 Riot 是怎么处理这一块的转化的。
先清理掉 comment
按行解析,遍历每一行
var l = line.trim()
// 方法开始,带 `(` 而且没有 `function` 关键词
if (l[0] != '}' && l.indexOf('(') > 0 && l.slice(-1) == '{' && l.indexOf('function') == -1) {
var m = /(\s+)([\w]+)\s*\(([\w,\s]*)\)\s*\{/.exec(line) // 正则匹配出函数名称以及参数列表
if (m && !/^(if|while|switch|for)$/.test(m[2])) { // 排除其他带 `(` 的情况
lines[i] = m[1] + 'this.' + m[2] + ' = function(' + m[3] + ') {'
es6_ident = m[1]
}
}
// method end
if (line.slice(0, es6_ident.length + 1) == es6_ident + '}') {
// 在函数方法后添加一个 `.bind(this)` 来确保调用时 this 的值
lines[i] = es6_ident + es6_ident + 'this.update()\n' + es6_ident + '}.bind(this);'
es6_ident = ''
}
Riot 提供了一个 riot+compiler.js 的版本,其实也是将这个 compiler 打包进去,这个 compiler 本身做了一些支持浏览器的事情。
- Ajax 方法来拉取模板内容
- 挂载 compile 方法到 riot 上
- 重写 mount 和 mountTo 方法来支持 compile
简单的区分 template 和 logic 的方式感觉很巧妙,这么写对于一个 component 代码看起来也蛮好看的。
compiler 代码精简的同时提供了不少优秀的功能,包括代码的基础压缩,支持多种 transpiler,自带的 ES6 class method 写法等。
谢谢总结,原来都是正则,感觉好琐碎啊