>웹 프론트엔드 >JS 튜토리얼 >간단한 js 템플릿 엔진 작성 방법

간단한 js 템플릿 엔진 작성 방법

一个新手
一个新手원래의
2017-10-20 10:30:032446검색

이전에는

JS 템플릿 엔진이 정말 많습니다. 예전에는 art-template을 자주 사용했고, 가끔 vue를 템플릿 엔진으로 사용하기도 했습니다.

까지...

연초에는 아직 이전 프로젝트 팀에 있었습니다. 당시 코드 사양은 [외부 코드]를 무단으로 사용할 수 없습니다, 囧였습니다.

필요하면 쓰지만 나중에는 어떤 이유로 사용되지 않습니다. 나중에는 우리가 직접 생산 라인을 나누어서 일련의 빌드를 만들었고, 몇 달 동안 사용해 본 후 저는 이 작은 코드를 좀 더 대중적인 표준에 맞게 다시 작성하여 모두와 공유했습니다.

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

Syntax

가장 먼저 할 일은 템플릿 구문을 선택하는 것이고, 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 생성자를 사용하여 생성된 함수는 생성된 컨텍스트에서 클로저를 생성하지 않으며 일반적으로 전역 범위에서 생성됩니다.
이러한 함수를 실행할 때 자체 지역 변수와 전역 변수에만 액세스할 수 있으며 호출되는 Function 생성자에 의해 생성된 컨텍스트 범위에는 액세스할 수 없습니다. (MDN)

new Function을 사용하여 동적으로 생성된 함수 정의 js 문을 실행하는 함수를 동적으로 생성할 수 있습니다. 🎜
  • 🎜새 함수를 통해 생성된 함수는 전역 범위를 갖습니다. 🎜
  • 🎜매개변수를 전달하는 방법에는 세 가지가 있습니다: 변수를 전역적으로 입력(말도 안되는), 함수별로 매개변수 전달, 호출/적용 사용 함수의 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 변수 자리 표시자🎜, c1f9a78d7d2cc9c5a069fc7eff4fb06c 패키지에서 나머지는 연결될 일반적인 🎜html 문자열🎜 부분입니다. 🎜🎜즉, 정규식을 사용하여 찾아야 하는 부분은 세 가지 유형입니다. 🎜
    1. 🎜a4558806285d5b7820bdaa0b90aa4d26 논리 부분 js 콘텐츠🎜
    2. 🎜4eed03230346664e54006e16f8581c1e 자리 표시자 부분의 js 콘텐츠🎜
    3. 🎜기타 일반 텍스트 Content🎜
    🎜 두 번째 항목인 js 자리 표시자 부분도 이어진 텍스트입니다. 그래서 이들을 하나로 묶을 수 있습니다. 즉, js 부분접합 부분입니다. 🎜🎜정규 추출🎜

    물론 정규식을 선택하세요!🎜🎜여기에서는 먼저 의사 배열의 내용과 브라우저 콘솔이 의사 배열을 처리하는 방법을 확장하겠습니다. :🎜🎜🎜🎜너무 멀리 가지 말고 결론만 말씀드리겠습니다. 🎜🎜 🎜 int 유형의 length 속성🎜이 있는 한, 🎜function 유형의 splice🎜 속성이 있습니다. 그러면 브라우저는 그것이 배열이라고 생각할 것입니다. 🎜🎜내부의 다른 속성이 인덱스별로 정렬되면 배열의 항목처럼 콘솔에 표시될 수도 있습니다. 🎜🎜 이 판단 방법을 🎜오리 타이핑🎜이라고 합니다. 뭔가가 오리처럼 보이고 오리처럼 꽥꽥거리면 오리입니다. 0_o🎜🎜텍스트로 돌아가서, 템플릿의 여러 js 로직이 필요합니다. 순차적으로 추출됩니다. 🎜🎜각 추출마다 추출된 내용을 얻어야 합니다. 이번에는 마지막 인덱스 항목을 일치시킵니다(텍스트 내용을 들어 올리는 데 사용). 그래서 저는 RegExp.prototype.exec를 선택했습니다. 🎜🎜🎜🎜예를 들어, RegExp.prototype.exec 무엇 반환되는 것은 컬렉션(의사 배열)이며 유형은 다음과 같습니다. 🎜

    🎜
    속성/색인 설명
    [0]🎜 모두 일치 strings🎜🎜
    [1],...[n]🎜 괄호 안의 그룹 캡처🎜🎜
    index 🎜<td>일치하는 문자는 원래 문자열의 0부터 시작하는 인덱스에 있습니다🎜🎜</td> <tr><td> <code>입력🎜🎜원래 문자열🎜🎜🎜🎜

    通过这样,就可以拿到匹配到的 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.