实现自己的前端模板轻量级框架

叁叁肆2018-10-29 10:45

此文已由作者杨帆授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


 在这个模板化的速食编程时代,工程师们已经习惯了使用各种框架去实现需求,常常会陷入一种固有和机械化的编程模式,在我看来这是非常恐怖的一件事,因为这种状态常常会使人感到疲惫和厌倦,创新的能力和思维会消失殆尽。又回到那个经典的问题,是“干一行爱一行”还是“爱一行干一行”?细细想想,时刻调整自己的状态应对各种挑战是非常重要的。这是一篇前端狂热分子写的寻找最简实现方式历程的文章,欢迎各种更新更好的方法砸向我!下面是以第一人称描述的文章:


假设我有这样的数据:

          {
                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: /{{#list\s*([^}]*?)\s*as\s*(\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+款云产品! 

更多网易技术、产品、运营经验分享请点击


相关文章:
【推荐】 用户研究如何获取更为真实的用户信息
【推荐】 Memcached Hash算法