最近身边学习Vue的人也越来越多了,大家都有很多的疑问,为什么前端要用框架、Vue 做了些什么、要怎么上手,等等。距离上一次写 Vue 也过去三年了,是时候再把这三年自己的理解再整理进去了。由于这个系列主要是希望写给可能前端掌握也不深的人,所以会铺垫一些自己认为需要的内容,本节先来讲讲入门前端,作为铺垫,来理解 Vue 这个框架吧。
前端页面解析
页面组成
我们打开一个前端项目,经常会看到很多不同后缀的文件,例如一个页面可能包括a.html、a.css、a.js,用了 Vue 还有a.vue,再加上 Typescript 可能还有a.ts。
相信有些没写过前端的开发们是有点崩溃的,我们先来分别看看一个前端页面都是由什么组成的。其实最终跑在浏览器中的代码,主要包括三种:HTML、CSS、Javascript。
HTML
直接从代码说起,最简单的莫过于:
这里面包括两个子模快:
<head>:常包括控制样式的<link>标签、控制浏览器特殊逻辑的<meta>标签、控制代码执行逻辑的<script>,不展示到页面。
<body>:包括展示在页面的内容。
通常来说,一段HTML代码,最终在浏览器中会生成一堆 DOM 节点树,例如:
这段代码在浏览器中渲染时,其实是长这个样子的:
这不一定是最终的结果,因为我们还可以这样操作:
1.通过CSS 样式,例如display: none来让其中某个节点消失。
2. 通过JS 代码操作 DOM,例如使用document.getElementById获取到某个节点元素,然后再通过设置.innerHTML来改变元素的内容。
CSS
CSS主要是给我们的 HTML 元素添加样式,可以通过几个方式匹配:
DOM元素匹配:例如p{color: red}会让所有<p>元素的文字都变成红色
class匹配:类的匹配,例如.color-red{color: red}会让所有<xxx class="color-red">元素的文字都变成红色
id匹配:id标识符的匹配,例如#color-red{color: red}会让<xxx id="color-red">元素的文字都变成红色(若页面内有多个相同的 id,则只有第一个生效)
CSS的调试和编写上手不简单,会需要很多的踩坑和练习,它也不像 JS 那样可以完全根据语法或是逻辑理解,总有兼容性会颠覆你之前的认知。不过 CSS 写好了,对写页面的效率会有很大的提升。
Javascript
HTML是简单的网页静态信息,而 JavaScript 可以在网页上实现复杂的功能。
我们常常使用Javascript来做以下事情:
处理事件(点击、输入等)
改变HTML内容、位置和样式
处理Http请求、各种业务逻辑的执行
很多其他的事情也可以做
Javascript是单线程的,更多是因为对页面交互的同步处理。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作 DOM,若是多线程会导致严重的同步问题。
页面渲染
浏览器的渲染机制
我们现在知道一个页面的代码里,主要包括了HTML、CSS、Javascript 三大块内容,那么浏览器是怎么解析和加载这些内容的呢?
一次浏览器的页面渲染过程中,浏览器会解析三种文件:
CSS规则树与 DOM 结构树结合,最终生成一个 Render 树(即最终呈现的页面,例如其中会移除 DOM 结构树中匹配到 CSS 里面display:none的 DOM 节点)。一般来说浏览器绘制页面的过程是:
1.计算 CSS 规则树。
2.生成 Render 树。
3.计算各个节点的大小/position/z-index。
4.绘制。
页面的局部刷新
一般看来,浏览器生成了最终的Render树,页面也已经渲染完毕,似乎浏览器已经完成了它的工作了。但现实中我们的页面更多的不只是静态的页面,还会包括点击、拖拽等事件操作,以及接口请求、数据渲染到页面等动态的交互逻辑,这时候我们会需要更新页面的信息。
我们的业务代码中情况会复杂得多,除了插入内容,还包括内容更新、删除元素节点等。不管是那种情况,目前来说前端一般分为两种方式:
1.绑定映射表方式。
2.直接替换内容方式。
1. 绑定映射表方式
这其实是挺经常使用的一种方式,例如下面这段代码:
这里拿到了<xxx id="a">的这样一个元素映射,我们在更新内容、处理节点的时候就可以用这个映射来直接操作,如:
如果我们一个页面里需要绑定变量的元素很多,那每次要更新某块的页面数据,可能会需要保存很多的元素映射,同时需要调用很多很多的createElement() /appendChild() /removeChild()这类方法,也是个不小的开销。这种情况下,我们可以使用直接替换内容的方式。
2. 直接替换内容方式
我们每次更新页面数据和状态,还可以通过innerHTML方法来用新的HTML String替换旧的,这种方法写起来很简单,无非是将各种节点使用字符串的方式拼接起来而已。
例如,上面的几次更新a 元素节点,可以调整成这样实现:
这种方式来更新页面简单粗暴,但是如果我们更新的节点范围比较大,这时候我们需要替换掉很大一片的HTML String。这种情况下,会面临着可能导致更多的浏览器计算。
页面回流、重绘
前面也介绍了,浏览器绘制页面的过程是:1.计算CSS规则树 => 2.生成Render树 => 3.计算各个节点的大小/position/z-index => 4.绘制。其中计算的环节也是消耗较大的地方。
我们使用DOM API和 CSS API 的时候,通常会触发浏览器的两种操作:Repaint(重绘) 和 Reflow(回流):
回流的花销跟render tree有多少节点需要重新构建有关系,这也是为什么前面说使用innerHTML会导致更多的开销。所以到底是使用绑定映射表方式,还是使用直接替换内容方式,都是需要具体问题具体分析的。
事件驱动
事件驱动其实是前端开发中最容易理解的编码方式,例如我们写一个提交表单的页面,用事件驱动的方式来写的话,会是这样一个流程:
1.编写静态页面
2.给对应的元素绑定对应的事件。例如给input 输入框绑定输入事件:
3.事件触发时,更新页面内容:
以上这个流程,是很常见的前端编码思维,我们称之为事件驱动模式。
前端思维转变
很多人不理解这几年来前端的变化,为什么不能再用jQuery统一天下呢?为什么要搞那么多的库,还要按照环境呢?不是用个 txt 编辑器就能写完一个页面吗,前端弄那么复杂是为了什么呢?
既然称之为思维转变,那么可以将事件驱动的思维模式作为过去常见的一种方式,而如今的前端开发过程中,多了很多的新框架、新工具,还有了工程化,这些带来了怎样的思维模式的转变呢?
前端框架的出现
其实最初是AngularJS开始占领了比较多的地位,后面 React 迎面赶上,在 Angular 断崖升级的帮助下,Vue 结合了各种框架的优势,以及非常容易入门的文档,成功成为了那一匹黑马。既然这一系列写的是 Vue 的入门和使用,那这里当然是基于 Vue 来介绍了。
Vue 文本插值
在Vue 中,最基础的模板语法是数据绑定,例如:
这里绑定了一个 msg 的变量,开发者在 Vue 实例 data 中绑定该变量: 最终页面展示内容为<div>测试文本</div>。
数据绑定的实现
这种使用双大括号来绑定变量的方式,我们称之为数据绑定。它是怎么实现的呢,数据绑定的过程其实不复杂:
上述这个过程,是模板引擎在做的事情。我们来看看上面在Vue 里的代码片段<div></div>,我们可以解析后获得这样一个AST 对象:
这样,我们在生成一个DOM 的时候,同时添加对data的监听,数据更新时我们会找到对应的nodeIndex,更新值:
虚拟DOM
虚拟DOM如今已经被作为前端面试基础题了,多多少少面试者都要去了解下,当初 React 就是因为虚拟 DOM 的提出,暂时赢下了浏览器性能之争。当然,这都是几年前的事情了,如今几大框架的性能问题,早也不是什么大问题了。
虚拟DOM大概是这么个过程:
(1)用 JS 对象模拟 DOM 树,得到一棵虚拟 DOM 树。
(2)当页面数据变更时,生成新的虚拟 DOM 树,比较新旧两棵虚拟 DOM 树的差异。
(3)把差异应用到真正的 DOM 树上。
为什么要用到虚拟DOM呢?这是因为一个 DOM 节点它包括了太多太多的属性、元素和事件对象,感觉有上百个。但是我们并不是全部都会用到,通常包括节点内容、元素位置、样式、节点的添加删除等方法,而我们通过用 JS 对象表示 DOM 元素的方式,大大降低了比较差异的计算量。
虚拟DOM中,差异对比也是很关键的一步,这里简单说一下。当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录:需要替换掉原来的节点、移动、删除、新增子节点、修改了节点的属性、 对于文本节点的文本内容改变。经过差异对比之后,我们能获得一组差异记录,接下里我们需要使用它。
如图,这里我们对比两棵 DOM 树,得到的差异有:p 元素插入了一个 span 元素子节点,然后原先的文本节点挪到了span 元素子节点下面,最后通过 JS 操作就可以实现完。
XSS 漏洞
模板引擎还可以协助预防下XSS 相关漏洞。我们知道,XSS 的整个攻击过程大概为:
避免XSS的方法之一主要是将用户所提供的内容进行过滤,而大多数模板引擎会自带 HTML 转义功能。在 Vue 中,默认的数据绑定方式(双大括号、v-bind等)会进行 HTML 转义,将数据解释为普通文本,而非 HTML 代码。
当然,如果你一定要输出HTML代码,也可以使用v-html指令输出。官方文档也有特殊说明:
Vue简介
看看这些模板引擎都做了啥事,原本就是一个
,经过 AST 生成一个对象,最终还是生成一个<div>,这是多余的步骤吗?不是的,在这个过程中我们可以实现一些功能:
排除无效DOM元素,并在构建过程可进行报错
使用自定义组件的时候,可匹配出来
可方便地实现数据绑定、事件绑定等,具备自动更新页面的功能
为虚拟DOM Diff过程打下铺垫
HTML转义(预防 XSS 漏洞)
所以Vue它只是一个模板引擎吗?怎么说呢,模板引擎可能是我们选择框架的原因里最重要的一个,毕竟如果没有框架的话,所有上述这些很好用的能力都需要自己搭建,不然开发效率会很受影响。
我们看看Vue官方的介绍:
数据驱动
前面也介绍了,在jQuery年代,我们通常是使用事件驱动的模式去进行开发了。那么使用了 Vue 之后,写代码的方式会有哪些不一样吗?
既然前面介绍了事件模型一般的编码流程,我们再来看看,同样的们写一个提交表单的页面,用数据驱动的方式来写的话,会变成这么一个流程:
1:设计数据结构。
2:把数据绑定到页面中需要使用/展示的地方
3.事件触发时,更新数据
我们在设置数据(this.name = event.target.value)的时候,Vue 已经替我们把将数据更新到页面的逻辑处理了。大家也可以去 codepen 或者 jsfiddle、stackblitz 这些在线编码网站上尝试下。
所以事件驱动和数据驱动一个很重要的区别在于,我们是从每个事件的触发开始设计我们的代码,还是以数据为中心,接收事件触发和更新数据状态的方式来写代码。
页面编写
如果要问,vue 和 jQuery 有什么不一样?其实它们从定位到设计到实现上都完全不一样,但是对开发者来说,我们可以做个简单直观的区别:
当我们在Vue 中,模板引擎帮我们处理了模板渲染、数据绑定的过程,我们只需要知道这里面只有一个有效数据,即输入框的值。
1.当我们需要渲染成列表时:
2.当我们需要更新一个列表中某个id 的其中一个数据时(这里需要更改 id 为 3 的 name 值):
在使用数据驱动的时候,模板渲染的事情会交给框架去完成,我们需要做的就是数据处理而已。那么转换了数据驱动之后,有什么好处呢?
当我们把一个页面抽象成数据来表示之后,可以做的事情就多了。如果我们把所有的页面、组件、展示内容和接口配置等都变成了配置数据,那么我们就可以把所有的东西都进行配置化。
前端工程化
其实前端工程化这块,我并不是能理解得很深刻,能讲给大家的,应该只是自己的一种理解了。一般来说,现在的主流构建工具应该是Webpack,包括我们使用 Vue 官方脚手架生成的代码,构建工具也是 Webpack。
我们在代码中会使用到很多的资源,图片、样式、代码,还有各式各样的依赖包,而打包的时候怎么实现按需分块、处理依赖关系、不包含多余的文件或内容,同时提供开发和生产环境,包括本地调试自动更新到浏览器这些能力,都是由Webpack 整合起来的。
npm 依赖包
要实现工程化,前端开发几乎都离不开nodejs 的包管理器 npm,比如前端在搭建本地开发服务以及打包编译前端代码等都会用到。在前端开发过程中,经常用到npm install来安装所需的依赖。
为什么会有npm 包的出现呢?因为一个人的力量是有限的,我们在写代码的时候,为了可以将注意力集中到业务开发中,会需要使用别人开源的很多工具、框架和代码包。在很早以前,我们是一个个地下载,或是通过的方式去引用。
当一个网站依赖的代码越来越多,其实管理依赖和版本是一件很麻烦的事情。然后npm 就出现了,作为一个把这些代码集中到一起来管理的工具。
同时,我们可以结合一些tree shaking 的能力,在本地构建的时候,把使用的别人的依赖包里没用用到的部分去掉,减小代码包的大小等。
在安装Node.js 的时候,npm 的能力也会一块提供到,按照完之后就能直接在机器终端使用npm install xxx这种能力啦,需要安装什么依赖包呢,你可以去npmjs官网搜一下。
vue cli
通常来说,脚手架可以让你快速地生成示例代码、搭建本地环境,也可以更新依赖的版本等,避免了每个开发者自行调整开发环境、打包逻辑等配置。Vue cli 也提供了这样的能力,更多的可以参考官方文档。
使用方式很简单:
生成之后的代码目录是这样的:
启动项目
一般来说,项目会有个README.md 文件,你能看到一些简单的说明,例如这里生成的:
yarn 跟 npm 都是差不多的包管理器,区别在于 yarn 在安装时会速度更快(并行、离线等),以及版本统一管理的比较好。但如果你不是有特殊的喜好或者习惯,其实两个都可以用,例如这里的yarn run serve也可以用npm run serve来运行。
如果有些习惯不好的项目缺了README,这种时候要怎么去启动一个项目呢?可以查看package.json文件:
一般来说,开发环境是dev、serve等,生产环境是build,scripts里是一些任务,运行命令npm run taskName就可以启动了。
上图中,我们可以看到任务已经启动了,访问输出的地址(这里是http://localhost:8080/或者http://10.40.120.53:8080/)就能看到我们的项目跑起来了。
第一part 结束了,更多的内容还是前端相关。这些主要是这几年对页面渲染的一些新的理解,然后简单用脚手架启动了个 demo 项目。内容是按照自己觉得较清晰的逻辑展开来讲的,如果说你有更好的想法,或是发现我的描述中有哪些不到位的地方,也十分欢迎来交流。
工具始终是工具没错,但一个工具为什么受这么多人追捧,它到底解决了什么,你可以从中学习到什么,这些才是个人认为的在使用工具时候收获的很重要的东西。在Vue 官方文档很完善的情况下,我来给你们补上文档以外的一些技巧和内容吧。
网友评论