cbT.js: 一个让模板继承变得优雅的 Node.js 模板引擎

166 阅读7分钟

在日常的 Web 开发中,我经常遇到模板复用的问题。特别是在开发一些需要大量页面的网站时,传统的模板引擎虽然能解决基本需求,但在模板继承方面往往显得不够灵活。基于这个痛点,我开发了 cbT.js —— 一个专注于模板继承的 Node.js 服务端模板引擎。

🎯 为什么要开发这个项目?

当时我在开发一个企业官网项目,遇到了这样的困扰:网站有几十个页面,每个页面都需要共享相同的头部、导航和尾部,但主体内容各不相同。使用传统的模板引擎,只能通过 include 方式来实现复用,但这种方式在面对复杂的布局嵌套时就显得力不从心了。

我希望能够像面向对象编程一样来组织模板结构,让子模板能够继承父模板的结构,并且可以选择性地重写某些部分。这就是 cbT.js 诞生的原因。

虽然市面上已经有 EJS、Pug、Handlebars 等成熟的模板引擎,但在模板继承方面,它们要么不支持,要么支持得不够灵活。所以我决定开发一个专门解决这个问题的模板引擎。

🚀 核心特性解析

1. 模板继承系统

这是我在开发 cbT.js 时最关注的功能。通过 extendsblockparentchild 等指令,可以构建出比较灵活的模板继承体系:

<!-- 父模板 parent.html -->
<!DOCTYPE html>
<html>
<head>
  <title><% block title %>默认标题<% /block %></title>
</head>
<body>
  <h1><% block heading %>默认标题<% /block %></h1>
  <div class="content">
    <% block content %>默认内容<% /block %>
  </div>
</body>
</html>

<!-- 子模板 child.html -->
<% extends parent %>

<% block title %>我的页面<% /block %>
<% block heading %>欢迎来到我的页面<% /block %>
<% block content %>
  <% parent %> <!-- 保留父模板的内容 -->
  <p>这是子模板添加的内容</p>
<% /block %>

这种设计让模板的复用变得更加灵活,可以选择完全替换、保留并扩展,甚至在父模板中为子模板预留插槽。

2. 实用的语法糖

除了继承功能,我还在 cbT.js 中添加了一些常用的语法糖:

<!-- 各种转义输出 -->
<%=name%>              <!-- HTML 转义输出 -->
<%:=html%>             <!-- 原样输出 -->
<%:u=url%>             <!-- URL 转义 -->
<%:v=attr%>            <!-- HTML 属性转义 -->
<%:a=array|,%>         <!-- 数组输出,逗号分隔 -->
<%:m=price%>           <!-- 金额格式化 -->
<%:s=title|10%>        <!-- 文本截取 -->

<!-- 控制结构 -->
<% if (user.isVip) %>
  <div class="vip-content">VIP 专享内容</div>
<% else %>
  <div class="normal-content">普通内容</div>
<% /if %>

<% foreach (item in products) %>
  <div><%=itemIndex%>: <%=item.name%></div>
<% foreachelse %>
  <div>暂无商品</div>
<% /foreach %>

这些语法糖让模板写起来更简洁,也考虑到了 Web 开发中的常见需求,比如 XSS 防护、数据格式化等。

3. 默认安全的设计

在开发过程中,我特别重视安全性。cbT.js 默认开启 HTML 转义(escape: true),所有变量输出都会自动进行 HTML 转义,帮助防止 XSS 攻击。虽然这可能会给某些场景带来一些不便,但我觉得默认安全比默认便利更重要。

4. 缓存机制

为了提升性能,我在 cbT.js 中实现了一套缓存系统:

  • 编译缓存:模板编译结果会被缓存,避免重复编译
  • 文件锁机制:通过 lockfile.js 确保并发环境下的缓存安全
  • 智能失效:基于文件修改时间的缓存失效机制

这套缓存机制在保证功能完整性的同时,也能提供比较好的性能表现。

5. 零依赖设计

在开发时,我选择了零依赖的设计方案,整个引擎完全基于 Node.js 原生 API 构建。这样做的好处是减小了项目体积,也避免了潜在的安全风险和版本冲突问题。

6. 测试覆盖

我比较重视代码质量,所以在开发过程中写了比较完整的测试。目前 cbT.js 的测试覆盖率达到了 100%

  • 语句覆盖率 100%:每一行代码都经过测试验证
  • 分支覆盖率 100%:所有条件分支都有对应的测试用例
  • 函数覆盖率 100%:每个函数都有完整的测试场景
  • 行覆盖率 100%:没有遗漏的代码行

项目包含了 169 个测试用例,覆盖了从核心编译功能到边缘情况处理的各种场景。虽然达到 100% 覆盖率花了不少时间,但我觉得这对于一个基础工具库来说还是很有必要的。

7. TypeScript 支持

考虑到现在很多项目都在使用 TypeScript,我为 cbT.js 编写了完整的类型定义文件(index.d.ts),包含了:

  • 详细的接口定义:从基本的 TemplateData 到复杂的 CompiledTemplate 接口
  • 完整的方法签名:所有 API 都有准确的类型注解和重载声明
  • 编译选项类型CompileOptions 接口覆盖了所有配置选项
  • 回调函数类型RenderCallbackCompileCallback 确保异步操作的类型安全

