最近刚结束一个项目,然后再客户的百般刁难下又增加了项目新需求:
后台传来当前用户对应权限的路由表,前端通过调接口拿到后处理(后端处理路由)
vue项目实现动态路由的方式大体可分为两种:
1.第一种就是我们前端这边把路由写好,登录的时候根据用户的角色权限来动态展示路由,(前端控制路由)
详情可以看看这个人写的,感觉挺好的
,我当时看这个项目看了好久才明白一点逻辑,
因为大神的动态路由那里有好多层判断,并且穿插各种vuex,把小白的我都快搞懵逼了,对我启发很大,也正是这篇文章,给我提供了很多逻辑
2.第二种就是后台传来当前用户对应权限的路由表,前端通过调接口拿到后处理(后端处理路由)
这两种方法各有优点,效果都能实现,我们公司这次是要求通过第二种方法实现,原因就是公司项目里有一个专门的用户中心,里边逻辑很复杂,不好返给前端用户权限,担心路由放到前端
不安全(以上的话是公司的后台同学讲的,其实我也不知道,按照要求总不会有错),那好吧,抱着都试试、锻炼下自己能力的态度,
我们来搞第二种方法。
思路:
1.后台同学返回一个json格式的路由表,我用easy mock造了一段:动态路由表,大家可参考(easymock就是个模拟后台数据接口的,你习惯用啥都可以。mock / RAP /等);
2.因为后端同学传回来的都是字符串格式的,但是前端这里需要的是一个组件对象,所以要写个方法遍历一下,将字符串转换为组件对象;
3.利用vue-router的beforeEach、addRoutes、localStorage来配合上边两步实现效果;
4.左侧菜单栏根据拿到转换好的路由列表进行展示;
操作的大体步骤:1.拦截路由->2.后台取到路由->3.保存路由到localStorage(用户登录进来只会从后台取一次,其余都从本地取,所以用户,只有退出在登录路由才会更新)
代码第一步,模拟的后台json数据路由表 :
每个路由都使用到组件Layout,这个组件是整体的页面布局:左侧菜单列,右侧页面,所以children下边的第一级路由就是你自己的开发的页面,meta里包含着路由的名字,以及路由对应的icon;因为可能会有多级菜单,所以会出现children下边嵌套children的情况;
"data": {
"router": [
{
"path": "", // 路径
"component": "Layout", // 组件
"redirect": "dashboard", // 重定向
"children": [ // 子路由菜单
{
"path": "dashboard",
"component": "dashboard/index",
name: 'dashboard'
"meta": { // meta 标签
"title": "首页",
"icon": "dashboard" // 图标
noCache: true
}
}
]
},
{
"path": "/example",
"component": "Layout",
"redirect": "/example/table",
"name": "Example",
"meta": {
"title": "案例",
"icon": "example"
},
"children": [
{
"path": "table",
"name": "Table",
"component": "table/index",
"meta": {
"title": "表格",
"icon": "table"
}
},
{
"path": "tree",
"name": "Tree",
"component": "tree/index",
"meta": {
"title": "树形菜单",
"icon": "tree"
}
}
]
},
{
"path": "/form",
"component": "Layout",
"children": [
{
"path": "index",
"name": "Form",
"component": "form/index",
"meta": {
"title": "表单",
"icon": "form"
}
}
]
},
{
"path": "*",
"redirect": "/404",
"hidden": true
}
]
}
当我们有了模拟数据后,我们可以写正常代码了,首先找到router文件夹下的index.js文件。咱们的路由配置一般都在router文件夹下,除非你们公司项目自己封装了多层,但是找到路由文件就行。
然后我们对router/index.js文件进行代码输入,导出一个空的路由表:
router/index.js
//清空路由表
export const asyncRouterMap = []
在然后我们在 src 文件permission.js文件中写代码(permission权限文件是直接在src文件夹下的,和router 平级,后边我们只需要把permission权限文件引入到main.js 文件下就好了)
我们引入文件
我们将后端传回的"component": "Layout", 转为"component": Layout组件对象处理:
因为有多级路由的出现,所以要写成遍历递归方法,确保把每个component转成对象,
因为后台传回的是字符串,所以要把加载组件的过程 封装成一个方法,用这个方法在遍历中使用;详情查看项目里的router文件夹下的 _import_development.js和_import_production.js文件。
permission.js文件代码如下:
(使用到的技术点 beforeEach、addRoutes、localStorage来配合实现
beforeEach路由拦截,进入判断,如果发现本地没有路由数据,那就利用axios后台取一次,取完以后,利用localStorage存储起来,利用addRoutes动态添加路由,
切记切记,得在一开始就加判断要不然一步小心就进入到了他的死循环,浏览器都tm崩了。
拿到路由了,就直接next(),
global.antRouter是为了传递数据给左侧菜单组件进行渲染)
import axios from 'axios'
const _import = require('./router/_import_' + process.env.NODE_ENV)//获取组件的方法
import Layout from '@/views/layout/Layout' // Layout 是架构组件,不在后台返回,在文件里单独引入
var getRouter;
// 路由拦截router.beforeEach
router.beforeEach((to, from, next) => {
if (!getRouter) {//不加这个判断,路由会陷入死循环
if (!getObjArr('router')) { (// getObjArr是封装的获取localStorage的方法
// axios.get('https://www.easy-mock.com/mock/5c70ba7331b3fb6533b94241/example/restful/:id/list').then(res => {
axios.get('实际的后台接口').then(res => {
getRouter = res.data.data.router // 后台拿到路由
saveObjArr('router', getRouter) // 存储路由到localStorage的封装方法
routerGo(to, next) //执行路由跳转方法(导航到一个新的路由,详细解说看官网: https://www.cntofu.com/book/132/api/go.md)
})
} else {//从localStorage拿到了路由
getRouter = getObjArr('router')//拿到路由
routerGo(to, next)
}
} else {
next()
}
}
// 导航到一个新的路由方法封装
function routerGo(to, next) {
getRouter = filterAsyncRouter(getRouter) // 过滤路由
router.options.routes = getRouter; //必须在addroutes前,使用router.options.routes=XXXXX的方法手动添加
router.addRoutes(getRouter) //动态添加路由
global.antRouter = getRouter //将路由数据传递给全局变量,做侧边栏菜单渲染工作
next({ ...to, replace: true })
}
// 存储 localStorage 数组对象的方法
function saveObjArr(name, data) { // localStorage 存储数组对象的方法
localStorage.setItem(name, JSON.stringify(data))
}
// 获取 localStorage数组对象的方法封装
function getObjArr(name) { // localStorage 获取数组对象的方法
return JSON.parse(window.localStorage.getItem(name));
}
//过滤路由方法封装
function filterAsyncRouter(asyncRouterMap) { //遍历后台传来的路由字符串,转换为组件对象
const accessedRouters = asyncRouterMap.filter(route => {
if (route.component) {
if (route.component === 'Layout') {//Layout组件特殊处理
route.component = Layout
} else {
route.component = _import(route.component)
}
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
})
return accessedRouters
}
然后我们在router文件夹下在新增两个js文件,就是做懒加载用的
router/_import_production.js
module.exports = file => () => import('@/views/' + file + '.vue')
router/_import_development.js
module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+
动态路由也就基本完成了!
拿到遍历好的路由,进行左侧菜单渲染
上边的permission.js文件代码处理中会给 global.antRouter赋值,这是一个全局变量(可以用vuex替代),菜单那边拿到路由,进行渲染,(对于路由拦截那部分,应该还有很多优化的地方的)
可能遇到的问题:
遇到了 Cannot find module '@/xx/xxx/xxx.vue'的错误.
思路是这样
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
我在App.vue中进行初始化路由. 完了后执行类似动作
router.addRoutes(newRouters);
只是路由在寻找这个异步component的时候提示找不到模块.
app.js:890 Error: Cannot find module '_c/layout/Layout'
另外 这个异步加载点有点疑问. 按照我的理解, webpack 根据这个import去分离代码. 那么在build的时候 webpack肯定会扫描到这个import点. 进而为这个点去创建分离点. 当实际网络请求到这个组件的时候在根据这个分离点去后端拿组件并注入.
然后现在的思路却是build后(在此之前并不知道分离点) 动态的生成import. 这时候webpack显然是不知道这个分离点的. 因为已经build完成了. 如果连分离点都不知道 又凭 什么去取组件异步加载呢?
按照上面的理解, 做了个简单的测试. 在import的工具中 我这样定义
export const Layout = () => import('_c/layout/Layout');
//module.exports = fileName => () => import(${fileName}.vue)
export default (file) => {
return Layout
}
也就是说 不管是什么component 我暂时都返回 Layout的异步加载组件.
唯一的区别就是 Layout是我事先定义好的一个异步加载的分离点.
而且确实这样过后 就不会在提示找不到module了.
如果这个思路是正确的 是不是意味着我得为所有的页面组件定义一个map. 然后统一定义分离点. 根据后端的返回key 动态的去mao里面把这个分离点加进去
解决方案:
在 main.js文件中
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
然后再APP.vue文件中
import { resetRouter } from "./router.js";
import AsyncImport from "./utils/AsyncImport.js";
export default {
name: "app",
data() {
return {
sysLoading: true,
initError: null
};
},
created() { // 挂载完成执行钩子函数
this.axios.get("/common/public/menus").then(
response => {
const { data: menuList } = response;
console.log("menuList", menuList);
this.initRouters(menuList);
console.log("routers after init:", menuList);
//设置新的路由
resetRouter().then(router => {
console.log("then router:", router);
router.addRoutes(menuList);
router.options.routes = menuList;
});
this.sysLoading = false;
},
() => {
this.initError = "初始化菜单失败";
this.sysLoading = false;
}
);
},
methods: {
initRouters(menuList = []) {
menuList.forEach(item => {
if (item.component && item.component != "") {
item.component = AsyncImport(item.component);
}
if (item.children && item.children.length > 0) {
this.initRouters(item.children);
}
});
}
},
};
AsyncImport.js
const routerViewMap = {
Layout: () => import('_c/layout/Layout'),
SysModule: () => import('@/views/sys/SysModule')
}
export default (key) => {
return routerViewMap[key] || null
}
router.js文件中
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const createRouter = () => new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: []
})
let router = createRouter();
export function resetRouter() {
return new Promise((resolve) => {
const newRouter = createRouter();
router.matcher = newRouter.matcher;
resolve(router);
});
}
export default router;
核心思路依然是动态的路由挂载.
只是事先定义好了分离点.
然后根据后端返回的component字符串去匹配这个分离点.
分离点不存在会抛Cannot find module xxxx
暂无评论内容