JavaScript原生模板引擎实现原理与代码示例
本文深入解析前端模板引擎的核心原理,阐述其通过解析模板字符串生成函数并渲染数据的机制。文章提供了基于原生JavaScript的完整实现代码,涵盖编译、渲染、缓存及辅助函数等关键模块,旨在帮助开发者理解模板引擎的工作方式并优化前端开发流程。
模板引擎背景与分享初衷
原本计划分享之前对artTemplate源码解析的笔记,但时隔一年已无法找回,因此决定将当时分析模板引擎原理后自行编写的模板引擎代码分享出来,作为纪念。
当时还对比了多个模板引擎,最终选择了这种实现方式。
本文所述的JavaScript模板引擎完全基于原生JavaScript语法编写,其设计理念与PHP的原生模板引擎类似。
前端模板引擎的作用
前端模板引擎的主要作用体现在以下几个方面:
首先,它能简化前端开发流程。开发者无需再通过繁琐的字符串拼接运算符来生成DOM结构,而是可以直接使用HTML模板片段、存储模板的变量或独立的模板文件。
其次,模板引擎有助于降低耦合度,提升代码的可维护性。当DOM结构发生变化时,只需修改对应的模板文件或模板字符串,而无需改动业务逻辑代码。
此外,模板支持缓存机制。如果模板以.tpl等独立文件形式存在,浏览器可以对其进行加载和缓存。
更进一步,可以将.tpl文件视为模块,通过模块加载器按需加载。这种方式不仅能节省带宽,还能显著提升页面加载速度。
当然,模板引擎的优势远不止于此,具体效果取决于实际应用场景。
前端模板引擎的原理
前端模板引擎的核心原理可以概括为:对象(数据)加上模板(包含变量的字符串)最终输出为HTML字符串。
具体而言,就是利用数据对象和包含占位符的模板,经过处理后生成最终的HTML字符串。
前端模板引擎的实现方式
前端模板引擎的具体实现方式如下:
通过词法分析解析模板字符串,将其转换为一个JavaScript函数。随后调用该函数并传入数据对象,最终输出渲染后的HTML字符串。
具体的实现细节需参考后续提供的代码。
基本流程示意如下:
核心逻辑代码示例
核心逻辑代码示例:
var tpl = 'i am <%= name%>, <%= age=> years old'; // <%=xxx>% 词法,标记为变量
var obj = {
name : 'lovesueee' ,
age : 24
};
var fn = Engine.compile(tpl); // 编译成函数
var str = fn(obj); // 渲染出字符串
完整应用示例:
完整应用示例
HTML结构及脚本引用代码:
<!DOCTYPE HTML>\n<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n<title>ice demo</title>\n<script src="/javascripts/jquery/jquery-1.7.2.js"></script>\n<script src="/javascripts/ice/ice.js"></script>\n<body>\n <div id="content"></div>\n</body>
<script type="text/html" id="tpl">\n <div>here is the render result:</div>\n <% = this.title() ;%>\n <table border=1>\n <% for(var i=0,tl = this.trs.length,tr;i<tl;i++){ %>\n <%\n tr = this.trs[i];\n if (tr.sex === "女") {\n %>\n <tr>\n <td><%= tr.name;; %></td> <td><%= tr.age; %></td> <td><%= tr.sex || "男" %></td>\n </tr>\n <% } %>\n <% } %>\n </table>\n <img src="<%= this.href %>">\n <%= this.include('tpl2',this); %>\n</script>\n<script type="text/html" id="tpl2">\n <div>here is the render result:</div>\n <% = this.print('Welcome to Ice Template') ;%>\n <table border=1>\n <% for(var i=0,tl = this.trs.length,tr;i<tl;i++){ %>\n <%\n tr = this.trs[i];\n if (tr.sex === "男") {\n %>\n <tr>\n <td><%= tr.name;; %></td> <td><%= tr.age; %></td> <td><%= tr.sex || "男" %></td>\n </tr>\n <% } %>\n <% } %>\n </table>\n <img src="<%= this.href %>">
</script>\n<script>\n var trs = [\n {name:"隐形杀手",age:29,sex:"男"},\n {name:"索拉",age:22,sex:"男"},\n {name:"fesyo",age:23,sex:"女"},\n {name:"恋妖壶",age:18,sex:"男"},\n {name:"竜崎",age:25,sex:"男"},\n {name:"你不懂的",age:30,sex:"女"}\n ]
// var html = ice("tpl",{\n // trs: trs,\n // href: "http://images.jb51.net/type4.jpg"\n // },{\n // title: function(){\n // return "<p>这是使用视图helper输出的代码片断</p>"\n // }\n // });\n var elem = document.getElementById('tpl');\n var tpl = elem.innerHTML;
var html = ice(tpl,{\n trs: trs,\n href: "http://images.jb51.net/type4.jpg"\n },{\n title: function(){\n return "<p>这是使用视图helper输出的代码片断</p>"\n }\n });\n console.log(html);\n $("#content").html(html);\n\n</script>\n</html>
模板引擎简化实现代码
以下是该模板引擎的简化实现代码:
模板引擎核心实现代码:
(function (win) {
// 模板引擎路由函数\n var ice = function (id, content) {\n return ice[\n typeof content === 'object' ? 'render' : 'compile'\n ].apply(ice, arguments);\n };
ice.version = '1.0.0';
// 模板配置\n var iConfig = {\n openTag : '<%',\n closeTag : '%>'\n };
var isNewEngine = !!String.prototype.trim;
// 模板缓存\n var iCache = ice.cache = {};
// 辅助函数\n var iHelper = {\n include : function (id, data) {\n return iRender(id, data);\n },\n print : function (str) {\n return str;\n }\n };
// 原型继承\n var iExtend = Object.create || function (object) {\n function Fn () {};\n Fn.prototype = object;\n return new Fn;\n };
// 模板编译\n var iCompile = ice.compile = function (id, tpl, options) {
var cache = null;
id && (cache = iCache[id]);
if (cache) {\n return cache;\n }
// [id | tpl]\n if (typeof tpl !== 'string') {
var elem = document.getElementById(id);
options = tpl;
if (elem) {\n // [id, options]\n options = tpl;\n tpl = elem.value || elem.innerHTML;
} else {\n //[tpl, options]\n tpl = id;\n id = null;\n }\n }
options = options || {};\n var render = iParse(tpl, options);
id && (iCache[id] = render);
return render;\n };
// 模板渲染\n var iRender = ice.render = function (id, data, options) {
return iCompile(id, options)(data);\n };
var iForEach = Array.prototype.forEach ?\n function(arr, fn) {\n arr.forEach(fn)\n } :\n function(arr, fn) {\n for (var i = 0; i < arr.length; i++) {\n fn(arr[i], i, arr)\n }\n };
// 模板解析\n var iParse = function (tpl, options) {
var html = [];
var js = [];
var openTag = options.openTag || iConfig['openTag'];
var closeTag = options.closeTag || iConfig['closeTag'];
// 根据浏览器采取不同的拼接字符串策略\n var replaces = isNewEngine\n ?["var out='',line=1;", "out+=", ";", "out+=html[", "];", "this.result=out;"]\n : ["var out=[],line=1;", "out.push(", ");", "out.push(html[", "]);", "this.result=out.join('');"];
// 函数体\n var body = replaces[0];
iForEach(tpl.split(openTag), function(val, i) {
if (!val) {\n return;\n }
var parts = val.split(closeTag);
var head = parts[0];
var foot = parts[1];
var len = parts.length;\n // html\n if (len === 1) {\n body += replaces[3] + html.length + replaces[4];\n html.push(head);
} else {
if (head ) {\n // code\n // 去除空格\n head = head\n .replace(/^\s+|\s+$/g, '')\n .replace(/[\n\r]+\s*/, '')
// 输出语句\n if (head.indexOf('=') === 0) {\n head = head.substring(1).replace(/^[\s]+|[\s;]+$/g, '');\n\n body += replaces[1] + head + replaces[2];\n } else {\n body += head;\n }
body += 'line+=1;';\n js.push(head);\n }\n // html\n if (foot) {\n _foot = foot.replace(/^[\n\r]+\s*/g, '');\n if (!_foot) {\n return;\n }\n body += replaces[3] + html.length + replaces[4];\n html.push(foot);\n }\n }\n });
body = "var Render=function(data){ice.mix(this, data);try{"\n + body\n + replaces[5]\n + "}catch(e){ice.log('rend error : ', line, 'line');ice.log('invalid statement : ', js[line-1]);throw e;}};"\n + "var proto=Render.prototype=iExtend(iHelper);"\n + "ice.mix(proto, options);"\n + "return function(data){return new Render(data).result;};";
var render = new Function('html', 'js', 'iExtend', 'iHelper', 'options', body);
return render(html, js, iExtend, iHelper, options);\n };
ice.log = function () {\n if (typeof console === 'undefined') {\n return;\n }
var args = Array.prototype.slice.call(arguments);
console.log.apply && console.log.apply(console, args);
};
// 合并对象\n ice.mix = function (target, source) {\n for (var key in source) {\n if (source.hasOwnProperty(key)) {\n target[key] = source[key];\n }\n }\n };
// 注册函数\n ice.on = function (name, fn) {\n iHelper[name] = fn;\n };
// 清除缓存\n ice.clearCache = function () {\n iCache = {};\n };
// 更改配置\n ice.set = function (name, value) {\n iConfig[name] = value;\n };
// 暴露接口\n if (typeof module !== 'undefined' && module.exports) {\n module.exports = template;\n } else {\n win.ice = ice;\n }
})(window);JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
