zoukankan      html  css  js  c++  java
  • 轻量级前端模板

    本来说的是轻量级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模板引擎原理解析

  • 相关阅读:
    python encode和decode函数说明【转载】
    python进阶--打包为exe文件
    python进阶--文件读写操作
    Python标准库_ sys,random,time
    Vmware 克隆CentOS 网络IP配置
    Esxi主机虚拟机迁移注意事项
    VMware ESXI4.1 常用命令
    ESX与ESXi管理员必备25个命令
    vcredist作用
    vim基本命令之剪切复制粘贴替换
  • 原文地址:https://www.cnblogs.com/cloud-/p/8664648.html
Copyright © 2011-2022 走看看