深入浅出:模板引擎详解

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,"&gt;")
                          .replace(/</g,"&lt;")
                          .replace(/"/g,"&quot;")
                          .replace(/'/g,"&#39;")
                          .replace(/&/g,"&amp;")
                
            }
            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 开发的核心工具之一,它通过分离数据和表现层,提高了代码可维护性、复用性和开发效率。从早期的服务端渲染到现在的组件化框架,模板引擎不断演进,始终不变的是它的核心:将动态数据注入到预定义的模板结构中,生成最终的输出内容

相关推荐
前端世界1 天前
前端跨域终极指南:3 种优雅解决方案 + 可运行 Demo
前端·javascript·状态模式
开发者小天1 天前
在Ant Design Vue 中使用图片预览的插件
前端·javascript·vue.js·前端框架
_月下闲人1 天前
已掌握 Vue2 的开发者,快速上手 Vue3
前端·javascript·vue.js
光头程序员1 天前
vite_react 插件 find_code 最终版本
前端·javascript·react.js
叶常落1 天前
【frontend】事件监听,事件捕获阶段(capture phaze),目标阶段,事件冒泡阶段
javascript
Java中文社群1 天前
面试官:如何提升项目并发性能?
java·后端·面试
云霄IT1 天前
vue3前端开发的基础教程——快速上手
前端·javascript·vue.js
一枚前端小能手1 天前
🚀 Webpack打包慢到怀疑人生?这6个配置让你的构建速度起飞
前端·javascript·webpack
前端缘梦1 天前
深入浅出 Vue 的 Diff 算法:最小化 DOM 操作的魔法
前端·vue.js·面试
月伤591 天前
Element Plus 表格表单校验功能详解
前端·javascript·vue.js