视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
简易js模板引擎写法
2020-11-27 20:11:56 责编:小采
文档


前面

js 模板引擎有很多很多,我以前经常用 art-template ,有时候也会拿 vue 来当模板引擎用。

直到......

年初的时候,我还在上个项目组,那时候代码规范是未经允许不能使用 【外部代码】,囧 。

有了需求,那么就去写吧,但是后来因为一些原因没用上。后来分了产线,自己搭了一套构建,用了几个月感觉挺爽,把这小段代码按照比较大众的规范重写,跟大家分享下。

https://github.com/shalldie/mini-tpl

语法

首先是选择模板语法,ejs语法是首选,因为大众,更无需去学习指令型模板引擎的那些东西。

如果写过 jsp 或者 asp/asp.net 的可以直接上手。

怎么用它?

我要这么用

<body>
 <p id="root"></p>
 <script id="tplContent" type="text/html">
 <ul>
 <% for(var i=0; i<data.length; i++){
 var item = data[i];
 if(item.age < 30){%>
 <li>我的名字是<%=item.name%>,我的年龄是<%=item.age%></li>
 <%}else{%>
 <li>my name is <%=item.name%>,my age is a sercet.</li>
 <%}%>
 <% } %>
 </ul>
 </script>
 <script src="../build/mini-tpl.min.js"></script>
 <script>
 var data = [{ name: 'tom', age: 12 }, { name: 'lily', age: 24 }, { name: 'lucy', age: 55 }];
 var content = document.getElementById('tplContent').innerHTML;
 var result = miniTpl(content, data);
 document.getElementById('root').innerHTML = result;
 </script>
 </body>

想要这么用,那么就分析一下怎么才能实现。

new Function

 1 const content = 'console.log("hello world");'; 
 2 
 3 let func = new Function(content); 
 4 
 5 func(); // hello world
new Function ([arg1[, arg2[, ...argN]],] functionBody)

functionBody 一个含有包括函数定义的JavaScript语句的字符串

使用Function构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。
当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,不能访问Function构造器被调用生成的上下文的作用域。(MDN)

也就是说:

  1. 可以用 new Function 来动态的创建一个函数,去执行某动态生成的函数定义js语句。

  2. 通过 new Function 生成的函数,作用域在全局。

  3. 那么传参有3种:把变量放到全局(扯淡)函数传参用call/apply把值传给函数的this

最初我用的是 call 来传值,如今想了想不太优雅,换成了用参数传递。也就是这样:

const content = 'console.log(data);';
 
 let func = new Function('data', content);
 
 func('hello world'); // hello world

到此为止,雏形有了。下面来拆分。

模板拆分

先看模板:

<% for(var i=0; i<data.length; i++){ 
var item = data[i]; 
if(item.age < 30){%>
 <li>我的名字是<%=item.name%>,我的年龄是<%=item.age%></li>
 <%}else{%>
 <li>my name is <%=item.name%>,my age is a sercet.</li>
 <%}%>
 <% } %>

js 逻辑部分,由 <%%> 包裹, js 变量的占位,由 <%= %> 包裹,剩下的是普通的要拼接的html字符串部分。

也就是说,需要用正则找出的部分有3种:

  1. <%%> 逻辑部分的js内容

  2. <%=%> 占位部分的js内容

  3. 其它的纯文本内容

其中第2项,js占位的部分,也属于拼接文本。所以可以放在一起,就是 js部分拼接部分

正则提取

当然是选择正则表达式啊!

这里先跟大家扩展一下关于伪数组方面的内容,以及浏览器的控制台如何看待伪数组:

不扯远,直接说结论:

只要有 int类型的 length属性,有 function类型 的 splice属性。 那么浏览器就会认为他是一个数组。

如果里面的其它属性按照索引来排序,甚至还可以像数组里面的项那样在控制台展示出来。

这种判断方式叫 duck typing ,如果一个东西长得像鸭子,而且叫起来像鸭子,,,那么它就是鸭子 0_o

回到正文,这个需要多次从模板中,把 js逻辑部分 和 文本 依次提取出来。

对于每一次提取,都要获取提取出的内容,本次匹配最后的索引项(用于提起文本内容)。所以我选择了 RegExp.prototype.exec 。

举个例子,RegExp.prototype.exec 返回的是一个集合(伪数组),它的类型是这样的:

属性/索引描述
[0]匹配的全部字符串
[1],...[n]括号中的分组捕获
index匹配到的字符位于原始字符串的基于0的索引值
input原始字符串

通过这样,就可以拿到匹配到的 js 逻辑部分,并通过 index 和本次匹配到的内容,来获取每个js逻辑部分之间的文本内容项。

要注意,在全局匹配模式下,正则表达式会接着上次匹配的结果继续匹配新的字符串。

 /**
 * 从原始模板中提取 文本/js 部分
 * 
 * @param {string} content 
 * @returns {Array<{type:number,txt:string}>} 
 */
 function transform(content) {
 var arr = []; //返回的数组,用于保存匹配结果
 var reg = /<%(?!=)([\s\S]*?)%>/g; //用于匹配js代码的正则
 var match; 	 //当前匹配到的match
 var nowIndex = 0;	 //当前匹配到的索引 

 while (match = reg.exec(content)) {
 // 保存当前匹配项之前的普通文本/占位
 appendTxt(arr, content.substring(nowIndex, match.index));
 //保存当前匹配项
 arr.push({
 type: 1, //js代码
 txt: match[1] //匹配到的内容
 });
 //更新当前匹配索引
 nowIndex = match.index + match[0].length;
 }
 //保存文本尾部
 appendTxt(arr, content.substr(nowIndex));
 return arr;
 }

 /**
 * 普通文本添加到数组,对换行部分进行转义
 * 
 * @param {Array<{type:number,txt:string}>} list 
 * @param {string} content 
 */
 function appendTxt(list, content) {
 content = content.replace(/\r?\n/g, "\\n");
 list.push({ txt: content });
 }

得到了js逻辑项 和 文本内容 ,就可以把他们拼在一起,来动态生成一个function。要注意的是,文本内容中,包含 js占位项,这个地方要转换一下。

 /**
 * 模板 + 数据 =》 渲染后的字符串
 * 
 * @param {string} content 模板
 * @param {any} data 数据
 * @returns 渲染后的字符串
 */
 function render(content, data) {
 data = data || {};
 var list = ['var tpl = "";'];
 var codeArr = transform(content); // 代码分割项数组

 for (var i = 0, len = codeArr.length; i < len; i++) {
 var item = codeArr[i]; // 当前分割项

 // 如果是文本类型,或者js占位项
 if (!item.type) {
 var txt = 'tpl+="' +
 item.txt.replace(/<%=(.*?)%>/g, function (g0, g1) {
 return '"+' + g1 + '+"';
 }) + '"';
 list.push(txt);
 }
 else { // 如果是js代码
 list.push(item.txt);
 }
 }
 list.push('return tpl;');

 return new Function('data', list.join('\n'))(data);
 }

这样就完成了简易的模板引擎,不要觉得拼字符串慢。

在现代浏览器(IE8开始)中,特地对字符串的操作做了大量的优化,用 += 拼字符串,要比用数组 push 再 join 的方式快很多很多,即使放到IE7(IE6不清楚)中,我这里测试也是拼字符串快。。

下载本文
显示全文
专题