前端模板引擎大家都不陌生。在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) // "yellowgreenblue
"
字符串操作利器: 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'+ (list[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可以在不修改源数据的前提下新创建一个新对象,这个对象可以看做对源对象的一个映射,所有针对源对象的操作在经过这个代理对象的过程中,我们可以做出特定的操作。