赏口饭吃┭┮﹏┭┮
Vue 系列
对 SPA(单页面应用)的理解
1.什么是 SPA
SPA,翻译过来就是单页面应用, 是以中网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换影响用户体验,在单页应用中所偶有必要的代码(HTML,JavaScript和CSS
)都通过单个页面的加载而检索,或者根据需要(响应用户操作)动态装载适当的资源并添加到页面,页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。
2.SPA 和 MPA 的区别
上面大家已经对单页面有所了解,下面来说多页面应用,在多页面应用中,每个页面都是一个主页面,都是独立的,当我们访问另一个页面的时候,都需要重新加载html
,css
,js
文件,公告文件比如Header
,Footer
按需加载。
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整夜刷新 |
url 模式 | 哈希模式 | 历史模式 |
SEO 搜索引擎优化 | 难实现,可使用 SSR 方式改善 | 容易实现 |
数据传递 | 容易 | 通过 url、cookie、localStorage 等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
v-if 和 v-show 怎么理解
1.v-show 与 v-if 的区别
控制手段:v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除
编译过程:v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于 css 切换
编译条件:v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。渲染条件为假时,并不做操作,直到为真才渲染
v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;
2.v-show 和 v-if 的使用场景
v-if
与 v-show
都能控制 dom 元素在页面的显示与隐藏,v-if
相比 v-show
开销更大的(直接操作 dom 节点增加与删除)
如果需要非常频繁地切换,则使用 v-show
较好,如果在运行时条件很少改变,则使用 v-if
较好
Vue 的 v-if 和 v-for 不建议一起使用
1. 作用
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true 值的时候被渲染v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组或者对象,而 item
则是被迭代的数组元素的别名
在 v-for
的时候,建议设置key
值,并且保证每个key
值是独一无二的,这便于 diff 算法进行优化
2. 优先级
v-for
优先级比v-if
高
注意事项:
1.永远不要把 v-if
和 v-for
同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
2.如果避免出现这种情况,则在外层嵌套template
(页面渲染不生成 dom 节点),在这一层进行v-if
判断,然后在内部进行v-for
循环
1 | <template v-if="isShow"> |
3.如果条件出现在循环内部,可通过计算属性computed
提前过滤掉那些不需要显示的项
1 | computed: { |
SPA(单页应用)首屏加载速度慢怎么解决
1.什么是首屏加载
首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
通过DOMContentLoad
或者performance
来计算出首屏时间
1 | // 方案一: |
2.加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
- 网络延时问题
- 资源文件体积是否过大
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
3.解决方案
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化
下图是更为全面的首屏优化的方案
SSR 解决了什么问题?
1.SSR 是什么
Server-Side Rendering
我们称其为 SSR,意为服务端渲染
指由服务器完成页面的 HTML
结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程.
先来看看 Web3 个阶段的发展史:
- 传统服务端渲染 SSR
- 单页面应用 SPA
- 服务端渲染 SSR
Vue
官方对SSR
的解释:
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记”激活”为客户端上完全可交互的应用程序.
服务器渲染的 Vue.js 应用程序也可以被认为是”同构”或”通用”,因为应用程序的大部分代码都可以在服务器和客户端上运行
我们从上面解释得到以下结论:
- Vue SSR 是一个在 SPA 上进行改良的服务端渲染
- 通过 Vue SSR 渲染的页面,需要在客户端激活才能实现交互
- Vue SSR 将包含两部分:服务端渲染的首屏,包含交互的 SPA
2.解决了什么
SSR 主要解决了以下两种问题:
- seo:搜索引擎优先爬取页面 HTML 结构,使用 ssr 时,服务端已经生成了和业务想关联的 HTML,有利于 seo
- 首屏呈现渲染:用户无需等待页面所有 js 加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)
但是使用 SSR 同样存在以下的缺点: - 复杂度:整个项目的复杂度
- 库的支持性,代码兼容
- 性能问题: 1.每个请求都是 n 个实例的创建,不然会污染,消耗会变得很大 2.缓存 node serve、 nginx 判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。 3.降级:监控 cpu、内存占用过多,就 spa,返回单个的壳
- 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用
所以在我们选择是否使用 SSR 前,我们需要慎重问问自己这些问题:
- 需要 SEO 的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实现
- 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢
Vue 中给对象添加新属性界面不刷新
Vue 组件间通信方式都有哪些
1.组件间通信的概念
开始之前,我们把组件间通信拆分
- 组件
- 通信
都知道组件是vue
最强大的功能之一,vue
中每一个.vue
我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信即指组件(.vue
)通过某种方式来传递信息以达到某个目的.举个栗子我们在使用 UI 框架中的table
组件,可能会往table
组件中传入某些数据,这个本质就形成了组件之间的通信
2.组件间通信解决了什么
通信的本质是信息同步,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统
3.组件间通信的分类
组件间通信的分类可以分成以下
- 父子组件之间的通信
- 兄弟组件之间的通信
- 祖孙与后代组件之间的通信
- 非关系组件间之间的通信
关系如图
4.组件间通信的方案
整理vue
中 8 种常规的通信方案
- 通过 props 传递
- 通过$emit 触发自定义事件
- 使用 ref
- EvenBus
- $parent或$root
- attrs 与 listeners
- Provide 与 inject
- Vuex
通过 prop 传递
适用场景:父组件传递数据给子组件
子组件设置props
属性,定义接收父组件传递过来的参数
父组件在使用子组件标签中通过字面量来传递值Children.vue
:
1 | props:{ |
Father.vue
组件:
1 | <Children name="jack" age=18 /> |
通过$emit 触发自定义事件
适用场景:子组件传递数据给父组件
子组件通过$emit触发自定义事件,$emit 第二个参数为传递的数值
父组件绑定监听器获取到子组件传递过来的参数Children.vue
:
1 | this.$emit('add', good) |
Father.vue
组件:
1 | <Children @add="cartAdd($event)" /> |
ref
父组件在使用子组件的时候设置 ref
父组件通过设置子组件 ref
来获取数据
父组件:
1 | ;<Children ref="foo" /> |
EventBus
使用场景:兄弟组件传值
创建一个中央事件总线 EventBus
兄弟组件通过$emit触发自定义事件,$emit 第二个参数为传递的数值
另一个兄弟组件通过$on 监听自定义事件
Bus.js
:
1 | // 创建一个中央时间总线类 |
Children1.vue
1 | this.$bus.$emit('foo') |
Children2.vue
1 | this.$bus.$on('foo', this.handle) |
$parent 或$ root
通过共同祖辈$parent
或者$root
搭建通信桥连
兄弟组件
this.$parent.on('add',this.add)
另一个兄弟组件
this.$parent.emit('add')
$attrs 与$ listeners
适用场景:祖先传递数据给子孙
设置批量向下传属性$attrs
和 $listeners
包含了父级作用域中不作为 prop
被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
可以通过 v-bind="$attrs"
传⼊内部组件
1 | // child:并未在props中声明foo |
1 | // 给Grandson隔代传值,communication/index.vue |
provide 与 inject
在祖先组件定义 provide 属性,返回传递的值
在后代组件通过 inject 接收组件传递过来的值
祖先组件
1 | provide(){ |
后代组件
1 | inject: ['foo'] // 获取到祖先组件传递过来的值 |
vuex
适用场景: 复杂关系的组件数据传递
Vuex 作用相当于一个用来存储共享变量的容器
state
用来存放共享变量的地方
getter
,可以增加一个getter
派生状态,(相当于 store 中的计算属性),用来获得共享变量的值
mutations
用来存放修改 state
的方法。
actions
也是用来存放修改state
的方法,不过 action
是在mutations
的基础上进行。常用来做一些异步操作
小结
- 父子关系的组件数据传递选择
props
与$emit
进行传递,也可选择ref
- 兄弟关系的组件数据传递可选择
$bus
,其次可以选择$parent
进行传递 - 祖先与后代组件数据传递可选择
attrs
与listeners
或者Provide
与Inject
- 复杂关系的组件数据传递可以通过
vuex
存放共享的变量
双向绑定的理解
对 nexttick 的理解
1.NextTick 是什么
官方定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
我们可以理解成,Vue
在更新 DOM
时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列
,视图需要等队列中所有数据变化完成之后,再统一进行更新
举个栗子:
1 | <div id="app">{{ message }}</div> |
1 | const vm = new Vue({ |
修改message
,并获取。
1 | this.message = '修改后的值1' |
这个时候获取页面最新的DOM
节点,却发现获取到的是旧值
这是因为message
数据在发现变化的时候,vue 并不会立刻去更新Dom
,而是将修改数据的操作放在了一个异步操作队列中
如果我们一直修改相同数据,异步操作队列还会进行去重
等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM
的更新
为什么要有 nexttick
1 | {{num}} |
如果没有 nextTick
更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新 10 万次视图),有了nextTick
机制,只需要更新一次,所以nextTick
本质是一种优化策略
2.使用场景
如果想要在修改数据后立刻得到更新后的DOM
结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM
结构)
第二个参数为:执行函数上下文
1 | // 修改数据 |
组件内使用 vm.$nextTick()
实例方法只需要通过this.$nextTick()
,并且回调函数中的 this 将自动绑定到当前的 Vue
实例上
1 | this.message = '修改后的值' |
$nextTick()
会返回一个 Promise 对象,可以是async/await
完成相同作用的事情
1 | this.message = '修改后的值' |
对 slot 的理解?使用场景
1.slot 是什么?
在 HTML 中 slot
元素 ,作为 Web Components
技术套件的一部分,是 Web 组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
1 | <template id="element-details-template"> |
template
不会展示到页面中,需要用先获取它的引用,然后添加到DOM
中
1 | customElements.define( |
在 Vue 中的概念也是如此Slot
艺名插槽,花名“占坑”,我们可以理解为solt
在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot
位置),作为承载分发内容的出口
可以将其类比为插卡式的 FC 游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)
2.使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过 slot 插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
3.分类
slot
可以分来以下三种
- 默认插槽
- 具名插槽
- 作用域插槽
默认插槽
子组件用<slot>
标签来确定渲染的位置,标签里面可以放DOM
结构,当父组件使用的时候没有往插槽传入内容,标签内DOM
结构就会显示在页面
父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue
1 | <template> |
父组件
1 | <Child> |
具名插槽
子组件用name
属性来表示插槽的名字,不传为默认插槽
父组件中在使用时在默认插槽的基础上加上slot
属性,值为子组件插槽name
属性值
子组件Child.vue
1 | <template> |
父组件
1 | <child> |
作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot
接受的对象上
父组件中在使用时通过v-slot
:(简写:#)获取子组件的信息,在内容中使用
子组件Child.vue
1 | <template> |
父组件
1 | <child> |
小结
v-slot
属性只能在<template>
上使用,但在只有默认插槽时可以在组件标签上使用- 默认插槽名为
default
,可以省略default
直接写v-slot
- 缩写为#时不能不写参数,写成
#default
- 可以通过解构获取
v-slot={user}
,还可以重命名v-slot="{user: newName}"
和定义默认值v-slot="{user = '默认值'}"
Vue 中 key 的理解
1. key 是什么
开始之前,我们先还原两个实际工作场景
1.当我们在使用v-for
时,需要给单元加上key
1 | <ul> |
2.用+new Date()
生成的时间戳作为key
,手动强制触发重新渲染
1 | <Comp :key="+new Date()" /> |
一句话来讲,key
是给每一个 vnode 的唯一 id,也是 diff
的一种优化策略,可以根据 key
,更准确, 更快的找到对应的 vnode 节点
背后的逻辑
当我们在使用 v-for 时,需要给单元加上 key
如果不用 key,Vue 会采用就地复地原则:最小化 element 的移动,并且会尝试尽最大程度在同适当的地方对相同类型的 element,做 patch 或者 reuse。
如果使用了 key,Vue 会根据 keys 的顺序记录 element,曾经拥有了 key 的 element 如果不再出现的话,会被直接 remove 或者 destoryed
用+new Date()生成的时间戳作为 key,手动强制触发重新渲染
当拥有新值的 rerender 作为 key 时,拥有了新 key 的 Comp 出现了,那么旧 key Comp 会被移除,新 key Comp 触发渲染
对 keep-alive 的理解
1. keep-alive 是什么
keep-alive
是 vue 中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染 DOM
keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive
可以设置以下 props 属性:
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
- max - 数字。最多可以缓存多少组件实例
关于keep-alive
的基本用法:
1 | <keep-alive> |
使用includes
和exclude
:
1 | <keep-alive include="a,b"> |
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配
设置了 keep-alive
缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated
再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated
2.使用场景
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
当我们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是需要keep-alive
从首页
–>列表页
–>商详页
–>返回到列表页(需要缓存)
–>返回到首页(需要缓存)
–>再次进入列表页(不需要缓存)
,这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive
属性判断是否需要缓存
1 | { path: 'list', name: 'itemList', // 列表页 component (resolve) { |
使用<keep-alive>
:
1 | <div id="app" class="wrapper"> |
3.缓存后如何获取数据
解决方案可以有以下两种:
- beforeRouteEnter
- actived
beforeRouteEnter
每次组件渲染的时候,都会执行beforeRouteEnter
1 | beforeRouteEnter(to, from, next){ |
actived
在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
1 | activated(){ |
注意:服务器端渲染期间avtived
不被调用
什么是虚拟 DOM?如何实现一个虚拟 DOM
1.什么是虚拟 DOM
虚拟 DOM
(Virtual DOM )这个概念相信大家都不陌生,从 React
到 Vue
,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-Native
和 Weex
)
实际上它只是一层对真实DOM
的抽象,以JavaScript 对象 (VNode 节点)
作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
在Javascript对象
中,虚拟DOM
表现为一个 Object对象
。并且最少包含标签名 (tag)
、属性 (attrs)
和子元素对象 (children)
三个属性,不同框架对这三个属性的名命可能会有差别
创建虚拟DOM
就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点
与真实DOM的属性
一一照应
在 vue 中同样使用到了虚拟 DOM 技术
定义真实DOM
1 | <div id="app"> |
实例化vue
1 | const app = new Vue({ |
观察render
的render
,我们能得到虚拟DOM
1 | ;(function anonymous() { |
通过VNode
,vue
可以对这颗抽象树进行创建节点
,删除节点
以及修改节点
的操作, 经过diff算法
得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能
2.为什么需要虚拟 DOM
DOM
是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作
引起的
真实的 DOM 节点,哪怕一个最简单的 div 也包含着很多属性
由此可见,操作
DOM
的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验
举个例子:
用传统的原生api
或jQuery
去操作 DOM 时,浏览器会从构建DOM树
开始从头到尾执行一遍流程
当你在一次操作时,需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行10次流程
而通过 VNode,同样更新 10 个 DOM 节点,虚拟DOM不会立即操作DOM
,而是将这 10 次更新的 diff 内容保存到本地的一个js对象
中,最终将这个js对象
一次性attach
到DOM树
上,避免大量的无谓计算
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。这是虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种 GUI
diff 算法
1.是什么
diff
算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
比较只会在同层级进行, 不会跨层级比较
在 diff 比较的过程中,循环从两边向中间比较
diff
算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
2.比较方式
diff
整体策略为:深度优先,同层比较
1.比较只会在同层级进行, 不会跨层级比较
2.循环从两边向中间靠拢
下面举个 vue 通过 diff 算法更新的例子:
新旧 VNode
节点如下图所示:
第一次循环后,发现旧节点 D 与新节点 D 相同,直接复用旧节点 D 作为 diff 后的第一个真实节点,同时旧节点 endIndex 移动到 C,新节点的 startIndex 移动到了 C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E
第三次循环中,发现 E 没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的 startIndex 移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex 移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdx 和 newEndIdx 之间的所有节点,也就是节点 F,直接创建 F 节点对应的真实节点放到 B 节点后面
vue 要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
1.是什么
权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源
而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发:
- 页面加载触发
- 页面上的按钮点击触发
总的来说,所有的请求发起都触发自前端路由或视图,所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:
- 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
- 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
- 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截
2.如何做
前端权限控制可以分为四个方面:
接口权限
接口权限目前一般采用jwt
的形式来验证,没有通过的话一般返回 401,跳转到登录页面重新进行登录
登录完拿到 token
,将 token
存起来,通过 axios
请求拦截器进行拦截,每次请求的时候头部携带 token
1 | axios.interceptors.request.use(config => { |
路由权限控制
方案一
初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验
1 | const routerMap = [ |
这种方式存在以下四种缺点:
方案二
初始化的时候先挂载不需要权限控制的路由,比如登录页,404 等错误页。如果用户通过 URL 进行强制访问,则会直接进入 404,相当于从源头上做了控制
登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用 addRoutes 添加路由
1 | import router from './router' |
按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限
这种方式也存在了以下的缺点:
菜单权限
菜单权限可以理解成将页面与路由进行解耦
方案一
菜单与路由分离,菜单由后端返回
前端定义路由信息
1 | { |
name
字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name
对应的字段,并且做唯一性校验
全局路由守卫里做判断
1 | function hasPermission(router, accessMenu) { |
每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name
与路由的name
是一一对应的,而后端返回的菜单就已经是经过权限过滤的
如果根据路由 name 找不到对应的菜单,就表示用户有没权限访问
如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes
动态挂载
这种方式的缺点:
方案二
菜单和路由都由后端返回
前端统一定义路由组件
1 | const Home = () => import('../pages/Home.vue') |
后端路由组件返回以下格式
1 | ;[ |
在将后端返回路由通过addRoutes
动态挂载之间,需要将数据处理一下,将component
字段换为真正的组件
如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理
这种方法也会存在缺点:
按钮权限
方案一
按钮权限也可以用 v-if 判断
但是如果页面过多,每个页面都要获取用户权限 role 和路由表里的 meta.btnPermissions,然后再做判断。
方案二
通过自定义指令进行按钮权限的判断
首先配置路由
1 | { |
自定义权限鉴定指令
1 | import Vue from 'vue' |
在使用的按钮中只需要引用v-has
指令
1 | <el-button @click='editClick' type="primary" v-has>编辑</el-button> |
小结
关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离
权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断
跨域问题
1.跨域是什么?
跨域本质是浏览器基于同源策略的一种安全手段
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能
所谓同源(即指在同一个域)具有以下三个相同点:
反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域
一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用 postman 请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。
2.如何解决
解决跨域的方法有很多,下面列举了三种:
而在 vue 项目中,我们主要针对CORS
或Proxy
这两种方案进行展开
CORSCORS
(Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应
CORS
实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源
只要后端实现了 CORS
,就实现了跨域
Proxy
代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击
方案一
如果是通过 vue-cli 脚手架工具搭建项目,我们可以通过 webpack 为我们起一个本地服务器作为请求的代理对象
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域
在 vue.config.js 文件,新增以下代码
1 | amodule.exports = { |
通过axios
发送请求中,配置请求的根路径
1 | axios.defaults.baseURL = '/api' |
方案二
此外,还可通过服务端实现代理请求转发
以express
框架为例
1 | var express = require('express') |
方案三
通过配置nginx
实现代理
1 | server { |
Vue2 和 Vue3 的区别
1.Vue3 介绍
关于vue3
的重构背景,尤大是这样说的:
「Vue 新版本的理念成型于 2018 年末,当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比了
在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题」
简要就是:
哪些变化
我们可以概览 Vue3 的新特性,如下:
速度更快
vue3
相比vue2
体积更小
通过webpack
的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking
,有两大好处
vue
可以开发出更多其他的功能,而不必担忧vue
打包出来的整体体积过多
更易维护
更好的 Typescript 支持
VUE3 是基于 typescipt 编写的,可以享受到自动的类型定义提示
更接近原生
可以自定义渲染 API
更易使用
响应式 Api 暴露出来
轻松识别组件重新渲染原因
2.Vue3 新特性
Vue 3 中需要关注的一些新功能包括:
framents
在 Vue3.x
中,组件现在支持有多个根节点
1 | <!-- Layout.vue --> |
Teleport
Teleport
是一种能够将我们的模板移动到 DOM
中Vue app
之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”
在 vue2 中,像 modals,toast
等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位
、z-index
和样式就会变得很困难
通过Teleport
,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它
1 | <button @click="showToast" class="btn">打开 toast</button> |
createRenderer
通过 createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台
我们可以将其生成在 canvas 画布上
关于 createRenderer,我们了解下基本使用,就不展开讲述了
1 | import { createRenderer } from '@vue/runtime-core' |
composition Api
composition Api,也就是组合式 api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
关于 compositon api 的使用,这里以下图展开
简单使用:
1 | export default { |
3.非兼容变更
Global Api
模板指令
组件
其他小改变
移除 api
—————-
ES6 系列
说说 var let const 之间的区别
1.var
在 ES5 中,顶层对象的属性和全局变量是等价的,用 var 声明的变量既是全局变量,也是顶层变量
顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象
1 | var a = 10 |
使用 var 声明的变量存在变量提升的情况
1 | console.log(a) // undefined |
在编译阶段,编译器会将其变成以下执行
1 | var a |
使用 var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
1 | var a = 20 |
在函数中使用使用 var 声明变量时候,该变量是局部的
1 | var a = 20 |
而如果在函数内不使用 var,该变量是全局的
1 | var a = 20 |
2. let
let
是ES6
新增的命令,用来声明变量
用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效
1 | { |
不存在变量提升
1 | console.log(a) // 报错ReferenceError |
这表示在声明它之前,变量 a 是不存在的,这时如果用到它,就会抛出一个错误
只要块级作用域内存在 let 命令,这个区域就不再受外部影响
1 | var a = 123 |
使用 let 声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”
最后,let 不允许在相同作用域中重复声明
1 | let a = 20 |
注意的是相同作用域,下面这种情况是不会报错的
1 | let a = 20 |
因此,我们不能在函数内部重新声明参数
1 | function func(arg) { |
3.const
const
声明一个只读的常量,一旦声明,常量的值就不能改变
1 | const a = 1 |
这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值
1 | const a; |
如果之前用 var 或 let 声明过变量,再用 const 声明同样会报错
1 | var a = 20 |
const
实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的,并不能确保改变量的结构不变
1 | const foo = {} |
其它情况,const
与let
一致
4.区别
var
、let
、const
三者区别可以围绕下面五点展开:
变量提升
var
声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
let
和const
不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
1 | // var |
暂时性死区
var
不存在暂时性死区let
和const
存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
1 | // var |
块级作用域
var
不存在块级作用域let
和const
存在块级作用域
1 | // var |
重复声明
var
允许重复声明变量let
和const
在同一作用域不允许重复声明变量
1 | // var |
修改声明的变量
var
和let
可以const
声明一个只读的常量。一旦声明,常量的值就不能改变
1 | // var |
使用
能用const
的情况尽量使用const
,其他情况下大多数使用let
,避免使用var
ES6 中数组新增了哪些扩展
1.扩展运算符的应用
ES6 通过扩展元素符...
,好比 rest
参数的逆运算,将一个数组转为用逗号分隔的参数序列
1 | console.log(...[1, 2, 3]) |
主要用于函数调用的时候,将一个数组变为参数序列
1 | function push(array, ...items) { |
可以将某些数据结构转为数组
1 | ;[...document.querySelectorAll('div')] |
能够更简单实现数组复制
1 | const a1 = [1, 2] |
数组的合并也更为简洁了
1 | const arr1 = ['a', 'b'] |
注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组
下面看个例子就清楚多了
1 | const arr1 = ['a', 'b', [1, 2]] |
扩展运算符可以与解构赋值结合起来,用于生成数组
1 | const [first, ...rest] = [1, 2, 3, 4, 5] |
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
1 | const [...butLast, last] = [1, 2, 3, 4, 5]; |
可以将字符串转为真正的数组
1 | ;[...'hello'] |
定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
1 | let nodeList = document.querySelectorAll('div') |
如果对没有 Iterator
接口的对象,使用扩展运算符,将会报错
1 | const obj = { a: 1, b: 2 } |
2. 构造函数新增的方法
关于构造函数,数组新增的方法有如下:
Array.from()
将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set
和 Map
)
1 | let arrayLike = { |
还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组
1 | Array.from([1, 2, 3], (x) => x * x) |
Array.of()
用于将一组值,转换为数组
1 | Array.of(3, 11, 8) // [3,11,8] |
没有参数的时候,返回一个空数组
当参数只有一个的时候,实际上是指定数组的长度
参数个数不少于 2 个时,Array()才会返回由参数组成的新数组
1 | Array() // [] |
ES6 中新增的 Set,Map 两种数据结构怎么理解
如果要用一句来描述,我们可以说
Set
是一种叫做集合的数据结构,Map
是一种叫做字典的数据结构
什么是集合?什么又是字典?
1.Set
Set
是 es6 新增的数据结构,类似于数组
,但是成员的值都是唯一的,没有重复的值,我们一般称为集合
Set
本身是一个构造函数,用来生成 Set
数据结构
1 | const s = new Set() |
增删改查
Set
的实例关于增删改查的方法:
add()
添加某个值,返回 Set
结构本身
当添加实例中已经存在的元素,set
不会进行处理添加
1 | s.add(1).add(2).add(2) // 2只被添加了一次 |
delete()
删除某个值,返回一个布尔值,表示删除是否成功
1 | s.delete(1) |
has()
返回一个布尔值,判断该值是否为 Set 的成员
1 | s.has(2) |
clear()
清除所有成员,没有返回值
1 | s.clear() |
遍历
Set
实例遍历的方法有如下:
2.Map
Map
类型是键值对的有序列表,而键和值都可以是任意类型Map
本身是一个构造函数,用来生成 Map
数据结构
1 | const m = new Map() |
增删改查
Map
结构的实例针对增删改查有以下属性和操作方法:
遍历
Map
结构原生提供三个遍历器生成函数和一个遍历方法:
ES6 中的 Promise 以及试用场景
1.介绍
Promise
,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大
在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码
1 | doSomething(function (result) { |
阅读上面代码,是不是很难受,上述形成了经典的回调地狱
现在通过Promise
的改写上面的代码
1 | doSomething() |
瞬间感受到promise
解决异步操作的优点:
下面我们正式来认识promise
:
状态promise
对象仅有三种状态
特点
流程
认真阅读下图,我们能够轻松了解promise
整个流程
2.用法
Promise
对象是一个构造函数,用来生成Promise
实例
1 | const promise = new Promise(function (resolve, reject) {}) |
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
实例方法
Promise
构建出来的实例存在以下方法:
then()
then
是实例状态发生改变时的回调函数,第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数then
方法返回的是一个新的Promise
实例,也就是promise
能链式书写的原因
1 | getJSON('/posts.json') |
catch()
catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数
1 | getJSON('/posts.json') |
Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止
1 | getJSON('/post/1.json') |
一般来说,使用 catch 方法代替then()
第二个参数Promise
对象抛出的错误不会传递到外层代码,即不会有任何反应
1 | const someAsyncThing = function () { |
浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined
,但是不会退出进程catch()
方法之中,还能再抛出错误,通过后面 catch
方法捕获到
finally()
finally()
方法用于指定不管 Promise
对象最后状态如何,都会执行的操作
1 | promise |
构造函数方法
Promise
构造函数存在以下方法:
怎么理解 ES6 中的 Proxy 以及使用场景
1.介绍
定义: 用于定义基本操作的自定义行为
本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)
元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
一段代码来理解
1 |
|
这段程序每执行一次能帮我们生成一个名为 program 的文件,文件内容为 1024 行 echo,如果我们手动来写 1024 行代码,效率显然低效
Proxy
亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等
2.用法
Proxy
为 构造函数,用来生成 Proxy
实例
1 | var proxy = new Proxy(target, handler) |
参数
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))
handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为
3. 使用场景
Proxy
其功能非常类似于设计模式中的代理模式,常用功能如下: