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跟 内部的模板以及 %>是一起的,所以在这个阶段,这三个结构应该放在一起。

img

转为对应的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': "是"}))
posted @   re大法好  阅读(21)  评论(0编辑  收藏  举报
相关博文:
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起