實現本身的前端模板輕量級框架

此文已由做者楊帆受權網易雲社區發佈。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+款雲產品! 

更多網易技術、產品、運營經驗分享請點擊


相關文章:
【推薦】 CEF與代理
【推薦】 一份ECMAScript2015的代碼規範(上)
【推薦】 Puppeteer入門初探

相關文章
相關標籤/搜索