实现一个响应式前端模板引擎

前端模板引擎大家都不陌生。在mvc开发模式下,为了修改页面节点,我们很多时候需要手动拼接字符串,非常繁琐且不好维护。
那让接下来让我们实现一个javascript模板引擎插件,解放双手吧!

我们的目标

  • 实现数据以及数据属性的绑定和获取
  • 实现一些简单js逻辑,如判断、循环、三元表达式等
  • 给dom事件的绑定方法,如点击、输入、鼠标事件等
  • dom上面绑定的方法可以操作数据,同时自动更新页面

ES6的模板字符串:``

模板字符串包裹在 反引号中,其中可通过 ${} 的语法进行插值,

/** 执行函数 */
// 模板字符串的插值可以执行函数,并把执行结果返回给当前字符串
// 执行下面这一段代码,页面输出:函数执行了吗
let str= `<div>${function(){console.log("函数执行了吗"); return 12}()}</div>`
console.log(str)  // "<div>12</div>"

/** 执行三元表达式 */
let str2 = `<p>${1 ?  "真": "假"}</p>`
console.log(str2);  // "<p>真</p>"

/** 执行map返回一个新数组  */
let age = 12
let str3 = `<h3>${
  ["yellow", "green", "blue"].map(item => "<div>"+item+"</div>").join("")
}</h3>`
console.log(str3) // "<h3><div>yellow</div><div>green</div><div>blue</div></h3>"

字符串操作利器: replace

模板引擎的基础就是操作字符串。

// 字符串的简单替换
let str = 'hello world!'
console.log(str.replace('o', '**' ))  // hell** world!
// 正则匹配
console.log(str.replace(/o/g, '**' )) // hell** w**rld!

// 正则组的捕获
let str2 = 'prev words<% if (?) { %>{{ hello world }}<% } %> back words'
let reg2 = /<%([\s\S]+?)%>|{{([\s\S]+?)}}/g
let res2 = str2.replace(reg2, (match, p1, p2, index) => {
  console.log(match, p1, p2, index)
  // 这个字符串总共匹配了3次,输出结构如下
  // <% if (?) { %>         undefined        if (?) {              10
  // {{ hello world }}        hello world      undefined       24
  // <%?%>                   undefined       }                   41
   if( p1) { // 匹配第一组
    return   `';${p1};tpl+='`
  } else if ( p2 ) {
    return `'+${p2}+'`
  }
})

// 最终返回的是一个可执行的字符串
let compiledStr = `let tpl = '${res2}'; return tpl;`
// let tpl = 'prev words'; if (?) { ;tpl+=''+ hello world +''; } ;tpl+=' back words'; return tpl;
console.log(compiledStr )   

把字符串作为js代码执行:Function

我们解析模板就是拆分字符串后拼接,最后当做js代码执行,已达到嵌入动态数据的目的

/** Function的执行与参数  */
// 最后一个参数是将要被执行的字符串,前面的所有参数都是要传入的数据
let data = {name: "kgm"}
let strFun= new Function('data', `alert(data.name); return data.name+"这段代码执行了啊"`)
let str = strFun(data)
// 页面先弹出框,再输出内容
console.log(str)  //kgm这段代码执行了啊

/** 配合使用with,改变作用域 */
let strFun2= new Function('data', `with(data){alert(name); return name+"这段代码执行了啊"}`)
let str2 = strFun(data)
// 执行结果同上
console.log(str2)

/** 给Function绑定作用域 */
let str3= new Function(`with(this){alert(name); return name+"这段代码执行了啊"}`).apply(data)
// 执行结果同上
console.log(str3)

