簡易js模板引擎

js 模板引擎有不少不少,我之前常常用 art-template ,有時候也會拿 vue 來當模板引擎用。html

直到......vue

年初的時候,我還在上個項目組,那時候代碼規範是未經容許不能使用 【外部代碼】,囧 。git

有了需求,那麼就去寫吧,可是後來由於一些緣由沒用上。後來分了產線,本身搭了一套構建,用了幾個月感受挺爽,把這小段代碼按照比較大衆的規範重寫,跟你們分享下。github

 https://github.com/shalldie/mini-tpl正則表達式

首先是選擇模板語法,ejs語法是首選,由於大衆,更無需去學習指令型模板引擎的那些東西。數組

若是寫過 jsp 或者 asp/asp.net 的能夠直接上手。瀏覽器

 

 

我要這麼用

<body>
        <div id="root"></div>
        <script id="tplContent" type="text/html">
        <ul>
            <% for(var i=0; i<data.length; i++){
                var item = data[i];
                if(item.age < 30){%>
                    <li>個人名字是<%=item.name%>,個人年齡是<%=item.age%></li>
                <%}else{%>
                    <li>my name is <%=item.name%>,my age is a sercet.</li>
                <%}%>
            <% } %>
        </ul>
        </script>
        <script src="../build/mini-tpl.min.js"></script>
        <script>
            var data = [{ name: 'tom', age: 12 }, { name: 'lily', age: 24 }, { name: 'lucy', age: 55 }];
            var content = document.getElementById('tplContent').innerHTML;
            var result = miniTpl(content, data);
            document.getElementById('root').innerHTML = result;
        </script>
    </body>

 

想要這麼用,那麼就分析一下怎麼才能實現。閉包

    1 const content = 'console.log("hello world");';
    2 
    3 let func = new Function(content);
    4 
    5 func(); // hello world

 

new Function ([arg1[, arg2[, ...argN]],] functionBody)app

functionBody  一個含有包括函數定義的JavaScript語句的字符串asp.net

使用Function構造器生成的函數,並不會在建立它們的上下文中建立閉包;它們通常在全局做用域中被建立。
當運行這些函數的時候,它們只能訪問本身的本地變量和全局變量,不能訪問Function構造器被調用生成的上下文的做用域。(MDN)

也就是說:

  1. 能夠用 new Function 來動態的建立一個函數,去執行某動態生成的函數定義js語句。
  2. 經過 new Function 生成的函數,做用域在全局。
  3. 那麼傳參有3種:把變量放到全局(扯淡)函數傳參用call/apply把值傳給函數的this

 

最初我用的是 call 來傳值,現在想了想不太優雅,換成了用參數傳遞。也就是這樣:

const content = 'console.log(data);';
    
    let func = new Function('data', content);
    
    func('hello world'); // hello world

到此爲止,雛形有了。下面來拆分。

 

先看模板:

<% for(var i=0; i<data.length; i++){
        var item = data[i];
        if(item.age < 30){%>
            <li>個人名字是<%=item.name%>,個人年齡是<%=item.age%></li>
        <%}else{%>
            <li>my name is <%=item.name%>,my age is a sercet.</li>
        <%}%>
    <% } %>

 

js 邏輯部分,由 <%%> 包裹, js 變量的佔位,由 <%= %> 包裹,剩下的是普通的要拼接的html字符串部分。

也就是說,須要用正則找出的部分有3種:

  1. <%%> 邏輯部分的js內容
  2. <%=%> 佔位部分的js內容
  3. 其它的純文本內容

其中第2項,js佔位的部分,也屬於拼接文本。因此能夠放在一塊兒,就是 js部分拼接部分

 

固然是選擇正則表達式啊!

 

這裏先跟你們擴展一下關於僞數組方面的內容,以及瀏覽器的控制檯如何看待僞數組:

不扯遠,直接說結論:

只要有 int類型的 length屬性,有 function類型 的 splice屬性。 那麼瀏覽器就會認爲他是一個數組。

若是裏面的其它屬性按照索引來排序,甚至還能夠像數組裏面的項那樣在控制檯展現出來。

 

