深入浅出:模板引擎详解

深入浅出:模板引擎详解

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(/

.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 = `

<%= name %>

年龄: <%= age %>

职业: <%= job %>

简介: <%= bio || '暂无简介' %>

是否成年: <%= age >= 18 ? '是' : '否' %>

原始HTML: <%- htmlContent %>

`;

const userData = {

name: "张三",

age: 30,

job: "软件工程师",

bio: '',

htmlContent: "高级工程师"

};

console.log("1. 基本插值示例:");

console.log(engine.render(userTemplate, userData));

// 2. 分支表达式示例

const permissionTemplate = `

用户权限

<% if (isAdmin) { %>

管理员权限: 可以访问所有功能

<% } else if (isEditor) { %>

编辑权限: 可以编辑内容

<% } else { %>

普通用户权限: 只能查看内容

<% } %>

`;

console.log("\n2. 分支表达式示例:");

console.log(engine.render(permissionTemplate, {isAdmin:false, isEditor: true }));

// 3. 循环表达式示例

const productsTemplate = `

商品列表

    <% for (let i = 0; i < products.length; i++) { %>

  • <%= i + 1 %>. <%= products[i].name %>

    价格: ¥<%= products[i].price.toFixed(2) %>

    <% if (products[i].inStock) { %>

    有货

    <% } else { %>

    缺货

    <% } %>

  • <% } %>

`;

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 = `

文章标签

<% if (tags && tags.length) { %>

<% tags.forEach((tag, index) => { %>

<%= tag %>

<% if (index !== tags.length - 1) { %>

|

<% } %>

<% }) %>

<% } else { %>

暂无标签

<% } %>

`;

console.log("\n4. 数组forEach循环示例:");

console.log(engine.render(tagsTemplate, {

tags: ["JavaScript", "模板引擎", "EJS", "前端开发"]

}));

6.总结

模板引擎是 web 开发的核心工具之一,它通过分离数据和表现层,提高了代码可维护性、复用性和开发效率。从早期的服务端渲染到现在的组件化框架,模板引擎不断演进,始终不变的是它的核心:将动态数据注入到预定义的模板结构中,生成最终的输出内容。

相关数据

坚果云好用吗?用户分享五大优势功能
2026世界杯独家版权央视拿下,104场精彩赛事免费观看
电脑慢?教你几招给电脑提速!

友情链接