编译原理之 html parser

更新日期: 2021-05-23阅读: 1104标签: html

halo 大家好,俺是 132,今天听说有人在公司里分享 asta 的实现原理,内心一万点伤害,fre 颜面何在?

asta 是一个类似于 svelte 的前端框架,它把 template 编译为 dom 指令,最大的好处是 no runtime,适合某些对内存珍贵的场景,比如小程序双线程,RN 的三线程等等

它的本质其实就是个 compiler,今天我们主要说 html parser 的部分,asta 是我写的第二个 html parser 了,第一个小程序的 wxml parser

这两个 parser 套路是一样的,但是针对不同的 case 我采取不同的技巧,希望大家看完能有所收获


编译原理

编译原理是一套死的概念,但是需要我们编译的东西和我们想要的 case 是完全不同的,所以我们设计整个编译器的细节也有所不同

1.tokenizer

我认为编译原理中最重要的环节,也是需要我们最开始要设计的环节

我们拿到一个 html,第一时间应该考虑我们应该怎么拆

asta 中是这么拆的:

<div class="bbb">{aaa}</div>
>
[<div class="bbb">, {aaa}, </div>]

但是在小程序 wxml 中,就不能这么拆了,因为小程序有这样的 case:

<div class="ccc {{bbb}}">ddd {{aaa.length > 0 ? 'eee': fff}}</div>

怎么样,是不是惊呆了,一个简单的 template 中,包含了表达式,纯字符串,变量

这个时候就考验 token 的设计能力了

[<div class=", [ccc ,{{bbb}}], ">, [ddd ,{{aaa.length > 0 ? 'eee': fff}}], </div>]

刺不刺激,就是这么迷乱,但是本质思想是,将 "ccc {{bbb}}" 这种单独拆开,不要放在一起拆

对于表达式,这是个难点,在 vue2 中使用了 with 的奇技淫巧,还是蛮骚的

 with(this.data){
    aaa.length > 0 ? 'eee': fff
 }

所以这个时候另一种 tokenizer 的实现思路就登场了——基于正则

上面讲的 tokenizer,主要是遍历字符串,然后记录索引,实现一个“有限状态机”,正则也可以实现,对于基于线性遍历很难实现的状态机,用正则最合适不过

基于正则的 tokenizer 长这样,感受一下先

