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

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

我们的目标

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

ES6的模板字符串:``

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

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

${1 ? "真": "假"}

` console.log(str2); // "

" /** 执行map返回一个新数组 */ let age = 12 let str3 = `

${ ["yellow", "green", "blue"].map(item => "
"+item+"
").join("") }

` console.log(str3) // "

yellow
green
blue

"

字符串操作利器: 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
  • 解析注释节点, :
    <!--([\s\S]+?)-->/g
  • 实现一个正则解析字符串的函数:
  /**
   * 获取编译后的字符串
   * @param {string} originStr 原始字符串
   */
  function _compile(originStr) {
    // 去除换行符
    let matchStr = originStr.replace(/[\n]/g, '')
    matchStr = matchStr.replace(  /{{([\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 ``
        }
      } )
     return `let tpl = '${matchStr}'; return tpl;`
  }
  • 测试一下吧
let str = `
  
<% if(message){ %>
{{message}}
<% }else{ %>
暂无消息
<% } %>
    <% for(let i = 0; i
  • {{list[i]}}
<% } %>
` console.log(_compile(str)) /** 下面是输出结果, 好像可以作为字符串执行啊,但是内部的变量从哪来呢?从window获取吗? let tpl = '
'; if(message){ ;tpl +='
'+ (message) +'
'; }else{ ;tpl +='
暂无消息
'; } ;tpl +='
    '; for(let i = 0; i '; } ;tpl +='
'; 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 = `
  
<% if(message){ %>
{{message}}
<% }else{ %>
暂无消息
<% } %>
    <% for(let i = 0; i
  • {{list[i]}}
<% } %>
` let data = { id: 'kgm', list: ['red', 'purple'], message: '这是模板语法' } let templateStr= _compile(str) let matchStr = _render(templateStr, data) console.log(matchStr) // 输出结果如下, 可以所有的数据都已经绑定到了字符串中,嵌套的js代码也已经执行 // 我们已经可以直接把下面这段字符串绑定到dom节点中了 //
这是模板语法
  • red
  • purple
  • 思考

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

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

    数据代理:Proxy

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

    你可能感兴趣的:(实现一个响应式前端模板引擎)