主题
模板引擎的核心原理
一. 前言
模板引擎有很多种,ejs、art-template等等,但是其核心原理都差不多,都是 字符串拼接 + new Function + with,这里会以ejs来举例
二. 依赖知识
1. new Function
在js里面,要想将字符串类型的函数执行,有几种方法,eval()
函数,new Function()
,以及nodejs里面的vm模块都可以,在“Nodejs模块加载机制”里面有说到它们三者的区别,其中eval执行时候,会找全局的上下文中的同名的变量,导致字符串内的作用域不干净
而new Function
和vm
模块都有一个相同的特点就是可以创造一个干净的上下文。 new Function
传入一个string,会返回一个function
2. with语句
作用:扩展一个语句的作用域链
JavaScript查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的context或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。
语法:
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. 效果展示
四. 模板复杂度更进一步
上面的例子里,仅仅只有<%=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);
});