资讯专栏INFORMATION COLUMN

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

gaara / 3059人阅读

摘要:前言模板引擎的作用就是将模板渲染成,,常见的模板引擎有等。网上一些制作模板引擎的文章大部分是用正则表达式做一些工作,看完能收获的东西很少。本文将使用编译原理那套理论来打造自己的模板引擎。最后因为考虑到空格和等情况,状态机又复杂了许多。

前言

模板引擎的作用就是将模板渲染成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

自己写有穷状态自动机进行解析

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

举例说明有穷状态自动机,解析

{{value}}

的过程

Init 状态

遇到<,转Char状态

直到遇到{转化为LeftBrace,返回一个token

再遇{转Variable状态,返回一个token

解析value,直到}},再返回一个token

}}后再转状态,再返回token,转init状态

结果是{type:Character,value:"

"},{type:Variable},{type:VariableName, valueName: "value"},{type:EndTag},{type:Character,value:"

"}这五个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 = `

how to

{%for num : list %} let say{{num.num}} {%endfor%} {%if obj%} {{obj.test}} {%else%} hello world {%endif%} `; // AST树为 let rootNode = { childNode:[ { type:"char", value: "

how to

" }, { 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功能还比较单薄,存在以下不足:

不支持模板继承

不支持过滤器

condition表达式支持有限

错误提示不够完善

单元测试,持续集成没有完善

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89674.html

相关文章

  • 手把手教你零写简单的 VUE--模板

    摘要:转换成为模板函数联系上一篇文章,其实模板函数的构造都大同小异,基本是都是通过拼接函数字符串,然后通过对象转换成一个函数,变成一个函数之后,只要传入对应的数据,函数就会返回一个模板数据渲染好的字符串。 教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的...

    feng409 评论0 收藏0
  • 手把手教你零写简单的 VUE

    摘要:本系列是一个教程,下面贴下目录手把手教你从零写一个简单的手把手教你从零写一个简单的模板篇今天给大家带来的是实现一个简单的类似一样的前端框架,框架现在应该算是非常主流的前端数据驱动框架,今天我们来从零开始写一个非常简单的框架,主要是让大家 本系列是一个教程,下面贴下目录~1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 今天给大家带来的是实现一个简单...

    RebeccaZhong 评论0 收藏0
  • 【微信小程序爬虫】表情包小程序图文视频教学,零写起,保姆教程!!!

    摘要:文章目录前言爬取分析视频教学成果展示福利入门到就业学习路线规划小白快速入门爬虫路线前言皮皮虾一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌游戏,当然除此之外还有写作的兴趣,,日子还很长,让我们一起加油努力叭话 ...

    coordinate35 评论0 收藏0

发表评论

0条评论

gaara

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<