近期在研究模版引擎的原理。自己定了一个语法来实现它。
模版语法其实跟编程语言极其类似,写着写着有一种在写之前
Q-lang
的时候的感觉。后来仔细想了想,我意识到模版语言其实是一种 DSL (领域专用语言),通俗点就是专用在某个领域的语言,比如 SQL 就是一种 DSL,用于处理关系型数据库。
模版引擎,其实是一种语言解释器,它能读取模版并解析出它的结构出来,最后进行求值 (求值的结果就是渲染的结果)
一般而言,解释执行的步骤通常都会做下面这些事:
- 解析源文件得到 tokens
- 处理 tokens 得到抽象语法树 AST
- 对语法树求值
上面三件事我在玩 Q-lang 的时候就接触过了,语法树最为重要,因为只要遍历语法树就可以极为容易的求值了。
按照上述思路,以下是我写的模版引擎的整一个解释执行的过程。
此外,为了写文的方便,暂定以下模版作为我们模版字符串的测试源码: (一个乘法小九九)
00<!DOCTYPE html> 01<html lang="en"> 02<head> 03 <meta charset="UTF-8"> 04 <title>{{ title }}</title> 05 <link rel="stylesheet" href="./main.css"> 06</head> 07<body> 08 <header> 09 {{ title }} 10 </header> 11 12 <ul><!-- outter --> 13 {{ get (q1, i1) >>>> list }} 14 <ul><!-- inner --> 15 {{ get (q2, i2) >>>> list }} 16 <li bigger="{{ isBigger q1 q2 }}">{{ q1 }} * {{ q2 }} = {{ mul q1 q2 }}</li> 17 {{ teg }} 18 </ul> 19 {{ teg }} 20 </ul> 21 22 {{ if show }} 23 确实如此 24 {{ else }} 25 然而并不是这样 26 {{ fi }} 27</body> 28</html>
# 设计语法 ↵
先设计语法,才能知道怎么样去得到 token 最后得到语法树
# 作用域
因为没有函数,也无所谓静态作用域、动态作用域之区别。
循环语句块会独立创建一个新的作用域并插入作用域链顶部,好比如下 js 代码:
00var data = { 01 list: [1, 2, 3] 02} 03 04list.forEach((elem, idx) => { 05 // 这儿创建了一个新作用域 06})
查找变量的时候从作用域链顶部查找,找不到则报错。
# 普通字符串和语句的区分
在一串模版里面,用
{{ }}
包着的是语句块,其他的均视作普通字符串。# 表达式求值
如果传入的
data
是 { a: 'this is a' }
那么
{{ a }}
的求值结果是 this is a
,特别的,如果 a 是函数,则会采用前缀调用的方式来进行求值:00var data = { 01 biggerThan: (a, b) => a > b ? 'yes, a > b' : 'no, a <= b', 02 a: 5, 03 b: 2 04} 05 06render('{{ biggerThan a b }}', data); 07// => 08// 'yes, a > b'
此外传入对象也是可以的 :
{{ person.name }}
# 循环
使用
get
关键字,语法格式如下:00{{ get (item, idx) >>>> list }} 01 <!-- 循环体 --> 02{{ teg }}
即在遍历
list
的过程中:每一次的开始:会创建一个作用域插入到作用域链顶部,然后把
item
和 index
放进这个新的作用域,然后对循环体求值。每一次的结束:销毁作用域
# 条件分支
00{{ if yes }} 01 确实如此 02{{ else }} 03 然而并不是这样 04{{ fi }}
如果对 yes 求值的结果是 true 那么,求值结果是
确实如此
,不然就是 然而并不是这样
。# 解释器的主要工作路径 ↵
# 第一步: template -> statements
将模版源代码 template 转化成中间变量 statements
我之所以引入 statements ,原因是模版的特殊性,它跟普通字符串耦合在一块了,为了区别语句块和普通的字符串,我加入了这步骤,专门用于检出哪些是语句,哪些不是,而且记录它们在模版字符串中的位置 (
offset
)# 第二步: (statements, template) -> codeTokens
利用 statements 和 template 转化成 codeTokens
# 第三步: codeTokens -> syntaxs
将 codeTokens 转成语法树。
codeTokens 很重要,算是前往语法树的最后一步了。
# 第四步: syntaxs -> string
这一步是对语法树进行求值的过程。
# 详细实现 ↵
以上四步的详细实现请看下篇