【RuoYi-Eggjs】:基于 Handlebars 模板引擎实现服务端渲染

前言

在 Web 开发中,选择合适的模板引擎能让前端开发事半功倍。Handlebars 作为一款轻量级的语义化模板引擎,以其简洁的语法和强大的功能深受开发者喜爱。[ruoyi-eggjs-handlebars](https://github.com/undsky/ruoyi-eggjs-handlebars) 就是为 Egg.js 框架量身定制的 Handlebars 插件,让你在 Egg.js 中也能享受 Handlebars 的便利。

为什么选择 Handlebars?

📝 语法简洁直观

Handlebars 使用双花括号 {{}} 作为标记,语法清晰易懂:


    
    
    
  <h1>Hello {{name}}!</h1>
<p>
年龄:{{age}} 岁</p>

🔒 逻辑分离

Handlebars 强调"无逻辑"模板(logic-less),将业务逻辑与视图层严格分离,模板只负责展示数据,逻辑处理在 Controller 或 Service 层完成。这种设计让代码结构更清晰、更易维护。

🎯 功能强大

虽然是"无逻辑"模板,但 Handlebars 通过 Helper 机制提供了丰富的功能扩展,既保持了模板的简洁性,又满足了实际开发的需求。

核心特性

🚀 性能优化

插件针对不同环境做了优化:

🧩 Partials 支持

Partials(代码片段)功能让你可以复用页面的公共部分,比如头部、尾部、侧边栏等:


    
    
    
  <!-- app/view/partials/header.html -->
<header>

  <h1>
{{title}}</h1>
  <nav>
...</nav>
</header>


<!-- 在其他模板中使用 -->

{{> header}}

🛠️ 内置实用 Helper

插件内置了几个常用的 Helper,开箱即用:

1. xif - 强化的条件判断

支持复杂表达式,比原生的 {{#if}} 更强大:


    
    
    
  {{#xif "age > 18"}}
  <p>
成年人</p>
{{else}}
  <p>
未成年</p>
{{/xif}}

{{#xif "score >= 60 && score < 80"}}
  <p>
及格</p>
{{/xif}}

2. math - 数学运算

在模板中直接进行数学计算:


    
    
    
  <p>原价:{{price}}</p>
<p>
折后价:{{math price "*" 0.8}}</p>
<p>
库存剩余:{{math total "-" sold}}</p>

3. link - CSS 引入

快速引入样式文件,自动处理时间戳:


    
    
    
  {{link "/static/css/style.css"}}
<!-- 输出: <link rel="stylesheet" href="/static/css/style.css?ts=1234567890"> -->

4. script - JS 引入

快速引入脚本文件,同样支持时间戳:


    
    
    
  {{script "/static/js/app.js"}}
<!-- 输出: <script src="/static/js/app.js?ts=1234567890"></script> -->

时间戳功能:默认开启,可有效避免浏览器缓存导致的静态资源更新问题。

快速上手

安装


    
    
    
  npm i ruoyi-eggjs-handlebars --save

配置

1. 启用插件


    
    
    
  // config/plugin.js
exports
.handlebars = {
  enable
: true,
  package
: "ruoyi-eggjs-handlebars",
};

2. 基础配置


    
    
    
  // config/config.default.js
config.handlebars = {
  options
: {
    data
: true,
    compat
: true,
    noEscape
: false,        // 是否转义 HTML
    strict
: false,          // 严格模式
    preventIndent
: true,    // 防止自动缩进
  },
  runtimeOptions
: {
    allowProtoMethodsByDefault
: true,
    allowProtoPropertiesByDefault
: true,
  },
  partialsPath
: null,       // Partials 目录,默认为 view.root/partials
  ts
: true,                 // 静态资源是否添加时间戳
};

使用示例

Controller 层


    
    
    
  // app/controller/home.js
class
 HomeController extends Controller {
  async
 index() {
    const
 { ctx } = this;
    await
 ctx.render('index.html', {
      title
: '欢迎使用 Handlebars',
      user
: {
        name
: '张三',
        age
: 25,
      },
      products
: [
        { name: '商品A', price: 99 },
        { name: '商品B', price: 199 },
      ],
    });
  }
}

模板文件


    
    
    
  <!-- app/view/index.html -->
<!DOCTYPE html>

<html>

<head>

  <title>
{{title}}</title>
  {{link "/static/css/style.css"}}
</head>

<body>

  {{> header}}
  
  <main>

    <h2>
用户信息</h2>
    <p>
姓名:{{user.name}}</p>
    <p>
年龄:{{user.age}}</p>
    
    {{#xif "user.age >= 18"}}
      <p class="badge">
成年用户</p>
    {{/xif}}
    
    <h2>
商品列表</h2>
    <ul>

      {{#each products}}
        <li>

          {{this.name}} - 
          <span class="price">
¥{{this.price}}</span>
          <span class="discount">
折后价:¥{{math this.price "*" 0.8}}</span>
        </li>

      {{/each}}
    </ul>

  </main>

  
  {{> footer}}
  {{script "/static/js/app.js"}}
</body>

</html>

实战场景

场景 1:后台管理系统布局

后台系统通常有固定的布局结构,使用 Partials 可以很好地复用:


    
    
    
  app/view/
├── partials/
│   ├── header.html      # 顶部导航
│   ├── sidebar.html     # 侧边栏
│   └── footer.html      # 底部
├── layout.html          # 布局模板
├── user/
│   ├── list.html        # 用户列表
│   └── detail.html      # 用户详情
└── dashboard.html       # 仪表盘

布局模板


    
    
    
  <!-- app/view/layout.html -->
<!DOCTYPE html>

<html>

<head>

  <title>
{{title}} - 管理后台</title>
  {{link "/static/css/admin.css"}}
</head>

<body>

  {{> header}}
  
  <div class="container">

    {{> sidebar}}
    
    <main class="content">

      {{{body}}}  <!-- 三个花括号表示不转义 HTML -->
    </main>

  </div>

  
  {{> footer}}
  {{script "/static/js/admin.js"}}
</body>

</html>

场景 2:数据列表渲染

使用 Handlebars 的 {{#each}} 和内置 Helper 轻松实现数据列表:


    
    
    
  <!-- app/view/user/list.html -->
<div class="user-list">

  <h2>
用户列表(共 {{users.length}} 人)</h2>
  
  <table>

    <thead>

      <tr>

        <th>
序号</th>
        <th>
姓名</th>
        <th>
年龄</th>
        <th>
状态</th>
        <th>
操作</th>
      </tr>

    </thead>

    <tbody>

      {{#each users}}
        <tr>

          <td>
{{math @index "+" 1}}</td>
          <td>
{{this.name}}</td>
          <td>
{{this.age}}</td>
          <td>

            {{#xif "this.age >= 18"}}
              <span class="badge success">
成年</span>
            {{else}}
              <span class="badge warning">
未成年</span>
            {{/xif}}
          </td>

          <td>

            <a href="/user/{{this.id}}">
查看</a>
            <a href="/user/{{this.id}}/edit">
编辑</a>
          </td>

        </tr>

      {{/each}}
    </tbody>

  </table>

</div>

场景 3:条件渲染

使用 xif Helper 实现复杂的条件判断:


    
    
    
  <!-- app/view/product/detail.html -->
<div class="product-detail">

  <h1>
{{product.name}}</h1>
  <p class="price">
¥{{product.price}}</p>
  
  <!-- 库存状态显示 -->

  {{#xif "product.stock > 10"}}
    <span class="stock success">
库存充足</span>
  {{else}}
    {{#xif "product.stock > 0"}}
      <span class="stock warning">
仅剩 {{product.stock}} 件</span>
    {{else}}
      <span class="stock danger">
已售罄</span>
    {{/xif}}
  {{/xif}}
  
  <!-- 折扣信息 -->

  {{#xif "product.discount < 1"}}
    <div class="discount-info">

      <p>
原价:¥{{product.price}}</p>
      <p>
折扣价:¥{{math product.price "*" product.discount}}</p>
      <p>
节省:¥{{math product.price "-" (math product.price "*" product.discount)}}</p>
    </div>

  {{/xif}}
</div>

最佳实践

1. 合理组织 Partials

将公共部分抽取为 Partials,提高代码复用性:


    
    
    
  partials/
├── common/
│   ├── header.html      # 页头
│   ├── footer.html      # 页脚
│   └── meta.html        # Meta 标签
├── components/
│   ├── pagination.html  # 分页组件
│   ├── breadcrumb.html  # 面包屑
│   └── alert.html       # 提示框
└── forms/
    ├── input.html       # 输入框
    └── select.html      # 下拉框

2. 善用内置 Helper

充分利用 xifmath 简化模板逻辑:


    
    
    
  <!-- ✅ 推荐:使用 xif 进行复杂判断 -->
{{#xif "score >= 90"}}
  <span class="grade">
优秀</span>
{{else}}
  {{#xif "score >= 60"}}
    <span class="grade">
及格</span>
  {{else}}
    <span class="grade">
不及格</span>
  {{/xif}}
{{/xif}}

<!-- ❌ 不推荐:在 Controller 中预处理 -->

<!-- 这会增加 Controller 的复杂度 -->

3. 静态资源管理

合理使用时间戳功能,避免缓存问题:


    
    
    
  // config/config.default.js
config.handlebars = {
  ts
: process.env.NODE_ENV === 'production',  // 生产环境启用
};

4. 避免过度嵌套

虽然 Handlebars 支持嵌套,但过度嵌套会降低可读性:


    
    
    
  <!-- ✅ 推荐:扁平化结构 -->
{{#each users}}
  {{> userCard}}
{{/each}}

<!-- ❌ 不推荐:过度嵌套 -->

{{#each categories}}
  {{#each this.products}}
    {{#each this.variants}}
      ...
    {{/each}}
  {{/each}}
{{/each}}

配置参数详解

编译选项(options)

参数类型默认值说明
dataBooleantrue是否启用 @data 跟踪
compatBooleantrue允许递归查找
noEscapeBooleanfalse是否禁用 HTML 转义
strictBooleanfalse严格模式(缺失参数时抛异常)
preventIndentBooleantrue防止自动缩进
ignoreStandaloneBooleantrue不移除单独的标签

运行时选项(runtimeOptions)

参数类型默认值说明
allowProtoMethodsByDefaultBooleantrue允许访问原型方法
allowProtoPropertiesByDefaultBooleantrue允许访问原型属性

插件选项

参数类型默认值说明
partialsPathString/ArraynullPartials 目录路径
tsBooleantrue静态资源是否添加时间戳

总结

ruoyi-eggjs-handlebars 是一个功能完善、性能优异的 Egg.js 模板引擎插件,它的优势在于:

如果你正在使用 Egg.js 开发传统的服务端渲染应用,或者需要一个轻量级的模板引擎来生成 HTML 邮件、导出报表等,ruoyi-eggjs-handlebars 都是一个不错的选择!


引用链接

[1] ruoyi-eggjs-handlebars: https://github.com/undsky/ruoyi-eggjs-handlebars
[2] RuoYi-Eggjs: https://github.com/undsky/ruoyi-eggjs
[3] RuoYi-Eggjs 文档: https://www.undsky.com/blog/?category=RuoYi-Eggjs#