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


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

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


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


    假设我有这样的数据:

              {
                    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入门初探

  • 相关阅读:
    进程的由来
    进程管理逻辑图
    OS的特征

    4.6 路由相关
    设计测试用例时应该考虑哪些方面,即不同的测试用例针对那些方面进行测试?
    如何测试一个纸杯?
    软件产品质量特性是什么?
    一个京东登录的安全漏洞
    文章、书阅读
  • 原文地址:https://www.cnblogs.com/163yun/p/9869323.html
Copyright © 2020-2023  润新知