本来说的是轻量级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(/
|
/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 : 84. 模板嵌套
一种是词法解析,一种是直接函数调用,当然最终肯定都是函数调用。
比如定义了一个函数为 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(/
/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.85009765625msJavaScript Micro-Templating
JavaScript template engine in just 20 lines
只有20行Javascript代码!手把手教你写一个页面模板引擎
template.js
各种JS模板引擎对比数据(高性能JavaScript模板引擎)
高性能JavaScript模板引擎原理解析
版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。


