js模板引擎4--构造渲染函数

在上一篇中我们已经将模板解析为了一条条的js语句,那么只要把这些语句连起来,构造一个可执行函数,然后传入模板数据,就可以得到填充过数据的html片段。

// 构造渲染函数
function buildRender(scriptTokens) {
    var codeArr = ['function(' + DATA + ') {','"use strict";'];
    // 变量声明
    var varDeclare = 'var';
    variableContext.forEach(function(item, index) {
        if(index === 0) {
            varDeclare = 'var ' + item.name + ' = ' + item.value;
        } else {
            varDeclare += ', ' + item.name + ' = ' + item.value;
        }
    });
    codeArr.push(varDeclare);
    scriptTokens.forEach(function(item) {
        codeArr.push(item.code);
    });

    codeArr.push('return ' + OUT_CODE);
    codeArr.push('}');

    // 构造渲染函数  利用闭包特性注入顶级变量
    var render = (new Function(GLOBAL_DATA,"return " + codeArr.join('\n') + ";"))(globalThis);
    return render;
}

用例子测试一下:

var tplStr = `
<% if (isAdmin) { %>
    <!--fhj-->
    <h1><%=title%></h1>
    <ul>
        <% for (var i = 0; i < list.length; i ++) { %>
            <li>索引 <%= i + 1 %> :<%= list[i] %></li>
        <% } %>
    </ul>
    <% var a = list.name; console.log(a); %>
<% } %>
`;

var tplTokens = tplToToken(tplStr,reg);
var scripts = getScriptTokens(tplTokens);
console.log(buildRender(scripts).toString());

生成的渲染函数如下(没有格式化):

function($DATA) {
"use strict";
var isAdmin = $DATA.isAdmin, $RES_OUT = "", title = $DATA.title, i = $DATA.i, list = $DATA.list, a = $DATA.a, console = $GLOBAL_DATA.console
$RES_OUT += ""
if (isAdmin) {
$RES_OUT += "    <!--fhj-->    <h1>"
$RES_OUT += title
$RES_OUT += "</h1>    <ul>        "
for (var i = 0; i < list.length; i ++) {
$RES_OUT += "            <li>索引 "
$RES_OUT += i + 1
$RES_OUT += " :"
$RES_OUT += list[i]
$RES_OUT += "</li>        "
}
$RES_OUT += "    </ul>    "
var a = list.name; console.log(a);
$RES_OUT += ""
}
return $RES_OUT
}

传入数据试试:

var data = {
	title: '基本例子',
	isAdmin: true,
	list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他']
};
console.log(buildRender(scripts)(data));

输出结果(格式化后):

<!--fhj-->    
<h1>基本例子</h1>    
<ul>                    
    <li>索引 1 :文艺</li>                    
    <li>索引 2 :博客</li>                    
    <li>索引 3 :摄影</li>                    
    <li>索引 4 :电影</li>
    <li>索引 5 :民谣</li>                    
    <li>索引 6 :旅行</li>                    
    <li>索引 7 :吉他</li>            
</ul>

至此,我们已经初步实现了js模板引擎的功能。

把前面的代码优化一下:

