此文已由作者杨帆授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
在这个模板化的速食编程时代,工程师们已经习惯了使用各种框架去实现需求,常常会陷入一种固有和机械化的编程模式,在我看来这是非常恐怖的一件事,因为这种状态常常会使人感到疲惫和厌倦,创新的能力和思维会消失殆尽。又回到那个经典的问题,是“干一行爱一行”还是“爱一行干一行”?细细想想,时刻调整自己的状态应对各种挑战是非常重要的。这是一篇前端狂热分子写的寻找最简实现方式历程的文章,欢迎各种更新更好的方法砸向我!下面是以第一人称描述的文章:
假设我有这样的数据:
{ info: { name: 'Yangfan', vip: true, level: 10, area: 'Hangzhou' }, books: [ {name: 'JavaScript高级程序设计', read: true}, {name: 'Node.js实战', read: true}, {name: 'Java程序设计', read: false} ], orders: [ {id: '1001', goods: "book1", state: "未发货"}, {id: '1002', goods: "book2", state: "已发货"} ] }
我需要根据一些条件渲染成不同的页面,我可以使用AngularJs等一些前端模板渲染框架,迅速完成手里的工作,就像这样:
{{!有用户信息!}} {{#if !!info}} <p>你好,{{info.name}}!</p> {{#if !!info.vip }} {{#if info.level < 5}} <p>普通会员</p> {{#elseif info.level >= 5 && info.level < 8}} <p>中级会员</p> {{#else}} <p>高级会员</p> {{/if}} {{#else}} <p>普通用户</p> {{/if}} <h3>阅读历史:</h3> {{!遍历阅读历史!}} {{#list books as book}} {{#if book.read}} {{book.name}}:已读 {{#else}} {{book.name}}:未读 {{/if}} {{/list}} <h3>购买信息:</h3> <table border="1"> {{!遍历订单信息!}} {{#list orders as order}} <tr> <td>{{order.id}}</td> <td>{{order.goods}}</td> <td>{{order.state.replace('发货','出库')}}</td> </tr> {{/list}} </table> {{/if}}
为什么我完成了手头工作,心情却难以平复?我非常好奇这些框架是怎样完成模板渲染的?在查看源代码之前,我喜欢自己思考一下,如果是我,我会怎样实现一样的功能。首先我认为他的工作机理是基于字符串加工的,只要我能有一些字符串的替换规律就能实现简单的模板工作,就像这样:
String.prototype._$inject = function (obj) { return this.replace(/{{(w+)}}/gi, function (matchs, key) { var __result = obj[key]; if (__result == undefined) { throw new Error('Object has no such key: ' + key); } else { return __result; } }); }
哈哈,没错我似乎找到了方法,可是继续深入的探究,我发现这样很难完成list和if的逻辑,我得静下心来,如果没有模板,我会怎样做?我肯定会把它套在function里 用一个for循环 和if判断来拼接一些字符串:
var _out = '';for (var i = 0; i < data.length; i++) { if (data.info.level < 5) { _out += '普通会员'; } _out += data.books[i].name; }
没错这样就能完成很复杂的逻辑,可是这样的代码可维护性和拓展性却很差,有一位工程师曾说过“代码是写给人看的,只是偶尔让计算机执行一下”,这样的代码明显可读性不如前端模板来的清晰爽快和风骚。我突然茅塞顿开,我可以用js反过来实现前端模板,让我的前端模板还是以字符串加工的方式进行,只不过在最后一步,并不是输出拼接好的字符串,而是把拼接好的字符串变成function执行一遍返回结果,这样就可以完成复杂的前端模板转换逻辑。我的第一反应是使用eval来执行我的字符串,可是eval的安全性实在太差了,我该怎么办呢?对了,还有一种我几乎没怎么使用过的方式
var myFunction = new Function("a", "b", "return a * b");
没错,function这样的声明,在这里实在是完美的介入。原生JS几乎提供给了我们所有的想象空间,不得不说基础扎实,才能走得更远!这样我的思路就理顺了,剩下的只需完成所有的方法逻辑,拼接组装我的目标函数就可以完成我的前端模板框架了。
以实现list方法为例:
首先声明list的方法调用: (我要匹配{{#list data as d}} xxx {{/list}} 这样的调用)
listStart: /{{#lists*([^}]*?)s*ass*(w*?)s*(,s*w*?)?}}/igm, listEnd: /{{/list}}/igm,
然后是我们要执行的目标函数:
'"use strict"; var _out = "";try { <%innerFunction%>";return _out;} catch(e) {throw new Error("pptpl: "+e.message);}'
在这里<%innerFunction%>就是我们所有拼接的逻辑层,推荐使用严格模式,记得要有错误提醒机制try和catch,_out就是执行完所有逻辑后的渲染好的html。注意这里的";return _out; 为什么return之前要有";? 这是因为我们要实现的逻辑有插值,list,if,else,else if,和注释,每一段都是一个新的字符串片段,要像C的链表一样有前后的对接逻辑,我约定所有的逻辑字符串片段都已 "; 开头 以 _out +=" 结尾,这样所有的片段都能以任何状态组装到一起。
接下来就是调用list方法时的 模板替换工作:
tpl // list expression .replace(_settings.listStart, function ($, _target, _object) { var _var = _object || 'value'; var _key = 'key' + _counter++; return '";~function() { for(var ' + _key + ' in ' + _target + ') {' + 'if(' + _target + '.hasOwnProperty(' + _key + ')) {' + 'var ' + _var + '=' + _target + '[' + _key + ']; _out += "' }) .replace(_settings.listEnd, '";}}}(); _out += "')
当用户渲染模板时 我的字符串function就会转成这样:
";~function() { for(var key0 in books) {if(books.hasOwnProperty(key0)) {var book=books[key0]; _out += "test";}}}(); _out += "
当然把用户的data加入到模板渲染函数中,也是有要求的,因为用户可能在任何地方插值,所以要在最开始的地方把data插入到字符串函数中,当然在list中插值时,要有局部变量。
var _variables = []; // 储存变量 for (var i = 0, l = _variables.length; i < l; i++) { var _variable = _variables[i].replace(/[.+]/g, ''); prefix += 'var ' + _variable + ' = _data.' + _variable + (i == l - 1 ? '||"' : '||"";'); }
不管在list中还是在"全局环境"中我们都要声明一次用户所要的变量,要保证用户的模板的不可控性,假设用户在list中进行插值,那么用户所插入的值有可能是data直属的变量,也可能是list as 某个变量的数值,很难只能判断用户插值的所属,所以最好在“全局环境”中声明一次并且在插值所属的list 循环中也要声明同名的变量,这样用户便能安全的插入变量
最后一步就是把用户输入的data放入到模板中,使我的字符串代码运行起来:
var _render = new Function('_data', _convert.replace(/<%innerFunction%>/g, prefix + _tpl));
return _render.call(this, _data);
对于其他的方法实现我就不一一说明了 完整的实现在这里对着移动端的流行,轻量化框架的需求也越来越多,完成这个,也算写了个轻量级的模板渲染工具。如果你也对某些功能的实现感兴趣,那么就动手实现属于你自己的它吧! keep moving forward! 请不要吝啬你的建议,谢谢~
最后要说是,对于前段模板工具,如果是以nodejs为服务的网站,我们也可以在用户浏览前进行预编译,所以最好留出供nodejs调用的接口
typeof(module) !== 'undefined' && module.exports ? module.exports = pptpl : window.pptpl = pptpl;
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 CEF与代理
【推荐】 一份ECMAScript2015的代码规范(上)
【推荐】 Puppeteer入门初探