主要总结了用vue以来的一些特殊却又可能常见的使用技术,哈哈(仅限个人经历,不一定满足你要的全部要求喔。但是基础使用是有的)
内容是按照我的理解去解释,如果有说的不清晰或者理解错误的希望大家告诉我,及时纠正!或者有什么迷惑想整的,也可以说说喔,让我看看自己会不会,不会我在学。哈哈哈哈哈。互相指教哟!
内容委实太长,就按目录找自己想看的吧!我还会在加的,哈哈哈。要是能从头看到尾,佩服你耐心十足啊???。
目录
- 1.组件通信
- 1.1父子组件
- 1.1.1(props,emit)
- 1.1.2(\$parent,\$children)
- 1.2祖孙组件
- 1.2.1(\$attrs,\$listeners,inheritAttrs :2.4.0 新增)
- 1.2.2( provide,inject:2.2.0 新增)
- 1.3组件通用
- 1.3.1状态管理(vuex)
- 1.3.1.1 vuex常用(四大:store,getters,mutations,Action)
- 1.3.1.2 进阶(辅助函数(mapState,mapGetters...),模块(Module))
- 1.3.2中央事件总线(eventBus)
- 1.3.2.1 Event Emitter (事件派发器,node.js语法 。js文件使用)
- 1.3.2.2( \$emit ,\$on 。vue使用)
- 2.keep-alive的使用
- 2.1 常用(匹配的组件会一直缓存)
- 2.2 动态设置(a跳转b刷新;c跳回到b缓存)
- 3.接口封装(axios拦截器)
- 4.项目权限
- 4.1界面权限
- 4.2按钮权限
1.组件通信
1.1父子组件
1.1.1(props,emit)
- 父组件使用组件添加属性的形式进行参数传递,要在js中引入子组件
//父组件 parent.vue
<template><div>父组件内容//parentList,parentClose为父级数据,方法<children :childrenList="parentList" @childrenClick="parentClose" /></div>
</template><script>
import children from "路径";
export default {name: "parent",components: {children},data() {return {parentList:[1,2,3]}},methods:{parentClose(parms){//子组件传递过来的参数console.log(parms); //点击子组件列表,打印出:我来自子组件}},
}
</script>
- 子组件使用props接收参数。使用$emit触发事件向父组件传递参数
//子组件 children.vue
<template><div>子组件的内容<ul><li v-for="(item,index) in childrenList" :key="index" @click="clickArea(item,index)">{{item}}</li></ul></div>
</template><script>export default {name: "children",props: {childrenList: {type: Array,default() {return ["xixi"]}}},data() {return {message:"我来自子组件"}},methods: {clickArea(item,index){this.$emit("childrenClick",this.message)}}}
</script>
1.1.2($parent,$children)
- vm.$children(当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的)
少用,因为不能保证子组件的排序
//父组件
this.$children[0].gameMsg;//访问子组件data及方法
- vm.$parent (父实例,如果当前实例有的话,如果存在多级子组件,通过parent 访问得到的是它最近一级的父组件)
//子组件
this.$parent.drawDeactive();//访问父组件的方法
this.$parent.Bflag = "";//修改父组件data
1.2祖孙组件
1.2.1($attrs,$listeners,inheritAttrs :2.4.0 新增)
可读性不好,适用于层级嵌套多的非父子组件,比较小的项目。
- vm.$attrs(包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外))
- vm.$listeners(包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器)
- inheritAttrs(默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。改为false可阻止)
//============父组件==============
<template><div>//向子组件传递参数及方法<child :childData1="parentData1" :childData2="parentData2" @clickParent="changeparentData2 @one.native="noAttr"></child></div>
</template>
<script>import child from "路径";export default {name:"parent",data() {return {parentData1:"哈哈哈",parentData2:"嘻嘻嘻"}},components:{child},methods: {changeparentData2(){this.parentData2=this.parentData1;},noAttr(){console.log("这个事件后代不能访问到")}}}
</script>//==================子组件==============
<template><div><span>我来自父级:{{$attrs.childData2}}</span>//$attrs 使孙子组件可访问父组件传递的所有属性,除去被prop获取了的//$listeners 使孙子组件可使用$emit触发父组件事件及传参,除去.native修饰器的事件<grandson v-bind="$attrs" v-on="$listeners"></grandson ></div>
</template>
<script>import grandson from '路径';export default {name:'child'props:["childData1"],inheritAttrs:false,//去除默认行为,不影响 class 和 style 绑定。created() {console.log(this.$attrs); // 结果:childData2,由于childData1被props接收console.log(this.$listeners); // clickParent: f},}
</script>//===============孙子组件==============
<template><div><span @click="clickchange">我来自祖父:{{childData2}}</span></div>
</template>
<script>export default {name:'grandson 'props:["childData2"],inheritAttrs:false,created() {console.log(this.$attrs.childData1); //childData1被child接收了console.log(this.$listeners); // clickParent: f},methods: {clickchange(){this.$emit("clickParent")//this.$listeners.clickParent()}},}
</script>
1.2.2( provide,inject:2.2.0 新增)
主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深(重点:provide 和 inject 绑定并不是可响应的,就是改了祖先的绑定的值的时候,后代得到的数据不会更改)
缺点:追踪数据不好追踪,不知道在哪个组件修改了值,并且不会响应式修改。
- provide(是一个对象,或者是一个返回对象的函数。里面包含有要传递给子孙后代的 资源)
//祖先组件
<template><div id="app"><span @click="changeData">修改data</span><router-view/></div>
</template><script>
export default {name: 'App',data () {return {list:["xixi","haha"]},//父组件中返回要传给下级的数据provide () {// 一个返回对象的函数return {reload: this.reload,dataList:this.list,//响应式修改方法:this;函数返回(后代组件使用compute实时获取)all:this, // 根组件提供将自身提供给后代组件getList: () => this.list}},//provide:{//一个对象//reload: this.reload,//dataList:this.list,//...//},methods: {changeData(){this.list=["only"]; //只改变了list。不会改变provide的dataList,但是getList后代用compute获取会变化。this.provided.dataList=["new words"];//provide的dataList变化了,但后代组件获得依旧是["xixi","haha"]},reload() {console.log("触发到顶级的事件了");}}
}
</script>
- inject(是一个字符串,或者一个对象)
//某一个后代组件
<template><div>//没有相应变化,除非使用all修改了祖父组件数据<ul><li v-for="(item,index) in copyList" :key="index" @click="clickEvent">{{item}}</li></ul>//响应式变化<ul><li v-for="(item,index) in newList" :key="index" @click="clickEvent">{{item}}</li></ul></div>
</template>
<script>
export default {name: 'son',inject: ['reload','dataList',"all","getList"],data () {return {copyList: this.dataList}},created() {this.all.dataList= ['baz']; // 修改成功,显示 'baz'。慎用:只要有一个组件修改了该状态,那么所有组件都会受到影响// this.dataList=["ha"]; //不要直接修改inject的对象,可以修改copyListthis.copyList=["ha"];},computed: {newList() {return this.getList()}},watch: {'newList': function (val) {console.log('当祖父组件变化list的时候打印', val)}}methods: {clickEvent(){this.reload();}},
}
1.3组件通用
1.3.1状态管理(vuex)
1.3.1.1 vuex常用(四大:store,getters,mutations,Action)
整体的模式,实现的流程。下面描述中 “四大” 就代表了:store,getters,mutations,Action。
简易列子( 新建store文件夹下index.js)
import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)const store = new Vuex.Store({state: {count: 0}, getters: {doneTodos: state => {//每次都会去进行调用,不会缓存结果。返回调用的结果return state.count++}},mutations: {//直接修改state,同步函数increment (state) {state.count++}},actions: {//可以异步操作,不知直接修改state,是提交mutationsincrement (context) {context.commit('increment')}}
})
根组件注入(main.js):
import store from './store'; //导入store
- store(state)
"store"基本上就是一个容器,它包含着你的应用中大部分的状态 (state);
两个特性:
1.vuex 的状态存储是响应式的,一旦状态发生变化,那么相应的组件也会相应地得到高效更新
2.不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。//使用this.$store.state.名字 访问(如:this.$store.getters.count)
- getter(可以认为是 store 的计算属性)
getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
//store index.js
const store = new Vuex.Store({state: {todos: [{ id: 1, text: '...', done: true },{ id: 2, text: '...', done: false }]},getters: {doneTodos: state => {return state.todos.filter(todo => todo.done)},//可接收其他 getter 作为第二个参数doneTodosCount: (state, getters) => {return getters.doneTodos.length}}
})//组件内部
//使用this.$store.getters.名字 访问(如:this.$store.getters.doneTodosCount)
- mutations(修改store的唯一方法,使用commit触发)
//index.js
const store = new Vuex.Store({state: {count: 1},mutations: {//类似于注册了一个事件。触发事件使用store.commit //increment (state) {state.count++},//额外的参数parms,即mutation的载荷(payload),要传递多个参数时,参数(载荷)是一个对象changCount(state,parms){state.count += parms}}
})//组件内部
//以载荷形式分发:this.$store.commit('xxx',参数)进行修改(如:this.$store.commit('changCount',10))
//以对象形式分发(少用):this.$store.commit({type: 'changCount',amount: 10}),这时候parms是一个对象,使用parms.amount
- Action(使用dispatch 触发)
与mutation的区别:1. Action 提交的是 mutation,而不是直接变更状态。2. Action 可以包含任意异步操作。
//index.js store中
actions: {increment1 ({ commit }) {//使用es6参数结构,免去context.commit复杂写法。写法{ state ,getters ,dispatch, commit },可dispatch其他actionscommit('increment')},changCount1 (state,parms){//载荷commit('increment',parms)}
}//组件内部
//使用this.$store.dispatch('xxx',参数)进行修改(如:this.$store.dispatch('changCount',10))
组合 Action:
//index.js store中
actions: {//actionA ({ commit }) {return new Promise((resolve, reject) => {setTimeout(() => {commit('someMutation')resolve()}, 1000)})},actionB ({ dispatch, commit }) {return dispatch('actionA').then(() => {commit('someOtherMutation')})},//使用 async / awaitasync actionA ({ commit }) {commit('gotData', await getData())},async actionB ({ dispatch, commit }) {await dispatch('actionA') // 等待 actionA 完成commit('gotOtherData', await getOtherData())}
}//组件内部
this.$store.dispatch('actionA').then(() => {// ...
})
1.3.1.2 进阶(辅助函数(mapState,mapGetters…),模块(Module))
- 辅助函数(mapState ,mapGetters,mapMutations,mapActions)
其实本质就是可以简化写法,一次性暴露出想要的state,getters,mutations,actions。
import { mapState,mapGetters,mapMutations } from 'vuex'export default {name: "register",data() {return {}},computed: {//辅助函数返回的都是对象,...es6对象展开运算符...mapState(['count',// 将 `this.count` 映射为 `this.$store.state.count`]),...mapGetters(['doneTodos',// 将 `this.doneTodos` 映射为 `this.$store.getter.doneTodos`anthorName: 'doneTodosCount'//可以另取一个名字]),},methods: {...mapMutations([//也可另取名字'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`'changCount' // 将 `this.changCount(amount)` 映射为 `this.$store.commit('changCount', amount)`]),...mapActions([//也可另取名字'increment1', // 将 `this.increment1()` 映射为 `this.$store.dispatch('increment1')`'changCount1' // 将 `this.changCount1(amount)` 映射为 `this.$store.dispatch('changCount1', amount)`]),}
}
- 模块(Module)
模块其实就是把项目中的某一页面,或者某一功能区域需要使用的store的进行分开创建。比如用户相关的操作(登录,退出登录,注册等)就创建一个user模块,页面权限的相关操作就创建一个permission模块。这样不会在项目内容多的时候出现复杂臃肿的store。每个模块都有自己的四大。
const moduleA = {state: () => ({ ... }),mutations: { ... },actions: { ... },getters: { ... }
}const moduleB = {state: () => ({ ... }),mutations: { ... },actions: { ... }
}const store = new Vuex.Store({modules: {a: moduleA,moduleB }
})
this.$store.state.a // -> moduleA 的状态
this.$store.state.moduleB // -> moduleB 的状态
在项目中可以这样搭建
- 命名空间
//组件使用
this.$store.state.user.name;
this.$store.dispatch('user/getInfo');
...
- 想在某一模块中注册全局 action:使用root属性,在handler中写函数
//user.js 模块
export default {namespaced: true,actions: {someAction: {root: true,handler (namespacedContext, payload) { // -> 'someAction'... } }}
}
- 在模块中访问全局的“四大”:
模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
模块内部的 getter:根节点状态rootState会作为第三个参数暴露出来,rootGetters 第四个参数
getters: {sumWithRootCount (state, getters, rootState,rootGetters ) {return state.count + rootState.count}}
对于模块内部的 action:通过 context.state 暴露出来
actions: {incrementIfOddOnRootSum ({ state, commit, rootState }) { //content的rootState表示根节点状态if ((state.count + rootState.count) % 2 === 1) {commit('increment')};//想要分发全局的action或提交全局的mutation: { root: true } 作为第三参数dispatch('someOtherAction') // -> '模块的/someOtherAction'dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'commit('someMutation') // -> '模块的/someMutation'commit('someMutation', null, { root: true }) // -> 'someMutation'}}
- 带命名空间的辅助函数
辅助函数:createNamespacedHelpers
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')export default {computed: {// 在 `some/nested/module` 中查找...mapState({a: state => state.a,b: state => state.b})},methods: {// 在 `some/nested/module` 中查找...mapActions(['foo','bar'])}
}
其实模块化的辅助函数常用的是mapGetters:
使用:
computed: {...mapGetters(['sidebar']),}
1.3.2中央事件总线(eventBus)
1.3.2.1 Event Emitter (事件派发器,node.js语法 。js文件使用)
EventEmitter 的核心就是事件触发与事件监听器功能的封装。
- events.EventEmitter
//EventBus.jsvar events = require('events');//定义对象并导出,存储所有事件名称,EventEmitter处理中心
export const eventBus = {}
eventBus.bus = new events.EventEmitter();//配置所有的事件名称
//地图加载完毕事件
eventBus.MAP_IS_LOAD = 'MAP_IS_LOAD';
//测量坐标
eventBus.COORD = 'COORD';
//点选查询
eventBus.MOUSE_CLICK_QUERY = 'MOUSE_CLICK_QUERY';
//时态图层添加事件
eventBus.LAYER_TIMER_ADD = 'LAYER_TIMER_ADD';
//绘制对象
eventBus.DRAW_GRAPH = 'DRAW_GRAPH';
- on ,addListener,once
on和addListener:本质上没什么却别(on是注册一个监听器,addListener是添加一个监听器到监听器数组的尾部。)
once:注册一个单次监听器,监听器最多只会触发一次
//某一个js监听某事件
import {eventBus} from "路径";eventBus.bus.on(eventBus.MAP_IS_LOAD, function(message1,message2) { console.log(message1);
});
//支持若干个事件监听器。按顺序执行...
eventBus.bus.addListener(eventBus.MAP_IS_LOAD, function(message1,message2) { console.log("第二个输出");
});
- emit,removeListener,removeAllListeners
emit:执行每个监听器,如果事件有注册监听返回 true,否则返回 false。
removeListener:移除指定事件的某个监听器,两个参数:第一个是事件名称,第二个是回调函数名称。
removeAllListeners:不传参移除所有事件的所有监听器。如果传事件参数,则移除指定事件的所有监听器。
//某一个js触发事件
import {eventBus} from "路径";setTimeout(function() { //一秒后触发地图加载完毕事件eventBus.bus.emit(eventBus.MAP_IS_LOAD,"我是参数1","我是参数二");
}, 1000); setTimeout(function() { //一秒后触发地图加载完毕事件eventEmitter.removeAllListeners(eventBus.MAP_IS_LOAD);
}, 2000);
1.3.2.2( $emit ,$on 。vue使用)
vue的eventBus本质是创建一个vue实例,与node不同的是:是使用vue实例构建,依赖于vue的api。
(重点:vue页面销毁时,同时移除EventBus事件监听。不然就会累加,出现问题)
- 使用的两种方式
//方法一:直接在main.js构建全局的事件总线
Vue.prototype.$eventBus = new Vue()//方法二:bus.js 专门处理的js。类似于1.3.2.1中构建方式。不同的是vue是以new Vue()构建实例
import Vue from 'vue'export const eventBus = {}
eventBus.bus = new Vue();eventBus.MAP_IS_LOAD = 'MAP_IS_LOAD';
//......
- 提交事件,传递参数($emit)
//A.vue
<template><span @click="clickEmit()">点击我触发事件</span>
</template><script>
import { eventBus } from "路径";
export default {methods: {clickEmit() {//全局的方式为:this.$eventBus.$emit()eventBus.bus.$emit("changemessage", '修改B界面的message,我来自A');}}
};
</script>
- 接收事件,使用参数($on,$once)
//B.vue
<template><span>{{msg}}</span>
</template><script>
import { eventBus } from "路径";
export default {data(){return {msg: '我只B的展示'}},mounted() {//全局的方式为:this.$eventBus.$on()eventBus.bus.$on("changemessage", (msg) => { //当A界面点击事件出发后,就执行下面的回调this.msg = msg;});},beforeDestroy(){//销毁前一定要移除。否则会累积触发事件回调函数eventBus.bus.$off("changemessage")},
};
</script>
- 移除事件($off)
1.如果没有提供参数,则移除所有的事件监听器;
2.如果只提供了事件,则移除该事件所有的监听器;
3.如果同时提供了事件与回调,则只移除这个回调的监听器。
2.keep-alive的使用
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例。共有三个属性:
include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max - 数字。最多可以缓存多少组件实例。//2.5.0 新增,超过这个值的时候,会销毁最久没被访问的实例
2.1 常用(匹配的组件会一直缓存)
重点:使用include/exclude 属性需要给所有vue类的name赋值(注意不是给route的name赋值),否则 include/exclude不生效
<keep-alive include="['Map']">// 逗号分隔字符串、正则表达式或一个数组来表示<component ></component>
</keep-alive>
页面级别可以在配置路由的时候,通过meta进行设置哪些页面进行缓存
//App.vue
<keep-alive><router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"/>//router index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router);
let router = new Router({routes: [{path: '/',name: 'login',component:(resolve) => {require(['@/components/user/Login'],resolve)}},{path: '/map',name: 'map',component:(resolve) => {require(['@/components/map/Map'],resolve)},meta:{keepAlive:true}},]})
export default router;
被包裹的组件有activated 和 deactivated 两个生命周期钩子函数:
- activated (组件激活时调用,只要页面切换加载组件就会执行一次)
- deactivated (组件停用时调用)
<template><span @click="changeMessage">{{msg}}</span>
</template><script>
import { eventBus } from "路径";
export default {data(){return {msg: 'old'}},activated(){//每次进入界面都要初始化的数据放这里console.log("每次进入这个界面msg都要是old,或者请求数据")this.msg!="old"&&this.msg="old";//获取最新的数据或者重置数据},deactivated(){},methods:{changeMessage(){this.mag="new";console.log("我修改了msg,会被keepalive缓存")}}
};
</script>
2.2 动态设置(a跳转b刷新;c跳回到b缓存)
nav——>map——>overView——>map——>nav;
- 总体就是实现nav跳转到map的时候是刷新的,map跳转到overView后再跳回map是缓存的。
- 曾经试过修改路由里面meta标签的keepAlive的值,但是失败了(可行的话希望大家告知一下喔),以下使用include和vuex设置,亲测有效。值得注意的是使用\vm.$destroy()后会导致页面不再缓存。
- 依旧要提醒的是include匹配的是组件实例的name,不是路由配置的name。
- 设置缓存数组的获取
//App.vue
<template><div id="app"><keep-alive :include="keepArr"><router-view ></router-view></keep-alive></div>
</template><script>
export default {name: "App",data(){return{keepArr:this.$store.state.keepArr,}},watch:{$route:{ //监听路由变化handler:function (to,from) {this.keepArr=this.$store.state.keepArr?this.$store.state.keepArr:[];}},},
};
</script>
- vuex设置修改数组的方法
//store index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);export default new Vuex.Store({state: {keepArr: ['Map'] //保存缓存的列表 (Map是固定要缓存的组件)},mutations: {// 缓存:判断该组件是否存在于该缓存数组,无则添加isKeepAlive(state, component) {!state.keepArr.includes(component) && state.keepArr.push(component);},// 不缓存:从缓存数组中删除对应的组件元素noKeepAlive(state, component) {let index = state.keepArr.indexOf(component);index > -1 && state.keepArr.splice(index, 1);},}
})
- 使用路由拦截器,判断是都进入的是需要缓存的界面
//router index.js
router.beforeEach((to, from, next) => {if (to.name === 'map') {// 只要进入Map,就进行缓存store.commit('isKeepAlive', "Map");}next();
})
- 在缓存界面设置离开的时候,调用清除自身缓存的方法
//map.vue
export default {name: "Map",data() {return {}},beforeRouteLeave (to, from, next) {if (to.name!="overView") {//去往其他界面,清除自己的缓存,this.$store.commit("noKeepAlive","Map")};next();},
}
3.接口封装(axios拦截器)
//utils下request.js文件
import axios from 'axios';
import store from '@/store';
import {MessageBox,Message} from 'element-ui';// 创建axios实例
const service = axios.create({baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url。环境变量timeout: 5000, // request timeout
})//请求拦截器
service.interceptors.request.use(config => {// console.log(config)const token =sessionStorage.getItem("manage_token");if (token) {//在请求头中添加token验证(视项目而定)config.headers.userId = sessionStorage.getItem("userId");config.headers.Authorization= token;};//解决IE浏览器接口会有缓存问题,添加时间戳if (!!window.ActiveXObject || "ActiveXObject" in window)config.url = config.url+"?time="+ Date.parse(new Date()) / 1000//处理后返回configreturn config},error => {console.log("error") // for debugreturn Promise.reject(error)}
)// 响应拦截器
service.interceptors.response.use(response => {const res = response.data//验证接口是否正确,根据后台商定好的code码进行验证if (res.code != 200 ) {Message({message: res.message || 'Error',type: 'error',duration: 5 * 1000})return Promise.reject(new Error(res.message || 'Error'))} else {return res}},error => {//接口报错处理if(error.message.indexOf("401")!=-1){//登录token超时或者错误if(error.response.config.url.indexOf('logOut')!=-1){//点击退出登录情况不提示return true}else{let message="登录超时或登录失败,请重新登陆";if(error.response.data.message.indexOf("correct")!=-1){//被占用message="您的帐号已在其他地方登陆,请重新登陆"};if(error.response.data.message.indexOf("logged out")!=-1){//退出message="已退出,请重新登陆"};MessageBox.alert(message,'重新登录', {confirmButtonText: '确认',type: 'warning'}).then(() => {store.dispatch('user/resetToken').then(() => {location.reload()})}).catch(()=>{store.dispatch('user/resetToken').then(() => {location.reload()})});}}else{Message({message: error.message,type: 'error',duration: 5 * 1000})}return Promise.reject(error)}
)//可能出现在一个项目中有多个地址的接口。就在创建一个axiosconst service1 = axios.create({baseURL: process.env.VUE_APP_BASE_API2, // url = base url + request urltimeout: 5000, // request timeout
})
// 返回拦截
service1.interceptors.response.use(response => {const res = response.dataif (res.code != 200) {Message({message: res.message || 'Error',type: 'error',duration: 5 * 1000})return Promise.reject(new Error(res.message || 'Error'))} else {return res}},error => {console.log('err' + error) // for debugreturn Promise.reject(error)}
)// 调试阶段使用mock
const testservice = axios.create({baseURL: process.env.VUE_APP_MOCK_API, // url = base url + request urltimeout: 5000 // request timeout
})export {service as request,service1 as request1,testservice as testRequest,
}
项目中会进行接口模块化封装:即在项目中创建一个api文件夹,文件夹下面是各个模块的js请求
//user.jsimport {request} from '@/tools/request'
import Qs from 'qs'//登录
export function login(data) {return request({url: 'sys/user/login',method:'post',headers: {'Content-Type': 'application/json'},data:data})
}//退出登录
export function loginout(data) {return request({url: 'sys/user/logout',method:'post',headers: {'Content-Type': 'application/json'},data:data})
}
...
使用接口:这样的模块封装可以清楚的整改某一模块的接口
4.项目权限
4.1界面权限
界面的权限主要在路由拦截器中进行处理
- permission.js(路由拦击器,要在main.js中导入permission:
import '@/permission'
)
//permission.js
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 styleNProgress.configure({ showSpinner: false }) // NProgress 配置const whiteList = ['/login'] // 不需要授权的白名单//路由跳转前拦截器
router.beforeEach(async(to, from, next) => {//进度条开启NProgress.start()//===================判断用户是否登录,获取token===============let hasToken = sessionStorage.getItem("manage_token")//===============页面刷新情况,需要重新生成路由=================let flag = 0;//======由其他系统进入使用token去更新用户数据,不重新登录(同一域名下不同项目)=========let token = sessionStorage.getItem('token');//其他系统存储的tokenif(token&&token!=hasToken&&to.path!= '/login'){//两个token不相同时,重置整个存储数据await store.dispatch('user/resetToken')sessionStorage.setItem("manage_token",token)hasToken=token;flag = 0;};//=================登录,路由整体处理=========================if (hasToken) {if (to.path === '/login') { //有token情况下跳转到login,回到首页next({ path: '/' })NProgress.done()} else {//跳转到其他页面const hasGetUserInfo = store.getters.name; //有没有用户数据,没有请求用户信息,用store是因为路由刷新后需要重新动态加载if (hasGetUserInfo ) {next()} else {//首次登录的情况下try {if (flag == 0) {//页面刷新情况,动态生成路由let roles=sessionStorage.getItem("roles");if(!sessionStorage.getItem("name")){//本地无数据在请求详细数据const { data } = await store.dispatch('user/getInfo');roles=data.roles;}else{//本地有数据,重新存储namestore.commit('user/SET_NAME',sessionStorage.getItem("name"))};//获取动态生成的路由,在vuex的permission.jsconst accessRoutes = await store.dispatch('permission/generateRoutes', roles);if(accessRoutes.length<=1){Message.error('该账号无登录权限!')await store.dispatch('user/resetToken')next(`/login?redirect=${to.path}`)NProgress.done()}else{router.addRoutes(accessRoutes)// 动态添加路由router.options.routes = store.state.permission.routes;flag++;next({ ...to, replace: true })}} else { // 页面跳转并非刷新next()}} catch (error) { // 失败重新登录await store.dispatch('user/resetToken')Message.error(error || 'Has Error')next(`/login?redirect=${to.path}`)NProgress.done()}}}} else {//没有token返回登录页,重新登录if (whiteList.indexOf(to.path) !== -1) {//路由为登录页next()} else {//不为登录页重定向到登录页next(`/login?redirect=${to.path}`)NProgress.done()}}
})//路由跳转后
router.afterEach(() => {NProgress.done()
})
- 路由js(定义固定加载的页面和动态添加的页面)
//router index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)import Layout from '@/layout'
//固定加载的界面
export const constantRoutes = [{path: '/login',component: () => import('@/views/login/index'),hidden: true},{path: '/404',component: () => import('@/views/404'),hidden: true,},
]//异步挂载的路由
//动态需要根据权限加载的路由表
export const asyncRoutes = [];const createRouter = () => new Router({// mode: 'history', // require service supportscrollBehavior: () => ({ y: 0 }),routes: constantRoutes
})
const router = createRouter()//重置路由
export function resetRouter() {const newRouter = createRouter()router.matcher = newRouter.matcher // reset router
}export default router
- Vuex创建permission模块。并在index.js中导入该模块。关于vuex模块上面1.3.1.2 有讲解喔
//store module permission.jsimport { asyncRoutes, constantRoutes } from '@/router'
import Layout from '@/layout'const state = {routes: [],addRoutes: []
}const mutations = {SET_ROUTES: (state, routes) => {state.addRoutes = routesstate.routes = constantRoutes.concat(routes)}
}const actions = {generateRoutes({ commit }, roles) {//第一步,有路由拦击器触发到这里。return new Promise(resolve => {const loadMenuData = sessionStorage.getItem("pages")? JSON.parse(sessionStorage.getItem("pages")):[];//接口返回的菜单树//重新生成的路由数组let asyncRoutes=[];generaMenu(asyncRoutes, loadMenuData);//生成导出的数组let accessedRoutes=[];if (roles.includes('admin')) {//角色包括管理员(roles是数组,用户多角色情况),就添加的生成的数组accessedRoutes = asyncRoutes || []} else {//返回的树里面的角色需要在过滤一次的accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)};//找不到页面重定向到404let find={ path: '*', redirect: '/404', hidden: true };accessedRoutes.push(find);//存储和返回commit('SET_ROUTES', accessedRoutes)resolve(accessedRoutes)})}
}/*** 后台查询的菜单数据拼装成路由格式的数据* @param routes*/
export function generaMenu(routes, data) {//第二步,生成路由data.forEach((item,index) => {const menu = {path: item.menuUrl?'/'+item.menuUrl:'/'+item.roles,component: item.menuUrl?resolve => require([`@/views/${item.menuUrl.trim()}`],resolve):Layout ,children: [],name: 'menu_' + item.id,//roles这个属性如果有需求再做一次筛选就为item.roles。如果是完全展示所有接口返回的树。就设置为["admin"]meta: { title: item.name, icon: item.icon?item.icon:'', roles: item.roles},//meta: { title: item.name, icon: item.icon?item.icon:'', roles: ['admin'] }};if(item.childs.length>0){if(index==0){// 添加"/"重定向第一个页面menu.path='/';menu.redirect='/'+item.childs[0].menuUrl;}else{//添加父级重定向到第一个子页面地址。父页面有自己的页面就不用menu.redirect='/'+item.childs[0].menuUrl;}};if (item.childs) {//有子页面在走函数添加子页面generaMenu(menu.children, item.childs)};routes.push(menu)})
}/*** 根据meta中的roles字段进行角色过滤页面* @param roles* @param route*/
function hasPermission(roles, route) {if (route.meta && route.meta.roles) {return roles.some(role => route.meta.roles.includes(role))} else {return true}
}/*** 返回的* @param routes asyncRoutes* @param roles*/
export function filterAsyncRoutes(routes, roles) {const res = []routes.forEach(route => {const tmp = { ...route }if (hasPermission(roles, tmp)) {if (tmp.children) {tmp.children = filterAsyncRoutes(tmp.children, roles)}res.push(tmp)}})return res
}
export default {namespaced: true,state,mutations,actions
}
//store module user.js
import { login, logout, tokenGetinfo,getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
import router from '@/router'const getDefaultState = () => {return {buttons: [],roles:[],token: "",name: '',avatar: ''}
}const state = getDefaultState();const mutations = {RESET_STATE: (state) => {Object.assign(state, getDefaultState())},SET_TOKEN: (state, token) => {state.token = token;},SET_NAME: (state, name) => {state.name = name;},SET_roles:(state,roles) =>{state.roles=roles;},SET_AVATAR: (state, avatar) => {state.avatar = avatar},SET_BUTTONS: (state, buttons) => {state.buttons = buttons},SET_pages: (state, pages) => {state.pages = pages},
}const actions = {// user loginlogin({ commit }, userInfo) {return new Promise((resolve, reject) => {login(userInfo).then(response => {const data= response.data;if(data && data.token){//存储tokensessionStorage.setItem("manage_token",data.token)commit('SET_TOKEN', data.token)resolve(true)}else{console.log("获取token失败,或者数据结构错误");reject(error);}}).catch(error => {reject(error)})})},// 根据token获取用户信息getInfo({ commit, state }) {return new Promise((resolve, reject) => {getInfo().then(response => {const data = response.dataif(data){sessionStorage.setItem("userInfo",JSON.stringify(data) )sessionStorage.setItem("userId",data.userId)sessionStorage.setItem("name",data.userName)commit('SET_NAME', data.userName)// 按钮级别权限sessionStorage.setItem("buttons",data.permDesc.button? data.permDesc.button:[])commit('SET_BUTTONS',data.permDesc.button?data.permDesc.button:[])// 动态路由sessionStorage.setItem("pages",JSON.stringify(data.permDesc.data))commit('SET_pages',JSON.stringify(data.permDesc.data))// 页面级别权限sessionStorage.setItem("roles",data.roles?data.roles:['admin'])commit('SET_roles', data.roles?data.roles:['admin'])resolve(data)}else{console.log("获取用户信息失败")reject(false)}}).catch(error => {reject(error)})})},// user logout 清除及退出logout({ commit, state }) {return new Promise((resolve, reject) => {console.log("退出登录")logout(state.token).then(() => {localStorage.clear();sessionStorage.clear();commit('RESET_STATE')resetRouter()resolve()}).catch(error => {reject(error)})})},// remove token 只清除,不退出resetToken({ commit }) {return new Promise(resolve => {localStorage.clear();sessionStorage.clear();commit('RESET_STATE')removeToken() resolve()})}
}export default {namespaced: true,state,mutations,actions
}
4.2按钮权限
按钮权限实现的逻辑就是后台接口会返回可以使用的按钮标识码数组,每个按钮对应有一个唯一的标识码。在页面加载的时候看数组中是否包含当前这个按钮的标识码
//verifyButton.js
export function hasButtonPermission(permission) {const myBtns = sessionStorage.getItem("buttons")return myBtns.indexOf(permission) > -1
}//main.js
import { hasButtonPermission} from './utils/verifyButton'; // button permission
Vue.prototype.hasButton = hasButtonPermission;//a.vue
<el-button v-if="hasButton ('system:post:edit')" size="small" @click="edit()">编辑</el-button>