不用正则表达式,用javascript从零写一个模板引擎(一)

1,992 阅读5分钟

前言

模板引擎的作用就是将模板渲染成html,html = render(template,data),常见的js模板引擎有Pug,Nunjucks,Mustache等。网上一些制作模板引擎的文章大部分是用正则表达式做一些hack工作,看完能收获的东西很少。本文将使用编译原理那套理论来打造自己的模板引擎。之前玩过一年Django,还是偏爱那套模板引擎,这次就打算自己用js写一个,就叫jstemp

预览功能

写一个库,不可能一次性把所有功能全部实现,所以我们第一版就挑一些比较核心的功能

var jstemp = require('jstemp');
// 渲染变量
jstemp.render('{{value}}', {value: 'hello world'});// hello world


// 渲染if/elseif/else表达式 
jstemp.render('{% if value1 %}hello{% elseif value %}world{% else %}byebye{% endif %}', {value: 'hello world'});// world

// 渲染列表
jstemp.render('{%for item : list %}{{item}}{%endfor%}', {list:[1, 2, 3]});// 123

词法分析

词法分析就是将字符串分割成一个一个有意义的token,每个token都有它要表达的意义,供语法分析器去建AST。
jstemp的token类型如下

{
    EOF: 0, // 文件结束
    Character: 1, // 字符串
    Variable: 2, // 变量开始{{
    VariableName: 3, // 变量名
    IfStatement: 4,// if 语句
    IfCondition: 5,// if 条件
    ElseIfStatement: 6,// else if 语句
    ElseStatement: 7,// else 语句
    EndTag: 8,// }},%}这种闭合标签
    EndIfStatement: 9,// endif标签
    ForStatement: 10,// for 语句
    ForItemName: 11,// for item 的变量名
    ForListName: 12,// for list 的变量名
    EndForStatement: 13// endfor 标签
};

一般来说,词法分析有几种方法(欢迎补充)

  • 使用正则表达式
  • 使用开源库解析,如ohm,yacc,lex
  • 自己写有穷状态自动机进行解析

作者本着自虐的心理,采取了第三种方法。

举例说明有穷状态自动机,解析<p>{{value}}</p>的过程

输入图片说明
输入图片说明

  1. Init 状态
  2. 遇到<,转Char状态
  3. 直到遇到{转化为LeftBrace,返回一个token
  4. 再遇{转Variable状态,返回一个token
  5. 解析value,直到}},再返回一个token
  6. }}后再转状态,再返回token,转init状态

结果是{type:Character,value:'<p>'},{type:Variable},{type:VariableName, valueName: 'value'},{type:EndTag},{type:Character,value:'</p>'}这五个token。(当然如果你喜欢,可以把{{value}}当作一个token,但是我这里分成了五个)。最后因为考虑到空格和if/elseif/else,for等情况,状态机又复杂了许多。

代码的话就是一个循环加一堆switch 转化状态(特别很累,也很容易出错),有一些情况我也没考虑全。截一部分代码下来看

nextToken() {
        Tokenizer.currentToken = '';
        while (this.baseoffset < this.template.length) {
            switch (this.state) {
                case Tokenizer.InitState:
                    if (this.template[this.baseoffset] === '{') {
                        this.state = Tokenizer.LeftBraceState;
                        this.baseoffset++;
                    }
                    else if (this.template[this.baseoffset] === '\\') {
                        this.state = Tokenizer.EscapeState;
                        this.baseoffset++;
                    }
                    else {
                        this.state = Tokenizer.CharState;
                        Tokenizer.currentToken += this.template[this.baseoffset++];
                    }
                    break;
                case Tokenizer.CharState:
                    if (this.template[this.baseoffset] === '{') {
                        this.state = Tokenizer.LeftBraceState;
                        this.baseoffset++;
                        return TokenType.Character;
                    }
                    else if (this.template[this.baseoffset] === '\\') {
                        this.state = Tokenizer.EscapeState;
                        this.baseoffset++;
                    }
                    else {
                        Tokenizer.currentToken += this.template[this.baseoffset++];
                    }
                    break;
                case Tokenizer.LeftBraceState:
                    if (this.template[this.baseoffset] === '{') {
                        this.baseoffset++;
                        this.state = Tokenizer.BeforeVariableState;
                        return TokenType.Variable;
                    }
                    else if (this.template[this.baseoffset] === '%') {
                        this.baseoffset++;
                        this.state = Tokenizer.BeforeStatementState;
                    }
                    else {
                        this.state = Tokenizer.CharState;
                        Tokenizer.currentToken += '{' + this.template[this.baseoffset++];
                    }
                    break;
                // ...此处省去无数case
                default:
                    console.log(this.state, this.template[this.baseoffset]);
                    throw Error('错误的语法');
            }
        }
        if (this.state === Tokenizer.InitState) {
            return TokenType.EOF;
        }
        else if (this.state === Tokenizer.CharState) {
            this.state = Tokenizer.InitState;
            return TokenType.Character;
        }
        else {
           throw Error('错误的语法');
        }
    }

