2017-07-30
模版引擎
template
模版解释器的原理和一个实现
近期在研究模版引擎的原理。自己定了一个语法来实现它。
模版语法其实跟编程语言极其类似,写着写着有一种在写之前 Q-lang 的时候的感觉。
后来仔细想了想,我意识到模版语言其实是一种 DSL (领域专用语言),通俗点就是专用在某个领域的语言,比如 SQL 就是一种 DSL,用于处理关系型数据库。
模版引擎,其实是一种语言解释器,它能读取模版并解析出它的结构出来,最后进行求值 (求值的结果就是渲染的结果)
一般而言,解释执行的步骤通常都会做下面这些事:
  1. 解析源文件得到 tokens
  2. 处理 tokens 得到抽象语法树 AST
  3. 对语法树求值
上面三件事我在玩 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 的过程中:
每一次的开始:会创建一个作用域插入到作用域链顶部,然后把 itemindex 放进这个新的作用域,然后对循环体求值。
每一次的结束:销毁作用域

# 条件分支

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

这一步是对语法树进行求值的过程。

# 详细实现

以上四步的详细实现请看下篇




回到顶部