Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Riot-compiler 代码的简单解读

Riot 源码阅读笔记 -- compiler

关于 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 来请求文件内容,跨域就拜拜了,请知悉。

在 Node 中的入口

npm install riot -g 之后便可以通过命令行的方式来启动 compiler 来处理。

源码中入口是:lib/cli.js,主要就是做命令参数解析和调用 lib/compiler.js 的工作,有兴趣请自己查阅。

compiler.js 干了什么

区分 template 和 logic

  /^<([\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 代码

对 template 做了什么

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, '\\$&')

对 js 做了什么

首先是根据参数调用对应的 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 写法等。

@freewind
Copy link

freewind commented Jun 13, 2018

谢谢总结,原来都是正则,感觉好琐碎啊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment