前端模版原理&实现(二)

【由简入深

前端模版的本质就是一个可以转换函数(渲染函数)的字符串。渲染函数放进一个充满数据的对象后,还原为一个全新的字符串。因此重点是如何构建一个渲染函数。最简单的方式就是正则,下面是一个轻型的填充数据的方法:

function format(str, object) {
    var array = Array.prototype.slice.call(arguments, 1);
    return str.replace(/\\?\#{([^{}]+)\}/gm, function(match, name) {
        if (match.charAt(0) == '\\')
            return match.slice(1);
        var index = Number(name)
        if (index >= 0)
            return array[index];
        if (object && object[name] !== void 0)
            return  object[name];
        return  '';
    });
}

format方法是通过#{ }来划分静态内容与动态内容的,一般来说它们称之为定界符(delimiter)。#{为前定界符,}为后界符,这个#{}其实是ruby风格的定界符。通常的定界符是<%%>{{}} 。通常在前定界符中还有一些修饰符号,比如=号,表示这个会输出到页面,-号,表示会去掉两旁的空白。
将下例,要编译成一个渲染函数

var tpl = '你好,我的名字啊{{name}}, 今年已经 {{info.age}}岁了'
var data = {
    name: "司徒正美",
    age: 20
}

大抵是这样:

var body = '你好,我的名字叫'+ data.name+ ', 今年已经 '+data.info.age+ '岁了'
var render = new Function('data', 'return '+ body)
var tpl = '你好,我的名字啊{{name}}, 今年已经 {{info.age}}岁了'
function tokenize(str){
  var openTag='{{';
  var closeTag='}}';
  var ret = [];
  do{
    var index = str.indexOf(openTag);
    index = index ===-1?str.length:index;
    var value = str.slice(0,index);
    // 抽取{{前面的静态内容
    ret.push({
      expr:value,
      type:'text'
    })
    //改变str字符串自身
    str = str.slice(index+openTag.length);
    if(str){
      index = str.indexOf(closeTag);
      var value= str.slice(0,index);
      //抽取{{与}}的动态内容
      ret.push({
        expr:value.trim(),//js逻辑两旁的空格可以省去
        type:'js'
      })
      //改变str字符串自身
      str = str.slice(index+closeTag.length);
    } 
   
  } while(str.length)
   return ret;
}
console.log(tokenize(tpl))

然后通过render方法将它们拼接起来

function render(str){
  var tokens = tokenize(str);
  var ret = [];
  for(var i = 0,token;token=tokens[i++];){
    if(token.type === 'text'){
      ret.push('"'+token.expr+'"');
    }else{
      ret.push(token.expr);
    }
  }
  return ret.join("+");
}

console.log(render(tpl))//"你好,我的名字啊"+name+", 今年已经 "+info.age+"岁了"

 这个方法还不完整,首先是在两旁加上双引号是不可靠的,万一里面还有双引号怎么办,因此,我们引入quote方法,当类型为文本时,ret.push(+quote(token.expr)+)。其次需要对动态部分的变量加上.data。怎么知道它是一个变量呢,我们回想一下变量的定义,就是以_,$或字母开头的字符串组合。为了简洁起见,我们暂时不会用中文情况。不过,info.age这个字符串里面,其实有两个字符串变量的子串,而我只需要在info前面加上data.。这时,我们需要设法匹配变量前,将对象的子级属性替换掉,替换成不符合变量的字符串,然后再替换回去。为此,我搞了一个dig与fill方法,将子级属性变成??12这样的字符串:

var quote = JSON.stringify
 var rident = /[$a-zA-Z_][$a-zA-Z0-9_]*/g
 var rproperty = /\.\s*[\w\.\$]+/g
 var number = 1
 var rfill = /\?\?\d+/g
 var stringPool = {}
 function dig(a) {
     var key = '??' + number++
     stringPool[key] = a
     return key
 }
 function fill(a) {
     return stringPool[a]
 }
 function render(str) {
     stringPool = {}
     var tokens = tokenize(str)
     var ret = []
     for (var i = 0, token; token = tokens[i++]; ) {
         if (token.type === 'text') {
             ret.push(quote(token.expr))
         } else {
             // 先去掉对象的子级属性,减少干扰因素
             var js = token.expr.replace(rproperty, dig)
             js = js.replace(rident, function (a) {
                 return 'data.' + a
             })
             js = js.replace(rfill, fill)
             ret.push(js)
         }
     }
     console.log("return " + ret.join('+'))
 }
render(tpl)
//
return "你好,我的名字叫"+data.name+", 今年已经 "+data.info.age+"岁了"

 

未待完续~

 

参照原文:

https://segmentfault.com/a/1190000006990480

http://www.codeceo.com/article/javascript-template-engine.html

https://blog.csdn.net/yczz/article/details/49585381

posted @ 2018-07-25 10:12  暖暖的风儿  阅读(136)  评论(0)    收藏  举报