具体代码看这里

语法分析

当我们将字符串序列化成一个个token后,就需要建AST树。树的根节点rootNode为一个childNodes数组用来连接子节点

let rootNode = {childNodes:[]}

字符串节点

{
    type:'character',
    value:'123'
}

变量节点

{
    type:'variable',
    valueName: 'name'
}

if 表达式的节点和for表达式节点可以嵌套其他语句,所以要多一个childNodes数组来装语句内的表达式,childNodes 可以装任意的node,然后我们解析的时候递归向下解析。elseifNodes 装elseif/else 节点,解析的时候,当if的conditon为false的时候,按顺序取elseifNodes数组里的节点,谁的condition为true,就执行谁的childNodes,然后返回结果。

// if node
{
    type:'if',
    condition: '',
    elseifNodes: [],
    childNodes:[],
}
// elseif node
{
    type: 'elseif',// 其实这个属性没用
    condition: '',
    childNodes:[]
}
// else node
{
    type: 'elseif',// 其实这个属性没用
    condition: true,
    childNodes:[]
}

for节点

{
    type:'for',
    itemName: '',
    listName: '',
    childNodes: []
}

举例:

let template = `
<p>how to</p>
{%for num : list %}
    let say{{num.num}}
{%endfor%}
{%if obj%}
    {{obj.test}}
{%else%}
    hello world
{%endif%}
`;

// AST树为
let rootNode = {
    childNode:[
        {
            type:'char',
            value: '<p>how to</p>'
        },
        {
            type:'for',
            itemName: 'num',
            listName: 'list',
            childNodes:[
                {
                    type:'char',
                    value:'let say',
                },
                {
                    type: 'variable',
                    valueName: 'num.num'
                }
            ]
        },
        {
            type:'if',
            condition: 'obj',
            childNodes: [
                {
                    type: 'variable',
                    valueName: 'obj.test'
                }
            ],
            elseifNodes: [
                {
                    type: 'elseif',
                    condition:true,
                    childNodes:[
                        {
                            type: 'char',
                            value: 'hello world'
                        }
                    ]
                }
            ]
        }
    ]
}

具体建树逻辑可以看代码

解析AST树

解析变量节点
从rootNode节点开始解析

let html = '';
for (let node of rootNode.childNodes) {
    html += calStatement(env, node);
}

calStatement为所有语句的解析入口

function calStatement(env, node) {
    let html = '';
    switch (node.type) {
        case NodeType.Character:
            html += node.value;
            break;
        case NodeType.Variable:
            html += calVariable(env, node.valueName);
            break;
        case NodeType.IfStatement:
            html += calIfStatement(env, node);
            break;
        case NodeType.ForStatement:
            html += calForStatement(env, node);
            break;
        default:
            throw Error('未知node type');
    }
    return html;
}

解析变量

// env为数据变量如{value:'hello world'},valueName为变量名
function calVariable(env, valueName) {
    if (!valueName) {
        return '';
    }
    let result = env;
    for (let name of valueName.split('.')) {
        result  = result[name];
    }
    return result;
}

解析if 语句及condition 条件

// 目前只支持变量值判断,不支持||,&&,<=之类的表达式
function calConditionStatement(env, condition) {
    if (typeof condition === 'string') {
        return !!calVariable(env, condition);
    }
    return !!condition;
}

function calIfStatement(env, node) {
    let status = calConditionStatement(env, node.condition);
    let result = '';
    if (status) {
        for (let childNode of node.childNodes) {
            // 递归向下解析子节点
            result += calStatement(env, childNode);
        }
        return result;
    }

    for (let elseifNode of node.elseifNodes) {
        let elseIfStatus = calConditionStatement(env, elseifNode.condition);
        if (elseIfStatus) {
            for (let childNode of elseifNode.childNodes) {
                // 递归向下解析子节点
                result += calStatement(env, childNode);
            }
            return result;
        }
    }
    return result;
}

解析for节点

function calForStatement(env, node) {
    let result = '';
    let obj = {};
    let name = node.itemName.split('.')[0];
    for (let item of env[node.listName]) {
        obj[name] = item;
        let statementEnv = Object.assign(env, obj);
        for (let childNode of node.childNodes) {
            // 递归向下解析子节点
            result += calStatement(statementEnv, childNode);
        }
    }
    return result;
}

结束语

目前的实现的jstemp功能还比较单薄,存在以下不足:

  1. 不支持模板继承
  2. 不支持过滤器
  3. condition表达式支持有限
  4. 错误提示不够完善
  5. 单元测试,持续集成没有完善

...
未来将一步步完善,另外无耻求个star
github地址