轻量级前端模板

小编 2026-06-05 阅读:144 评论:0
本来说的是轻量级ETemplate的实现,Git地址说起模板引擎还是得提到jQuery之父Joh...

本来说的是轻量级ETemplate的实现,Git地址

说起模板引擎还是得提到jQuery之父John Resig的JavaScript Micro-Templating
之前我这里有文章专门解读Micro-Templating源码
其核心

  1. 标签解析
  2. 属性映射
  3. 函数构建

当然,因为Micro-Templating相当的短小,并没有增强的功能,比如:

  1. 模板嵌套
  2. 函数扩展
  3. 远程加载
  4. 错误捕捉和提示

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>↵  

仔细瞅瞅,会发现,基本是两类情况

  1. 文本(html)
  2. 脚本 + %> + 文本(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 : 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(/

/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模板引擎原理解析

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表