ホームページ  >  記事  >  ウェブフロントエンド  >  簡単なjsテンプレートエンジンの書き方

簡単なjsテンプレートエンジンの書き方

一个新手
一个新手オリジナル
2017-10-20 10:30:032414ブラウズ

以前

js テンプレート エンジンはたくさんありますが、私は art-template をよく使用しており、テンプレート エンジンとして vue を使用することもありました。

まで…

今年の初め、私はまだ前のプロジェクトチームに所属していましたが、その時のコード仕様は[外部コード]を無断で使用することはできませんでした。

必要があれば書きますが、後で何らかの理由で使用されません。その後、生産ラインを分割し、自分で一連のビルドを構築しました。数か月間使用した後、この小さなコードをより一般的な標準に従って書き直して、全員と共有することができました。

https://github.com/shalldie/mini-tpl

構文

最初にテンプレート構文を選択することです。一般の人はディレクティブのことを学ぶ必要がないため、ejs 構文が最初の選択肢ですテンプレートエンジン。

jsp または asp/asp.net を作成した場合は、直接開始できます。

どうやって使うの?

こんな風に使いたい

<body>
        <p id="root"></p>
        <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: &#39;tom&#39;, age: 12 }, { name: &#39;lily&#39;, age: 24 }, { name: &#39;lucy&#39;, age: 55 }];
            var content = document.getElementById(&#39;tplContent&#39;).innerHTML;
            var result = miniTpl(content, data);
            document.getElementById(&#39;root&#39;).innerHTML = result;
        </script>
    </body>

こんな風に使いたいなら、どうすれば実現できるかを分析してみましょう。

new Function

    1 const content = &#39;console.log("hello world");&#39;;    
    2 
    3 let func = new Function(content);    
    4 
    5 func(); // hello world
new Function ([arg1[, arg2[, ...argN]],] functionBody)

functionBody 関数定義を含む JavaScript ステートメントを含む <code>JavaScript语句的<strong>字符串</strong>

使用Function构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。
当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,不能访问Function构造器被调用生成的上下文的作用域。(MDN)

也就是说:

  1. 可以用 new Function 来动态的创建一个函数,去执行某动态生成的函数定义js语句。

  2. 通过 new Function 生成的函数,作用域在全局。

  3. 那么传参有3种:把变量放到全局(扯淡)函数传参用call/apply把值传给函数的this

最初我用的是 call 来传值,如今想了想不太优雅,换成了用参数传递。也就是这样:

const content = &#39;console.log(data);&#39;;
    
    let func = new Function(&#39;data&#39;, content);
    
    func(&#39;hello world&#39;); // 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 逻辑部分,由 a4558806285d5b7820bdaa0b90aa4d26 包裹, js 变量的占位,由 332000003288cabbdff89f9a8e5a919b 包裹,剩下的是普通的要拼接的html字符串部分。

也就是说,需要用正则找出的部分有3种:

  1. a4558806285d5b7820bdaa0b90aa4d26 逻辑部分的js内容

  2. 4eed03230346664e54006e16f8581c1e 占位部分的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的索引值
inputstring

Function コンストラクターを使用して生成された関数は、作成されたコンテキスト内にクロージャーを作成しません。通常、グローバル スコープ内で作成されます。
これらの関数を実行すると、独自のローカル変数とグローバル変数にのみアクセスでき、呼び出される関数コンストラクターによって生成されたコンテキストのスコープにはアクセスできません。 (MDN)

new Function を使用して、動的に生成された関数定義の js ステートメントを実行する関数を動的に作成できます。 🎜
  • 🎜 new Function によって生成された関数にはグローバル スコープがあります。 🎜
  • 🎜パラメータを渡すには 3 つの方法があります: 変数をグローバルに配置する (ナンセンス)関数でパラメータを渡すcall/apply を使用する関数の this に値を渡します。 🎜
  • 🎜最初は call を使って値を渡していたのですが、今考えてみるとあまりエレガントではなかったので、パラメータで渡すように変更しました。以上: 🎜
        /**
         * 从原始模板中提取 文本/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 });
        }
    🎜 以上です。プロトタイプはここにあります。以下で詳しく見ていきましょう。 🎜🎜テンプレートの分割🎜🎜 まずテンプレートを見てください: 🎜
        /**
         * 模板 + 数据 =》 渲染后的字符串
         * 
         * @param {string} content 模板
         * @param {any} data 数据
         * @returns 渲染后的字符串
         */
        function render(content, data) {
            data = data || {};
            var list = [&#39;var tpl = "";&#39;];
            var codeArr = transform(content);  // 代码分割项数组
    
            for (var i = 0, len = codeArr.length; i < len; i++) {
                var item = codeArr[i]; // 当前分割项
    
                // 如果是文本类型,或者js占位项
                if (!item.type) {
                    var txt = &#39;tpl+="&#39; +
                        item.txt.replace(/<%=(.*?)%>/g, function (g0, g1) {
                            return &#39;"+&#39; + g1 + &#39;+"&#39;;
                        }) + &#39;"&#39;;
                    list.push(txt);
                }
                else {  // 如果是js代码
                    list.push(item.txt);
                }
            }
            list.push(&#39;return tpl;&#39;);
    
            return new Function(&#39;data&#39;, list.join(&#39;\n&#39;))(data);
        }
    🎜 🎜js 論理部分🎜、a4558806285d5b7820bdaa0b90aa4d26 でラップされ、🎜js 変数プレースホルダー🎜、ff0e662c9e9868ace673dc689ab5bda4 パッケージ、残りは結合される通常の 🎜html 文字列🎜 部分です。 🎜🎜つまり、正規表現を使用して検索する必要がある部分は 3 種類あります: 🎜
    1. 🎜a4558806285d5b7820bdaa0b90aa4d26 論理部分の js コンテンツ🎜
    2. 🎜4eed03230346664e54006e16f8581c1e プレースホルダー部分の js コンテンツ🎜
    3. 🎜その他の プレーンテキストContent🎜
    🎜 2 番目の項目である js プレースホルダー部分も、結合されたテキストです。つまり、js 部分splicing 部分 をまとめることができます。 🎜🎜正規抽出🎜

    もちろん、正規表現を選択してください!🎜🎜ここでは、まず疑似配列の内容と、ブラウザ コンソールが疑似配列を処理する方法について詳しく説明します。:🎜🎜🎜🎜言い過ぎはやめて、結論だけ言いましょう。 🎜🎜int型のlength属性🎜がある限り、🎜function型のsplice🎜属性も存在します。 そうすれば、ブラウザはそれを配列であると認識します。 🎜🎜内部の他のプロパティがインデックスによって並べ替えられている場合、配列内の項目のようにコンソールに表示することもできます。 🎜🎜この判定方法は🎜ダックタイピング🎜と呼ばれるもので、何かがアヒルのように見え、アヒルのように鳴く場合、それはアヒルです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 = [&#39;var tpl = "";&#39;];
            var codeArr = transform(content);  // 代码分割项数组
    
            for (var i = 0, len = codeArr.length; i < len; i++) {
                var item = codeArr[i]; // 当前分割项
    
                // 如果是文本类型,或者js占位项
                if (!item.type) {
                    var txt = &#39;tpl+="&#39; +
                        item.txt.replace(/<%=(.*?)%>/g, function (g0, g1) {
                            return &#39;"+&#39; + g1 + &#39;+"&#39;;
                        }) + &#39;"&#39;;
                    list.push(txt);
                }
                else {  // 如果是js代码
                    list.push(item.txt);
                }
            }
            list.push(&#39;return tpl;&#39;);
    
            return new Function(&#39;data&#39;, list.join(&#39;\n&#39;))(data);
        }

    这样就完成了简易的模板引擎,不要觉得拼字符串慢。

    在现代浏览器(IE8开始)中,特地对字符串的操作做了大量的优化,用 += 拼字符串,要比用数组 push 再 join 的方式快很多很多,即使放到IE7(IE6不清楚)中,我这里测试也是拼字符串快。。。

    以上が簡単なjsテンプレートエンジンの書き方の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。