let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^``` *(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t|  {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:!\[([^\]]*?)\]\(([^)]+?)\))|(\[)|(\](?:\(([^)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,6})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|(  \n\n*|\n{2,}|__|\*\*|[_*]|~~)/gm,

我们总结的时候再讲这个,但是总体来说,tokenizer 怎么设计怎么拆,直接决定了整个编译器的水准

2. parser

parser 是构建一棵 AST 树的过程,还是那句话,原理是死的,case 是不一样的,我们还是要去“设计”

比如小程序 wxml 的这种 case

<!--template.wxml-->
<template name="aaa">
  <view>
    <text>bbb</text>
  </view>
</template>

<template name="bbb">
  <view>
    <text>bbb</text>
  </view>
</template>

<!--aaa.wxml-->
<import src ="../template/template.wxml"/>
<template is="aaa"/>

<!--bbb.wxml-->
<import src ="../template/template.wxml"/>
<template is="bbb"/>

跨文件的 import/include,想象一下,如果是你,你该怎么办?

遇到 import 就去 readFile?答案肯定是不

遇到这种情况,我也冥思了很久,最终读了 rollup 的代码才有了思路

那就是后置 import

// input
<import src ="../template/template.wxml"/>
<template is="bbb"/>

// output
<template is="bbb"/>

我们忽略所有 import,只保留真正的 templateis=,对应到 rollup 里就是只保留函数调用

// input
import {aaa} from './a.js'
aaa()

// output
aaa()

我们会构建一个图,然后分析到了 aaa() 的调用的时候,再到图里找 aaa 在哪

这种思路对于跨文件的依赖导入特别受用,我把它放到 parser 里来做,在生成 ast 树的时候,顺便生成 block graph


其实类似的思路在 vue3 中叫做 block tree,但不跨文件,没什么难度

值得一提的是,因为 asta 没有这种跨文件的引用关系,所以 asta 中我没有用 block graph,甚至大道从简,我使用了 singlepass 的思路,一次遍历,tokens 和 ast 一起生成了

所以嘛,编译原理是死的,但是 case 是活的,能在不同的 case 下设计出最合理的代码安排,就可以了

3. generator

如果 tokenizer 和 parser 安排的足够好,其实 generator 就是拼字符串了

可是如果 tokenizer 和 parser 设计的不好,那么 generator 就真的变成火葬场了,各种 case by case 应接不暇

所以一个编译器写的怎么样,其实看 generator 的 dirty code 的数量就可以判断出来

根据 case 不同,这块是完全不同的实现,比如 wxml 我最终变异成 jsx,而 asta 则是直接变异成 dom 指令

没什么好讲的,next step


基于正则的 tokenizer

上面讲的是正常的编译思路,但是有很多字符串,一看就不好搞,写 parser 就小题大做了

比如 asta 中对 js 的解析,比如 wean 中 esbuild 插件对 js 的转换

这种如果想不开就会引入 babel 或 acorn 生成 AST 了,但是等你生成 AST 的时候,esbuild 已经从 0.3 s 变成 13s 了……

所以这个时候正则就派上用场了

let count = 0

function add(){
  count++
}

以上是 asta 也就是 svetle 的 js 内容,这个思路是魔术 let 基于分配的响应式,说白了就是匹配

let [count] = [0]

function add(){
  [count]++
}

说的直白点就是这样的,匹配魔术 let 和分配 = 的内容

asta 我还没写完,这块我还是设计,具体实现大家可以期待一下,反正肯定不可能引入 acorn 的,只能走正则

同样的是,esbuild 不提供 AST,也不可能引入会拖慢速度的第三方库,回归正则才是王道


总结

编译原理是死的,case 是活的,人也是活的,之所以编译原理会这么大家这么喜欢,不是这个技术很难

而是这个技术真的很考验水准,同样一个 case 不同的人会做出来不同的代码安排

我一直在寻找某个 case 的最好安排,而不是无脑 case by case

好啦,最近真是绝了,小作文都日更了,绝了绝了

我还在研究 web container,web container 对于 asta 这类编译框架来说,也是很重要的,编译为 wasm 就可以把编译器跑到浏览器中,就不用和 vue 一样同时保持一套 runtime 的 api

米娜,再贱!

原文:https://zhuanlan.zhihu.com/p/374429065


链接: https://www.fly63.com/article/detial/10318

css/html 空格,html空格符的显示、标示方式【html空格代码】

css/html 空格,html空格符的显示、标示方式。在html里面空格的话,你直接敲打出来的空格是不可以的,这篇文章将通过html和css中设置显示空格的实现总结

HTML/CSS代码风格指南(Google)

本文档定义了HTML/CSS的编写格式和风格规则,适用于HTML/CSS文件,包括GSS文件。如果你要编辑代码,先花几分钟看看它的代码风格,如果它这么做,那你也应该这么做。

html鼠标自定义右键菜单:css+js实现自定义html右键菜单

我们在网页中很多都有右键菜单的功能,一般点击右键显示的是浏览器默认的菜单选项,那么我们直接如何通过css+js实现html的右键菜单呢?这篇文章将讲解html鼠标自定义右键菜单的实现原理和实现代码。

基于 HTML5 Canvas 的交互式地铁线路图

地图稍微内容有点多,要全部展示,字显得有点小了,但是没关系,可以按照需求放大缩小,字体和绘制的内容并不会失真,毕竟都是用矢量绘制的~

如何用 Python 解析 HTML

用一些简单的脚本,可以很容易地清理文档和其它大量的 HTML 文件。但是首先你需要解析它们。我决定为自己创建一个项目来解决这个问题。 一种方法是搜索未使用的现有图像文件。

你可能从未听说过的15种HTML元素方法!

我们来讨论HTML和DOM之间的区别。显然,一个普通<table>元素就是HTML。您可以在.html的文件中使用它。它有一组属性影响它的外观和行为方式。这就是HTML,不过它与JavaScript无关。

KeyPress 和KeyDown 、KeUp之间的区别

input 框在输入查询内容之后,按回车键居然有两种不同的表现形式(input 框没有绑定键盘事件),谷歌和火狐功能正常,但IE在按了回车键以后居然自动调用方法。

在html中做嵌套页面_客户端实现html页面的嵌套

如何在客户端利用html、或js将一个html页面嵌套在另一个页面中,这篇文章主要讲解以下几种方式:IFrame引入 、<object>方式、Behavior的download方式 、使用JQuery的load方法

HTML的Doctype

<!DOCTYPE> 声明位于文档中的最前面,处于 <html> 标签之前。告知浏览器的解析器,用什么文档类型 规范来解析这个文档。DOCTYPE不存在或格式不正确会导致文档以混杂(兼容)模式呈现。

VSCode中快捷输入HTML代码

VSCode中有一些快捷编辑HTML的方法,能大大提高工作效率,在这记录一些。文是在VSCode下编写的,其他编辑器如Atom、Sublime Text都支持Emmet

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!