Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jimczj opened this issue Nov 9, 2017 · 1 comment
Open

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

jimczj opened this issue Nov 9, 2017 · 1 comment

Comments

@jimczj
Copy link
Owner

jimczj commented Nov 9, 2017

前言

模板引擎的作用就是将模板渲染成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) ? true : false;
    }
    return condition ? true : false;
}

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地址

@jimczj jimczj changed the title 不用正则表达式,用javascript从零写一个模板引擎(一) 不用正则表达式,用javascript从零写一个模板引擎 Mar 25, 2020
@jimczj jimczj removed the Javascript label Jul 3, 2020
@RadiumAg
Copy link

RadiumAg commented Dec 7, 2020

666

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants