前端高频 vue 面试题合集
路由的 hash 和 history 模式的区别
Vue-Router 有两种模式:hash 模式和 history 模式。默认的路由模式是 hash 模式。
1. hash 模式
简介: hash 模式是开发中默认的模式,它的 URL 带着一个 #
特点:hash 值会出现在 URL 里面,但是不会出现在 HTTP 请求中,对后端完全没有影响。所以改变 hash 值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的 IE 浏览器也支持这种模式。hash 路由被称为是前端路由,已经成为 SPA(单页面应用)的标配。
原理: hash 模式的主要原理就是 onhashchange()事件:
使用 onhashchange()事件的好处就是,在页面的 hash 值发生变化时,无需向后端发起请求,window 就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash 值变化对应的 URL 都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的 hash 值和对应的 URL 关联起来了。
2. history 模式
简介: history 模式的 URL 中没有 #,它使用的是传统的路由分发模式,即用户在输入一个 URL 时,服务器会接收这个请求,并解析这个 URL,然后做出相应的逻辑处理。 特点: 相比 hash 模式更加好看。但是,history 模式需要后台配置支持。如果后台没有正确配置,访问时会返回 404。 API: history api 可以分为两大部分,切换历史状态和修改历史状态:
修改历史状态:包括了 HTML5 History Interface 中新增的
pushState()
和replaceState()
方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了 url,但浏览器不会立即向后端发送请求。如果要做到改变 url 但又不刷新页面的效果,就需要前端用上这两个 API。切换历史状态: 包括
forward()
、back()
、go()
三个方法,对应浏览器的前进,后退,跳转操作。
虽然 history 模式丢弃了丑陋的 #。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出 404 来。
如果想要切换到 history 模式,就要进行以下配置(后端也要进行配置):
3. 两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
pushState() 可额外设置 title 属性供后续使用。
hash 模式下,仅 hash 符号之前的 url 会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回 404 错误;history 模式下,前端的 url 必须和实际向后端发起请求的 url 一致,如果没有对用的路由处理,将返回 404 错误。
hash 模式和 history 模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
vue 和 react 的区别
=> 相同点:
=> 不同点:
虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
diff 算法 — 比较两棵虚拟 DOM 树的差异;
pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
Vue.js 的 template 编译
简而言之,就是先转化成 AST 树,再得到的 render 函数返回 VNode(Vue 的虚拟 DOM 节点),详细步骤如下:
首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。
然后,AST 会经过 generate(将 AST 语法树转化成 render funtion 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有(标签名、子节点、文本等等)
Vue 是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
以上只保留了关键代码,主要就是 const dep = new Dep()
实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend()
进行依赖收集。 (1)Dep Dep 是整个依赖收集的核心,其关键代码如下:
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
(2)Watcher
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。
MVVM、MVC、MVP 的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel:
Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
View 代表 UI 视图,负责数据的展示;
ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作 DOM。
(3)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的 Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
参考:前端vue面试题详细解答
为什么 vue 组件中 data 必须是一个函数?
对象为引用类型,当复用组件时,由于数据对象都指向同一个 data 对象,当在一个组件中修改 data 时,其他重用的组件中的 data 会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object 的实例),引用地址不同,则不会出现这个问题。
Vue 中封装的数组方法有哪些,其如何实现页面更新
在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods
来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。
vue-cli 工程常用的 npm 命令有哪些
下载
node_modules
资源包的命令:
启动
vue-cli
开发环境的 npm 命令:
vue-cli
生成 生产环境部署资源 的npm
命令:
用于查看
vue-cli
生产环境部署资源文件大小的npm
命令:
在浏览器上自动弹出一个 展示
vue-cli
工程打包后app.js
、manifest.js
、vendor.js
文件里面所包含代码的页面。可以具此优化vue-cli
生产环境部署的静态资源,提升 页面 的加载速度
简述 mixin、extends 的覆盖逻辑
(1)mixin 和 extends mixin 和 extends 均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。
(2)mergeOptions 的执行过程
规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
对未合并的选项,进行判断
合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
返回合并结果 options。
Vue 项目本地开发完成后部署到服务器后报 404 是什么原因呢
如何部署
前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web
容器指定的静态目录下即可
我们知道vue
项目在构建后,是生成一系列的静态文件
常规布署我们只需要将这个目录上传至目标服务器即可
让web
容器跑起来,以nginx
为例
配置完成记得重启nginx
操作完后就可以在浏览器输入域名进行访问了
当然上面只是提到最简单也是最直接的一种布署方式
什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开
404 问题
这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?
我们先还原一下场景:
vue
项目在本地时运行正常,但部署到服务器中,刷新页面,出现了 404 错误
先定位一下,HTTP 404 错误意味着链接指向的资源不存在
问题在于为什么不存在?且为什么只有history
模式下会出现这个问题?
为什么 history 模式下有问题
Vue
是属于单页应用(single-page application)
而SPA
是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html
现在,我们回头来看一下我们的nginx
配置
可以根据 nginx
配置得出,当我们在地址栏输入 www.xxx.com
时,这时会打开我们 dist
目录下的 index.html
文件,然后我们在跳转路由进入到 www.xxx.com/login
关键在这里,当我们在 website.com/login
页执行刷新操作,nginx location
是没有相关配置的,所以就会出现 404
的情况
为什么 hash 模式下没有问题
router hash
模式我们都知道是用符号 #表示的,如 website.com/#/login
, hash
的值为 #/login
它的特点在于:hash
虽然出现在 URL
中,但不会被包括在 HTTP
请求中,对服务端完全没有影响,因此改变 hash
不会重新加载页面
hash
模式下,仅 hash
符号之前的内容会被包含在请求中,如 website.com/#/login
只有 website.com
会被包含在请求中 ,因此对于服务端来说,即使没有配置location
,也不会返回404
错误
解决方案
看到这里我相信大部分同学都能想到怎么解决问题了,
产生问题的本质是因为我们的路由是通过 JS 来执行视图切换的,
当我们进入到子路由时刷新页面,web
容器没有相对应的页面此时会出现404
所以我们只需要配置将任意页面都重定向到 index.html
,把路由交由前端处理
对nginx
配置文件.conf
修改,添加try_files $uri $uri/ /index.html;
修改完配置文件后记得配置的更新
这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html
文件
为了避免这种情况,你应该在 Vue
应用里面覆盖所有的路由情况,然后在给出一个 404 页面
父子组件生命周期调用顺序(简单)
渲染顺序:先父后子,完成顺序:先子后父
更新顺序:父更新导致子更新,子更新完成后父
销毁顺序:先父后子,完成顺序:先子后父
SPA 首屏加载速度慢的怎么解决
一、什么是首屏加载
首屏时间(First Contentful Paint
),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
关于计算首屏时间
利用performance.timing
提供的数据:
通过DOMContentLoad
或者performance
来计算出首屏时间
二、加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
网络延时问题
资源文件体积是否过大
资源是否重复发送请求去加载了
加载脚本的时候,渲染内容堵塞了
三、解决方案
常见的几种 SPA 首屏优化方式
减小入口文件积
静态资源本地缓存
UI 框架按需加载
图片资源的压缩
组件重复打包
开启 GZip 压缩
使用 SSR
1. 减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
在vue-router
配置路由的时候,采用动态加载路由的形式
以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
2. 静态资源本地缓存
后端返回资源问题:
采用
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头采用
Service Worker
离线缓存
前端合理利用localStorage
3. UI 框架按需加载
在日常使用UI
框架,例如element-UI
、或者antd
,我们经常性直接引用整个UI
库
但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用
4. 组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks
为 3 表示会把使用 3 次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5. 图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http
请求压力。
6. 开启 GZip 压缩
拆完包之后,我们再用gzip
做一下压缩 安装compression-webpack-plugin
在vue.congig.js
中引入并修改webpack
配置
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip
,就发送给它gzip
格式的文件 我的服务器是用express
框架搭建的 只要安装一下compression
就能使用
7. 使用 SSR
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成 html 字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,vue
应用建议使用Nuxt.js
实现服务端渲染
四、小结
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化
和 页面渲染优化
下图是更为全面的首屏优化的方案
大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化
Vue 项目性能优化-详细
Vue
框架通过数据双向绑定和虚拟DOM
技术,帮我们处理了前端开发中最脏最累的DOM
操作部分, 我们不再需要去考虑如何操作DOM
以及如何最高效地操作DOM
;但Vue
项目中仍然存在项目首屏优化、Webpack
编译配置优化等问题,所以我们仍然需要去关注Vue
项目性能方面的优化,使项目具有更高效的性能、更好的用户体验
代码层面的优化
1. v-if 和 v-show 区分使用场景
v-if
是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块v-show
就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS
display
的none/block
属性进行切换。所以,
v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show
则适用于需要非常频繁切换条件的场景
2. computed 和 watch 区分使用场景
computed
: 是计算属性,依赖其它属性值,并且computed
的值有缓存,只有它依赖的属性值发生改变,下一次获取computed
的值时才会重新计算 computed 的值;watch
: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用
computed
,因为可以利用computed
的缓存特性,避免每次获取值时,都要重新计算;当我们需要在数据变化时执行异步或开销较大的操作时,应该使用
watch
,使用watch
选项允许我们执行异步操作 ( 访问一个API
),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的
3. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
v-for
遍历必须为item
添加key
在列表数据进行遍历渲染时,需要为每一项
item
设置唯一key
值,方便Vue.js
内部机制精准找到该条列表数据。当state
更新时,新的状态值和旧的状态值对比,较快地定位到diff
v-for
遍历避免同时使用v-if
vue2.x
中v-for
比v-if
优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成computed
属性
推荐:
不推荐:
4. 长列表性能优化
Vue
会通过Object.defineProperty
对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止Vue
劫持我们的数据呢?可以通过Object.freeze
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了
5. 事件的销毁
Vue
组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js
内使用 addEventListener
等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
6. 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue
的 vue-lazyload
插件
在入口文件 man.js
中引入并使用
在 vue
文件中将 img
标签的 src
属性直接改为 v-lazy
,从而将图片显示方式更改为懒加载显示
以上为 vue-lazyload
插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址(opens new window)
7. 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak
打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来
路由懒加载:
8. 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component
,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui
组件库为例
将 .babelrc
修改为:
在 main.js
中引入部分组件:
9. 优化无限列表性能
如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表
的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom
节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种无限列表的场景的
10. 服务端渲染 SSR or 预渲染
服务端渲染是指 Vue
在客户端将标签渲染成的整个 html
片段的工作在服务端完成,服务端形成的 html
片段直接返回给客户端这个过程就叫做服务端渲染。
如果你的项目的
SEO
和首屏渲染
是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和SEO
如果你的
Vue
项目只需改善少数营销页面(例如/
,/about
,/contact
等)的SEO
,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态HTML
文件。 优点是设置预渲染更简单 ,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染
Webpack 层面的优化
1. Webpack 对图片进行压缩
对小于 limit
的图片转化为 base64
格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader
来压缩图片
2. 减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
在默认情况下, Babel
会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass')
的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime
插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小
修改 .babelrc
配置文件为:
3. 提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
相同的资源被重复加载,浪费用户的流量和服务器的成本。
每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack
内置了专门用于提取多个Chunk
中的公共部分的插件 CommonsChunkPlugin
,我们在项目中 CommonsChunkPlugin
的配置如下:
4. 模板预编译
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数
5. 提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存
6. 优化 SourceMap
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel 编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug 的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap
出现了,它就是为了解决不好调式代码问题的
SourceMap
的可选值如下(+
号越多,代表速度越快,-
号越多,代表速度越慢,o
代表中等速度)
开发环境推荐:
cheap-module-eval-source-map
生产环境推荐:
cheap-module-source-map
原因如下:
cheap
: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加cheap
的基本类型来忽略打包前后的列信息;module
:不管是开发环境还是正式环境,我们都希望能定位到bug
的源代码具体的位置,比如说某个Vue
文件报错了,我们希望能定位到具体的Vue
文件,因此我们也需要module
配置;soure-map
:source-map
会为每一个打包后的模块生成独立的soucemap
文件 ,因此我们需要增加source-map
属性;eval-source-map
:eval
打包代码的速度非常快,因为它不生成map
文件,但是可以对eval
组合使用eval-source-map
使用会将map
文件以DataURL
的形式存在打包后的js
文件中。在正式环境中不要使用eval-source-map
, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
7. 构建结果输出分析
Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer
执行 $ npm run build --report
后生成分析报告如下
基础的 Web 技术优化
1. 开启 gzip 压缩
gzip
是GNUzip
的缩写,最早用于UNIX
系统的文件压缩。HTTP
协议上的gzip
编码是一种用来改进web
应用程序性能的技术,web
服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE 等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip
压缩效率非常高,通常可以达到70%
的压缩率,也就是说,如果你的网页有30K
,压缩之后就变成了9K
左右
以下我们以服务端使用我们熟悉的 express
为例,开启 gzip
非常简单,相关步骤如下:
重启服务,观察网络面板里面的 response header
,如果看到如下红圈里的字段则表明 gzip
开启成功
Nginx 开启 gzip 压缩
要想配置生效,记得重启nginx
服务
2. 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
3. CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且 CDN 具有更好的可用性,更低的网络延迟和丢包率
4. 使用 Chrome Performance 查找性能瓶颈
Chrome
的 Performance
面板可以录制一段时间内的 js
执行细节及时间。使用 Chrome
开发者工具分析页面性能的步骤如下。
打开
Chrome
开发者工具,切换到Performance
面板点击
Record
开始录制刷新页面或展开某个节点
点击
Stop
停止录制
Vue 模版编译原理知道吗,能简单说一下吗?
简单说,Vue 的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
生成 AST 树
优化
codegen
首先解析模版,生成AST语法树
(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
SPA、SSR 的区别是什么
我们现在编写的Vue
、React
和Angular
应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO
不友好的问题,因此有了SSR
,这也是为什么面试中会问到两者的区别
SPA
(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称CSR
。SSR
(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称MPA
SPA
应用只会首次请求html
文件,后续只需要请求JSON
数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO
不友好。为了解决以上缺点,就有了SSR
方案,由于HTML
内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时 SSR 方案也会有性能,开发受限等问题在选择上,如果我们的应用存在首屏加载优化需求,
SEO
需求时,就可以考虑SSR
但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR 反而浪费资源,我们可以考虑预渲染(
prerender
)方案。另外nuxt.js/next.js
中给我们提供了SSG(Static Site Generate)
静态网站生成方案也是很好的静态站点解决方案,结合一些CI
手段,可以起到很好的优化效果,且能节约服务器资源
内容生成上的区别:
SSR
SPA
部署上的区别
Vue 中常见性能优化
编码优化 :
使用
v-show
复用DOM
:避免重复创建组件
合理使用路由懒加载、异步组件,有效拆分
App
尺寸,访问时才异步加载
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态
v-once
和v-memo
:不再变化的数据使用v-once
按条件跳过更新时使用v-momo
:下面这个列表只会更新选中状态变化项
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
防止内部泄漏,组件销毁后把全局变量和事件销毁:
Vue
组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
滚动到可视区域动态加载
https://tangbc.github.io/vue-virtual-scroll-list(opens new window)
第三方插件按需引入:(
babel-plugin-component
)
像element-plus
这样的第三方组件库可以按需引入避免体积太大
服务端渲染:SSR
如果SPA
应用有首屏渲染慢的问题,可以考虑SSR
以及下面的其他方法
不要将所有的数据都放在
data
中,data
中的数据都会增加getter
和setter
,会收集对应的watcher
v-for
遍历为item
添加key
v-for
遍历避免同时使用v-if
区分
computed
和watch
的使用拆分组件(提高复用性、增加代码的可维护性,减少不必要的渲染 )
防抖、节流
用户体验
app-skeleton
骨架屏pwa
serviceworker
SEO 优化
预渲染插件
prerender-spa-plugin
服务端渲染
ssr
打包优化
Webpack
对图片进行压缩使用
cdn
的方式加载第三方模块多线程打包
happypack
splitChunks
抽离公共文件优化
SourceMap
构建结果输出分析,利用
webpack-bundle-analyzer
可视化分析工具
基础的 Web 技术的优化
服务端
gzip
压缩浏览器缓存
CDN
的使用使用
Chrome Performance
查找性能瓶颈
Vue 项目中你是如何解决跨域的呢
一、跨域是什么
跨域本质是浏览器基于同源策略的一种安全手段
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能
所谓同源(即指在同一个域)具有以下三个相同点
协议相同(protocol)
主机相同(host)
端口相同(port)
反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域
一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用 postman 请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。
如果让你从零开始写一个 vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态 API。
vuex
需求分析如何实现这些需求
回答范例
官方说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
要实现一个
Store
存储全局状态要提供修改状态所需 API:
commit(type, payload), dispatch(type, payload)
实现
Store
时,可以定义Store
类,构造函数接收选项options
,设置属性state
对外暴露状态,提供commit
和dispatch
修改属性state
。这里需要设置state
为响应式对象,同时将Store
定义为一个Vue
插件commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise
给用户以处理异步结果
实践
Store
的实现:
vuex 简易版
验证方式
评论