from http://jlongster.com/Compiling-JSX-with-Sweet.js-using-Readtables
JSX http://facebook.github.io/react/docs/jsx-in-depth.html 是一个 Facebook 项目,给 js 嵌入了 xml-like 语言,它是 React http://facebook.github.io/react/ 中标志性的使用特色。很多人喜欢它并且发现它非常好用。不幸的是他需要独立的编译器,并且不能和其他语言混合或者扩展。我用 sweet.js 宏 http://sweetjs.org/ 实现了一个 JSX “编译器” https://github.com/jlongster/jsx-reader,因而你可以把 jsx 和其他任何宏语言扩展一起放手边随时供使用。
我预见,预见有人能给 js 语言添加一些难懂的特性,比如模式匹配 https://github.com/natefaubion/sparkler ,然后我只需要安装模块然后使用它。在 js 影响深远的今天,我认为这种语言扩展能力是非常重要滴。
这很重要,不仅仅是因为我或者你能拥有原生 goroutines https://gobyexample.com/goroutines 或者 原生持久数据解构 http://swannodette.github.io/mori/ 语法 。它更令人难以置信的是,我们用的野生的特性,可能成为 ES 标准的一部分,甚至成为标准的 JS 的一部分。未来的 js 会由于我们的反馈变得更好。我们需要模块化的方式来扩展 js,一但这个可行,会无缝的与无数的扩展共享成果。
我不准备解释为啥 sweet.js 宏是这个目标的答案。如果你想听更多,看我的 JSConf 2014 演讲 https://www.youtube.com/watch?v=wTkcGprt5rU 。如果你准备写负面的评论,请先阅读这篇 http://jlongster.com/Compiling-JSX-with-Sweet.js-using-Readtables#concerned
我为啥在 sweet.js 中实现一个 JSX 呢?如果你使用 JSX,现在你可以在任何其他的可用宏 https://www.npmjs.org/search?q=sweet-macros 边上使用 jsx-reader https://github.com/jlongster/jsx-reader。想要原生语法给持久化数据结构?继续读下去。。。
遇到的问题
jsx 这么工作:xml 元素转换为简单的 js 对象。
var div = <div>
<h1>{ header }</h1>
</div>;
这转化为:
var div = React.Dom.div(null, React.DOM.h1(null, header));
http://sweetjs.org/ 直到这周 jsx 都无法把它实现,是什么原因让这做到了? 可读表 Readtables http://sweetjs.org/doc/main/sweet.html#reader-extensions
我简单解释下 sweet.js 的一些技术环境。Sweet.js 主要工作在口令层面,不是 AST,AST是唯一能扩展语言的组成部分的方式 见 *。其中的算法大量的来自 10 多年的 Lisp Scheme 社区尤其是 Racket http://docs.racket-lang.org/ 的工作基础之上
我们的想法是,你在一个轻量的口令树之上提供语言的宏定义,然后展开这些口令。定制的模式匹配 http://sweetjs.org/doc/main/sweet.html#rule-macros 和 全自动卫生 http://sweetjs.org/doc/main/sweet.html#hygiene 让它可以做非常复杂的扩展,并且你可以从 sourcemaps 上获得更多。
生成的管道看起来像这样:
- read 读一段字符串代码,产生一个口令树。它会消除歧义的正则表达式语法,分类,以及其他的有趣的东西。你会得到一个很漂亮的口令树,上面是原子的语法片段。它是一颗用 {}()[] 作为叶子来作为分隔符口令的树。
- expand 遍历口令树,展开任何找到的宏。它用名字查找宏,用剩下的语法来调用宏。这个阶段还会做一个轻快的解析,比如指出一些不合法的表达式
- parse 处理展开后的树,生成一些真正的 js AST。目前 sweet.js 使用了一个打了一些补丁的 esparima 版本来做这个工作
- generate 从 AST 生成最后的 js 代码。 sweet.js 使用 escodegen 来做这个工作
expand 阶段本质上是给语言解析器添加了展开性。这对很多特性都很好。比如 类型 模块 等那些需要整个程序的信息,还有那些在解析 parse 阶段或者生成阶段最好有 AST 支持的特性。现在我们还没有能展开到那个阶段。
很少有特性需要扩展到 read 阶段, JSX 就是其中之一。 Lisp 早就有一个叫做 readtables 的东西了,我最近意识到我们的 js 需要类似的东西,以支持 JSX 的实现。因此我实现了它 https://github.com/mozilla/sweet.js/pull/340 !
有几个原因 jsx 需要作为一个 reader 生效而不是作为宏:
- 最重要的,闭合标签 是完全无效的 js 语法。默认的 reader 认为这开始了一个正则表达式,但是他不能找到它的结束符号。因此他直接在 read 阶段抛出错误
- jsx 有非常特殊的关于空白的处理规则。空白元素需要被保存,例如 兄弟表达式之间插入了一个空格。宏不知道任何空白的故事。
Reader 扩展允许你安装一个自定义的 reader 当在源码中遇到一个特殊的单词的时候调用。你能读取你需要的足够多的源码,然后返回一个口令组。reader 扩展只能被用标点的 punctuators 触发。(比如 < % 等符号),因此你不能做比如改变引号起作用的方式这类可怕的事情。
一个可读表是一个给 reader 扩展用的字符表。在这里 http://sweetjs.org/doc/main/sweet.html#reader-extensions 的这个文档中阅读更多来深入了解。
- 关于这点我有许多思考和论点,我会在未来的文章中展开来说。在 AST 层面工作是一个很棒的特性,这需要整个程序,比如类型和代码分析的优化。看这篇 http://blog.fogus.me/2012/04/25/the-clojurescript-compilation-pipeline/ 来了解关于这个想法的一个非常好的解释。
npm 上可用的: jsx-reader
现在我们有了可读表,我们可以实现 jsx 啦!我已经用 jsx-reader https://github.com/jlongster/jsx-reader 做到了。他是一个文字的 jsx 编译器端口,机智的做到了所有的空白规则以及其他的边缘情况。(希望如此)
加载一个 reader 扩展,传入模块名给 sweet.js 用 -l 编译 sjs 。这是所有的步骤:
$ npm install sweet.js
$ npm install jsx-reader
$ sjs -l jsx-reader file.js
当然你也可以加载任何其他的宏,一起作用在你的文件上。这是一个组合的很漂亮的语言扩展(试试 es6-macroshttps://github.com/jlongster/es6-macros)
我已经创建了一个 webpack loader https://github.com/jlongster/sweetjs-loader 和一个 gulp loader https://github.com/jlongster/gulp-sweetjs 而且是最新的支持 readtable 加载的。
这是测试版的软件我在一些小的原生的 jsx 编译器的测试用例上测试通过了,此外它也在一些大文件上工作良好。However,依然有一些小 bugs 和边缘情况需要被人们发现他之后修补。
你不仅仅能获得可靠的 sourcemaps (传入 -c 给 sjs),而且你也会比原生 jsx complier 有更好的错误提示。例如:如果你忘了关闭一个标签:
var div = <div>
<h1>Title</h1>
<p>
</div>
你会得到一个漂亮的,感觉的错误信息,明确的指出你的代码中的错误
SyntaxError: [JSX] Expected corresponding closing tag for p
4: </div>
^
at Object.readtables.readerAPI.throwSyntaxError (/Users/james/tmp/poop/node_modules/sweet.js/lib/parser.js:5075:23)
有一个退步就是,这比原生的 jsx compiler 慢。一个 2000 行代码的大文件会花费 ~.7s 来编译(排序了预热时间,因为你会在大多数项目中使用监听器 watcher),原生的只要 ~0.4s。实际上,这不是特别明显,因为大多数文件是很多的更小的东西,大部分在几百毫秒就编译完了。当然,sweet.js 会更努力滴优化这个时间滴。
例子: 持久化数据结构
React 和持久化数据结构一起工作会表现得更好。问题是 js 原生没有它们,但是幸运的是有 mori http://swannodette.github.io/mori/ 这些库可用。只是你再也不能用对象字面值了;你不得不 mori.vector(1,2,3) 来代替 [1,2,3];
如果我们给 mori 实现一个字面的语法会怎样?可能你就用 #[1,2,3] 来创建持久的矢量,而 #{x: 1, y: 2} 来创建持久的表 map 。那也太棒了吧!(不幸的是我现在还没做到,但我非常渴望做到这点。。。)
现在每个用 jsx 的人都可以在 React 中用我的字面语法来持久化数据结构。这真的是一个给力的工具包。
jsx 的 read 算法
给 js 添加新的语法,特别拥有大块展示区域的比如 jsx。必须仔细的实现。它必须 100% 向后兼容,并且和 ES.next 特性一样在脑中完成。
jsx-reader 和 原生 jsx 编译器都查找 < 口令并触发一个 jsx 表达式的解析。虽然这个有一个关键的不同。你可能注意到 jsx-reader 作为一个 reader,被源代码调用的时候是没有环境的。原生的 jsx 编译器用被猴子们打了补丁 monkeypatches 的 esprima 来调用 < ,当解析一个表达式的时候,因此它更容易保证正确的解析。 < 在 js 表达式中从不会有效,因此它能这么用它。
jsx-reader 也是用 < 调用,当它在源码的任何地方出现的时候,甚至是比较大小的操作符的时候。这让人提心吊胆的;我们需要更仔细。但是我想到了一个可行的算法。你不会一直需要一个 ast。
jsx-reader 开始解析 < 然后任何它后面的东西作为 jsx 表达式,如果它遇到一些确定不期望的点,它停下来。大多数时候,它能很早的发觉 < 之后是或者不是 jsx 表达式。算法来了:
- Read <
- Read 一个标识符 失败了 bail
- If 接下来的不是 > :
- 跳过空白
- 读取一个标识符。 失败了 bail
- if 下一个是 > 跳转到 4.1
- 读取 =
- if 下一个是 { 读取 js 表达式 类似 4.3.1
- if 下一个是 < 跳转到 1
- 否则,读取一个字符串
- if 下一个是 > 跳转到 4.1
- 否则 跳转到 3.1
- 否则:
- 读取 >
- 读取任何原始文本直到 { 或者 <
- if 下一个是 {
- 读取 {
- 读取所有的 js 口令直到 }
- 读取 }
- 跳转到 4.2
- if 下一个是 <:
- if 下一个是 < 和 / 跳转到 5
- 否则 跳转到 1
- if 是文件的底部, bail
- 读取 <
- 企图读取一个正则。如果成功了 bail
- 读取 /
- 读取一个标识符 确保他匹配了打开的标识符
- 读取>
我很快的打出了这些,可能有更好的方式啦,但是你应该了解了这思路。主要是一般情况下很容易消除歧义,但是所有的边缘情况都需要生效。边缘情况不是特别高性能,因为我们的 reader 可能要做很多工作然后丢弃它,但是 99.9% 的情况下不会发生这种事情。
在我们的算法中,如果我们说 read 而不说相应的的 “如果失败了 bail”,它会抛出一个错误。我们依然可以给出好的错误提示,当遇到二义性的 js 边缘情况的时候。
这儿有几个我们的 reader bails的地儿:
- if (x < y) {} 它 bails 因为它查找一个 y 后面的属性 以及 ) 不是一个合法的标识符字符
- if (x < y > z) {} 它 bails 是因为它 reads 所有的到文件末尾的方式但没有找到一个闭合的标签。这只发生在顶级的元素上,而且是个性能的最差的情况。但是 x < y > z 不会做你想要的,而且没人会这么用
- if (x < div > y < /foo> /) {} 这是最难懂的情况,他是完全有效的 js 。它 bails 因为它最后读取了一个正则
我们获取这个实际的优点:表达式像 x < y foo 在 js 中不会存在。这儿,它既不找 = 来解析属性,也不找 > 来关闭元素,而是直接报错,如果它没找到的话。
你担心吗?
宏调用,弄混了一些人,还有很多迟疑的认为它们是好用的东西。你可能也这么想,而且争论一件事情:像 readtables 这样的东西标志着我们进入了很深的地方。
我请你们仔细想想 sweet.js。给它 5 分钟。也许几个小时。玩玩它:设置一个 gulp 监控 wathcer,从 npm 安装一些宏并且用它。别直接反对它你实际上除非你理解了我们尝试用它来解决的一些问题。许多人们给出的争论都没有很容易体会到那种场景(但有一些有!)
不管那些,甚至你认为这是错误的方法。他当然是有效的。软件工业对我老说最困惑的一件事情就是:我们对其他人到底能有多恶毒,所以请做些建设性的事情。
今天就试试 jsx-reader https://github.com/jlongster/jsx-reader 如果发现 bugs https://github.com/jlongster/jsx-reader/issues
以上;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。