问题记录-前端开发避坑(5)--权限验证模块
问题来源和技术背景
通过 vue+vuex+vueRouter+axios 开发权限管理的前端部分,mock服务器选择 express,以及通过 vue-devtool 观察数据变化。
问题列表
问题1
问题描述及分析
安装 vue-devtool 的谷歌插件,需要先将 git 源码编译为插件。如果会科学/上网,可以直接到应用商店下载,这个官方链接包含了所有下载途径,包括 git 和应用商店:https://devtools.vuejs.org/guide/installation.html
以下是通过 git 安装的详细步骤:
-
下载 git 源码到本地:https://github.com/vuejs/devtools#vue-devtools
想简单点就直接下压缩包:
-
进入 devtools-main 文件夹,通过 yarn install 安装所需依赖(npm 或 cnpm 都不行,部分依赖一直安装失败)。注意,即使使用 yarn 安装依赖,依旧可能会卡主(网络连接中断会自动恢复,但是卡主可能是依赖安装失败),此时可以尝试删除 node_modules 文件夹,之后重新安装。
-
之后 yarn run build 将源码编译。如果提示 WARNING 没关系,不能有 ERROR,出现 ERROR 就有可能是依赖未完整安装,需要重试
-
进入 devtools-main/packages/shell-chrome 文件夹,将 manifest.json 文件中的 persistent 值改为true
-
将 shell-chrome 文件夹拖入谷歌浏览器插件页面(chrome://extensions/)即可
问题2
问题描述
加载动态路由的配置对象
[Vue Devtools] No routes found in router
问题分析
- 检查关键组件是否已添加
元素 - 检查路由配置对象的结构和属性是否正确
- 若配置对象是单独的文件,检查导入导出的名称是否正确。我在这一步错误地将定义好的 dynRoutes 对象导出为 route,IDE 还没提示,导致导入失败
问题3
问题描述
在router中添加路由守卫,并在登录成时调用 store 子模块permission(权限)下的 action
:INIT_PERMISSION() 获取用户权限数据,提示以下错误信息:
[vuex] unknown action type: permission/INIT_PERMISSION
问题分析
首先,调用 action
需要使用 router.dispatch(actionName)
方法,该方法的第一个参数是表示 action
名称的字符串。
其次,调用子模块的 action
时,需要加上模块路径,如当前有一个 a 模块,则调用时 actionName
应为 a/actionName
。
需要注意,子模块统一导出后,在 store 主模块中导入时,直接将导入的 modules
对象作为主模块的 `modules 属性即可,无需在 modules 对象外嵌套一层 {},因为嵌套后就多了一层路径。如下:
import modules from './module';
export default new Vuex.Store{
...
modules: modules // 正确写法(仅针对本例)
// modules: { modules } // 错误写法
}
问题4
问题描述
在vueRouter中,通过 addRoute()
方法添加动态路由,提示以下错误信息:
[vue-router] "path" is required in a route configuration
问题分析
老版的 addRoutes()
已废弃。新的 addRoute()
方法有两个重载签名:
- addRoute(route: RouteConfig): () => void
- addRoute(parentName: string, route: RouteConfig): () => void
此处使用第一个重载,需要传入一个路由配置对象而不是数组。
问题5
问题描述
按下登录按钮,无法重定向到后台首页,
同时,控制台可能出现以下两种提示:
[vue-router] invalid redirect option: {}
Uncaught (in promise) Error: Navigation cancelled from "/login" to "/" with a new navigation
问题分析
出现以上信息的原因是,登录时指定了跳转到根路径 "/",同时根路径设置了重定向 "/home",因此点击登录按钮后,导航守卫首先拦截了到根路径的跳转,然后在尚未完成跳转时可能触发了重定向,导致到根路径的跳转动作被取消。
网上很多都提到以下这种方法,但这种方法本质上只是不输出错误信息,并没有解决问题(这里的问题本质上是 VueRouter 不允许重复进入相同的路由,这是源码内部设计好的逻辑,无法通过外部扩展来修改,只能是避免出现这种场景时被内部错误中断,因此捕获该错误,后续可以处理也可以不处理):
- 简单点,可以在
push()
或repalce()
方法后添加.catch(() => true)
- 或者在 store/index.js 中重写 push 等方法并添加 catch 块:
const originalPush = VueRouter.prototype.push;
const originalReplace = VueRouter.prototype.replace;
VueRouter.prototype.push = function (location, onResolve, onReject) {
if (onResolve || onReject)
return originalPush.call(this, location, onResolve, onReject);
return originalPush.call(this, location).catch((err) => err);
};
VueRouter.prototype.replace = function (location, onResolve, onReject) {
if (onResolve || onReject)
return originalReplace.call(this, location, onResolve, onReject);
return originalReplace.call(this, location).catch((err) => err);
};
但是,重定向的问题还没有解决。首页跳转还好解决,可以将根路径替换为 "/home",但子路由的导航和重定向都无法正常使用。
问题6
问题描述
创建动态层级的菜单时,提示以下信息:
Error in render: "TypeError: Cannot read property 'xxxxxx' of undefined"。1 recursive calls
或者:
[Vue warn]: Unknown custom element: <**> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
问题分析
检查当前提示了 (1 recursive calls) 的组件的名称是否与引用的组件冲突导致递归调用自身。确实需要递归的,需要将嵌套中的组件改为异步加载而非同步加载,也就是将 import 语句改为 import() 函数。
在最里层组件中又引入了外层组件导致循环引用,打包工具无法识别里层组件中引入的组件。解决方法是异步加载组件:const recursiveOuterComp = () => import(path)
问题 7
问题描述
封装动态组件时,想通过操作符 ?. 判断属性是否存在,之后继续向下判断属性的属性是否存在,但是提示:
ERROR Failed to compile with 1 error 00:02:43
error in .....
Syntax Error: Unexpected token (1:70)
问题分析
v-if 指令不支持使用该操作符访问属性,将其去掉,使用旧的语法逐层判定。
问题 8
问题描述
将 store 中的 permission 模块通过 mapState() 方法导入到菜单组件,解析失败,可能提示以下两种错误信息之一:
Uncuaght Reference Error: state is not defined
[vuex] module namespace not found in mapState(): xxx
问题分析
主要是语法问题,通过 mapState 方法导入模块下的状态时,正确格式(第二参数是数组) mapState('moduleName',['stateProp1','stateProp2',...])
问题 9
问题分析
vuex 调用异步 actions ,在导航守卫中设置当前进入的路由,以便更新菜单向高亮,此时提示:
state is no defined
问题分析
问题出在使用了异步,但异步方法本身没写好,这里简单点直接调用同步 mutation 方法即可,也可以修改 action 方法至符合语法。
问题 10
问题描述及分析
对导航守卫的 next 方法应用命名路由时,提示超过最大调用栈。因为此时尚未设置路由的 name 属性,将 name 属性加上确实解决了。
之后又遇到了这个问题,原因还是一样:账号登出时会退出到登录页,一直提示找不到 Login 组件,但是登录页组件忘记设置 name 属性。
问题 11
问题描述
配置动态嵌套路由对象时,尝试将路径前的斜杠去掉,即从 path: '/home'
改为 path: 'home'
这样,以便显示嵌套的路径,但在点击了其中某个子路由后,再点击其他子路由,此时父级路径会重复
出现,比如 /user/user/profile
。
问题分析
一开始没分析出问题原因,搜了一下解决方法,发现原因是一级路由以下的深层级路由没有添加最前面的斜杠,导致每次点击路由时,vueRouter 会解析成需要再次添加上级路由,表现出来就是重复了上级路由的地址。参考链接:
https://blog.csdn.net/eva_feng/article/details/105444709
问题 12
问题描述
由于路由对象是通过路由模块下的所有权限对象和后台返回的用户权限数据对比后,过滤出其中相同的部分得到的,因此在使用基于 el-menu 二次封装的多层级动态菜单组件时,菜单样式相关的 prop 与路由相关的 prop 并不是一起定义的,需要在加载组件前先合并;同时还需要再执行一次遍历,将菜单组件的配置对象中符合用户权限的菜单项保留下来。
其中关于 icon (element 2.*,plus 的组件还没测试过)遇到了一点问题:由于使用图标时必须输入完整的类名,因此希望通过二次封装,使得每次使用图标时只需要输入 el-icon-name 的 name 部分即可。但实际封装后,通过 prop 传入的 name 无法正确解析。通过在浏览器中检查元素发现,假设传入的 name 是 'menu',编译出来的元素是 <i class="el-icon-undefined" name="menu"></i>
;通过控制台输出通用是 undefined,而且会报错:
Property or method "name" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
问题分析
查看 el-icon 源码发现,其写法与我个人二次封装的代码基本相同。与其他组件对比,el-icon 的调用方式显得有些怪异: element 定义的 el-icon 在使用时并不是通过 el-icon 组件名调用,而是直接通过 i 标签加上 icon 类名创建,之后可能是 element 插件内部通过一些机制转成 el-icon 。
因此,考虑寻找其他替代的方式插入 icon 组件。虽然 vue 提供了动态组件的机制,通过 <component :is="compName"></component>
的形式,可以将代表组件视图部分的字符串作为 prop 参数传入。但是使用动态组件的地方,必须首先导入动态组件,否则只是将动态组件的名字作为参数传给子组件,子组件并不知道这个字符串所代表的动态组件在哪里。
然后又回到原来的方式,重新检查了代码,结果发现是 prop 参数没有在嵌套的父组件中进行传值,导致菜单项组件一直获取不到值,加上之后就可以了。不过不能二次封装,而是直接使用 i 标签,否则还是会报上面的错误信息。
参考信息
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
vuex 命名空间使用注意点
- 开启命名空间需要加上 namespaced: true 这条属性,注意是 namespaced ,d 一定不能掉。
- 定义命名空间时,注意不能在{}外多嵌套一层,层数不对的话会导致 store 模块解析错误:
// 正确写法
export default {
namespaced: true:
state: {
...
}
...
}
// 可能的错误写法
export default {
// 如果从全局模块中复制到单独的模块文件中,可能不小心会多复制一层
moduleA: {
namespaced: true:
state: {
...
}
...
}
}
- 而在全局 store 导入命名空间的模块时,也要注意嵌套层数。
- 在模块内部使用 mutations / actions 等方法时,不需要在 commit / disapatch 方法的第一个参数前加模块名字符串,只有模块外部使用时才需要加模块名,如:
moduleA/mutation1
。