前端模板是什么?前端模板该如何实现?很多朋友可能对这个不太了解,那么,下面这篇文章将给大家介绍一下关于前端模板的原理以及简单的实现代码。
前端模板的发展
模板可以说是前端开发最常接触的工具之一。将页面固定不变的内容抽出成模板,服务端返回的动态数据装填到模板中预留的坑位,最后组装成完整的页面html字符串交给浏览器去解析。
模板可以大大提升开发效率,如果没有模板开发人员怕是要手动拼写字符串。
var tpl = '<p>' + user.name + '</p>'; $('body').append(tpl);
在近些年前端发展过程中,模板也跟着变化:
1. php模板 JSP模板
早期还没有前后端分离时代,前端只是后端项目中的一个文件夹,这时期的php和java都提供了各自的模板引擎。以JSP为例:java web应用的页面通常是一个个.jsp的文件,这个文件内容是大部分的html以及一些模板自带语法,本质上是纯文本,但是既不是html也不是java。
JSP语法:index.jsp
<html> <head><title>Hello World</title></head> <body> Hello World!<br/> <% out.println("Your IP address is " + request.getRemoteAddr()); %> </body> </html>
这个时期的模板引擎,往往是服务端来编译模板字符串,生成html字符串给客户端。
2. handlebar mustache通用模板
09年node发布,JavaScript也可以来实现服务端的功能,这也大大的方便了开发人员。mustache和handlebar模板的诞生方便了前端开发人员,这两个模板均使用JavaScript来实现,从此前端模板既可以在服务端运行,也可以在客户端运行,但是大多数使用场景都是js根据服务端异步获取的数据套入模板,生成新的dom插入页码。 对前端后端开发都非常有利。
mustache语法:index.mustache
<p>Username: {{user.name}}</p> {{#if (user.gender === 2)}} <p>女</p> {{/if}}
3. vue中的模板 React中的JSX
接下来到了新生代,vue中的模板写法跟之前的模板有所不同,而且功能更加强大。既可以在客户端使用也可以在服务端使用,但是使用场景上差距非常大:页面往往根据数据变化,模板生成的dom发生变化,这对于模板的性能要求很高。
vue语法:index.vue
<p>Username: {{user.name}}</p> <template v-if="user.gender === 2"> <p>女</p> </div>
模板实现的功能
无论是从JSP到vue的模板,模板在语法上越来越简便,功能越来越丰富,但是基本功能是不能少的:
变量输出(转义/不转义):出于安全考虑,模板基本默认都会将变量的字符串转义输出,当然也实现了不转义输出的功能,慎重使用。
条件判断(if else):开发中经常需要的功能。
循环变量:循环数组,生成很多重复的代码片段。
模板嵌套:有了模板嵌套,可以减少很多重复代码,并且嵌套模板集成作用域。
以上功能基本涵盖了大多数模板的基础功能,针对这些基础功能就可以探究模板如何实现的。
模板实现原理
正如标题所说的,模板本质上都是纯文本的字符串,字符串是如何操作js程序的呢?
模板用法上:
var domString = template(templateString, data);
模板引擎获得到模板字符串和模板的作用域,经过编译之后生成完整的DOM字符串。
大多数模板实现原理基本一致:
模板字符串首先通过各种手段剥离出普通字符串和模板语法字符串生成抽象语法树AST;然后针对模板语法片段进行编译,期间模板变量均去引擎输入的变量中查找;模板语法片段生成出普通html片段,与原始普通字符串进行拼接输出。
其实模板编译逻辑并没有特别复杂,至于vue这种动态绑定数据的模板有时间可以参考文末链接。
快速实现简单的模板
现在以mustache模板为例,手动实现一个实现基本功能的模板。
模板字符串模板:index.txt
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" media="screen" href="main.css" /> <script src="main.js"></script> </head> <body> <h1>Panda模板编译</h1> <h2>普通变量输出</h2> <p>username: {{common.username}}</p> <p>escape:{{common.escape}}</p> <h2>不转义输出</h2> <p>unescape:{{&common.escape}}</p> <h2>列表输出:</h2> <ul> {{#each list}} <li class="{{value}}">{{key}}</li> {{/each}} </ul> <h2>条件输出:</h2> {{#if shouldEscape}} <p>escape{{common.escape}}</p> {{else}} <p>unescape:{{&common.escape}}</p> {{/if}} </body> </html>
模板对应数据:
module.exports = { common: { username: 'Aus', escape: '<p>Aus</p>' }, 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这种可以实现双向绑定的模板可以抽空多看一看。
相关推荐:
javascript - 大家前端js模板是用underscore还是handlebars呢?
学习前端模板引擎 jade (一)_html/css_WEB-ITnose
以上是前端模板是什么?前端模板的原理介绍以及实例的详细内容。更多信息请关注PHP中文网其他相关文章!

JavaScript框架的强大之处在于简化开发、提升用户体验和应用性能。选择框架时应考虑:1.项目规模和复杂度,2.团队经验,3.生态系统和社区支持。

引言我知道你可能会觉得奇怪,JavaScript、C 和浏览器之间到底有什么关系?它们之间看似毫无关联,但实际上,它们在现代网络开发中扮演着非常重要的角色。今天我们就来深入探讨一下这三者之间的紧密联系。通过这篇文章,你将了解到JavaScript如何在浏览器中运行,C 在浏览器引擎中的作用,以及它们如何共同推动网页的渲染和交互。JavaScript与浏览器的关系我们都知道,JavaScript是前端开发的核心语言,它直接在浏览器中运行,让网页变得生动有趣。你是否曾经想过,为什么JavaScr

Node.js擅长于高效I/O,这在很大程度上要归功于流。 流媒体汇总处理数据,避免内存过载 - 大型文件,网络任务和实时应用程序的理想。将流与打字稿的类型安全结合起来创建POWE

Python和JavaScript在性能和效率方面的差异主要体现在:1)Python作为解释型语言,运行速度较慢,但开发效率高,适合快速原型开发;2)JavaScript在浏览器中受限于单线程,但在Node.js中可利用多线程和异步I/O提升性能,两者在实际项目中各有优势。

JavaScript起源于1995年,由布兰登·艾克创造,实现语言为C语言。1.C语言为JavaScript提供了高性能和系统级编程能力。2.JavaScript的内存管理和性能优化依赖于C语言。3.C语言的跨平台特性帮助JavaScript在不同操作系统上高效运行。

JavaScript在浏览器和Node.js环境中运行,依赖JavaScript引擎解析和执行代码。1)解析阶段生成抽象语法树(AST);2)编译阶段将AST转换为字节码或机器码;3)执行阶段执行编译后的代码。

Python和JavaScript的未来趋势包括:1.Python将巩固在科学计算和AI领域的地位,2.JavaScript将推动Web技术发展,3.跨平台开发将成为热门,4.性能优化将是重点。两者都将继续在各自领域扩展应用场景,并在性能上有更多突破。

Python和JavaScript在开发环境上的选择都很重要。1)Python的开发环境包括PyCharm、JupyterNotebook和Anaconda,适合数据科学和快速原型开发。2)JavaScript的开发环境包括Node.js、VSCode和Webpack,适用于前端和后端开发。根据项目需求选择合适的工具可以提高开发效率和项目成功率。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

螳螂BT
Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

适用于 Eclipse 的 SAP NetWeaver 服务器适配器
将Eclipse与SAP NetWeaver应用服务器集成。

SublimeText3汉化版
中文版,非常好用

MinGW - 适用于 Windows 的极简 GNU
这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

安全考试浏览器
Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。