摘要:大多数模板实现原理基本一致模板字符串首先通过各种手段剥离出普通字符串和模板语法字符串生成抽象语法树然后针对模板语法片段进行编译,期间模板变量均去引擎输入的变量中查找模板语法片段生成出普通片段,与原始普通字符串进行拼接输出。
前端模板的发展
模板可以说是前端开发最常接触的工具之一。将页面固定不变的内容抽出成模板,服务端返回的动态数据装填到模板中预留的坑位,最后组装成完整的页面html字符串交给浏览器去解析。
模板可以大大提升开发效率,如果没有模板开发人员怕是要手动拼写字符串。
var tpl = "" + user.name + "
"; $("body").append(tpl);
在近些年前端发展过程中,模板也跟着变化:
1. php模板 JSP模板
早期还没有前后端分离时代,前端只是后端项目中的一个文件夹,这时期的php和java都提供了各自的模板引擎。以JSP为例:java web应用的页面通常是一个个.jsp的文件,这个文件内容是大部分的html以及一些模板自带语法,本质上是纯文本,但是既不是html也不是java。
JSP语法:index.jsp
Hello World Hello World!
<% out.println("Your IP address is " + request.getRemoteAddr()); %>
这个时期的模板引擎,往往是服务端来编译模板字符串,生成html字符串给客户端。
2. handlebar mustache通用模板
09年node发布,JavaScript也可以来实现服务端的功能,这也大大的方便了开发人员。mustache和handlebar模板的诞生方便了前端开发人员,这两个模板均使用JavaScript来实现,从此前端模板既可以在服务端运行,也可以在客户端运行,但是大多数使用场景都是js根据服务端异步获取的数据套入模板,生成新的dom插入页码。 对前端后端开发都非常有利。
mustache语法:index.mustache
Username: {{user.name}}
{{#if (user.gender === 2)}}女
{{/if}}
3. vue中的模板 React中的JSX
接下来到了新生代,vue中的模板写法跟之前的模板有所不同,而且功能更加强大。既可以在客户端使用也可以在服务端使用,但是使用场景上差距非常大:页面往往根据数据变化,模板生成的dom发生变化,这对于模板的性能要求很高。
vue语法:index.vue
Username: {{user.name}}
女
无论是从JSP到vue的模板,模板在语法上越来越简便,功能越来越丰富,但是基本功能是不能少的:
变量输出(转义/不转义):出于安全考虑,模板基本默认都会将变量的字符串转义输出,当然也实现了不转义输出的功能,慎重使用。
条件判断(if else):开发中经常需要的功能。
循环变量:循环数组,生成很多重复的代码片段。
模板嵌套:有了模板嵌套,可以减少很多重复代码,并且嵌套模板集成作用域。
以上功能基本涵盖了大多数模板的基础功能,针对这些基础功能就可以探究模板如何实现的。
模板实现原理正如标题所说的,模板本质上都是纯文本的字符串,字符串是如何操作js程序的呢?
模板用法上:
var domString = template(templateString, data);
模板引擎获得到模板字符串和模板的作用域,经过编译之后生成完整的DOM字符串。
大多数模板实现原理基本一致:
模板字符串首先通过各种手段剥离出普通字符串和模板语法字符串生成抽象语法树AST;然后针对模板语法片段进行编译,期间模板变量均去引擎输入的变量中查找;模板语法片段生成出普通html片段,与原始普通字符串进行拼接输出。
其实模板编译逻辑并没有特别复杂,至于vue这种动态绑定数据的模板有时间可以参考文末链接。
快速实现简单的模板现在以mustache模板为例,手动实现一个实现基本功能的模板。
模板字符串模板:index.txt
Page Title Panda模板编译
普通变量输出
username: {{common.username}}
escape:{{common.escape}}
不转义输出
unescape:{{&common.escape}}
列表输出:
escape{{common.escape}}
{{else}}unescape:{{&common.escape}}
{{/if}}模板对应数据:
module.exports = { common: { username: "Aus", escape: "Aus
" }, shouldEscape: false, list: [ {key: "a", value: 1}, {key: "b", value: 2}, {key: "c", value: 3}, {key: "d", value: 4} ] };
模板的使用方法:
var fs = require("fs"); var tpl = fs.readFileSync("./index.txt", "utf8"); var state = require("./test"); var Panda = require("./panda"); Panda.render(tpl, state)
然后来实现模板:
1. 正则切割字符串模板引擎获取到模板字符串之后,通常要使用正则切割字符串,区分出那些是静态的字符串,那些是需要编译的代码块,生成抽象语法树(AST)。
// 将未处理过的字符串进行分词,形成字符组tokens Panda.prototype.parse = function (tpl) { var tokens = []; var tplStart = 0; var tagStart = 0; var tagEnd = 0; while (tagStart >= 0) { tagStart = tpl.indexOf(openTag, tplStart); if (tagStart < 0) break; // 纯文本 tokens.push(new Token("text", tpl.slice(tplStart, tagStart))); tagEnd = tpl.indexOf(closeTag, tagStart) + 2; if (tagEnd < 0) throw new Error("{{}}标签未闭合"); // 细分js var tplValue = tpl.slice(tagStart + 2, tagEnd - 2); var token = this.classifyJs(tplValue); tokens.push(token); tplStart = tagEnd; } // 最后一段 tokens.push(new Token("text", tpl.slice(tagEnd, tpl.length))); return this.parseJs(tokens); };
这一步分割字符串通常使用正则来完成的,后面检索字符串会大量用到正则方法。
在这一步通常可以检查出模板标签闭合异常,并报错。2. 模板语法的分类
生成AST之后,普通字符串不需要再管了,最后会直接输出,专注于模板语法的分类。
// 专门处理模板中的js Panda.prototype.parseJs = function (tokens) { var sections = []; var nestedTokens = []; var conditionsArray = []; var collector = nestedTokens; var section; var currentCondition; for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; var value = token.value; var symbol = token.type; switch (symbol) { case "#": { collector.push(token); sections.push(token); if(token.action === "each"){ collector = token.children = []; } else if (token.action === "if") { currentCondition = value; var conditionArray; collector = conditionArray = []; token.conditions = token.conditions || conditionsArray; conditionsArray.push({ condition: currentCondition, collector: collector }); } break; } case "else": { if(sections.length === 0 || sections[sections.length - 1].action !== "if") { throw new Error("else 使用错误"); } currentCondition = value; collector = []; conditionsArray.push({ condition: currentCondition, collector: collector }); break; } case "/": { section = sections.pop(); if (section && section.action !== token.value) { throw new Error("指令标签未闭合"); } if(sections.length > 0){ var lastSection = sections[sections.length - 1]; if(lastSection.action === "each"){ collector = lastSection.chidlren; } else if (lastSection.action = "if") { conditionsArray = []; collector = nestedTokens; } } else { collector = nestedTokens; } break; } default: { collector.push(token); break; } } } return nestedTokens; }
上一步我们生成了AST,这个AST在这里就是一个分词token数组:
[ Token {}, Token {}, Token {}, ]
这个token就是每一段字符串,分别记录了token的类型,动作,子token,条件token等信息。
/** * token类表示每个分词的标准数据结构 */ function Token (type, value, action, children, conditions) { this.type = type; this.value = value; this.action = action; this.children = children; this.conditions = conditions; }
在这一步要将循环方法中的子token嵌套到对应的token中,以及条件渲染子token嵌套到对应token中。
这步完成之后,一个标准的带有嵌套关系的AST完成了。
3. 变量查找与赋值现在开始根据token中的变量查找到对应的值,根据相应功能生成值得字符串。
/** * 解析数据结构的类 */ function Context (data, parentContext) { this.data = data; this.cache = { ".": this.data }; this.parent = parentContext; } Context.prototype.push = function (data) { return new Context(data, this); } // 根据字符串name找到真实的变量值 Context.prototype.lookup = function lookup (name) { name = trim(name); var cache = this.cache; var value; // 查询过缓存 if (cache.hasOwnProperty(name)) { value = cache[name]; } else { var context = this, names, index, lookupHit = false; while (context) { // user.username if (name.indexOf(".") > 0) { value = context.data; names = name.split("."); index = 0; while (value != null && index < names.length) { if (index === names.length - 1) { lookupHit = hasProperty(value, names[index]); } value = value[names[index++]]; } } else { value = context.data[name]; lookupHit = hasProperty(context.data, name); } if (lookupHit) { break; } context = context.parent; } cache[name] = value; } return value; }
为了提高查找效率,采用缓存代理,每次查找到的变量存储路径方便下次快速查找。
不同于JavaScript编译器,模板引擎在查找变量的时候找不到对应变量即终止查找,返回空并不会报错。4. 节点的条件渲染与嵌套
这里开始讲模板语法token和普通字符串token开始统一编译生成字符串,并拼接成完整的字符串。
// 根据tokens和context混合拼接字符串输出结果 Panda.prototype.renderTokens = function (tokens, context) { var result = ""; var token, symbol, value; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { value = undefined; token = tokens[i]; symbol = token.type; if (symbol === "#") value = this.renderSection(token, context); else if (symbol === "&") value = this.unescapedValue(token, context); else if (symbol === "=") value = this.escapedValue(token, context); else if (symbol === "text") value = this.rawValue(token); if (value !== undefined) result += value; } return result; }5. 绘制页面
页面字符串已经解析完成,可以直接输出:
Panda.prototype.render = function (tpl, state) { if (typeof tpl !== "string") { return new Error("请输入字符串!"); } // 解析字符串 var tokens = this.cache[tpl] ? tokens : this.parse(tpl); // 解析数据结构 var context = state instanceof Context ? state : new Context(state); // 渲染模板 return this.renderTokens(tokens, context); };
输出页面字符串被浏览器解析,就出现了页面。
以上只是简单的模板实现,并没有经过系统测试,仅供学习使用,源码传送门。成熟的模板引擎是有完整的异常处理,变量查找解析,作用域替换,优化渲染,断点调试等功能的。
总结前端模板这块能做的东西还很多,很多框架都是集成模板的功能,配合css,js等混合编译生成解析好样式和绑定成功事件的dom。
另外实现模板的方式也有很多,本文的实现方式参考了mustache源码,模板标签内的代码被解析,但是是通过代码片段分类,变量查找的方式来执行的,将纯字符串的代码变成了被解释器执行的代码。
另外向vue这种可以实现双向绑定的模板可以抽空多看一看。
参考资料前端模板的原理与实现
Vue 模板编译原理
现一个前端模板引擎
mustache
[如何选择-Web-前端模板引擎
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/97342.html
摘要:消息队列技术介绍后端掘金一消息队列概述消息队列中间件是分布式系统中重要的组件,主要解决应用耦合异步消息流量削锋等问题。的内存优化后端掘金声明本文内容来自开发与运维一书第八章,如转载请声明。 消息队列技术介绍 - 后端 - 掘金一、 消息队列概述 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合、异步消息、流量削锋等问题。实现高性能、高可用、可伸缩和最终一致性架构。是大型分布式系...
摘要:并总结经典面试题集各种算法和插件前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快速搭建项目。 本文是关注微信小程序的开发和面试问题,由基础到困难循序渐进,适合面试和开发小程序。并总结vue React html css js 经典面试题 集各种算法和插件、前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快...
阅读 1060·2021-11-12 10:34
阅读 987·2021-09-30 09:56
阅读 670·2019-08-30 15:54
阅读 2604·2019-08-30 11:14
阅读 1467·2019-08-29 16:44
阅读 3205·2019-08-29 16:35
阅读 2490·2019-08-29 16:22
阅读 2443·2019-08-29 15:39