1.前言
为什么会出现模板引擎呢?在早期显示页面与业务数据是紧耦合在一起的,难以维护,可读性差,为了将用户页面与业务数据分离,出现了模板引擎。
2.是什么
模板引擎是一种将模板文件 与数据结合生成最终输出内容(通常是 HTML 、XML等)的工具或者库, 它的核心作用是将用户页面与业务数据能够分离,让开发者关注"数据如何呈现"而不是"字符串如何拼接"。它的运行流程是让开发者创建具有占位符的模板文件,运行时将实际数据替换占位符,生成最终的动态内容。
3.场景
在日常开发中,用到最多的场景就是在 web 开发中的页面渲染,大致分为下面两类:
- 服务端渲染 SSR:首屏加载快,SEO 友好,适合内容搜索型网站
- Node.js+EJS模板引擎
- Python+Jinja2模板引擎
- 客户端渲染 CSR
- 浏览器端获取到数据后,需要前端模板引擎动态渲染 DOM,我们可以使用前端框架中的Vue 模板、JSX 等
4.优缺点
模板引擎使用也需要权衡
-
优点:
✅ 数据与视图分离,提高可维护性,将模板编写完毕,自动处理好动态数据的注入
✅ 提高代码复用性,多条数据可以使用一个模板进行渲染
✅ 提高安全性,防止 XSS 攻击,模板引擎默认对动态内容进行 HTML 转义,防止输入恶意代码被执行,在下面代码实现上也有体现,
escape
函数就是对代码进行了转义✅ 性能优化,多数模板引擎进行"预编译"和"缓存",首次使用被编译的可执行函数,后续直接调用可执行函数,避免重复解析模板,在下面代码实现上也有体现,
compile
函数中对编译过的模板函数进行了缓存,性能效率有所提升 -
缺点:
❌ 增加学习成本,需要学习模板中的语法,指令,比如
v-if
、<%= %>
等,每个指令如何使用❌ 增加调试难度,模板引擎在编译过程将模板转换为底层代码,可能导致报错信息无法溯源,比如 Vue模板中报错信息是
_c is not defined
,需要熟悉编译底层原理才能定位问题❌ 简单场景使用直接字符串拼接性能比模板引擎要好,很简单场景,反而不太需要模板引擎的编译缓存处理等,直接拼接字符串就能满足需求
5.代码实现
5.1 简单实现ejs模板引擎
javascript
class EJSEngine {
// 定义缓存和需要匹配的正则
constructor() {
// 对编译过的模板函数进行缓存提高性能
this.cache = new Map()
// 需要分别匹配独立的 <%= ... %> 标签,所以使用非贪婪匹配 *?;\s\S 指的是匹配所有字符空白字符和非空白字符
// 四个分支,三个捕获组(前三个分支具备括号所以是三个捕获组),所以 match[1]-match[3]分别对应匹配的结果
// 最后一个只匹配,避免出现模板中闭合标签的异常情况
this.tagRegex = /<%=([\s\S]*?)%>|<%-([\s\S]*?)%>|<%([\s\S]*?)%>|%>/g
}
// 编译模板并缓存
compile(template,options = {}) {
if(!options.onCache && this.cache.get(template)) {
return this.cache.get(template)
}
const code = this.generateCode(template)
// options预留扩展,比如可以添加属控制输出格式等
const renderFn = new Function("data","options",code)
if(!options.onCache) {
this.cache.set(template,renderFn)
}
return renderFn
}
// 生成js代码
generateCode(template) {
let code = `
const output = []
const escape = (str) =>{
if(typeof str !== "string") return str
return str.replace(/>/g,">")
.replace(/</g,"<")
.replace(/"/g,""")
.replace(/'/g,"'")
.replace(/&/g,"&")
}
with(data || {}){
`
let index = 0,match = null
// 模板前中后,前面有字符串、中间是模板、后面字符串
while((match = this.tagRegex.exec(template)) !== null){
if(match.index > index) {
const text = this.escapeString(template.slice(index,match.index))
code += `output.push("${text}");`
}
if(match[1]) {
const expr = match[1].trim()
code += `output.push(escape(${expr}));`
}else if(match[2]){
const expr = match[2].trim()
code += `output.push(${expr});`
}else if(match[3]){
const script = match[3].trim()
code += `${script};`
}
index = this.tagRegex.lastIndex
}
if(index < template.length){
const text = this.escapeString(template.slice(index))
code += `output.push("${text}");`
}
code += `
}
return output.join("")
`
return code
}
// 将字符串中的特殊字符需要转义
escapeString(str) {
if(typeof str !== "string") return str
// t tab r 回车 n 换行
return str.replace(/\\/g,"\\\\")
.replace(/"/g,'\\"')
.replace(/'/g,"\\'")
.replace(/\r/g,"\\r")
.replace(/\n/g,"\\n")
.replace(/\t/g,"\\t")
}
// 渲染
render(template,data,options) {
const renderFn = this.compile(template,options)
return renderFn(data,options)
}
}
5.2 测试验证
less
// 使用示例
const engine = new EJSEngine();
// 1. 基本插值示例
const userTemplate = `
<div class="user">
<h2><%= name %></h2>
<p>年龄: <%= age %></p>
<p>职业: <%= job %></p>
<p>简介: <%= bio || '暂无简介' %></p>
<p>是否成年: <%= age >= 18 ? '是' : '否' %></p>
<p>原始HTML: <%- htmlContent %></p>
</div>
`;
const userData = {
name: "张三",
age: 30,
job: "软件工程师",
bio: '<script>alert("XSS攻击")</script>',
htmlContent: "<strong>高级工程师</strong>"
};
console.log("1. 基本插值示例:");
console.log(engine.render(userTemplate, userData));
// 2. 分支表达式示例
const permissionTemplate = `
<div class="permissions">
<h3>用户权限</h3>
<% if (isAdmin) { %>
<p>管理员权限: 可以访问所有功能</p>
<button>管理用户</button>
<% } else if (isEditor) { %>
<p>编辑权限: 可以编辑内容</p>
<button>编辑文章</button>
<% } else { %>
<p>普通用户权限: 只能查看内容</p>
<% } %>
</div>
`;
console.log("\n2. 分支表达式示例:");
console.log(engine.render(permissionTemplate, {isAdmin:false, isEditor: true }));
// 3. 循环表达式示例
const productsTemplate = `
<div class="products">
<h3>商品列表</h3>
<ul>
<% for (let i = 0; i < products.length; i++) { %>
<li>
<span><%= i + 1 %>. <%= products[i].name %></span>
<span>价格: ¥<%= products[i].price.toFixed(2) %></span>
<% if (products[i].inStock) { %>
<span class="in-stock">有货</span>
<% } else { %>
<span class="out-of-stock">缺货</span>
<% } %>
</li>
<% } %>
</ul>
</div>
`;
const productsData = {
products: [
{ name: "笔记本电脑", price: 5999, inStock: true },
{ name: "智能手机", price: 3999, inStock: true },
{ name: "平板电脑", price: 2999, inStock: false },
{ name: "智能手表", price: 1599, inStock: true }
]
};
console.log("\n3. 循环表达式示例:");
console.log(engine.render(productsTemplate, productsData));
// 4. 数组forEach循环示例
const tagsTemplate = `
<div class="tags-container">
<h3>文章标签</h3>
<% if (tags && tags.length) { %>
<div class="tags">
<% tags.forEach((tag, index) => { %>
<span class="tag"><%= tag %></span>
<% if (index !== tags.length - 1) { %>
<span class="separator">|</span>
<% } %>
<% }) %>
</div>
<% } else { %>
<p>暂无标签</p>
<% } %>
</div>
`;
console.log("\n4. 数组forEach循环示例:");
console.log(engine.render(tagsTemplate, {
tags: ["JavaScript", "模板引擎", "EJS", "前端开发"]
}));
6.总结
模板引擎是 web 开发的核心工具之一,它通过分离数据和表现层,提高了代码可维护性、复用性和开发效率。从早期的服务端渲染到现在的组件化框架,模板引擎不断演进,始终不变的是它的核心:将动态数据注入到预定义的模板结构中,生成最终的输出内容。