实现模板解析的思路

  • 首先:我们需要定义一个可以解析字符串的正则
  • 解析变量,我们可以用{{ data }}这种双大括号方式解析,正则如下:
    /{{([\s\S]+?)}}/g
  • 解析JS表达式,我决定用<% if(true){ %>这种类标签的形式:
    /<%([\s\S]+?)%>/g
  • 解析注释节点, :
    &lt;!--([\s\S]+?)--&gt;/g
  • 实现一个正则解析字符串的函数:
  /**
   * 获取编译后的字符串
   * @param {string} originStr 原始字符串
   */
  function _compile(originStr) {
    // 去除换行符
    let matchStr = originStr.replace(/[\n]/g, '')
    matchStr = matchStr.replace(  /{{([\s\S]+?)}}|<%([\s\S]+?)%>|<!--([\s\S]+?)-->/g, 
      (match, str_var, str_js, str_com) => {
        if(str_var) {  // 匹配变量
          return `'+ (${str_var}) +'`
        } else if (str_js) { // 匹配js表达式
          return `';${str_js};tpl +='`
        } else if (str_com) { // 匹配注释节点
          return `<!-- ${str_com} -->`
        }
      } )
     return `let tpl = '${matchStr}'; return tpl;`
  }
  • 测试一下吧
let str = `
  <div>
    <% if(message){ %>
      <div>{{message}}</div>
    <% }else{ %>
      <div>暂无消息</div>
    <% } %>
    <ul id="{{id}}">
    <%  for(let i = 0; i<list.length; i++){ %>
      <li>{{list[i]}}</ul>
    <% } %>
    </ul>
    <!-- 这是注释啦 -->
  </div>
`
console.log(_compile(str))
/** 下面是输出结果, 好像可以作为字符串执行啊,但是内部的变量从哪来呢?从window获取吗?
let tpl = '  <div>    '; if(message){ ;tpl +='      <div>'+ (message) +'</div>    '; }else{ ;tpl +='      <div>暂无消息</div>    '; } ;tpl +='    <ul id="'+ (id) +'">    ';  for(let i = 0; i<list.length; i++){ ;tpl +='      <li>'+ (list[i]) +'</ul>    '; } ;tpl +='    </ul>    <!--  这是注释啦  -->  </div>'; return tpl;
*/

在模板中嵌入数据

  • 利用Function可以执行字符串
  • 利用with可以简化对象属性的调用
  • 利用apply可以绑定作用域的功能
  • 实现一个在字符串中嵌入数据并且绑定作用域的方法:
  /**
   * 绑定数据,获取渲染后的字符串
   * @param {string} matchStr 
   * @param {object} data 
   */
  function _render(matchStr, data) {
    return new Function(`with(this){${matchStr}}`).apply(data)
  }
  • 测试一下吧
let str = `
  <div>
    <% if(message){ %>
      <div>{{message}}</div>
    <% }else{ %>
      <div>暂无消息</div>
    <% } %>
    <ul id="{{id}}">
    <%  for(let i = 0; i<list.length; i++){ %>
      <li>{{list[i]}}</ul>
    <% } %>
    </ul>
    <!-- 这是注释啦 -->
  </div>
`
let data = {
  id: 'kgm', 
  list: ['red', 'purple'],
  message: '这是模板语法'
}
let templateStr= _compile(str)
let matchStr = _render(templateStr, data)
console.log(matchStr)
// 输出结果如下, 可以所有的数据都已经绑定到了字符串中,嵌套的js代码也已经执行
// 我们已经可以直接把下面这段字符串绑定到dom节点中了
// <div>          <div>这是模板语法</div>        <ul id="kgm">          <li>red</ul>          <li>purple</ul>        </ul>    <!--  这是注释啦  -->  </div>

思考

好像上面的代码已经实现了一个简单的模板解析功能了,但是我们还有很多问题:

  • 我们生成的字符串怎么绑定各种事件呢,能不能像Vue那样直接调用我们自己定义的事件呢?
  • 我们自己绑定的事件该怎样调用,如何传递参数,函数内部怎么修改我们自定义的data数据能?
  • data数据修改之后,怎么样及时触发页面更新呢

数据代理:Proxy

不同于es5的defineProperty,Proxy可以在不修改源数据的前提下新创建一个新对象,这个对象可以看做对源对象的一个映射,所有针对源对象的操作在经过这个代理对象的过程中,我们可以做出特定的操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343