对如今的前端来讲,模板是很是熟悉的概念。毕竟如今三大框架那么火,不会用框架还能叫前端吗🐶,而框架是一定有模板的。那咱们写的模板是如何转换成 HTML 显示在网页上的呢?前端
咱们先从简单的提及,静态模板通常用于须要 SEO 且页面数据是动态的网页。由前端编写好静态模板,后端负责将动态的数据和静态模板交给模板引擎,最终编译成 HTML 字符串返回给浏览器。这种时候咱们用到的模板引擎多是远古的 jsp,或是如今用的比较多的 pug(原来叫 jade)、ejs。后端
模板引擎作的就是编译模板的工做。它说白了就是一个函数:将模板字符串转换成 HTML 字符串。数组
咱们先写一个最简单的静态模板编译函数:浏览器
咱们的模板和数据以下:框架
const tpl = '<p>hello,我是{{name}},职业:{{job}}<p>' const data = { name: 'hugo', job: 'FE' }
那咱们想到的最简单的办法就是正则替换,固然咱们别忘了要把前缀加上,name
要转换成 data.name
jsp
function compile(tpl, data) { const regex = /\{\{([^}]*)\}\}/g const string = tpl.trim().replace(regex, function(match, $1) { if ($1) { return data[$1] } else { return '' } }) console.log(string) // <p>hello,我是hugo,职业:FE<p> } compile(tpl, data)
上面的编译函数在例子中是能够工做的,但要是我把模板和数据改一下呢?函数
const tpl = '<p>hello,我是{{name}},年龄:{{info.age}}<p>' const data = { name: 'hugo', info: { age: 26 } }
这个时候控制台打印的就是:rest
<p>hello,我是hugo,年龄:undefined<p>
由于 data["info.age"]
的值是 undefined
。因此咱们还要处理正则匹配到的字符串,这个时候再用正则已经很是很差作了。既然这样,不如就直接全改用字符串匹配:code
function compile(tpl) { let string = '' tpl = tpl.trim() while (tpl) { const start = tpl.indexOf('{{') const end = tpl.indexOf('}}') if (start > -1 && end > -1) { if (start > 0) { string += JSON.stringify(tpl.slice(0, start)) } string += '+ data.' + tpl.slice(start + 2, end).trim() + ' +' tpl = tpl.slice(end + 2) } else { string += JSON.stringify(tpl) tpl = '' } } console.log(string) // "<p>hello,我是"+ data.name +",年龄:"+ data.info.age +"<p>" return new Function('data', 'return ' + string) } compile(tpl)(data) // <p>hello,我是hugo,年龄:26<p>
这样咱们新的编译函数就能够处理 {{info.age}}
这种嵌套属性的状况了。上面的 JSON.stringify
做用是给字符串的两端加上 "
,而后转义字符串中的特殊字符。模板引擎
虽然咱们解决了嵌套属性的问题,但又面临更困难的问题,就是怎样让模板里插值支持像 {{ '名字是: ' + name }}
这样表达式。在这种状况下,咱们是很难在每一个正确的地方加 data.
前缀的,由于前缀只能加上变量前,而表达式里可能还有字符串。
with
语句咱们考虑最简单的处理方式,也就是不加前缀了,使用 with
语句指定变量的做用域。因此咱们只要编译后返回一个函数,在这个函数内使用 with
语句指定做用域,函数再返回 HTML 字符串。在下面的例子中,我使用的是 ejs
模板的语法:
const tpl = `<p>hello,个人<%= '名字是: ' + name %>,年龄:<%= info.age %><p>` const data = { name: 'hugo', info: { age: 26 } }
function compile(tpl) { const ret = [] tpl = tpl.trim() ret.push('var _data_ = [];') ret.push('with(data) {') while (tpl) { let start = tpl.indexOf('<%=') const end = tpl.indexOf('%>') if (start > -1 && end > -1) { if (start > 0) { ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');') } ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');') tpl = tpl.slice(end + 2) } else { ret.push('_data_.push(' + JSON.stringify(tpl) + ');') tpl = '' } } ret.push('}') ret.push('return _data_.join("")') return new Function('data', ret.join('\n')) } const fn = compile(tpl) fn(data) // <p>hello,个人名字是: hugo,年龄:26<p>
上面的编译函数将模板根据模板语法 <%=%>
分割成各个部分放入数组中,再将数组中的元素由换行符链接,成为 new Function
的函数体,生成的函数以下:
function(data/*``*/) { var _data_ = []; with(data) { _data_.push("<p>hello,个人"); _data_.push('名字是: ' + name); _data_.push(",年龄:"); _data_.push(info.age); _data_.push("<p>"); } return _data_.join("") }
咱们再将 data
做为参数传入这个函数就能够获得指望的 HTML 字符串。
如今咱们已经实现了可以编译插值是表达式的模板引擎。但咱们还差一个很是重要的功能,那就是编译模板中的语句,如:for
循环和 if
语句。要实现编译语句的功能,咱们必须将语句和插值区分开,所以要使用不一样的模板语法:语句用 <% %>
,插值则用<%= %>
。那咱们就能够将上面的编译函数稍微修改下,根据不一样的语法分别处理,就能够支持模板语句了:
const tpl = ` <p>hello,我是<%= name + '-seth' %>,年龄:<%= info.age %><p> <% if (info.age > 18 && info.age < 28){ %> <p>是个九零后中年人</p> <% } %> <h3>兴趣</h3> <ul> <% for (var i = 0; i < interests.length; i++) { %> <li><%= interests[i] %></li> <% } %> </ul> `
const data = { name: 'hugo', info: { age: 26 }, interests: ['movie'] }
function compile(tpl) { const ret = [] tpl = tpl.trim() ret.push('var _data_ = [];') ret.push('with(data) {') while (tpl) { let start = tpl.indexOf('<%') const end = tpl.indexOf('%>') if (start > -1 && end > -1) { if (start > 0) { ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');') } if (tpl.charAt(start + 2) === '=') { ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');') } else { ret.push(tpl.slice(start + 2, end)) } tpl = tpl.slice(end + 2) } else { ret.push('_data_.push(' + JSON.stringify(tpl) + ');') tpl = '' } } ret.push('}') ret.push('return _data_.join("")') return new Function('data', ret.join('\n')) }
const fn = compile(tpl) fn(data) // <p>hello,个人名字是: hugo,年龄:26<p> // <p>是个九零后中年人</p> // <h3>兴趣</h3> // <ul> // <li>movie</li> // </ul>
这个修改后的编译函数没什么好解释的,就是根据不一样的模板语法作不一样的处理,最终返回的函数以下:
function(data /*``*/ ) { var _data_ = []; with(data) { _data_.push("<p>hello,个人"); _data_.push('名字是: ' + name); _data_.push(",年龄:"); _data_.push(info.age); _data_.push("<p>\n"); if (info.age > 18 && info.age < 28) { _data_.push("\n <p>是个九零后中年人</p>\n"); } _data_.push("\n<h3>兴趣</h3>\n<ul>\n "); for (var i = 0; i < interests.length; i++) { _data_.push("\n <li>"); _data_.push(interests[i]); _data_.push("</li>\n "); } _data_.push("\n</ul>"); } return _data_.join("") }
这样咱们就已经完成了一个功能简单的模板引擎。