在日常的 Web 开发中,我经常遇到模板复用的问题。特别是在开发一些需要大量页面的网站时,传统的模板引擎虽然能解决基本需求,但在模板继承方面往往显得不够灵活。基于这个痛点,我开发了 cbT.js —— 一个专注于模板继承的 Node.js 服务端模板引擎。
🎯 为什么要开发这个项目?
当时我在开发一个企业官网项目,遇到了这样的困扰:网站有几十个页面,每个页面都需要共享相同的头部、导航和尾部,但主体内容各不相同。使用传统的模板引擎,只能通过 include 方式来实现复用,但这种方式在面对复杂的布局嵌套时就显得力不从心了。
我希望能够像面向对象编程一样来组织模板结构,让子模板能够继承父模板的结构,并且可以选择性地重写某些部分。这就是 cbT.js 诞生的原因。
虽然市面上已经有 EJS、Pug、Handlebars 等成熟的模板引擎,但在模板继承方面,它们要么不支持,要么支持得不够灵活。所以我决定开发一个专门解决这个问题的模板引擎。
🚀 核心特性解析
1. 模板继承系统
这是我在开发 cbT.js 时最关注的功能。通过 extends、block、parent、child 等指令,可以构建出比较灵活的模板继承体系:
<!-- 父模板 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接口覆盖了所有配置选项 - 回调函数类型:
RenderCallback和CompileCallback确保异步操作的类型安全
这样 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.js | EJS | Handlebars | Pug |
|---|---|---|---|---|
| 模板继承 | ✅ 多级继承 | ❌ | ❌ | ✅ 基础继承 |
| 学习曲线 | 📈 中等 | 📈 简单 | 📈 中等 | 📈 陡峭 |
| 安全性 | ✅ 默认转义 | ❌ 需手动 | ✅ 默认转义 | ✅ 默认转义 |
| 运行时依赖 | ✅ 零依赖 | ✅ 零依赖 | ❌ 有依赖 | ❌ 有依赖 |
| 缓存机制 | ✅ 完善 | ✅ 基础 | ✅ 基础 | ✅ 基础 |
| 测试覆盖率 | 🎯 100% | ⚠️ 不完整 | ⚠️ 不完整 | ⚠️ 不完整 |
| TypeScript支持 | 💯 完整 | ✅ 社区 | ✅ 内置 | ✅ 内置 |
🎨 开发时的一些思考
在开发 cbT.js 的过程中,我主要考虑了以下几个方面:
- 实用性:每个功能都尽量来自真实的开发需求,比如金额格式化、URL 协议自适应等,都是我在实际项目中遇到过的问题
- 安全性:默认转义、文件锁机制等细节,主要是考虑到 Web 应用的安全性要求
- 性能:编译缓存和零依赖设计,是为了在功能完整的前提下尽量保证性能
- 质量:100% 的测试覆盖率确实花了不少时间,但我觉得对于一个基础工具库来说这还是很有必要的
- 现代化:TypeScript 支持主要是考虑到现在很多项目都在使用,希望能降低集成成本
- 开发体验:语法糖和继承机制的设计,主要是希望能让模板写起来更舒服一些
🔧 快速上手
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
如果你也在使用模板引擎,或者在模板复用方面有什么经验和想法,欢迎在评论区交流!