;(function(globalThis) {
    var TPL_TOKEN_TYPE_STRING = 'string';
    var TPL_TOKEN_TYPE_EXPRESSION = 'expression';

    var jsTokens = {
        default: /((['"])(?:(?!\2|\\).|\\(?:\r\n|[\s\S]))*(\2)?|`(?:[^`\\$]|\\[\s\S]|\$(?!\{)|\$\{(?:[^{}]|\{[^}]*\}?)*\}?)*(`)?)|(\/\/.*)|(\/\*(?:[^*]|\*(?!\/))*(\*\/)?)|(\/(?!\*)(?:\[(?:(?![\]\\]).|\\.)*\]|(?![\/\]\\]).|\\.)+\/(?:(?!\s*(?:\b|[\u0080-\uFFFF$\\'"~({]|[+\-!](?!=)|\.?\d))|[gmiyu]{1,5}\b(?![\u0080-\uFFFF$\\]|\s*(?:[+\-*%&|^<>!=?({]|\/(?![\/*])))))|(0[xX][\da-fA-F]+|0[oO][0-7]+|0[bB][01]+|(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?)|((?!\d)(?:(?!\s)[$\w\u0080-\uFFFF]|\\u[\da-fA-F]{4}|\\u\{[\da-fA-F]+\})+)|(--|\+\+|&&|\|\||=>|\.{3}|(?:[+\-\/%&|^]|\*{1,2}|<{1,2}|>{1,3}|!=?|={1,2})=?|[?~.,:;[\](){}])|(\s+)|(^$|[\s\S])/g,
        matchToToken: function(match) {
            var token = {type: "invalid", value: match[0]}
            if (match[ 1]) token.type = "string" , token.closed = !!(match[3] || match[4])
            else if (match[ 5]) token.type = "comment"
            else if (match[ 6]) token.type = "comment", token.closed = !!match[7]
            else if (match[ 8]) token.type = "regex"
            else if (match[ 9]) token.type = "number"
            else if (match[10]) token.type = "name"
            else if (match[11]) token.type = "punctuator"
            else if (match[12]) token.type = "whitespace"
            return token
        }
    };
    var isJsKeywords = {
        reservedKeywords: {
            'abstract': true,
            'await': true,
            'boolean': true,
            'break': true,
            'byte': true,
            'case': true,
            'catch': true,
            'char': true,
            'class': true,
            'const': true,
            'continue': true,
            'debugger': true,
            'default': true,
            'delete': true,
            'do': true,
            'double': true,
            'else': true,
            'enum': true,
            'export': true,
            'extends': true,
            'false': true,
            'final': true,
            'finally': true,
            'float': true,
            'for': true,
            'function': true,
            'goto': true,
            'if': true,
            'implements': true,
            'import': true,
            'in': true,
            'instanceof': true,
            'int': true,
            'interface': true,
            'let': true,
            'long': true,
            'native': true,
            'new': true,
            'null': true,
            'package': true,
            'private': true,
            'protected': true,
            'public': true,
            'return': true,
            'short': true,
            'static': true,
            'super': true,
            'switch': true,
            'synchronized': true,
            'this': true,
            'throw': true,
            'transient': true,
            'true': true,
            'try': true,
            'typeof': true,
            'var': true,
            'void': true,
            'volatile': true,
            'while': true,
            'with': true,
            'yield': true
        },
        test: function(str) {
            return this.reservedKeywords.hasOwnProperty(str);
        }
    };
    
    var reg = {
        test: /<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g,
        // 对识别出的模板语法语句做进一步处理变量
        use: function (match, comment, output, code) {
            if(output === '=') { // 变量 / 表达式
                output = true;
            } else {
                output = false;
            }
            if(comment) { // 注释
                code = '/*' + code + '*/';
                output = false;
            }
        
            return {
                code: code,
                output: output
            }
        }
    };
    
    function Token(type, value, preToken) {
        this.type = type;
        this.value = value;
        this.script = null;
        if(preToken) {
            this.line = preToken.value.split('\n').length - 1 + preToken.line;
            this.start = preToken.end;
        } else {
            this.start = 0;
            this.line = 0;
        }
        this.end = this.start + value.length; // 包含起点start,不包含终点end
    }
    
    function CompileTemplate(tplTokens, globalRunTime) {
        this.variableContext = [];
        this.variableContextMap = {};
        this.scriptTokens = [];
        this.tplTokens = tplTokens;
        this.globalRunTime = globalRunTime;
        // 预置变量
        this.persetVariable = {
            OUT_CODE: '$RES_OUT',
            DATA: '$DATA',// 模板数据变量名
            GLOBAL_DATA: '$GLOBAL_DATA'// 全局变量名
        };
    
        this.init();
    }
    CompileTemplate.prototype = {
        constructor: CompileTemplate,
        init: function() {
            this.getScriptTokens();
        },
        // 从js表达式中提取出变量,用来构建执行上下文
        getVariableContext: function(code) {
            var that = this;
            var jsExpressTokens = code.match(jsTokens.default).map(function(item) {
                jsTokens.default.lastIndex = 0;
                return jsTokens.matchToToken(jsTokens.default.exec(item));
            }).map(function(item) {
                if(item.type === 'name' && isJsKeywords.test(item.value)) {
                    item.type = 'keywords';
                }
                return item;
            });
            // console.log(jsExpressTokens);
            // return;
            var ignore = false; // 跳过变量的属性
            var variableTokens = jsExpressTokens.filter(function(item) {
                if(item.type === 'name' && !ignore) {
                    return true;
                }
                ignore = item.type === 'punctuator' && item.value === '.';
                return false;
            });
            // console.log(variableTokens);
            // return;
            variableTokens.forEach(function(item) {
                var val;
                if(!Object.prototype.hasOwnProperty.call(that.variableContextMap, item.value)) {
                    if(item.value === that.persetVariable.OUT_CODE) {
                        val = '""';
                    } else if(Object.prototype.hasOwnProperty.call(that.globalRunTime, item.value)) {
                        val = that.persetVariable.GLOBAL_DATA + '.' + item.value;
                    } else {
                        val = that.persetVariable.DATA + '.' + item.value;
                    }
                    that.variableContextMap[item.value] = val;
                    that.variableContext.push({
                        name: item.value,
                        value: val
                    });
                }
           
            });
        },
    
        getScriptTokens: function() {
            var tplTokens = this.tplTokens;
            var scripts = [];
            for(var i = 0; i < tplTokens.length; i++) {
                var source = tplTokens[i].value.replace(/\n/g,'');
                var code = '';
                if(tplTokens[i].type === TPL_TOKEN_TYPE_STRING) {
                    code = this.persetVariable.OUT_CODE + ' += "' + source + '"';
                } else if(tplTokens[i].type === TPL_TOKEN_TYPE_EXPRESSION) {
                    if(tplTokens[i].script.output) {
                        code = this.persetVariable.OUT_CODE + ' += ' + tplTokens[i].script.code;
                    } else {
                        // js表达式
                        code = tplTokens[i].script.code;
                    }
                    this.getVariableContext(code);
                }
                scripts.push({
                    source: source,
                    tplToken: tplTokens[i],
                    code: code
                });
            }
            this.scriptTokens = scripts;
            return scripts;
        },
    
        // 构造渲染函数
        buildRender: function() {
            var codeArr = ['function(' + this.persetVariable.DATA + ') {','"use strict";'];
            var varDeclare = 'var';
            this.variableContext.forEach(function(item, index) {
                if(index === 0) {
                    varDeclare = 'var ' + item.name + ' = ' + item.value;
                } else {
                    varDeclare += ', ' + item.name + ' = ' + item.value;
                }
            });
            codeArr.push(varDeclare);
            this.scriptTokens.forEach(function(item) {
                codeArr.push(item.code);
            });
    
            codeArr.push('return ' + this.persetVariable.OUT_CODE);
            codeArr.push('}');
        
            // console.log(codeArr.join('\n'));
    
            var render = (new Function(this.persetVariable.GLOBAL_DATA,"return " + codeArr.join('\n') + ";"))(this.globalRunTime);
            return render;
        }
        
    };
    
    function tplToToken(str,reg) {
        var tokens = [], 
            match, 
            preToken, 
            index = 0;
        while((match = reg.test.exec(str)) !== null) {
            preToken = tokens[tokens.length - 1];
            // 如果匹配结果的开始下标比上一次匹配结果的结束下标大,说明两个模板语法之间有字符串
            if(match.index > index) {
                preToken = new Token(TPL_TOKEN_TYPE_STRING, str.slice(index,match.index), preToken);
                tokens.push(preToken);
            }
            preToken = new Token(TPL_TOKEN_TYPE_EXPRESSION, match[0], preToken);
            preToken.script = reg.use.apply(reg,match);
            tokens.push(preToken);
            // 保存本次匹配结果的结束下标
            index = match.index + match[0].length;
        }
    
        return tokens;
    }
    
    function templateRender(tplId,tplData) {
        var tplStr = document.getElementById(tplId).innerHTML;
        var tplTokens = tplToToken(tplStr,reg);
        var compiler = new CompileTemplate(tplTokens, globalThis);
    
        var render = compiler.buildRender();
    
        return render(tplData);
    }
    
    globalThis.templateRender = templateRender;
    globalThis.__templateRender = function(tplStr,tplData) {
        var tplTokens = tplToToken(tplStr,reg);
        var compiler = new CompileTemplate(tplTokens, globalThis);
    
        var render = compiler.buildRender();
    
        return render(tplData);
    }
    
})( typeof self !== 'undefined' ? self
: typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global : {});

遗留问题

前面提到的问题有两个:

  1. 模板内声明的变量会被作为$DATA的一个属性处理;
  2. 模板变量值的确定还有一种简单的方法;

模板内声明的变量

针对第一个问题,在不更换js解析器的情况下,没法完美解决 issue

模板变量值的确定

template.js中没有使用解析js语句提取变量的思路确定模板变量的值,而是遍历模板数据对象的键,然后使用eval函数运行变量声明语句,下面是相关源码(注意看headerCode部分):

function compiler(tpl, opt) {
    var mainCode = parse(tpl, opt);
    var headerCode = '\n' + 
    '    var html = (function (__data__, __modifierMap__) {\n' + 
    '        var __str__ = "", __code__ = "";\n' + 
    '        for(var key in __data__) {\n' + 
    '            __str__+=("var " + key + "=__data__[\'" + key + "\'];");\n' + 
    '        }\n' + 
    '        eval(__str__);\n\n';

    var footerCode = '\n' + 
    '        ;return __code__;\n' + 
    '    }(__data__, __modifierMap__));\n' + 
    '    return html;\n';

    var code = headerCode + mainCode + footerCode;
    code = code.replace(/[\r]/g, ' '); // ie 7 8 会报错,不知道为什么
    try {
        var Render = new Function('__data__', '__modifierMap__', code); 
        // Render.toString = function () {
        //     return mainCode;
        // }
        return Render;
    } catch(e) {
        e.temp = 'function anonymous(__data__, __modifierMap__) {' + code + '}';
        throw e;
    }  
}

依照这个思路把我们之前写的代码进行改造,删除jsTokensisJsKeywords,同时删除CompileTemplategetVariableContext方法:

;(function(globalThis) {
    var TPL_TOKEN_TYPE_STRING = 'string';
    var TPL_TOKEN_TYPE_EXPRESSION = 'expression';
    var reg = {
        test: /<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g,
        // 对识别出的模板语法语句做进一步处理变量
        use: function (match, comment, output, code) {
            if(output === '=') { // 变量 / 表达式
                output = true;
            } else {
                output = false;
            }
            if(comment) { // 注释
                code = '/*' + code + '*/';
                output = false;
            }
        
            return {
                code: code,
                output: output
            }
        }
    };
    
    function Token(type, value, preToken) {
        this.type = type;
        this.value = value;
        this.script = null;
        if(preToken) {
            this.line = preToken.value.split('\n').length - 1 + preToken.line;
            this.start = preToken.end;
        } else {
            this.start = 0;
            this.line = 0;
        }
        this.end = this.start + value.length; // 包含起点start,不包含终点end
    }
    
    function CompileTemplate(tplTokens, globalRunTime) {
        this.scriptTokens = [];
        this.tplTokens = tplTokens;
        this.globalRunTime = globalRunTime;
        // 预置变量
        this.persetVariable = {
            OUT_CODE: '$RES_OUT',
            GLOBAL_DATA: '$GLOBAL_DATA'// 全局变量名
        };
    
        this.init();
    }
    CompileTemplate.prototype = {
        constructor: CompileTemplate,
        init: function() {
            this.getScriptTokens();
        },
        getScriptTokens: function() {
            var tplTokens = this.tplTokens;
            var scripts = [];
            for(var i = 0; i < tplTokens.length; i++) {
                var source = tplTokens[i].value.replace(/\n/g,'');
                var code = '';
                if(tplTokens[i].type === TPL_TOKEN_TYPE_STRING) {
                    code = this.persetVariable.OUT_CODE + ' += "' + source + '"';
                } else if(tplTokens[i].type === TPL_TOKEN_TYPE_EXPRESSION) {
                    if(tplTokens[i].script.output) {
                        code = this.persetVariable.OUT_CODE + ' += ' + tplTokens[i].script.code;
                    } else {
                        // js表达式
                        code = tplTokens[i].script.code;
                    }
                }
                scripts.push({
                    source: source,
                    tplToken: tplTokens[i],
                    code: code
                });
            }
            this.scriptTokens = scripts;
            return scripts;
        },
    
        // 构造渲染函数
        buildRender: function() {
            var codeArr = ['function($DATA) {', 'var ' + this.persetVariable.OUT_CODE + ' = "";'];
            
            var varDeclare = "var varStatement = '';" + "\n"
            + "   for(var key in $DATA) {" + "\n"
            + "        varStatement += ('var ' + key + ' = ' + '$DATA[\"' + key + '\"]' + ';');" + "\n"
            + "    }" + "\n"
            + "    eval(varStatement);" + "\n"

            codeArr.push(varDeclare);
            this.scriptTokens.forEach(function(item) {
                codeArr.push(item.code);
            });
    
            codeArr.push('return ' + this.persetVariable.OUT_CODE);
            codeArr.push('}');
        
            // console.log(codeArr.join('\n'));

            var render = (new Function(this.persetVariable.GLOBAL_DATA,"return " + codeArr.join('\n') + ";"))(this.globalRunTime);
            return render;
        }
        
    };
    
    function tplToToken(str,reg) {
        var tokens = [], 
            match, 
            preToken, 
            index = 0;
        while((match = reg.test.exec(str)) !== null) {
            preToken = tokens[tokens.length - 1];
            // 如果匹配结果的开始下标比上一次匹配结果的结束下标大,说明两个模板语法之间有字符串
            if(match.index > index) {
                preToken = new Token(TPL_TOKEN_TYPE_STRING, str.slice(index,match.index), preToken);
                tokens.push(preToken);
            }
            preToken = new Token(TPL_TOKEN_TYPE_EXPRESSION, match[0], preToken);
            preToken.script = reg.use.apply(reg,match);
            tokens.push(preToken);
            // 保存本次匹配结果的结束下标
            index = match.index + match[0].length;
        }
    
        return tokens;
    }
    
    function templateRender(tplId,tplData) {
        var tplStr = document.getElementById(tplId).innerHTML;
        var tplTokens = tplToToken(tplStr,reg);
        var compiler = new CompileTemplate(tplTokens, globalThis);
    
        var render = compiler.buildRender();
    
        return render(tplData);
    }
    
    globalThis.templateRender = templateRender;
    globalThis.__templateRender = function(tplStr,tplData) {
        var tplTokens = tplToToken(tplStr,reg);
        var compiler = new CompileTemplate(tplTokens, globalThis);
    
        var render = compiler.buildRender();
    
        return render(tplData);
    }
    
})( typeof self !== 'undefined' ? self
: typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global : {});

用前面的例子测试一下:

var tplStr = `
<% if (isAdmin) { %>
    <!--fhj-->
    <h1><%=title%></h1>
    <ul>
        <% for (var i = 0; i < list.length; i ++) { %>
            <li>索引 <%= i + 1 %> :<%= list[i] %></li>
        <% } %>
    </ul>
    <% var a = list.name; console.log(a); %>
<% } %>
`;
var data = {
	title: '基本例子',
	isAdmin: true,
	list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他']
};
console.log(__templateRender(tplStr,data));

输出结果如下:

<!--fhj-->    
<h1>基本例子</h1>    
<ul>                    
    <li>索引 1 :文艺</li>                    
    <li>索引 2 :博客</li>                    
    <li>索引 3 :摄影</li>                    
    <li>索引 4 :电影</li>                    
    <li>索引 5 :民谣</li>                    
    <li>索引 6 :旅行</li>                    
    <li>索引 7 :吉他</li>            
</ul>    
posted @ 2023-08-30 17:18  Fogwind  阅读(6)  评论(0编辑  收藏  举报