如今说起前端开发,基本上都离不开前端框架。随着前端技术不断迭代,前端框架相关的文档和社区日益完善,前端入门也越来越简单了。我们可以快速上手一些工具和框架,但常常会忽略其中的设计和原理。
对框架和工具的了解不够深入,会导致我们在遇到一些偏门的问题时容易找不到方向,也不利于个人的知识领域扩展,不能很好地进行技术选型。
今天,我会带你了解前端框架为什么会这么热门,以及介绍前端框架的核心能力——模板引擎的实现原理。在讲解的过程中,一些代码会以 Vue.js 作为示例。
我们先来看一下,为什么要使用前端框架。
一个工具被大多数人使用、成为热门,离不开相关技术发展的历史进程。了解这些工具和框架出现的原因,我们可以及时掌握技术的发展方向,保持对技术的敏感度、更新自身的认知,这些都会成为我们自身的竞争力。
前端框架也一样。在前端框架出现之前,jQuery 也是前端开发必备的工具库,大多数项目中都会使用。短短几年间,前端开发却变得无法离开前端框架,这中间到底发生了什么呢?
曾几何时,一提到前端,大家都会想到 jQuery。那是 jQuery 一把梭的年代,不管前端后台都会用 jQuery 完成页面开发。那时候前端开发的工作倾向于切图和重构,重页面样式而轻逻辑,工作内容常常是拼接 JSP 模板、拼 PHP 模板以及调节浏览器兼容。
为什么 jQuery 那么热门呢?除了超方便的 Sizzle 引擎元素选择器、简单易用的异步请求库 ajax,还有链式调用的编程方式使得代码如行云流水般流畅。jQuery 提供的便捷几乎满足了当时前端的大部分工作(所以说 jQuery 一把梭不是毫无道理的)。
接下来短短的几年时间,前端经历了特别多的改变。随着 Node.js 的出现、NPM 包管理的完善,再加上热闹的开源社区,前端领域获得了千千万万开发者的支援。从页面开发到工具库开发、框架开发、脚本开发、到服务端开发,单线程的 JavaScript 正在不断进行自我革新,从而将领域不断拓宽,形成了如今你所能看到的、获得赋能的前端。
那么,是什么原因导致了 jQuery 被逐渐冷落,前端框架站上了舞台中央呢?其中的原因有很多,包括业务场景的进化、技术的更新迭代,比如前端应用逐渐复杂、单页应用的出现、前端模块化等。
前面第 8 讲中,我们知道了浏览器是如何渲染页面的。从用户的角度来看,浏览器生成了最终的渲染树,并通过光栅化来将页面显示在屏幕上,页面渲染的工作就完成了。
实际上,浏览器页面更多的不只是静态页面的渲染,还包括点击、拖拽等事件操作以及接口请求、数据渲染到页面等动态的交互逻辑,因此我们还常常需要更新页面的内容。
要理解前端框架为什么如此重要,需要先看看在框架出现前,前端开发是如何实现和用户进行交互的。
这个过程跟上一讲事件驱动的内容很相似,以一个常见的表单提交作为例子,会包括编写静态页面、给对应的元素绑定对应的事件、事件触发时更新页面内容等步骤,这是最简单的页面交互。
对于更新页面内容这个步骤,如果我们页面中有很多的内容需要更新,光拼接字符串我们可能就有一大堆代码。
以下的例子,为了不占用大量的篇幅,使用了 jQuery,否则代码量会更多。
举个例子,抢答活动中常常会出现题目和多个答案进行选择,我们现在需要开发一个管理端,对这些抢答卡片进行管理。假设一个问题会包括两个答案,我们可以通过新增卡片的方式来添加一套问答,编辑卡片的过程包括这些步骤。
1. 新增一个卡片时,通过插入 DOM 节点的方式添加卡片样式。
var index = 0;
// 用来新增一个卡片,卡片内需要填写一些内容
function addCard() {
// 获取一个id为the-dom的元素
var body = $("#the-dom");
// 从该元素内获取class为the-class的元素
var addDom = body.find(".the-class");
// 在the-class元素前方插入一个div
addDom.before(''">');
// 同时保存下来该DOM节点,方便更新内容
var theDom = body.find('[data-index="' + index + '"]');
theDom.innerHTML(
`"text" class="form-control question" placeholder="你的问题">
"text" class="form-control option-a" placeholder="回答1">
"text" class="form-control option-b" placeholder="回答2">
`
);
// 做完上面这堆之后index自增
index++;
return theDom;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
2. 卡片内编辑题目和答案时,会有字数限制(使用 jQuery 对输入框的输入事件进行监听,并限制输入内容)。
// theDom使用上面代码保存下来的引用
// 问题绑定值
theDom
.on("keyup", ".question", function (ev) {
ev.target.value = ev.target.value.substr(0, 20);
})
// 答案a绑定值
.on("keyup", ".option-a", function (ev) {
ev.target.value = ev.target.value.substr(0, 10);
})
// 答案b绑定值
.on("keyup", ".option-b", function (ev) {
ev.target.value = ev.target.value.substr(0, 10);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
3. 获取输入框内的内容(使用 jQuery 选择元素并获取内容),用于提交到后台。
// 获取卡片的输入值
// theDom 使用上面代码保存下来的引用
function getCardValue(index) {
var body = $("#the-dom");
var theDom = body.find('[data-index="' + index + '"]');
var questionName = theDom.find(".question").val();
var optionA = theDom.find(".option-a").val();
var optionB = theDom.find(".option-b").val();
return { questionName, optionA, optionB };
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
可以看到,仅是实现一个问答卡片的编辑就需要编写不少的代码,大多数代码内容都是为了拼接 HTML 内容、获取 DOM 节点、操作 DOM 节点。
这些代码逻辑,如果我们使用 Vue 来实现,只需要这么写:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
可见,前端框架提供了便利的数据绑定、界面更新、事件监听等 API,我们不需要再手动更新前端页面的内容、维护一大堆的 HTML 和变量拼接的动态内容了。
使用前端框架对开发效率有很大的提升,同时也在一定程度上避免了代码可读性、可维护性等问题。这也是为什么前端框架这么热门,大家都会使用它来进行开发的原因。
那么,前端框架是怎么做到这些的呢?要实现这些能力,离不开其中的模板引擎。
当用户对页面进行操作、页面内容更新,我们需要实现的功能流程包括:
监听操作;
获取数据变量;
使用数据变量拼接成 HTML 模板;
将 HTML 内容塞到页面对应的地方;
将 HTML 片段内需要监听的点击等事件进行绑定。
可以看到,实现逻辑会比较复杂和烦琐。
如果使用前端框架,我们可以:
使用将数据变量绑定到 HTML 模板的方式,来控制展示的内容;
配合一些条件判断、条件循环等逻辑,控制交互的具体逻辑;
通过改变数据变量,框架会自动更新页面内容。
这样,我们可以快速高效地完成功能开发,代码的可读性和维护性都远胜于纯手工实现。
如果使用数据驱动的方式,还可以通过让逻辑与 UI 解耦的方式,提升代码的可维护性。其中的数据绑定、事件绑定等功能,前端框架是依赖模板引擎的方式来实现的。
以 Vue 为例子,对于开发者编写的 Vue 代码,Vue 会将其进行以下处理从而渲染到页面中:
解析语法生成 AST 对象;
根据生成的 AST 对象,完成data数据初始化;
根据 AST 对象和data数据绑定情况,生成虚拟 DOM 对象;
将虚拟 DOM 对象生成真正的 DOM 元素插入到页面中,此时页面会被渲染。
模板引擎将模板语法进行解析,分别生成 HTML DOM,使用像 HTML 拼接的方式(在对应的位置绑定变量、指令解析获取拼接逻辑等等),同时配合事件的管理、虚拟 DOM 的设计,可以最大化地提升页面的性能。
这些便是模板引擎主要的工作,我们来分别看一下。
抽象语法树(Abstract Syntax Tree)也称为 AST 语法树,指的是源代码语法所对应的树状结构。其实我们的 DOM 结构树,也是 AST 的一种,浏览器会对 HTML DOM 进行语法解析、并生成最终的页面。
生成 AST 的过程涉及编译器的原理,一般经过以下过程。
语法分析。模板引擎需要在这个过程中识别出特定的语法,比如v-if/v-for这样的指令,或是这样的自定义 DOM 标签,还有@click/:props这样的简化绑定语法等。
语义分析。这个过程会审查源程序有无语义错误,为代码生成阶段收集类型信息,一般类型检查也会在这个过程中进行。例如我们绑定了某个不存在的变量或者事件,又或者是使用了某个未定义的自定义组件等,都会在这个阶段进行报错提示。
生成 AST 对象。
以 Vue 为例,生成 AST 的过程包括 HTML 模板解析、元素检查和预处理:
/**
* 将HTML编译成AST对象
* 该代码片段基于Vue2.x版本
*/
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void {
// 返回AST对象
// 篇幅原因,一些前置定义省略
// 此处开始解析HTML模板
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
start(tag, attrs, unary) {
// 一些前置检查和设置、兼容处理此处省略
// 此处定义了初始化的元素AST对象
const element: ASTElement = {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent: currentParent,
children: [],
};
// 检查元素标签是否合法(不是保留命名)
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
process.env.NODE_ENV !== "production" &&
warn(
"Templates should only be responsible for mapping the state to the " +
"UI. Avoid placing tags with side-effects in your templates, such as " +
`<${tag}>` +
", as they will not be parsed."
);
}
// 执行一些前置的元素预处理
for (let i = 0; i < preTransforms.length; i++) {
preTransforms[i](element, options);
}
// 是否原生元素
if (inVPre) {
// 处理元素的一些属性
processRawAttrs(element);
} else {
// 处理指令,此处包括v-for/v-if/v-once/key等等
processFor(element);
processIf(element);
processOnce(element);
processKey(element); // 删除结构属性
// 确定这是否是一个简单的元素
element.plain = !element.key && !attrs.length;
// 处理ref/slot/component等属性
processRef(element);
processSlot(element);
processComponent(element);
for (let i = 0; i < transforms.length; i++) {
transforms[i](element, options);
}
processAttrs(element);
}
// 后面还有一些父子节点等处理,此处省略
},
// 其他省略
});
return root;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
到这里,Vue 将开发者的模板代码解析成 AST 对象,我们来看看这样的 AST 对象是怎样生成 DOM 元素的。
前面提到,在编译解析和渲染过程中,模板引擎会识别和解析模板语法语义、生成 AST 对象,最后根据 AST 对象会生成最终的 DOM 元素。
举个例子,我们写了以下这么一段 HTML 模板:
123
456789
- 1
- 2
- 3
- 4
模板引擎可以在语法分析、语义分析等步骤后,得到这样的一个 AST 对象:
thisDiv = {
dom: {
type: "dom",
ele: "div",
nodeIndex: 0,
children: [
{
type: "dom",
ele: "a",
nodeIndex: 1,
children: [{ type: "text", value: "123" }],
},
{
type: "dom",
ele: "p",
nodeIndex: 2,
children: [
{ type: "text", value: "456" },
{
type: "dom",
ele: "span",
nodeIndex: 3,
children: [{ type: "text", value: "789" }],
},
],
},
],
},
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
这个 AST 对象维护我们需要的一些信息,包括 HTML 元素里:
需要绑定哪些变量(变量更新的时候需要更新该节点内容);
是否有其他的逻辑需要处理(比如含有逻辑指令,如v-if、v-for等);
哪些节点绑定了事件监听事件(是否匹配一些常用的事件能力支持,如@click)。
模板引擎会根据 AST 对象生成最终的页面片段和逻辑,在这个过程中会通过添加特殊标识(例如元素 ID、属性标记等)的方式来标记 DOM 节点,配合 DOM 元素选择方式、事件监听方式等,在需要更新的时候可快速定位到该 DOM 节点,并进行节点内容更新,从而实现页面内容的更新。
目前来说,前端模板渲染的实现一般分为以下两种方式。
字符串模版方式:使用拼接的方式生成 DOM 字符串,直接通过innderHTML()插入页面。
节点模版方式:使用createElement()/appendChild()/textContent等方法动态地插入 DOM 节点。
在使用字符串模版的时候,我们将nodeIndex绑定在元素属性上,主要用于在数据更新时追寻节点进行内容更新。
在使用节点模版的时候,我们可在创建节点时将该节点保存下来,直接用于数据更新:
// 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听
function generateDOM(astObject) {
const { dom, binding = [] } = astObject;
// 生成DOM,这里假装当前节点是baseDom
baseDom.innerHTML = getDOMString(dom);
// 对于数据绑定的,来进行监听更新吧
baseDom.addEventListener("data:change", (name, value) => {
// 寻找匹配的数据绑定
const obj = binding.find((x) => x.valueName == name);
// 若找到值绑定的对应节点,则更新其值。
if (obj) {
baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;
}
});
}
// 获取DOM字符串,这里简单拼成字符串
function getDOMString(domObj) {
// 无效对象返回''
if (!domObj) return "";
const { type, children = [], nodeIndex, ele, value } = domObj;
if (type == "dom") {
// 若有子对象,递归返回生成的字符串拼接
const childString = "";
children.forEach((x) => {
childString += getDOMString(x);
});
// dom对象,拼接生成对象字符串
return `<${ele} data-node-index="${nodeIndex}">${childString}${ele}>`;
} else if (type == "text") {
// 若为textNode,返回text的值
return value;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
通过上面的方式,前端框架实现了将 AST 对象生成 DOM 元素,并将这些 DOM 元素渲染或更新到页面上。
或许你会觉得疑惑:原本就是一个 实际上,在这个过程中,模板引擎可以实现更多功能。 将 HTML 模板解析成 AST 对象,再根据 AST 对象生成 DOM 节点,在这个过程中前端框架可以实现以下功能: 排除无效 DOM 元素(非自定义组件、也非默认组件的 DOM 元素),在构建阶段可及时发现并进行报错; 可识别出自定义组件,并渲染对应的组件; 可方便地实现数据绑定、事件绑定等功能; 为虚拟 DOM Diff 过程打下铺垫; HTML 转义(预防 XSS 漏洞)。 这里我们以第 5 点预防 XSS 漏洞为例子,详细地介绍一下模板引擎是如何避免 XSS 攻击的。 我们知道 XSS 的整个攻击过程大概为: 攻击者提交含有恶意代码的内容(比如 JavaScript 脚本); 页面渲染的时候,这些内容未被过滤就被加载处理,比如获取 Cookie、执行操作等; 其他用户在浏览页面的时候,就会在加载到恶意代码时受到攻击。 要避免网站用户受到 XSS 攻击,主要方法是将用户提交的内容进行过滤处理。大多数前端框架会自带 HTML 转义功能,从而避免的 XSS 攻击。 以 Vue 为例,使用默认的数据绑定方式(双大括号、 除此预防 XSS 漏洞之外,前端框架还做了一些性能、安全性等方面的优化,也提供了一些用于项目开发配套的工具,包括路由的管理、状态和数据的管理等工具。 今天我带大家了解了前端框架的出现,由于前端框架帮开发者解决了很多重复性的工作(拼接 HTML 模板、DOM 元素查找、DOM 元素事件监听等),极大地提升了开发者的效率,同时还提升了代码的可读性和可维护性,因此受到很多前端开发的追捧。 除此之外,我还介绍了前端框架中模板引擎的实现原理,包括解析语法生成 AST 对象、根据 AST 对象生成 DOM 元素,并对生成的 DOM 元素进行标记,则可以在变量改变的时候,解析找到相应的 DOM 元素进行内容的更新。 在了解这些内容之后,我们可以在页面渲染遇到性能问题的时候,根据所使用框架的具体实现,找到可能导致页面渲染卡顿或是不流畅的原因。除此之外,在使用框架的过程中,遇到一些语法报错、XSS 安全漏洞等问题的时候,也可以快速找到解决办法。 今日思考:React.js 中的 JSX 和模板引擎是什么关系?在留言区写出你的想法! jsx会被babel编译成React.createElement(),用来创建相应的虚拟dom对象,后面会被reactdom或者react native等不同平台的渲染库渲染成ui或者SSR。之所以发明jsx是因为,React.createElement太过繁琐,jsx可以像写html一样写ui,同时也保留了部分js的能力。题外话:其实Facebook最初并没有打算让createElement作为jsx的编译产物,因为它里面的defaultValue,propType,ref key的拦截等等逻辑比较浪费性能,对props的属性有O(n)级别的复杂度,本来只是作为让用户临时手动生产虚拟dom的一个补充手段,但因为当时只有它是最好的选择,所以才这样了,react17以后可能有变化。 今日思考的回答:React中的jsx本质是JavaScript语法的扩展,充分具备JavaScript能力,这就能让developer像写JavaScript一样写UI,我们写的虽然是jsx,借助babel等类似的工具的转化,最终还是会编译成React.createElement()这样的函数去生成虚拟DOM,后面生成AST,再由AST生成真实DOM,这些过程应该跟老师经的一样吧,有写错的地方,望老师批评指正。 赞!思路很清晰~ jsx 是把代码转成react可以解读的代码,并没有生成dom模板引擎是通过自己的语法生成dom 相同点都是用自己的语法 写html ,底层实现操作dom 大体上差不多,但也不一定是通过自己的语法生成 DOM,可以考虑下 jsx 是如何对接 react 和 vue 的模板引擎可以做更多
预防 XSS 漏洞
v-bind等)会进行 HTML 转义,将数据解释为普通文本,而非 HTML 代码。小结
精选评论
**宇:
*浩:
讲师回复:
856:
讲师回复:
JavaScript面向对象和原型
【数据结构--八大排序】之归并排序
R语言使用lightgbm包构建多分类的LightGBM模型、使用predict函数和训练好的模型进行预测推理、将推理后的概率值转化为预测标签
没有二十年功力,写不出这一行“看似无用”的代码!
C++ 引用
洛谷 P2183 [国家集训队]礼物)(扩展卢卡斯定理)
R语言的计量经济学实践技术应用
Unity3D 拖拽赋值组件与通过Find赋值组件的优点与缺点详解
【开源微服务项目】论如何在微服务中优雅的实现Redis序列化配置