Skip to content

模板引擎的核心原理

一. 前言

模板引擎有很多种,ejs、art-template等等,但是其核心原理都差不多,都是 字符串拼接 + new Function + with,这里会以ejs来举例

二. 依赖知识

1. new Function

在js里面,要想将字符串类型的函数执行,有几种方法,eval()函数,new Function(),以及nodejs里面的vm模块都可以,在“Nodejs模块加载机制”里面有说到它们三者的区别,其中eval执行时候,会找全局的上下文中的同名的变量,导致字符串内的作用域不干净

new Functionvm模块都有一个相同的特点就是可以创造一个干净的上下文。 new Function传入一个string,会返回一个function

2. with语句

作用:扩展一个语句的作用域链

JavaScript查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的context或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。

image.png

语法:

js
with (expression) {
    statement
}
  • expression:将给定的表达式添加到在评估语句时使用的作用域链上。表达式周围的括号是必需的。
  • statement:任何语句。要执行多个语句,请使用一个块语句 ({ ... })对这些语句进行分组。

MDN上对width语句的描述:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with

三. 实现简单的ejs模板引擎

1.创建ejs模板文件

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    姓名:<%=name%>
    年龄:<%=age%>
</body>
</html>

2. 编写解析模板的indx.js文件(核心)

index.js文件主要作用,读取ejs模板文件,替换掉里面的ejs变量,返回一个替换好的html字符串

js
const path = require('path');
const fs = require('fs');
function complileTplToHtmlStr(filepath,param) {
    filepath = path.resolve(__dirname,filepath);
    let ejsStr = fs.readFileSync(filepath,'utf8');
    let html = ejsStr.replace(/<%=(.+?)%>/g,function() {
       return param[arguments[1]]
    });
    return html
};

let str = complileTplToHtmlStr('./ejs.ejs',{name:'张三',age:18});
console.log(str);

3. 效果展示

image.png

四. 模板复杂度更进一步

上面的例子里,仅仅只有<%=name%>这种简单的ejs语法,针对循环,if语句之类的稍微复杂一点的语法还需要对模板编译函数做一个改造;

在ejs模板里面,要循环一个数组,语法如下:

html
<ul>
    <%arr.forEach(item=>{%>
      <li><%=item%></li>  
    <%})%>
</ul>

实现 正则替换 + 字符串拼接 + with语句 + new Function

js
const path = require('path');
const fs = require('fs');
const renderFile = (filePath,obj,cb) =>{
    fs.readFile(filePath,'utf8',function (err,html) {
        if(err){
            return cb(err,html);
        }
        // arguments[0] 就是匹配到的原字符串 arguments[1] 就是第一个原来括号
        html = html.replace(/<%=(.+?)%>/g,function () { 
            let key = arguments[1].trim();
            return '${'+key+'}' // <%=name%> => ${name}  
        });
        let head = `let str = '';\r\n with(obj){\r\n`;
        head += 'str+=`'
        html = html.replace(/<%(.+?)%>/g,function () {
            return '`\r\n'+arguments[1] + '\r\nstr+=`\r\n'
        })
        let tail = '`}\r\n return str;'
        // console.log(head + html + tail);
        let fn = new Function('obj',head + html + tail)
        cb(err,fn(obj));
    });
}
renderFile(path.resolve(__dirname,'ejs.ejs'),{name:'张三',age:18,arr:[1,2,3]},function (err,data) {
    console.log(data);
});