唠一唠VUE的常见 / 高级用法(组件通信(Vuex,eventBus...),页面动态设置缓存(keepAlive),项目模块化接口(axios拦截器),项目权限(页面,按钮权限)...)

主要总结了用vue以来的一些特殊却又可能常见的使用技术,哈哈(仅限个人经历,不一定满足你要的全部要求喔。但是基础使用是有的)

内容是按照我的理解去解释,如果有说的不清晰或者理解错误的希望大家告诉我,及时纠正!或者有什么迷惑想整的,也可以说说喔,让我看看自己会不会,不会我在学。哈哈哈哈哈。互相指教哟!

内容委实太长,就按目录找自己想看的吧!我还会在加的,哈哈哈。要是能从头看到尾,佩服你耐心十足啊👍👍👍。

1.组件通信

1.1父子组件

1.1.1(props,emit)

  1. 父组件使用组件添加属性的形式进行参数传递,要在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>
  1. 子组件使用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)

  1. vm.$children(当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的)
    少用,因为不能保证子组件的排序
//父组件 
this.$children[0].gameMsg;//访问子组件data及方法
  1. vm.$parent (父实例,如果当前实例有的话,如果存在多级子组件,通过parent 访问得到的是它最近一级的父组件)
//子组件
this.$parent.drawDeactive();//访问父组件的方法
this.$parent.Bflag = "";//修改父组件data

1.2祖孙组件

1.2.1($attrs,$listeners,inheritAttrs :2.4.0 新增)

可读性不好,适用于层级嵌套多的非父子组件,比较小的项目。

  1. vm.$attrs(包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外))
  2. vm.$listeners(包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器)
  3. 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 绑定并不是可响应的,就是改了祖先的绑定的值的时候,后代得到的数据不会更改)

缺点:追踪数据不好追踪,不知道在哪个组件修改了值,并且不会响应式修改。

  1. 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>
  1. 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的对象,可以修改copyList
     this.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,是提交mutations
    increment (context) {
      context.commit('increment')
    }
  }
})

根组件注入(main.js):

import store from './store';  //导入store

在这里插入图片描述

  1. store(state)
    "store"基本上就是一个容器,它包含着你的应用中大部分的状态 (state);
两个特性:
1.vuex 的状态存储是响应式的,一旦状态发生变化,那么相应的组件也会相应地得到高效更新
2.不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。

//使用this.$store.state.名字 访问(如:this.$store.getters.count)
  1. 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)
  1. 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 
  1. Action(使用dispatch 触发)
与mutation的区别:
 1. Action 提交的是 mutation,而不是直接变更状态。
 2. Action 可以包含任意异步操作。
//index.js  store中
actions: {
  increment1 ({ commit }) {//使用es6参数结构,免去context.commit复杂写法。写法{ state ,getters ,dispatch, commit },可dispatch其他actions
    commit('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 / await
  async 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))
  1. 辅助函数(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)`
    ]),
  }
}
  1. 模块(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.js

var 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的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

  1. 总体就是实现nav跳转到map的时候是刷新的,map跳转到overView后再跳回map是缓存的。
  2. 曾经试过修改路由里面meta标签的keepAlive的值,但是失败了(可行的话希望大家告知一下喔),以下使用include和vuex设置,亲测有效。值得注意的是使用\vm.$destroy()后会导致页面不再缓存。
  3. 依旧要提醒的是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
   	//处理后返回config
    return config
  },
  error => {
    console.log("error") // for debug
    return 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)
  }
)

//可能出现在一个项目中有多个地址的接口。就在创建一个axios

const service1 = axios.create({
  baseURL: process.env.VUE_APP_BASE_API2, // url = base url + request url
  timeout: 5000, // request timeout
})
// 返回拦截
service1.interceptors.response.use(
  response => {
    const res = response.data
    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 => {
    console.log('err' + error) // for debug
    return Promise.reject(error)
  }
)

// 调试阶段使用mock
const testservice = axios.create({
  baseURL: process.env.VUE_APP_MOCK_API, // url = base url + request url
  timeout: 5000 // request timeout
})

export {
  service as request,
  service1 as request1,
  testservice as testRequest,
}

项目中会进行接口模块化封装:即在项目中创建一个api文件夹,文件夹下面是各个模块的js请求
在这里插入图片描述

//user.js

import {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界面权限

界面的权限主要在路由拦截器中进行处理

  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 style

NProgress.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');//其他系统存储的token
  
  if(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{//本地有数据,重新存储name
              store.commit('user/SET_NAME',sessionStorage.getItem("name"))
            };
            
            //获取动态生成的路由,在vuex的permission.js
            const 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()
})

  1. 路由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 support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})
const router = createRouter()

//重置路由
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router
  1. Vuex创建permission模块。并在index.js中导入该模块。关于vuex模块上面1.3.1.2 有讲解喔
    在这里插入图片描述在这里插入图片描述
//store module permission.js

import { asyncRoutes, constantRoutes } from '@/router'
import Layout from '@/layout'

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.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)
      };
      //找不到页面重定向到404
      let 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 login
  login({ commit }, userInfo) {
    return new Promise((resolve, reject) => {
      login(userInfo).then(response => {
        const data= response.data;
        if(data && data.token){//存储token
          sessionStorage.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.data
        if(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>
  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值