这样 TypeScript 开发者就可以享受到智能提示、类型检查和编译时错误检测,应该能提升一些开发体验。

🛠️ 实战应用场景

场景一:企业官网开发

企业官网通常有相似的页面结构但内容不同,这正是我开发 cbT.js 时遇到的典型场景:

const cbT = require('cb-template');

// 设置模板根目录
cbT.basePath = './templates';

// 渲染不同页面
cbT.renderFile('about.html', {
  pageTitle: '关于我们',
  breadcrumb: ['首页', '关于我们']
}, {}, (err, content) => {
  // 处理渲染结果
});

场景二:邮件模板系统

邮件模板往往需要复用相同的样式但内容各异,cbT.js 的 block 系统可以很好地解决这个问题:

<!-- 邮件基础模板 -->
<% block header %>
  <div style="background: #007cff;">
    <h1><%=companyName%></h1>
  </div>
<% /block %>

<% block content %>
  <!-- 邮件具体内容 -->
<% /block %>

<% block footer %>
  <div style="text-align: center; color: #666;">
    © 2024 <%=companyName%>
  </div>
<% /block %>

📈 与其他模板引擎的对比

特性cbT.jsEJSHandlebarsPug
模板继承✅ 多级继承✅ 基础继承
学习曲线📈 中等📈 简单📈 中等📈 陡峭
安全性✅ 默认转义❌ 需手动✅ 默认转义✅ 默认转义
运行时依赖✅ 零依赖✅ 零依赖❌ 有依赖❌ 有依赖
缓存机制✅ 完善✅ 基础✅ 基础✅ 基础
测试覆盖率🎯 100%⚠️ 不完整⚠️ 不完整⚠️ 不完整
TypeScript支持💯 完整✅ 社区✅ 内置✅ 内置

🎨 开发时的一些思考

在开发 cbT.js 的过程中,我主要考虑了以下几个方面:

  1. 实用性:每个功能都尽量来自真实的开发需求,比如金额格式化、URL 协议自适应等,都是我在实际项目中遇到过的问题
  2. 安全性:默认转义、文件锁机制等细节,主要是考虑到 Web 应用的安全性要求
  3. 性能:编译缓存和零依赖设计,是为了在功能完整的前提下尽量保证性能
  4. 质量:100% 的测试覆盖率确实花了不少时间,但我觉得对于一个基础工具库来说这还是很有必要的
  5. 现代化:TypeScript 支持主要是考虑到现在很多项目都在使用,希望能降低集成成本
  6. 开发体验:语法糖和继承机制的设计,主要是希望能让模板写起来更舒服一些

🔧 快速上手

npm install cb-template
const cbT = require('cb-template');

// 简单模板渲染
const result = cbT.render('Hello <%=name%>!', { name: 'World' });
console.log(result); // Hello World!

// 文件模板渲染(支持继承)
cbT.renderFile('template.html', data, {}, (err, content) => {
  if (!err) {
    console.log(content);
  }
});

TypeScript 用法示例

import cbT, { TemplateData, CompileOptions } from 'cb-template';

// 类型安全的数据定义
interface PageData extends TemplateData {
  title: string;
  user: {
    name: string;
    isVip: boolean;
  };
  products: Array<{
    id: number;
    name: string;
    price: number;
  }>;
}

// 编译选项
const options: CompileOptions = {
  cache: true,
  cacheName: 'my-templates'
};

// 类型安全的模板渲染
const data: PageData = {
  title: '产品列表',
  user: { name: '张三', isVip: true },
  products: [
    { id: 1, name: '商品1', price: 100 },
    { id: 2, name: '商品2', price: 200 }
  ]
};

cbT.renderFile('product-list.html', data, options, (err, content) => {
  if (!err) {
    console.log(content);
  }
});

💭 一些总结

开发 cbT.js 主要是为了解决我自己在实际项目中遇到的模板继承问题。虽然现在已经有很多成熟的模板引擎,但我觉得在模板继承这个特定需求上,cbT.js 还是有一些自己的特色。

对于需要大量模板复用的项目来说,多级继承功能可能会比较有用。零依赖的设计和相对完整的测试覆盖,也让我在一些对稳定性要求较高的项目中使用起来比较放心。

当然,每个项目的情况不同,如果你的项目已经在使用其他模板引擎,而且运行良好,那没必要为了换而换。但如果你正好在寻找一个支持模板继承的方案,不妨试试 cbT.js。

虽然功能还不够完善,但我会持续维护和改进这个项目。如果你在使用过程中遇到问题或者有什么建议,欢迎在 GitHub 上提 issue。


项目地址: github.com/hex-ci/cbT
NPM 地址: www.npmjs.com/package/cb-…
当前版本: 3.0.3

如果你也在使用模板引擎,或者在模板复用方面有什么经验和想法,欢迎在评论区交流!