js实现简易版模板引擎

79 阅读1分钟

1、基础js

  
    //还原html字
 function htmlUnEscape(str) {
        return str.replace(/$#39;|<|>|"|&/g, (match) => {
          switch (match) {
            case '<':
              return '<';
            case '&gt;':
              return '>';
            case '&quot;':
              return '"';
            case '&amp;':
              return '&';
            case "$#39;":
              return '\\';
          }
        });
      }
      //替换模板
      function myTemplate(str) {
        // 在一个 replace 完成全部替换
        var [interpolate, evaluate] = [/<%=(.+?)%>/g, /<%(.+?)%>/g] // interpolate插值 和 evaluate语句  开始和结束
        var matcher = new RegExp(`${interpolate.source}|${evaluate.source}|$`,   'g'  ) //RegExp正则表达式
        var index = 0,
          p = ''
        // 1. 处理JS的行终止符(\n),由于要把字符串代码通过new Function包装产生函数,其本质是解析传入的字符串,所以诸如`new Function("var a = 'ab\nc';")`直接报错,而是要`"var a = 'ab\\nc';"`,前者源代码文本就被换行了,导致了语法解析错误,JS一共有4个终止符,\r \n \u2028 \u2029,除了IE遵守这个标准,其他浏览器只把\n当作行终止符,其他都视作零宽空白字符
        // 2. 处理单引号和反斜杠这两个特殊字符
        // 最终:**把真正的 \r \n \u2028 \u2029 \' \\ 变成对应的文本形式,即从1个字符变成2个字符,从而防止这些特殊字符参与JS源代码的语法解析**
        // 有点难理解的地方是 '\\': '\\' 和 '\'': '\''
        // 这是因为单字符的 \' 和双字符的 \' 表现是一样的
        var escapes = {
          '\n': 'n',// 防止由于换行导致语法解析错误
          '\r': 'r',// 防止由于换行导致语法解析错误
          '\u2028': 'u2028',
          '\u2029': 'u2029',
          '\\': '\\',
          "'": "'",
        }
        var escapeReg = /[\n\r\u2028\u2029\\']/g
        var escapeChar = (match) => '\\' + escapes[match]
        // 换了一种实现方式,不再使用数组,直接使用字符串拼接(效率高了)
          
        str.replace(matcher, function (match, interpolate, evaluate, offset) {
          // 正则对象的lastIndex属性只有在开启g标志且在regexp.exec和regexp.test方法有效,指定下次匹配的开始位置,此属性可读可写,如果方法没找到任何匹配则自动将它设置0,而在这里,用index来模拟lastIndex的作用
          // 另外需要注意,matcher最后的`$`目的是匹配字符串结束位置,从而得到结束位置的offset,当`$`发生匹配时,match是空字符串,因为`$`是零宽断言,确实发生匹配但是没有匹配内容,故返回空字符串
 
          // 使用slice方法取子字符串的副本,确保str保持不变
          // 将本次匹配到的<%=xxx%>或<%xxx%>之前的文本进行特殊字符文本化
          p += str.slice(index, offset).replace(escapeReg, escapeChar)
 
          // 记录下次replace匹配的开始位置
          index = offset + match.length
 
          // 进行替换
          // 这里巧妙利用了正则表达式的 捕获分组 和 或运算
          // `/part1(group1)|part2(group2)|part3/g`这是上面matcher的结构,由于或运算的存在,只要三者之一匹配成功,整个正则表达式匹配成功,就会执行replace的回调函数,由于group1和group2必然要存在(因为它们写在正则表达式里面),那么其中某一个就得是undefined,如果是part3发生的匹配,那么group1和group2都是undefined
          if (interpolate) {
            p += `' + (${interpolate} || \'\') + '`
          } else if (evaluate) {
            p += `'; ${evaluate} p+='`
          }
 
          // 把匹配到的字符串原封不动地还回去,确保str保持不变
          return match
        })
        // 给p拼上头部和尾部的代码
        p = "var p = ''; with(data){ p+='" + p + "';} return p;"
        // 可以在`new Function`包上try-catch语句,避免创建函数失败
        return new Function('data', p)
      } 

2、模板中使用以上js,如果单独封装,请引入

<div id="app">
     
     <ol>
        <%for(let i = 0; i < users.length; i++ ){%>
          <li>
            <a href="<%=users[i].url%>">
              <%=users[i].name%>
              is
              <%=users[i].age%>
              years old.
            </a>
                <ul class="sub-menu">
                    <% if(users[i].age==1){%>
                       <%for(let i = 0; i < users.length; i++ ){%>
                          <li>
                            <a href="<%=users[i].url%>">
                              <%=users[i].name%>
                              is
                              <%=users[i].age%>
                              years old.
                            </a>
                                </li>
                        <% } %>
                     <% } %>
                </ul>
          </li>
        <% } %>
        </ol>
     
     
 </div>
<script src="https://www.jq22.com/jquery/jquery-3.3.1.js"></script>
  
<script>
var userListView =  htmlUnEscape($("#app").html());
//   alert(userListView);
  var userListData = [
    { name: 'nat', age: 1, url: 'http://localhost:3000/nat' },
    { name: 'jack', age: 22, url: 'http://localhost:3000/jack' },
  ] 
  var userListRender3 = myTemplate3(userListView) 
    // console.log('userListRender3', userListRender3)
  var res3 = userListRender3({ users: userListData }) 
 $("#app").html(res3);
  
</script>