分析
vue-router
中两个重要组件router-link
和router-view
,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度
回答范例
vue-router
中两个重要组件router-link
和router-view
,分别起到路由导航作用和组件内容渲染作用router-link
默认生成一个a
标签,设置to
属性定义跳转path
。实际上也可以通过custom
和插槽自定义最终的展现形式。router-view
是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name
可以显示具名组件,起到更强的布局作用。router-link
组件内部根据custom
属性判断如何渲染最终生成节点,内部提供导航方法navigate
,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes
匹配出数组结果,router-view
则根据其所处深度deep
在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。vuex
就是一个仓库,仓库里放了很多对象。其中state
就是数据源存放地,对应于一般 vue 对象里面的data
里面存放的数据是响应式的,vue
组件从store
读取数据,若是store
中的数据发生改变,依赖这相数据的组件也会发生更新它通过mapState
把全局的state
和getters
映射到当前组件的computed
计算属性
vuex
一般用于中大型 web
单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex
的必要性不是很大,因为完全可以用组件 prop
属性或者事件来完成父子组件之间的通信,vuex
更多地用于解决跨组件通信以及作为数据中心集中式存储数据。Vuex
解决非父子组件之间通信问题 vuex
是通过将 state
作为数据中心、各个组件共享 state
实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State
中能有效解决多层级组件嵌套的跨组件通信问题
vuex
的State
在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在State
中,并在Action
中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在State
中呢? 目前主要有两种数据会使用vuex
进行管理:
包括以下几个模块
state
:Vuex
使用单一状态树,即每个应用将仅仅包含一个store
实例。里面存放的数据是响应式的,vue
组件从 store
读取数据,若是 store
中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState
把全局的 state
和 getters
映射到当前组件的 computed
计算属性mutations
:更改Vuex
的store
中的状态的唯一方法是提交mutation
getters
:getter
可以对 state
进行计算操作,它就是 store
的计算属性虽然在组件内也可以做计算属性,但是 getters
可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
action
:action
类似于 muation
, 不同在于:action
提交的是 mutation
,而不是直接变更状态action
可以包含任意异步操作modules
:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex
的store
对象分割成模块(modules
)
modules
:项目特别复杂的时候,可以让每一个模块拥有自己的state
、mutation
、action
、getters
,使得结构非常清晰,方便管理
回答范例
思路
回答范例
Vuex
是一个专为 Vue.js
应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex
存在的必要性,它和react
生态中的redux
之类是一个概念Vuex
解决状态管理的同时引入了不少概念:例如state
、mutation
、action
等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex
反而是繁琐冗余的,一个简单的 store
模式就足够了。但是,如果要构建一个中大型单页应用,Vuex
基本是标配。vuex
过程中感受到一些等可能的追问
vuex
有什么缺点吗?你在开发过程中有遇到什么问题吗?vuex
中的state
会重新变为初始状态。解决方案-插件 vuex-persistedstate
action
和mutation
的区别是什么?为什么要区分它们?
action
中处理异步,mutation
不可以mutation
做原子操作action
可以整合多个mutation
的集合mutation
是同步更新数据(内部会进行是否为异步方式更新数据的检测)$watch
严格模式下会报错action
异步操作,可以获取数据后调佣mutation
提交最终数据
Action
,Action再触发
Mutation`。Mutation
:专注于修改State
,理论上是修改State
的唯一途径。Action
:业务代码、异步请求Mutation
:必须同步执行。Action
:可以异步,但不能直接操作State
slot
可以分来以下三种:
1. 默认插槽
子组件用
标签来确定渲染的位置,标签里面可以放DOM
结构,当父组件使用的时候没有往插槽传入内容,标签内DOM
结构就会显示在页面
父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue
<template>
<slot>
<p>插槽后备的内容p>
slot>
template>
父组件
<Child>
<div>默认插槽div>
Child>
2. 具名插槽
子组件用name
属性来表示插槽的名字,不传为默认插槽
父组件中在使用时在默认插槽的基础上加上slot
属性,值为子组件插槽name
属性值
子组件Child.vue
<template>
<slot>插槽后备的内容slot>
<slot name="content">插槽后备的内容slot>
template>
父组件
<child>
<template v-slot:default>具名插槽template>
<template v-slot:content>内容...template>
child>
3. 作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot
接受的对象上
父组件中在使用时通过v-slot:
(简写:#)获取子组件的信息,在内容中使用
子组件Child.vue
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽h3>
slot>
template>
父组件
<child>
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
template>
child>
小结:
v-slot
属性只能在
上使用,但在只有默认插槽时可以在组件标签上使用default
,可以省略default直接写v-slot
#
时不能不写参数,写成#default
v-slot={user}
,还可以重命名v-slot="{user: newName}"
和定义默认值v-slot="{user = '默认值'}"
slot
本质上是返回VNode
的函数,一般情况下,Vue
中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM
过程,这里看看slot
如何实现:
编写一个buttonCounter
组件,使用匿名插槽
Vue.component('button-counter', {
template: ' 我是默认内容 '
})
使用该组件
new Vue({
el: '#app',
template: '我是slot传入内容 ',
components:{buttonCounter}
})
获取buttonCounter
组件渲染函数
(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})
_v
表示穿件普通文本节点,_t
表示渲染插槽的函数
渲染插槽函数renderSlot
(做了简化)
function renderSlot (
name,
fallback,
props,
bindObject
) {
// 得到渲染插槽内容的函数
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
// 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回
// 否则使用默认值
nodes = scopedSlotFn(props) || fallback;
return nodes;
}
name
属性表示定义插槽的名字,默认值为default
,fallback
表示子组件中的slot
节点的默认值
关于this.$scopredSlots
是什么,我们可以先看看vm.slot
function initRender (vm) {
...
vm.$slots = resolveSlots(options._renderChildren, renderContext);
...
}
resolveSlots
函数会对children
节点做归类和过滤处理,返回slots
function resolveSlots (
children,
context
) {
if (!children || !children.length) {
return {}
}
var slots = {};
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var data = child.data;
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
// 如果slot存在(slot="header") 则拿对应的值作为key
var name = data.slot;
var slot = (slots[name] || (slots[name] = []));
// 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
// 如果没有就默认是default
(slots.default || (slots.default = [])).push(child);
}
}
// ignore slots that contains only whitespace
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}
_render
渲染函数通过normalizeScopedSlots
得到vm.$scopedSlots
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
作用域插槽中父组件能够得到子组件的值是因为在renderSlot
的时候执行会传入props
,也就是上述_t
第三个参数,父组件则能够得到子组件传递过来的值
非懒加载:
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(1)方案一(常用):使用箭头函数+import动态加载
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(2)方案二:使用箭头函数+require动态加载
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
dom
元素this.$refs.box
datathis.$refs.box.msg
this.$refs.box.open()
权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源
而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发
总的来说,所有的请求发起都触发自前端路由或视图
所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:
4xx
提示页前端权限控制可以分为四个方面:
接口权限
接口权限目前一般采用jwt
的形式来验证,没有通过的话一般返回401
,跳转到登录页面重新进行登录
登录完拿到token
,将token
存起来,通过axios
请求拦截器进行拦截,每次请求的时候头部携带token
axios.interceptors.request.use(config => {
config.headers['token'] = cookie.get('token')
return config
})
axios.interceptors.response.use(res=>{},{response}=>{
if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
router.push('/login')
}
})
路由权限控制
方案一
初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验
const routerMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true, // will always show the root menu
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'pagePermission',
meta: {
title: 'pagePermission',
roles: ['admin'] // or you can only set roles in sub nav
}
}, {
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'directivePermission',
meta: {
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
}]
}]
这种方式存在以下四种缺点:
方案二
初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制
登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes
添加路由
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration
// permission judge function
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}
const whiteList = ['/login', '/authredirect']// no redirect whitelist
router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (getToken()) { // determine if there has token
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) {
next()//
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
// 可删 ↑
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login') // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
})
router.afterEach(() => {
NProgress.done() // finish progress bar
})
按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限
这种方式也存在了以下的缺点:
菜单权限
菜单权限可以理解成将页面与理由进行解耦
方案一
菜单与路由分离,菜单由后端返回
前端定义路由信息
{
name: "login",
path: "/login",
component: () => import("@/pages/Login.vue")
}
name
字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name
对应的字段,并且做唯一性校验
全局路由守卫里做判断
function hasPermission(router, accessMenu) {
if (whiteList.indexOf(router.path) !== -1) {
return true;
}
let menu = Util.getMenuByName(router.name, accessMenu);
if (menu.name) {
return true;
}
return false;
}
Router.beforeEach(async (to, from, next) => {
if (getToken()) {
let userInfo = store.state.user.userInfo;
if (!userInfo.name) {
try {
await store.dispatch("GetUserInfo")
await store.dispatch('updateAccessMenu')
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
//Util.toDefaultPage([...routers], to.name, router, next);
next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
}
}
catch (e) {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
} else {
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
if (hasPermission(to, store.getters.accessMenu)) {
Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
} else {
next({ path: '/403',replace:true })
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
Util.title(menu.title);
});
Router.afterEach((to) => {
window.scrollTo(0, 0);
});
每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name
与路由的name
是一一对应的,而后端返回的菜单就已经是经过权限过滤的
如果根据路由name
找不到对应的菜单,就表示用户有没权限访问
如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes
动态挂载
这种方式的缺点:
方案二
菜单和路由都由后端返回
前端统一定义路由组件
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
home: Home,
userInfo: UserInfo
};
后端路由组件返回以下格式
[
{
name: "home",
path: "/",
component: "home"
},
{
name: "home",
path: "/userinfo",
component: "userInfo"
}
]
在将后端返回路由通过addRoutes
动态挂载之间,需要将数据处理一下,将component
字段换为真正的组件
如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理
这种方法也会存在缺点:
按钮权限
方案一
按钮权限也可以用v-if
判断
但是如果页面过多,每个页面页面都要获取用户权限role
和路由表里的meta.btnPermissions
,然后再做判断
这种方式就不展开举例了
方案二
通过自定义指令进行按钮权限的判断
首先配置路由
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: {
btnPermissions: ['admin', 'supper', 'normal']
},
//页面需要的权限
children: [{
path: 'supper',
component: _import('system/supper'),
name: '权限测试页',
meta: {
btnPermissions: ['admin', 'supper']
} //页面需要的权限
},
{
path: 'normal',
component: _import('system/normal'),
name: '权限测试页',
meta: {
btnPermissions: ['admin']
} //页面需要的权限
}]
}
自定义权限鉴定指令
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// 获取页面按钮权限
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}
在使用的按钮中只需要引用v-has
指令
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
小结
关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离
权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断
axios
是一个轻量的 HTTP
客户端
基于 XMLHttpRequest
服务来执行 HTTP
请求,支持丰富的配置,支持 Promise
,支持浏览器端和 Node.js
端。自Vue
2.0起,尤大宣布取消对 vue-resource
的官方推荐,转而推荐 axios
。现在 axios
已经成为大部分 Vue
开发者的首选
特性
XMLHttpRequests
node.js
创建 http
请求Promise
APIJSON
数据XSRF
基本使用
安装
// 项目中安装
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
导入
import axios from 'axios'
发送请求
axios({
url:'xxx', // 设置请求的地址
method:"GET", // 设置请求方法
params:{ // get请求使用params进行参数凭借,如果是post请求用data
type: '',
page: 1
}
}).then(res => {
// res为后端返回的数据
console.log(res);
})
并发请求axios.all([])
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (res1, res2) {
// res1第一个请求的返回的内容,res2第二个请求返回的内容
// 两个请求都执行完成才会执行
}));
axios
的 API 很友好,你完全可以很轻松地在项目中直接使用。
不过随着项目规模增大,如果每发起一次HTTP
请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍
这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios
再使用
举个例子:
axios('http://localhost:3000/data', {
// 配置代码
method: 'GET',
timeout: 1000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Authorization: 'xxx',
},
transformRequest: [function (data, headers) {
return data;
}],
// 其他请求配置...
})
.then((data) => {
// todo: 真正业务逻辑代码
console.log(data);
}, (err) => {
// 错误处理代码
if (err.response.status === 401) {
// handle authorization error
}
if (err.response.status === 403) {
// handle server forbidden error
}
// 其他错误处理.....
console.log(err);
});
如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了
这时候我们就需要对axios
进行二次封装,让使用更为便利
status
, 来执行不同的业务,这块需要和后端约定好get
、post
等方法进行一个再次封装,使用起来更为方便设置接口请求前缀
利用node
环境变量来作判断,用来区分开发、测试、生产环境
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}
在本地调试的时候,还需要在vue.config.js
文件中配置devServer
实现代理转发,从而实现跨域
devServer: {
proxy: {
'/proxyApi': {
target: 'http://dev.xxx.com',
changeOrigin: true,
pathRewrite: {
'/proxyApi': ''
}
}
}
}
设置请求头与超时时间
大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置
const service = axios.create({
...
timeout: 30000, // 请求 30s 超时
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
},
post: {
'Content-Type': 'application/json;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
}
},
})
封装请求方法
先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去
// get 请求
export function httpGet({
url,
params = {}
}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params
}).then((res) => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}
// post
// post请求
export function httpPost({
url,
data = {},
params = {}
}) {
return new Promise((resolve, reject) => {
axios({
url,
method: 'post',
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
// 发送的数据
data,
// url参数
params
}).then(res => {
resolve(res.data)
})
})
}
把封装的方法放在一个api.js
文件中
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })
页面中就能直接调用
// .vue
import { getorglist } from '@/assets/js/api'
getorglist({ id: 200 }).then(res => {
console.log(res)
})
这样可以把api
统一管理起来,以后维护修改只需要在api.js
文件操作即可
请求拦截器
请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token
// 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
token && (config.headers.Authorization = token)
return config
},
error => {
return Promise.error(error)
})
响应拦截器
响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权
// 响应拦截器
axios.interceptors.response.use(response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
if (response.data.code === 511) {
// 未授权调取授权接口
} else if (response.data.code === 510) {
// 未登录跳转登录页
} else {
return Promise.resolve(response)
}
} else {
return Promise.reject(response)
}
}, error => {
// 我们可以在这里对异常状态作统一处理
if (error.response.status) {
// 处理请求失败的情况
// 对不同返回码对相应处理
return Promise.reject(error.response)
}
})
小结
axios
封装,就可以让我们可以领略到它的魅力axios
没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案参考:前端vue面试题详细解答
可以监听多个方法
<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />
v-on 常用修饰符
.stop
该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation()
方法.prevent
该修饰符会阻止当前事件的默认行为。同理于调用 event.preventDefault()
方法.self
该指令只当事件是从事件绑定的元素本身触发时才触发回调.once
该修饰符表示绑定的事件只会被触发一次
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
属性推荐:
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
不推荐:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
li>
ul>
4. 长列表性能优化
Vue
会通过Object.defineProperty
对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止Vue
劫持我们的数据呢?可以通过Object.freeze
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
5. 事件的销毁
Vue
组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js
内使用 addEventListener
等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
6. 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue
的 vue-lazyload
插件
npm install vue-lazyload --save-dev
在入口文件 man.js
中引入并使用
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 或者添加自定义选项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
在 vue
文件中将 img
标签的 src
属性直接改为 v-lazy
,从而将图片显示方式更改为懒加载显示
<img v-lazy="/static/img/1.png">
以上为 vue-lazyload
插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址(opens new window)
7. 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak
打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来
路由懒加载:
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
8. 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component
,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui
组件库为例
npm install babel-plugin-component -D
将 .babelrc
修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
在 main.js
中引入部分组件:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
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) 就可以轻松地添加预渲染1. Webpack 对图片进行压缩
对小于 limit
的图片转化为 base64
格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader
来压缩图片
npm install image-webpack-loader --save-dev
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
2. 减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码
class HelloWebpack extends Component{...}
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
babel-runtime/helpers/createClass // 用于实现 class 语法
babel-runtime/helpers/inherits // 用于实现 extends 语法
在默认情况下, Babel
会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass')
的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime
插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小
npm install babel-plugin-transform-runtime --save-dev
修改 .babelrc
配置文件为:
"plugins": [
"transform-runtime"
]
3. 提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack
内置了专门用于提取多个Chunk
中的公共部分的插件 CommonsChunkPlugin
,我们在项目中 CommonsChunkPlugin
的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
}
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
4. 模板预编译
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
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
执行 $ npm run build --report
后生成分析报告如下
1. 开启 gzip 压缩
gzip
是GNUzip
的缩写,最早用于UNIX
系统的文件压缩。HTTP
协议上的gzip
编码是一种用来改进web
应用程序性能的技术,web
服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip
压缩效率非常高,通常可以达到70%
的压缩率,也就是说,如果你的网页有30K
,压缩之后就变成了9K
左右
以下我们以服务端使用我们熟悉的 express
为例,开启 gzip
非常简单,相关步骤如下:
npm install compression --save
var compression = require('compression');
var app = express();
app.use(compression())
重启服务,观察网络面板里面的 response header
,如果看到如下红圈里的字段则表明 gzip
开启成功
Nginx开启gzip压缩
#是否启动gzip压缩,on代表启动,off代表开启
gzip on;
#需要压缩的常见静态资源
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
#由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选
项是禁止ie6发生压缩
gzip_disable "MSIE [1-6]\.";
#如果文件大于1k就启动压缩
gzip_min_length 1k;
#以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改
gzip_buffers 4 16k;
#压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大
gzip_comp_level 2;
要想配置生效,记得重启nginx
服务
nginx -t
nginx -s reload
2. 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
3. CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
4. 使用 Chrome Performance 查找性能瓶颈
Chrome
的 Performance
面板可以录制一段时间内的 js
执行细节及时间。使用 Chrome
开发者工具分析页面性能的步骤如下。
Chrome
开发者工具,切换到 Performance
面板Record
开始录制Stop
停止录制vue.js
:vue-cli
工程的核心,主要特点是 双向数据绑定 和 组件系统。vue-router
:vue
官方推荐使用的路由框架。vuex
:专为 Vue.js
应用项目开发的状态管理器,主要用于维护vue
组件间共用的一些 变量 和 方法。axios
( 或者 fetch
、ajax
):用于发起 GET
、或 POST
等 http
请求,基于 Promise
设计。vuex
等:一个专为vue
设计的移动端UI组件库。emit.js
文件,用于vue
事件机制的管理。webpack
:模块加载和vue-cli
工程打包器。Vue
确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher
用于更新的话,会产生大量的watcher
从而降低性能vue
采用了组件级的watcher
配合diff
来检测差异有五种,分别是 State、 Getter、Mutation 、Action、 Module
分析
路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。
体验
全局守卫:
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
路由独享守卫:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
组件内的守卫:
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
},
}
回答
vue-router
中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。全局
、路由独享
、组件级
。影响范围由大到小,例如全局的router.beforeEach()
,可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter
,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter
,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。navigate
方法,内部有个guards
队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。原理
runGuardQueue(guards)
链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch
流程取消原本导航
// 源码
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check the route beforeEnter
guards = []
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter && !from.matched.includes(record)) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
(1)监测机制的改变
(2)只能监测属性,不能监测对象
(3)模板
(4)对象式的组件声明方式
(5)其它方面的更改
用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.name
和 this.$route.params.name
。
url地址显示:query更加类似于ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
注意:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。
函数组件的特点
functional:true
this
,this
通过render
函数的第二个参数context
来代替watch
$emit
对外暴露事件,调用事件只能通过context.listeners.click
的方式调用外部传入的事件ref
去引用组件时,实际引用的是HTMLElement
props
可以不用显示声明,所以没有在props
里面声明的属性都会被自动隐式解析为prop
,而普通组件所有未声明的属性都解析到$attrs
里面,并自动挂载到组件根元素上面(可以通过inheritAttrs
属性禁止)优点
使用场景:
router-view
就是一个函数式组件例子
Vue.component('functional',{ // 构造函数产生虚拟节点的
functional:true, // 函数式组件 // data={attrs:{}}
render(h){
return h('div','test')
}
})
const vm = new Vue({
el: '#app'
})
源码相关
// functional component
if (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 处理原生事件
// install component management hooks onto the placeholder node
installComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)
使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中data
中获取并初始化视图,这个过程发生在Compile
中Watcher
,将来对应数据变化时Watcher
会调用更新函数data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
Dep
,通知所有Watcher
执行更新函数流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对data
选项执行响应化具体操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例key
,例如name1
,创建⼀个watcher1
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
声明Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
创建watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建Dep
实例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
1. pinia是什么?
- 在
Vue3
中,可以使用传统的Vuex
来实现状态管理,也可以使用最新的pinia
来实现状态管理,我们来看看官网如何解释pinia
的:Pinia
是Vue
的存储库,它允许您跨组件/页面共享状态。- 实际上,
pinia
就是Vuex
的升级版,官网也说过,为了尊重原作者,所以取名pinia
,而没有取名Vuex
,所以大家可以直接将pinia
比作为Vue3
的Vuex
2. 为什么要使用pinia?
Vue2
和Vue3
都支持,这让我们同时使用Vue2
和Vue3
的小伙伴都能很快上手。pinia
中只有state
、getter
、action
,抛弃了Vuex
中的Mutation
,Vuex
中mutation
一直都不太受小伙伴们的待见,pinia
直接抛弃它了,这无疑减少了我们工作量。pinia
中action
支持同步和异步,Vuex
不支持Typescript
支持,毕竟我们Vue3
都推荐使用TS
来编写,这个时候使用pinia
就非常合适了Vuex
中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia
中每个store
都是独立的,互相不影响。1KB
左右。pinia
支持插件来扩展自身功能。3. pinna使用
我们这里搭建一个最新的Vue3 + TS + Vite
项目
npm create vite@latest my-vite-app --template vue-ts
pinia
基础使用yarn add pinia
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");
2.1 创建store
//sbinsrc/store/user.ts
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
})
创建store
很简单,调用pinia
中的defineStore
函数即可,该函数接收两个参数:
name
:一个字符串,必传项,该store
的唯一id
。options
:一个对象,store
的配置项,比如配置store
内的数据,修改数据的方法等等。我们可以定义任意数量的store
,因为我们其实一个store
就是一个函数,这也是pinia
的好处之一,让我们的代码扁平化了,这和Vue3
的实现思想是一样的
2.2 使用store
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
script>
2.3 添加state
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
});
2.4 读取state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}p>
<p>年龄:{{ age }}p>
<p>性别:{{ sex }}p>
template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
script>
上段代码中我们直接通过store.age
等方式获取到了store
存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
2.5 修改state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}p>
<p>年龄:{{ age }}p>
<p>性别:{{ sex }}p>
<button @click="changeName">更改姓名button>
template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
store.name = "张三";
console.log(store);
};
script>
2.6 重置state
state
数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。store
的$reset()
方法即可,继续使用我们的例子,添加一个重置按钮<button @click="reset">重置store</button>
// 重置store
const reset = () => {
store.$reset();
};
当我们点击重置按钮时,store
中的数据会变为初始状态,页面也会更新
2.7 批量更改state
数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store
的$patch
方法,修改app.vue
代码,添加一个批量更改数据的方法
<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
state
中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。pinia
提供的$patch
方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
2.8 直接替换整个state
pinia
提供了方法让我们直接替换整个state
对象,使用store
的$state
方法
store.$state = { counter: 666, name: '张三' }
上段代码会将我们提前声明的state
替换为新的对象,可能这种场景用得比较少
getters
属性getters
是defineStore
参数配置项里面的另一个属性getter
想象成Vue
中的计算属性,它的作用就是返回一个新的结果,既然它和Vue
中的计算属性类似,那么它肯定也是会被缓存的,就和computed
一样3.1 添加getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 10,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
},
})
上段代码中我们在配置项参数中添加了getter
属性,该属性对象中定义了一个getAddAge
方法,该方法会默认接收一个state
参数,也就是state
对象,然后该方法返回的是一个新的数据
3.2 使用getter
<template>
<p>新年龄:{{ store.getAddAge }}p>
<button @click="patchStore">批量修改数据button>
template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
script>
上段代码中我们直接在标签上使用了store.gettAddAge
方法,这样可以保证响应式,其实我们state
中的name
等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter
中调用其它getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
3.3 getter
传参
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
<p>新年龄:{{ store.getAddAge(1100) }}p>
actions
属性state
和getter
s属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data
数据和computed
计算属性一样。actions
属性里面,该属性就和我们组件代码中的methods
相似,用来放置一些处理业务逻辑的方法。actions
属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法4.1 添加actions
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
// 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store
saveName(name: string) {
this.name = name;
},
},
});
4.2 使用actions
使用actions
中的方法也非常简单,比如我们在App.vue
中想要调用该方法
const saveName = () => {
store.saveName("poetries");
};
总结
pinia
的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌
pinia无非就是以下3个大点:
state
getters
actions