实现本身的前端模板轻量级框架

此文已由做者杨帆受权网易云社区发布。javascript

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


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


假设我有这样的数据:
java

          {
                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等一些前端模板渲染框架,迅速完成手里的工做,就像这样:node

            {{!有用户信息!}}
            {{#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}}

为何我完成了手头工做,心情却难以平复?我很是好奇这些框架是怎样完成模板渲染的?在查看源代码以前,我喜欢本身思考一下,若是是我,我会怎样实现同样的功能。首先我认为他的工做机理是基于字符串加工的,只要我能有一些字符串的替换规律就能实现简单的模板工做,就像这样:git

    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判断来拼接一些字符串:
github

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的安全性实在太差了,我该怎么办呢?对了,还有一种我几乎没怎么使用过的方式
express

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+款云产品! 

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


相关文章:
【推荐】 使用QUIC

相关文章
相关标签/搜索