轻量级前端模板
本来说的是轻量级ETemplate的实现,Git地址
说起模板引擎还是得提到jQuery之父John Resig的JavaScript Micro-Templating。
之前我这里有文章专门解读Micro-Templating源码。
其核心
- 标签解析
- 属性映射
- 函数构建
当然,因为Micro-Templating相当的短小,并没有增强的功能,比如:
- 模板嵌套
- 函数扩展
- 远程加载
- 错误捕捉和提示
1. 标签解析
一般情况下都是定义<% %>等类似这种标签,然后标签里面被认为是脚本,这和jsp,asp等是一样的思想。在前端一般是利用正则匹配去实现的。
比如看看下面模板
<script type="text/template" id='list'>
<h3>账户信息</h3>
<%if(logined) {%>
已登陆
<%} else{%>
未登陆
<%}%>
<p>欢迎来到IT世界</p>
</script>
开始标签为<%,结束标签为 %>,我们先按照开始标签<%拆分字符串,得到如下四组字符串
0:"↵ <h3>账户信息</h3>↵ "
1:"if(logined) {%>↵ 已登陆↵ "
2:"} else{%>↵ 未登陆↵ "
3:"}%>↵ <p>欢迎来到IT世界</p>↵
仔细瞅瞅,会发现,基本是两类情况
- 文本(html)
- 脚本 + %> + 文本(html)
我们再对子项用结尾标签 %>的话,你会发现第二类会再转为 脚本 + 文本(html),
这样下来,就直白很多了。 那么我先放下下面的代码短, 其中的parseHTML和parseJS是分别对文本(html)和脚本的处理。
function parse(tpl) {
let codes = [],
{
startTag,
endTag
} = config
// 按照开始标签拆分
let fragments = tpl.split(startTag);
for (var i = 0, len = fragments.length; i < len; i++) {
var fragment = fragments[i].split(endTag);
// 长度为1为文本
if (fragment.length === 1) {
codes.push(parseHTML(fragment[0]))
} else {
//长度大于1为2的是 脚本 + endTag + 文本
codes.push(parseJS(fragment[0]))
if (fragment[1]) {
codes.push(parseHTML(fragment[1]))
}
}
}
return codes.join('')
}
处理后的依旧是字符串,用来动态构建函数。
Micro-Templating本质思想也是这样,只不过正则利用得非常666。
2. 属性映射
属性映射,就是我传入JSON对象或者数组,模板可以获得其属性。比如传入的data如下
{
name: "Jack",
age: 18,
sex: "男"
}
在模板中是如何直接使用name属性的,而不是 ata.name
with
Micro-Templating使用的是with,都说with有性能问题,(-_-)
eval
构建 var = data[p]; 这种语句,然后eval
var data = {
name: "Jack",
age: 18,
sex: "男"
};
var ps = ''
for(var p in data){
ps += `var ${p} = data['${p}'];`
}
eval(ps)
new Function()参数传入
这种方法可能比难处理一点
var data = {
name: "Jack",
age: 18,
sex: "男"
};
new Function('name','age','console.log(name,age)')(data.name, data.age)
可能上面还是比较抽象,因为这并没有动态化,是的,我这里倒是有一个简单的case,
原理就是利用Object.keys, Object.values获取属性和值的数组,然后通过扩展运算符获取,
具体的逻辑如下, 当你复制这段代码,并执行的时候,会输出"name is :Jack, age is : 8",
这就说明属性被很好的展开了
const encode = function (code) {
return code.replace(/\r|\n/g, '')
.replace(/('|")/g, '\\$1')
}
// 获取属性名
const getProperties = function (obj = {}, include = false) {
return include ? Object.keys(obj).concat('_data_') : Object.keys(obj)
}
// 获取值
const getValues = function (obj = {}, include = false) {
return include ? Object.values(obj).concat(obj) : Object.values(obj)
}
// 创建动态参数函数
const getFunction = (function () {
function _getFunction(params, code) {
params = params.map(c => `'${c}'`).join()
const funStr = `return new Function(${params}, ${code})`
return (new Function(funStr))()
}
return function getFunction(...args) {
if (args.length < 0) {
return null
}
const code = args.pop()
return _getFunction([...args], `'return(\`${encode(code)}\`)'`)
}
})()
function getParamterNames(d) {
return getProperties(d, true)
}
function getParamterValues(d) {
return getValues(d, true)
}
function innerRender(tpl, data) {
var params = getParamterNames(data)
var values = getParamterValues(data)
return getFunction(...params, tpl)(...values)
}
var code = 'name is :${name}, age is : ${age}',
data = {
name: "Jack",
age: 18,
sex: "男"
};
innerRender(code, data)
//name is :Jack, age is : 8
解构 + eval , 本质还是eval
function spreadProperties(obj, name) {
return `var {${Object.keys(obj).join(',')}} = ${name};`
}
var data = {
name: "Jack",
age: 18,
sex: "男"
};
var ps = spreadProperties(data, 'data')
var code = `eval('${ps}') ;console.log(name, age, sex)`
var fn = new Function('data',code)
fn(data)
// Jack 18 男
3. 函数构建
在属性解析的时候已经说到了函数构建,其核心就是参数命令和值的传入,当然你也可以通过arguments来忽略参数命令问题,
比如修改为var ps = spreadProperties(data, 'arguments[0]'),
那么,你并不介意参数名是什么
function spreadProperties(obj, name) {
return `var {${Object.keys(obj).join(',')}} = ${name};`
}
var data = {
name: "Jack",
age: 18,
sex: "男"
};
var ps = spreadProperties(data, 'arguments[0]')
var code = `eval('${ps}') ;console.log(name, age, sex)`
var fn = new Function('data',code)
fn(data)
//name is :Jack, age is : 8
4. 模板嵌套
一种是词法解析,一种是直接函数调用,当然最终肯定都是函数调用。
比如定义了一个函数为 render(data,tplName), 参数data为数据, tpl为模板名字。
那么你在模板上 <% render(address,'address') %>就可以了。
当然为了方便我们调用,render上面可以大动手脚。
比如传入一个template的id值,内部编译和生成模板,然后使用。这么的话你调用可能就是 <% render(address,'#address') %>
<script id='address' type='text/html'>
<%province%>省<%city%>市
</script>
5. 函数扩展 && 模板注册
举例来说,我经常输出当前时间,用语句是怎么用?
<span>当前时间:<% new Date().toString() %> </span>
额,然后呢,当前你也可以在传入数据之前,先处理数据。但是多一种方式,多一种爽感。
所以我们可以类似如下提供registerFun方法,绑定到某个对象上面。在构建函数的时候,传入,你就可以直接使用了。
xTemplate.registerFun('getNowDate', function(){
var d = new Date();
return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' + ....
})
这里是为了方便模板处理数据,其根本原理还是属性解析。想想就明白了。
模板注册,本质是把字符串编译成了可执行函数,那么模板注册只是函数扩展的一种应用。
6. 远程加载
这个嘛,属于增强。比如
eTemplate.ajaxLoad('/demo/config/tpl/simpleNoTag.html', '/demo/config/data/simple.js', '#result')
// simple.js
{
"loc":"北京",
"com":"阿里",
"title":"开发"
}
//simpleNoTag.html
<div>
<div>${loc}</div>
<div>${com}</div>
<div>${title}</div>
<div>
//结果
北京
阿里
开发
上面的一些交代后,那我说我今天要实现的模板实现原理。
1. 标签解析
基本就是上面说的,除此以外,对文本部分的解析,采用了ES6的字符串传模板,算是增强了。
增加了parseHTML和parseJS的详细代码。
parseHTML: 利用ES6字符串模板,当然最文本中的`符号进行了转义。
parseJS:没有进行任何处理
/* 词法解析:Begin */
function parseHTML(html) {
const r = html.replace(/\n\r/g, ' ').replace('`', '\\`')
return !!r ? `codes.push(\`${r}\`);` : ''
}
function parseJS(code) {
return code
}
function parse(tpl) {
let codes = [],
{
startTag,
endTag
} = config
// 按照开始标签拆分
let fragments = tpl.split(startTag);
for (var i = 0, len = fragments.length; i < len; i++) {
var fragment = fragments[i].split(endTag);
// 长度为1为文本
if (fragment.length === 1) {
codes.push(parseHTML(fragment[0]))
} else {
//长度大于1为2的是 脚本 + endTag + 文本
codes.push(parseJS(fragment[0]))
if (fragment[1]) {
codes.push(parseHTML(fragment[1]))
}
}
}
return codes.join('')
}
2. 属性映射
采用的是 解构 + eval, 核心代码为, 这里把内置的函数或者自己注册的函数展开了。
内置函数builtFnCode,我们是明确知道属性的,每次添加和删除的函数的时候,都会去更新。
但我们并不知道"data"有哪些属性,所以动态展开。这里其实可以去考虑,是不是可以显示的传入属性值数组来提升性能呢?看好你们!
let code = `
let codes = [];
// 展开内置函数
${builtFnCode};
// 展开属性
eval(spreadProperties(__data__,'__data__'));
// 执行代码
${tpl};
return codes.join('')
`
相关代码
function spreadProperties(obj, name) {
if (isJSONObject(obj)) {
return `var {${Object.keys(obj).join(',')}} = ${name};`
}
return ''
}
const builtInFunctions = {
each,
log,
spreadProperties,
encode,
render
}
let builtFnCode = spreadProperties(builtInFunctions, BUILTINFN)
function refreshBuiltFnCode() {
builtFnCode = spreadProperties(builtInFunctions, BUILTINFN)
return builtFnCode
}
function compileRender(tpl) {
let code = `
let codes = [];
// 展开内置函数
${builtFnCode};
// 展开属性
eval(spreadProperties(__data__,'__data__'));
// 执行代码
${tpl};
return codes.join('')
`
try {
var render = new Function('__data__', BUILTINFN, code);
return function (data) {
return render(data, builtInFunctions)
}
} catch (e) {
e.code = `function anonymous(__data__, __builtFn__) {${code}}`
throw e;
}
}
3. 函数构建
当然是 new Function
new Function('data', BUILTINFN, code),有两个参数,"data"是数据本身,"BUILTINFN"("builtInFn")是内置和用户自己注册的函数宿主。 因为编译后渲染函数只需要data就能渲染,我们再用高阶函数封装一下。
function compileRender(tpl) {
let code = `
let codes = [];
// 展开内置函数
${builtFnCode};
// 展开属性
eval(spreadProperties(__data__,'__data__'));
// 执行代码
${tpl};
return codes.join('')
`
try {
var render = new Function('__data__', BUILTINFN, code);
return function (data) {
return render(data, builtInFunctions)
}
} catch (e) {
e.code = `function anonymous(__data__, __builtFn__) {${code}}`
throw e;
}
}
4. 模板嵌套
采用函数调用形式,不过如开始说的,提供了一些便捷的使用方式。
注册的模板函数,实际上是挂在builtInFunctions内置对象上面的,基于此就有缓存的概念了,不会多次编译渲染函数。
同时提供了getRenderFromStr和getRenderFromId方法,getRenderFromId可以提供一个模板id,内部会获取模板文本再调用getRenderFromStr生成渲染函数,然后缓存起来。
这就让我们可以有两种形式调用子模板。方便也是很重要的
// 方式一 已注册的模板名
<div>${render(data, 'renderAddress')}</div>
// 方式一 模板节点id
<div>${render(data, '#renderAddress')}</div>
相关代码
function getRenderFromCache(name) {
const fn = builtInFunctions[name]
if (name && isFunction(fn)) {
return fn
}
return null
}
function getRenderFromId(name, id) {
if (!id) {
id = name
name = id.slice(1)
}
// 检查缓存
let fn = getRenderFromCache(name)
if (fn) {
return fn
}
const tpl = doc.querySelector(id).innerHTML
return getRenderFromStr(name, tpl)
}
function getRenderFromStr(name, tpl) {
// 只传入name时, name为tpl
if (!tpl) {
tpl = name
name = undefined
}
// 检查缓存
let fn = getRenderFromCache(name)
if (fn) {
return fn
}
const code = parse(tpl)
fn = compileRender(code)
if (name) {
builtInFunctions[name] = fn
refreshBuiltFnCode()
}
return fn
}
function getRender(idOrName) {
if (idOrName.indexOf('#') === 0) {
return getRenderFromId(idOrName)
} else {
return getRenderFromCache(idOrName) || getRenderFromStr(idOrName)
}
}
5. 函数扩展
就是把方法注册到一个内置对象,并维护一些相关变量。
每次注册和取消注册的时候刷新内置函数扩展字符串。
这样做是有注意的地方的,一般情况是注册,不会取消注册。
但是注册函数后,编译了某个模板,然后取消注册,你再想想。
/**
* 註冊函數
* @param {函數名,也可以是對象} name
* @param {函數} fn
*/
eTemplate.registerFun = function (name, fn) {
if (isString(name) && isFunction(fn)) {
builtInFunctions[name] = fn
}
if (isObject(name)) {
for (let p in name) {
if (isFunction(name[p])) {
builtInFunctions[name] = name[p]
}
}
}
refreshBuiltFnCode()
}
/**
* 取消注册的函数
* @param {函数名} name
*/
eTemplate.unregisterFun = function (name) {
delete builtInFunctions[name]
refreshBuiltFnCode()
}
6. 远程加载
虽然叫做ajaxTemplete,实际上内部采用fetch来实现。通过对参数的判断,来实现类似java的重载。
这里有一个小地方有点意思,就是Promise顺序执行的时候,怎么保存中间执行结果。我能想到的是两种思路。
1. Promise返回对象,每次都是在这个对象上添加属性
2. 闭包
因为做了类似重载的实现,那么你调用的时候,可以类似下面的调用
eTemplate.ajaxTemplate('/demo/config/tpl/simpleNoTag.html',function(tpl){
console.log(tpl )
})
eTemplate.ajaxTemplate( 'demo2', '/demo/config/tpl/simpleNoTag.html',function(tpl){
console.log(tpl )
})
eTemplate.ajaxLoad('/demo/config/tpl/simpleNoTag.html', '/demo/config/data/simple.js')
eTemplate.ajaxLoad('/demo/config/tpl/simpleNoTag.html', '/demo/config/data/simple.js',function(tpl,d){
console.log(tpl,d)
})
eTemplate.ajaxLoad('/demo/config/tpl/simpleNoTag.html', '/demo/config/data/simple.js', '#result')
eTemplate.ajaxLoad('/demo/config/tpl/simpleNoTag.html', '/demo/config/data/simple.js', '#result' ,function(tpl,d){
console.log(tpl,d)
})
相关代码
function loadResource(url, options = {}) {
if (isJSONObject(url)) {
url = url.url
delete options.url
options = url
}
return fetch(url, options).then(res => res.text())
}
/**
*
* @param {模板名字} name
* @param {地址} url
* @param {回调} cb
*/
eTemplate.ajaxTemplate = function (name, url, cb) {
if (!name && !url && !cb) {
return
}
if (!cb && !url) {
url = name
name = undefined
cb = undefined
}
if (!cb && isFunction(url)) {
cb = url
url = name
name = undefined
}
return loadResource(url).then(function (tpl) {
if (name) {
eTemplate.register(name, tpl)
}
if (isFunction(cb)) {
cb(tpl)
}
return tpl
})
}
eTemplate.ajaxLoad = function (tplOption, dataOption, selector, cb) {
if (!dataOption || !tplOption) {
return
}
if (!cb && isFunction(selector)) {
cb = selector
selector = undefined
}
let data, tpl
loadResource(dataOption) // 加载数据
.then(function (text) {
data = JSON.parse(text)
}) // 转为json
.then(function () {
return loadResource(tplOption)
}) // 加载模板
.then(function (tplText) {
tpl = tplText
return tplText
})
.then(function (tplText) {
if (selector) {
Array.from(document.querySelectorAll(selector)).forEach(function (el) {
el.innerHTML = eTemplate(tpl, data)
})
}
if (isFunction(cb)) {
cb(tpl, data)
}
})
}
7. 错误捕捉和提示
目前处理比较弱,甚至不准确, 高性能JavaScript模板引擎原理解析 有提到方案,但是个人有点芥蒂。
try {
var render = new Function('__data__', BUILTINFN, code);
return function (data) {
return render(data, builtInFunctions)
}
} catch (e) {
e.code = `function anonymous(__data__, __builtFn__) {${code}}`
throw e;
}
完整代码Git地址
最后提一下,字符串拼接性能。
一种是数组push然后join
一种是字符串+=
先看一下chrome65 PC下的执行时间。千万(都不会数了)级别相差才明显。
其实吧,简单好用才是好。
for(var c =0 ; c < 10 ; c++){
console.time('arr_plus')
var arr = []
for(var i =0; i<10000000; i++){
arr.push(Math.random() + '')
}
arr.join('')
console.timeEnd('arr_plus')
}
//arr_plus: 5621.26611328125ms
//arr_plus: 5482.8779296875ms
//arr_plus: 5046.17236328125ms
//arr_plus: 5179.06689453125ms
//arr_plus: 5294.338134765625ms
//arr_plus: 5025.296875ms
//arr_plus: 5032.095947265625ms
//arr_plus: 5027.239990234375ms
//arr_plus: 5024.44873046875ms
//arr_plus: 5040.51611328125ms
另外一种是str+=
for(var c =0 ; c < 10 ; c++){
console.time('str_plus')
var str = ''
for(var i =0; i<10000000; i++){
str += Math.random() + ''
}
console.timeEnd('str_plus')
}
//str_plus: 7578.81787109375ms
//str_plus: 7479.97802734375ms
//str_plus: 7058.68115234375ms
//str_plus: 7150.984130859375ms
//str_plus: 7077.995849609375ms
//str_plus: 7063.9580078125ms
//str_plus: 6778.908203125ms
//str_plus: 7136.634033203125ms
//str_plus: 7117.470947265625ms
//str_plus: 6984.85009765625ms
JavaScript Micro-Templating
JavaScript template engine in just 20 lines
只有20行Javascript代码!手把手教你写一个页面模板引擎
template.js
各种JS模板引擎对比数据(高性能JavaScript模板引擎)
高性能JavaScript模板引擎原理解析