JavaScript模板引擎的核心作用是将包含变量、逻辑语法的模板字符串,结合传入的数据对象,生成最终的HTML或文本内容。它的编译过程本质是将模板字符串转换为可执行的JavaScript函数,通过函数调用完成数据替换和逻辑处理。

模板引擎的核心编译流程
一个基础的JavaScript模板引擎通常包含三个核心步骤,每个步骤对应不同的处理逻辑:
- 模板解析:将输入的模板字符串拆分为普通文本、变量占位符、逻辑语法(如条件判断、循环)等不同类型的片段。
- 代码生成:根据解析得到的片段,拼接生成可执行的JavaScript函数字符串,或者构建抽象语法树(AST)后生成执行逻辑。
- 渲染执行:将生成的函数与传入的数据绑定,执行函数得到最终的渲染结果。
基于正则匹配的简单实现
最基础的模板引擎可以通过正则表达式匹配模板中的特殊标记来实现,比如用<%= 变量名 %>表示变量输出,用<% 逻辑代码 %>表示逻辑执行。下面是完整的实现代码:
// 定义模板引擎编译函数
function compileTemplate(templateStr) {
// 正则匹配模板中的特殊标记,分为逻辑代码块和变量输出块
const reg = /<%([sS]*?)%>/g;
let match;
let lastIndex = 0;
let codeArr = [];
// 遍历匹配到的所有标记
while ((match = reg.exec(templateStr)) !== null) {
// 将标记前的普通文本加入代码数组
if (match.index > lastIndex) {
const text = templateStr.slice(lastIndex, match.index);
codeArr.push(`_push(`${text}`);`);
}
const content = match[1].trim();
// 判断是变量输出还是逻辑代码
if (content.startsWith('=')) {
// 变量输出,去掉开头的=,加入转义处理避免XSS
const varName = content.slice(1).trim();
codeArr.push(`_push(_escape(${varName}));`);
} else {
// 逻辑代码直接加入
codeArr.push(content);
}
lastIndex = reg.lastIndex;
}
// 处理最后剩余的普通文本
if (lastIndex < templateStr.length) {
const text = templateStr.slice(lastIndex);
codeArr.push(`_push(`${text}`);`);
}
// 拼接完整的函数体
const funcBody = `
const _output = [];
const _push = (str) => { _output.push(str); };
const _escape = (str) => {
if (typeof str !== 'string') return str;
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
${codeArr.join('n')}
return _output.join('');
`;
// 返回编译后的函数
return new Function('data', funcBody);
}
// 使用示例
const template = `
<div class="user-card">
<h3><%= user.name %></h3>
<p>年龄:<%= user.age %></p>
<% if (user.hobby.length > 0) { %>
<ul>
<% user.hobby.forEach(item => { %>
<li><%= item %></li>
<% }) %>
</ul>
<% } %>
</div>
`;
const renderFunc = compileTemplate(template);
const data = {
user: {
name: '张三',
age: 25,
hobby: ['篮球', '编程', '阅读']
}
};
const result = renderFunc(data);
console.log(result);
基于AST的高级编译实现
当模板语法更复杂时,正则匹配的方式容易出现边界问题,此时可以通过生成AST的方式实现更稳定的编译逻辑。AST编译的核心是先对模板进行词法分析和语法分析,生成结构化的语法树,再遍历语法树生成执行代码。
AST编译的核心步骤
AST方式的编译流程比正则方式更清晰,扩展性也更强:
- 词法分析:将模板字符串拆分为一个个最小的语法单元(Token),比如开始标记、结束标记、文本、变量名等。
- 语法分析:将Token序列按照模板语法规则组合成AST节点,每个节点对应一种模板元素,比如文本节点、变量节点、条件节点、循环节点等。
- 代码生成:遍历AST树,将每个节点转换为对应的JavaScript代码片段,最终拼接成完整的渲染函数。
AST实现示例
下面是简化版的AST模板引擎实现,支持变量输出和基础条件判断:
// 词法分析:生成Token数组
function tokenize(templateStr) {
const tokens = [];
const reg = /<%([sS]*?)%>/g;
let lastIndex = 0;
let match;
while ((match = reg.exec(templateStr)) !== null) {
// 普通文本Token
if (match.index > lastIndex) {
tokens.push({
type: 'text',
value: templateStr.slice(lastIndex, match.index)
});
}
const content = match[1].trim();
// 变量输出Token
if (content.startsWith('=')) {
tokens.push({
type: 'output',
value: content.slice(1).trim()
});
} else if (content.startsWith('if')) {
// 条件开始Token
tokens.push({
type: 'if_start',
value: content.slice(2).trim()
});
} else if (content === 'endif') {
// 条件结束Token
tokens.push({
type: 'if_end'
});
} else {
// 普通逻辑Token
tokens.push({
type: 'logic',
value: content
});
}
lastIndex = reg.lastIndex;
}
// 剩余文本Token
if (lastIndex < templateStr.length) {
tokens.push({
type: 'text',
value: templateStr.slice(lastIndex)
});
}
return tokens;
}
// 语法分析:生成AST
function parse(tokens) {
const ast = {
type: 'root',
children: []
};
let currentParent = ast;
const stack = [ast];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'if_start') {
const node = {
type: 'if',
condition: token.value,
children: []
};
currentParent.children.push(node);
stack.push(node);
currentParent = node;
} else if (token.type === 'if_end') {
stack.pop();
currentParent = stack[stack.length - 1];
} else {
currentParent.children.push(token);
}
}
return ast;
}
// 代码生成:遍历AST生成渲染函数
function generate(ast) {
let code = `
const _output = [];
const _push = (str) => _output.push(str);
const _escape = (str) => {
if (typeof str !== 'string') return str;
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
`;
function walk(node) {
if (node.type === 'text') {
code += `_push(`${node.value}`);n`;
} else if (node.type === 'output') {
code += `_push(_escape(${node.value}));n`;
} else if (node.type === 'logic') {
code += `${node.value}n`;
} else if (node.type === 'if') {
code += `if (${node.condition}) {n`;
node.children.forEach(child => walk(child));
code += `}n`;
} else if (node.type === 'root') {
node.children.forEach(child => walk(child));
}
}
walk(ast);
code += `return _output.join('');`;
return new Function('data', code);
}
// 使用示例
const templateStr = `
<div>
<%= title %>
<% if (data.showContent) { %>
<p><%= data.content %></p>
<% } %>
</div>
`;
const tokens = tokenize(templateStr);
const ast = parse(tokens);
const render = generate(ast);
const result = render({
title: 'AST模板引擎示例',
data: {
showContent: true,
content: '这是条件渲染的内容'
}
});
console.log(result);
两种实现方式的对比
正则匹配和AST两种实现方式各有适用场景,下面是两者的核心差异对比:
| 对比维度 | 正则匹配实现 | AST实现 |
|---|---|---|
| 实现复杂度 | 低,代码量少,容易上手 | 高,需要处理词法、语法分析多个环节 |
| 语法扩展性 | 弱,新增语法需要修改正则规则,容易出现冲突 | 强,新增语法只需新增对应的AST节点和处理逻辑 |
| 错误提示能力 | 弱,模板语法错误难以定位 | 强,语法分析阶段可以精准定位错误位置 |
| 适用场景 | 简单模板场景,语法规则少 | 复杂模板场景,需要支持多种语法规则 |
模板引擎的优化方向
实际生产中的模板引擎还会有更多优化点,比如:
- 缓存编译结果:对相同模板只编译一次,后续直接使用缓存的渲染函数,减少重复编译开销。
- 支持自定义分隔符:允许用户自定义模板标记的分隔符,避免和业务内容冲突。
- 增加过滤器功能:支持类似
<%= date | formatDate %>的过滤器语法,方便对输出内容做格式化处理。 - 预编译支持:在构建阶段将模板提前编译为渲染函数,减少运行时的编译耗时。
JavaScript模板引擎编译原理AST正则匹配修改时间:2026-06-29 23:27:48