underscore 模板编译
什么是模板编译?
-
以前要渲染动态数据的话,都是服务器端渲染好了然后返回给客户端。类似于这样的模板(存在服务器上面,用户不可见的)
copy<!-- 模板文件 --> <div>姓名 <%= name %></div> <div>是否是会员 <%= isVip %></div>
-
然后服务器可以从数据库查询出姓名已经是否为vip,然后替换上面的模板后,转化成如下的结果返回给前端。
copy<!-- 渲染完成发送给浏览器端的html文件 --> <div>姓名 李四</div> <div>是否是会员 是</div>
-
具体的模板编译代码可以参考nodejs端的ejs或者python django端。
-
不过现在前端框架vue也流行这样的做法,将模板以及数据发送到前端,让用户的浏览器完成数据渲染。这样可以大大降低服务器渲染的压力。
如何在前端实现这样的模板编译?
目标实现
copy
const template = `
<div>姓名 <%= name %></div>
<div>是否是会员 <%= isVip %></div>
`;
render(template)({name: '李四', 'isVip': '是'}) /* ' <div>姓名 李四</div> <div>是否是会员 是</div> ' */
const template = `
<ul>
<% for (var i = 0, l = list.length; i < l; i ++) { %>
<li>用户: <%=list[i].user%>/ 网站:<%=list[i].site%></li>
<% } %>
</ul>
`;
render(template)({list: [{user: 'aaa', site: 'www.baidu.com'}, {user: 'bbb', site: 'www.baidu2.com'}]}))
/* '<ul><li>用户:aaa/ 网站:www.baidu.com</li><li>用户:bbb/ 网站:www.baidu2.com</li></ul>' */
编译语言方式实现
词法阶段(lex, scan)
-
将传进来的字符串转为一个个的token, 比如 ‘
姓名’ 是文本部分, <%= 是模板开始标记, %>是模板结束标记,<% 是 js语法开始标记。 -
做好这几类的分类,然后push到一个数组里即可,这个数组将会继续在 解析阶段被转化成 ast结构。
copy
let position = 0;
let tokens = [];
let openScript = false;
let openTemplate = false;
while(position < text.length){
const currentChar = text[position];
switch(currentChar){
case '<':
{
const start = position;
position ++;
if (
text[position] == '%'
){
position ++;
if (text[position] == '='){
position ++;
tokens.push({
type: 'openTemplate',
text: text.slice(start, position)
});
openTemplate = true;
break;
}else{
tokens.push({
type: 'openScript',
text: text.slice(start, position)
});
openScript = true;
break;
}
}
while(
!((text[position] == '<' && text[position+1] == '%' ) ||
(text[position] == '%' && text[position+1] == '>' ))){
if (text[position]=== undefined)break;
position++;
}
tokens.push({
type: 'text',
text: text.slice(start, position)
});
break;
}
case '%':
{
const start = position;
position ++;
if(
text[position] == '>'
){
position ++;
if (openScript){
openScript = false;
tokens.push({
type: 'closeScript',
text: text.slice(start, position)
});
}else if (openTemplate){
openTemplate = false;
tokens.push({
type: 'closeTemplate',
text: text.slice(start, position)
});
}else{
tokens.push({
type: 'singleClose',
text: text.slice(start, position)
});
}
break;
}else{
tokens.push({
type: 'text',
text: text.slice(start, position)
});
break;
}
}
default:
{
const start = position;
while(
!((text[position] == '<' && text[position+1] == '%' ) ||
(text[position] == '%' && text[position+1] == '>' ))){
if (text[position]=== undefined)break;
position++;
}
if (openScript){
tokens.push({
type: 'scriptText',
text: text.slice(start, position)
});
}else if (openTemplate){
tokens.push({
type: 'templateText',
text: text.slice(start, position)
});
}else{
tokens.push({
type: 'text',
text: text.slice(start, position)
});
}
break;
}
}
}
词法阶段代码实现
- 这里有一个注意点,当进入 <%= 或者 <% 应该提供flag, 这样解析文本就知道是当成模板处理还是普通文本处理了。
copy
const ast = [];
for (let i=0; i<tokens.length; i++){
const {type, text} = tokens[i];
switch(type){
case 'text':
ast.push(tokens[i]);
break;
case 'openTemplate':
{
let template = '';
if (tokens[i+1].type === 'templateText'){
i++;
template = tokens[i].text;
}else if (tokens[i+1].type === 'closeTemplate'){
i++;
}else{
console.error('缺少匹配的 %>')
}
ast.push({type: 'template', text: template});
break;
}
case 'openScript':
{
let template = '';
if (tokens[i+1].type === 'scriptText'){
i++;
template = tokens[i].text;
}else if (tokens[i+1].type === 'closeScript'){
i++;
}else{
console.error('缺少匹配的 %>')
}
ast.push({type: 'script', text: template});
break;
}
}
}
console.log(ast);
解析阶段(parser)
- 将上一个阶段的token数据转为树状结构,主要是 <%= 的token跟 内部的模板以及 %>是一起的,所以在这个阶段,这三个结构应该放在一起。
转为对应的js(emit)
- 通过上面的ast生成对应的js代码
copy
let source = `let __outer='';\n`;
for (let i=0; i<ast.length; i++){
const {type, text} = ast[i];
if (type === 'text'){
source += `__outer+='${text.trim()}';\n`
continue
}
if (type === 'script'){
source += `\n${text}\n`;
continue
}
if (type === 'template'){
source += `__outer+=${text};`;
}
}
return new Function(`
with(this){
${source}
return __outer;
}
`)
完整实现
copy
function template(text){
let position = 0;
let tokens = [];
let openScript = false;
let openTemplate = false;
while(position < text.length){
const currentChar = text[position];
switch(currentChar){
case '<':
{
const start = position;
position ++;
if (
text[position] == '%'
){
position ++;
if (text[position] == '='){
position ++;
tokens.push({
type: 'openTemplate',
text: text.slice(start, position)
});
openTemplate = true;
break;
}else{
tokens.push({
type: 'openScript',
text: text.slice(start, position)
});
openScript = true;
break;
}
}
while(
!((text[position] == '<' && text[position+1] == '%' ) ||
(text[position] == '%' && text[position+1] == '>' ))){
if (text[position]=== undefined)break;
position++;
}
tokens.push({
type: 'text',
text: text.slice(start, position)
});
break;
}
case '%':
{
const start = position;
position ++;
if(
text[position] == '>'
){
position ++;
if (openScript){
openScript = false;
tokens.push({
type: 'closeScript',
text: text.slice(start, position)
});
}else if (openTemplate){
openTemplate = false;
tokens.push({
type: 'closeTemplate',
text: text.slice(start, position)
});
}else{
tokens.push({
type: 'singleClose',
text: text.slice(start, position)
});
}
break;
}else{
tokens.push({
type: 'text',
text: text.slice(start, position)
});
break;
}
}
default:
{
const start = position;
while(
!((text[position] == '<' && text[position+1] == '%' ) ||
(text[position] == '%' && text[position+1] == '>' ))){
if (text[position]=== undefined)break;
position++;
}
if (openScript){
tokens.push({
type: 'scriptText',
text: text.slice(start, position)
});
}else if (openTemplate){
tokens.push({
type: 'templateText',
text: text.slice(start, position)
});
}else{
tokens.push({
type: 'text',
text: text.slice(start, position)
});
}
break;
}
}
}
console.log(tokens)
const ast = [];
for (let i=0; i<tokens.length; i++){
const {type, text} = tokens[i];
switch(type){
case 'text':
ast.push(tokens[i]);
break;
case 'openTemplate':
{
let template = '';
if (tokens[i+1].type === 'templateText'){
i++;
template = tokens[i].text;
}else if (tokens[i+1].type === 'closeTemplate'){
i++;
}else{
console.error('缺少匹配的 %>')
}
ast.push({type: 'template', text: template});
break;
}
case 'openScript':
{
let template = '';
if (tokens[i+1].type === 'scriptText'){
i++;
template = tokens[i].text;
}else if (tokens[i+1].type === 'closeScript'){
i++;
}else{
console.error('缺少匹配的 %>')
}
ast.push({type: 'script', text: template});
break;
}
}
}
console.log(ast);
// let source = `
// function render(obj){
// let result = ``;
// with(obj){
// result +=
// for (var i = 0, l = list.length; i < l; i ++) {
// }
// }
// }
// `;
let source = `let __outer='';\n`;
for (let i=0; i<ast.length; i++){
const {type, text} = ast[i];
if (type === 'text'){
source += `__outer+='${text.trim()}';\n`
continue
}
if (type === 'script'){
source += `\n${text}\n`;
continue
}
if (type === 'template'){
source += `__outer+=${text};`;
}
}
return new Function(`
with(this){
${source}
return __outer;
}
`)
}
const render = template(`<ul>
<% for (var i = 0, l = list.length; i < l; i ++) { %>
<li>用户: <%=list[i].user%>/ 网站:<%=list[i].site%></li>
<% } %>
</ul>`);
console.log(render.call({list: [{user: 'aaa', site: 'www.baidu.com'}, {user: 'bbb', site: 'www.baidu2.com'}]}));
console.log(template(`<li>用户: <%=user%>/ 是否为vip: <%=isVip%>
`).call({'user': 'aaa', 'isVip': "是"}))
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步