前端模板引擎大家都不陌生。在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
- 解析注释节点, :
<!--([\s\S]+?)-->/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可以在不修改源数据的前提下新创建一个新对象,这个对象可以看做对源对象的一个映射,所有针对源对象的操作在经过这个代理对象的过程中,我们可以做出特定的操作。