這種判斷方式叫 duck typing ,若是一個東西長得像鴨子,並且叫起來像鴨子,,,那麼它就是鴨子  0_o

 

 

回到正文,這個須要屢次從模板中,把 js邏輯部分 和 文本 依次提取出來。

對於每一次提取,都要獲取提取出的內容,本次匹配最後的索引項(用於提起文本內容)。因此我選擇了 RegExp.prototype.exec 。

 

舉個例子,RegExp.prototype.exec 返回的是一個集合(僞數組),它的類型是這樣的:

 

屬性/索引 描述
[0] 匹配的所有字符串
[1],...[n] 括號中的分組捕獲
index 匹配到的字符位於原始字符串的基於0的索引值
input 原始字符串

 

經過這樣,就能夠拿到匹配到的 js 邏輯部分,並經過 index 和本次匹配到的內容,來獲取每一個js邏輯部分之間的文本內容項。

要注意,在全局匹配模式下,正則表達式會接着上次匹配的結果繼續匹配新的字符串。

 

    /**
     * 從原始模板中提取 文本/js 部分
     * 
     * @param {string} content 
     * @returns {Array<{type:number,txt:string}>} 
     */
    function transform(content) {
        var arr = [];                 //返回的數組,用於保存匹配結果
        var reg = /<%(?!=)([\s\S]*?)%>/g;  //用於匹配js代碼的正則
        var match;   				  //當前匹配到的match
        var nowIndex = 0;			  //當前匹配到的索引        

        while (match = reg.exec(content)) {
            // 保存當前匹配項以前的普通文本/佔位
            appendTxt(arr, content.substring(nowIndex, match.index));
            //保存當前匹配項
            arr.push({
                type: 1,  //js代碼
                txt: match[1]  //匹配到的內容
            });
            //更新當前匹配索引
            nowIndex = match.index + match[0].length;
        }
        //保存文本尾部
        appendTxt(arr, content.substr(nowIndex));
        return arr;
    }

    /**
     * 普通文本添加到數組,對換行部分進行轉義
     * 
     * @param {Array<{type:number,txt:string}>} list 
     * @param {string} content 
     */
    function appendTxt(list, content) {
        content = content.replace(/\r?\n/g, "\\n");
        list.push({ txt: content });
    }

...

獲得了js邏輯項 和 文本內容 ,就能夠把他們拼在一塊兒,來動態生成一個function。要注意的是,文本內容中,包含 js佔位項,這個地方要轉換一下。

 

    /**
     * 模板 + 數據 =》 渲染後的字符串
     * 
     * @param {string} content 模板
     * @param {any} data 數據
     * @returns 渲染後的字符串
     */
    function render(content, data) {
        data = data || {};
        var list = ['var tpl = "";'];
        var codeArr = transform(content);  // 代碼分割項數組

        for (var i = 0, len = codeArr.length; i < len; i++) {
            var item = codeArr[i]; // 當前分割項

            // 若是是文本類型,或者js佔位項
            if (!item.type) {
                var txt = 'tpl+="' +
                    item.txt.replace(/<%=(.*?)%>/g, function (g0, g1) {
                        return '"+' + g1 + '+"';
                    }) + '"';
                list.push(txt);
            }
            else {  // 若是是js代碼
                list.push(item.txt);
            }
        }
        list.push('return tpl;');

        return new Function('data', list.join('\n'))(data);
    }

這樣就完成了簡易的模板引擎,不要以爲拼字符串慢。

在現代瀏覽器(IE8開始)中,特意對字符串的操做作了大量的優化,用 += 拼字符串,要比用數組 push 再 join 的方式快不少不少,即便放到IE7(IE6不清楚)中,我這裏測試也是拼字符串快。。。

 

模板引擎這東西我搜了一下園子裏面有很多,我這是炒炒冷飯。

造輪子這事偶爾爲之會提升成就感,可是幹什麼都要本身造就很caodan了。

附上 github 地址:https://github.com/shalldie/mini-tpl

 

但願你們錢途無量,少加班,能寫喜歡的代碼 :D

 

.

相關文章
相關標籤/搜索