实现模板引擎
手写 Mustache 风格的模板引擎
问题
实现一个 Mustache 风格的模板引擎,支持以下语法:
{{name}}- 变量替换{{a.b}}- 嵌套属性访问{{#list}}...{{/list}}- 循环/条件渲染{{^show}}...{{/show}}- 反向条件(falsy 时渲染)
解答
基础版本:变量替换
function render(template, data) {
// 匹配 {{xxx}} 或 {{a.b.c}}
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, key) => {
return getNestedValue(data, key) ?? '';
});
}
// 获取嵌套属性值:'a.b.c' -> data.a.b.c
function getNestedValue(obj, path) {
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
// 测试
const template = '你好,{{name}}!你的邮箱是 {{user.email}}';
const data = { name: '张三', user: { email: 'test@example.com' } };
console.log(render(template, data));
// 输出:你好,张三!你的邮箱是 test@example.com
完整版本:支持循环和条件
function render(template, data) {
let result = template;
// 1. 处理循环/条件块 {{#key}}...{{/key}}
result = result.replace(
/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g,
(match, key, content) => {
const value = getNestedValue(data, key);
// 数组:循环渲染
if (Array.isArray(value)) {
return value.map(item => render(content, { ...data, ...item, '.': item })).join('');
}
// truthy:渲染内容
if (value) {
return render(content, typeof value === 'object' ? { ...data, ...value } : data);
}
// falsy:不渲染
return '';
}
);
// 2. 处理反向条件 {{^key}}...{{/key}}
result = result.replace(
/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g,
(match, key, content) => {
const value = getNestedValue(data, key);
// falsy 或空数组时渲染
if (!value || (Array.isArray(value) && value.length === 0)) {
return render(content, data);
}
return '';
}
);
// 3. 处理变量 {{key}} 或 {{.}}
result = result.replace(/\{\{(\.|[\w.]+)\}\}/g, (match, key) => {
if (key === '.') return escapeHtml(data['.'] ?? '');
const value = getNestedValue(data, key);
return escapeHtml(value ?? '');
});
return result;
}
// 获取嵌套属性
function getNestedValue(obj, path) {
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
// HTML 转义,防止 XSS
function escapeHtml(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return String(str).replace(/[&<>"']/g, char => escapeMap[char]);
}
使用示例
const template = `
<h1>{{title}}</h1>
{{#showIntro}}
<p>欢迎来到 {{siteName}}</p>
{{/showIntro}}
<ul>
{{#users}}
<li>{{name}} - {{email}}</li>
{{/users}}
</ul>
{{^users}}
<p>暂无用户</p>
{{/users}}
`;
const data = {
title: '用户列表',
siteName: 'MyApp',
showIntro: true,
users: [
{ name: '张三', email: 'zhang@test.com' },
{ name: '李四', email: 'li@test.com' }
]
};
console.log(render(template, data));
输出:
<h1>用户列表</h1>
<p>欢迎来到 MyApp</p>
<ul>
<li>张三 - zhang@test.com</li>
<li>李四 - li@test.com</li>
</ul>
基于词法分析的实现(更健壮)
class MustacheEngine {
constructor(template) {
this.tokens = this.tokenize(template);
}
// 词法分析:将模板拆分为 token
tokenize(template) {
const tokens = [];
const regex = /\{\{([#^/])?(\.|[\w.]+)\}\}/g;
let lastIndex = 0;
let match;
while ((match = regex.exec(template)) !== null) {
// 添加文本节点
if (match.index > lastIndex) {
tokens.push({ type: 'text', value: template.slice(lastIndex, match.index) });
}
const [, modifier, key] = match;
if (modifier === '#') {
tokens.push({ type: 'section', key });
} else if (modifier === '^') {
tokens.push({ type: 'inverted', key });
} else if (modifier === '/') {
tokens.push({ type: 'end', key });
} else {
tokens.push({ type: 'variable', key });
}
lastIndex = regex.lastIndex;
}
// 添加剩余文本
if (lastIndex < template.length) {
tokens.push({ type: 'text', value: template.slice(lastIndex) });
}
return this.buildTree(tokens);
}
// 构建 AST
buildTree(tokens) {
const root = { type: 'root', children: [] };
const stack = [root];
for (const token of tokens) {
const parent = stack[stack.length - 1];
if (token.type === 'section' || token.type === 'inverted') {
const node = { ...token, children: [] };
parent.children.push(node);
stack.push(node);
} else if (token.type === 'end') {
stack.pop();
} else {
parent.children.push(token);
}
}
return root;
}
// 渲染
render(data) {
return this.renderNode(this.tokens, data);
}
renderNode(node, data) {
if (node.type === 'text') {
return node.value;
}
if (node.type === 'variable') {
const value = this.getValue(data, node.key);
return this.escapeHtml(value ?? '');
}
if (node.type === 'section') {
const value = this.getValue(data, node.key);
if (Array.isArray(value)) {
return value.map(item =>
this.renderChildren(node.children, { ...data, ...item, '.': item })
).join('');
}
if (value) {
return this.renderChildren(node.children,
typeof value === 'object' ? { ...data, ...value } : data
);
}
return '';
}
if (node.type === 'inverted') {
const value = this.getValue(data, node.key);
if (!value || (Array.isArray(value) && value.length === 0)) {
return this.renderChildren(node.children, data);
}
return '';
}
if (node.type === 'root') {
return this.renderChildren(node.children, data);
}
return '';
}
renderChildren(children, data) {
return children.map(child => this.renderNode(child, data)).join('');
}
getValue(obj, path) {
if (path === '.') return obj['.'];
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
escapeHtml(str) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return String(str).replace(/[&<>"']/g, c => map[c]);
}
}
// 使用
const engine = new MustacheEngine(template);
console.log(engine.render(data));
关键点
- 正则匹配:使用
/\{\{(\w+(?:\.\w+)*)\}\}/g匹配变量,[\s\S]*?非贪婪匹配块内容 - 嵌套属性:通过
split('.')+reduce实现a.b.c路径访问 - 递归渲染:循环和条件块需要递归调用 render,处理嵌套模板
- XSS 防护:输出变量时进行 HTML 转义
- 词法分析:复杂场景下,先 tokenize 再构建 AST,比纯正则更健壮
目录