共计 6188 个字符,预计需要花费 16 分钟才能阅读完成。
开发记录
从零开始开发后台管理系统,还是有很多值得记录的地方。构建工具 Vite、使用 Vue3。
1. 批量导入指定目录的组件
/*
* @author 友人 a 丶
* @date 2022-07-11
* @app Vue 应用对象
* */
export default function (app) {
/*
* 指定要导入的文件目录
* 直接加载用{eager:true}, 懒加载用 glob
* */
const modules = import.meta.glob(['@/layouts/*/index.js', '@/components/*/index.js']);
for (let i in modules) {let name = /(.*)?/(.*)/index.js/.exec(i);
/* 直接引入组件 */
app.component(name[2], modules[i].default);
/* 异步组件 */
app.component(name[2], defineAsyncComponent(modules[i]));
}
}
直接加载的时候 modules 就是元素为组件对象的数组,懒加载的时候是元素为 import 方法的函数数组。
2. 应用初始化
/*
* @author 友人 a 丶
* @date 2022-07-11
*
* 引导系统初始化
* 初始化全局响应拦截器
* 初始化路由守卫
* 初始化用户登录
* */
import loadGuard from "./loadGuard"
import loadInterceptor from "./loadInterceptor"
import watchRem from "./watchRem";
export default async function (){
/*
* 自动调整 rem
* */
watchRem();
/*
* 加载拦截器
* */
loadInterceptor();
/*
* 加载路由守卫
* */
loadGuard();}
3. 自动调整 rem
/*
* @author 友人 a 丶
* @date 2022-07-11
* 自动调整 rem 的大小
* */
export default function () {let l = () => {
let r = document.documentElement, o = r.offsetWidth / 100;
o <16 && (o = 16), r.style.fontSize = o + "px", window.rem = o
};
l();
window.addEventListener("resize",()=>l());
}
4. 导航守卫
/*
* @author 友人 a 丶
* @date 2022-07-11
*
* 加载全局路由守卫
* */
import {router} from "@/router";
import userStore from "@/stores/user";
import load from "@/common/load";
import systems from "@/stores/system";
import NProgress from 'nprogress'
import loadUser from "@/service/loadUser";
/* 进度条 */
NProgress.configure({showSpinner: false})
// 不需要拦截的路由配置
const ignoreRoute = {names: ['404', '403'], // 根据路由名称匹配
paths: ['/login'], // 根据路由 fullPath 匹配
/**
* 判断路由是否包含在该配置中
* @param route vue-router 的 route 对象
* @returns {boolean}
*/
includes(route) {return ignoreRoute.names.includes(route.name) || ignoreRoute.paths.includes(route.path)
}
}
/*
* 加载路由守卫
* */
export default function () {console.log("加载路由守卫...");
let user = userStore();// 全局状态
/*
* 加载进度条
* */
router.beforeEach((to, from, next) => {
// start progress bar
if (!NProgress.isStarted()) {NProgress.start()
}
next()});
/*
* 判断系统是否初始化
* */
router.beforeEach(async (to, from, next) => {if(!systems().loaded){await loadUser();// 加载用户信息初始化系统
}
next(); // 下一个});
/*
* 已登录时,访问登录页面,让它走
* */
router.beforeEach((to, from, next) => {if (user.role != 0 && (to.path == "/login")) {next('/');
}else{next(); // 下一个
}
});
/*
* 判断是否登录
* */
router.beforeEach((to, from, next) => {
/*
* 判断是否需要拦截
* */
console.log("登录判断守卫激活....")
if (ignoreRoute.includes(to)) {next();
} else {
/*
* 角色为 0,代表未登录
* */
if (user.role == 0) {next({path: '/login'});
} else {next();
}
}
})
/*
* 判断用户权限
* */
router.beforeEach((to, from, next) => {console.log("权限判断守卫激活....")
/*
* 判断是否需要拦截
* */
if (ignoreRoute.includes(to)) {next();
} else {
/*
* 判断用户权限
* */
if (user.role < to.meta.role) {load.error("您无权限访问该页面....");
next({path: '/403'});
} else {next();
}
}
})
/*
* 切换页面标题
* */
router.beforeEach((to, from, next) => {
document.title = to.name
next();})
/*
* 结束进度条
* */
router.afterEach(() => {
// finish progress bar
NProgress.done()});
}
5.axios 拦截器
/*
* @author 友人 a 丶
* @date 2022-07-11
*
* 加载 axio 拦截器
* */
import axios from "axios";
import load from "@/common/load";
import {router} from "@/router";
import apis from '@/service/api';
// 不需要拦截的接口
const ignoreApi = {
api: [apis.login],
includes(api) {
/*
* 判断当前请求的接口是否在忽略的列表
* */
for(let item of ignoreApi.api){let reg=new RegExp(`.*${item}.*`);
if(reg.test(api)){return true;}
}
return false;
}
}
/*
* 注册响应拦截器
* */
export default function (){console.log("加载拦截器...");
axios.interceptors.response.use(function (res){console.log("请求接口:"+res.config.url)
console.log(res);
/*
* 判断是否需要拦截
* */
if(ignoreApi.includes(res.config.url)){return res;}
/*
* 判断用户登录是否失效
* */
if(res.code == -1){load.confirm("当前登录状态已失效,请您重新登录!",()=>{router.replace('/login')
})
}
return res;
});
}
6. 获取某个路由的子路由(用于生成菜单)
/*
* 操作路由的相关方法
* */
import {router} from "@/router/index";
/*
* 获取某个路由项的子项
* */
export function getChildren(path) {let routes=router.getRoutes();
for (let i of routes) {if (i.path == path) {return i.children;}
}
}
7. 简单的弹出封装(antd design vue)
import {
message,
Modal
} from "ant-design-vue";
let hide = [];
export default {loading(text = '加载中...') {hide.push(message.loading(text, 0))
},
loaded() {if (hide.length> 0) {let timer=setTimeout(()=>{hide[hide.length - 1]()
hide.splice(hide.length - 1, 1)
},500);
}
},
error(text = '加载异常') {message.error(text);
},
success(text = 'ok!') {message.success(text);
},
confirm(text, callback = null) {
Modal.confirm({
title: '提示',
centered: true,
content: text,
maskClosable: false,
onOk: (close) => {close(); // 关闭
if (callback) {callback()
}
}
})
}
}
8. 退出登录
/*
* @author 友人 a 丶
* @date 2022-07-11
* 用户退出登录
* */
import user from "@/stores/user";
import Cookies from "js-cookie";
import {router} from "@/router";
import load from "@/common/load";
export default function () {load.confirm("确认退出登录吗?",()=>{user().$reset(); // 重置用户数据的状态管理器
/*
* 清空 cookie
* */
Object.keys(Cookies.get()).forEach((item)=>{Cookies.remove(item);
})
/*
* 跳转登录界面
* */
router.replace('/login');
})
}
9. 获取需要缓存的组件列表
/*
* @author 友人 a 丶
* @date 2022-07-11
* name 代表组件名
* 获取需要缓存的组件
* */
import routes from '@/router'
export function getChached(path='') {let routes=router.getRoutes();
let cahced=[]; // 是否开启缓存
/*
* 为空代表获取所有一级组件
* */
if(path == ""){routes.forEach((item)=>{if(item.meta.cache){cahced.push(item.meta.cache);
}
});
}else{
/* 遍历目标子组件 */
for (let i of routes) {if (i.path == path) {i.children.forEach((item)=>{if(item.meta.cache){cahced.push(item.meta.cache);
}
});
}
}
}
return cahced;
}
需要考虑
1. 如何让显示的菜单响应路由的变化(跳转到某个页面,自动选中某个菜单)?
本身菜单被点击了,自己会变化被选中的状态,需要考虑的是从其他页面跳转过来的时候,如何正常匹配显示被选的菜单;
路由包括静态的路由和有变化的参数路由,某些情况下还会具有参数。
- router.matched,与给定路由地址匹配的标准化的路由记录数组。
- 正则匹配,搭配计算属性;假设业务场景:【顶部是一级菜单,用于打开一个新页面,每个页面都有自身的菜单(二级菜单),菜单下面加包括子菜单】,首先就需要根据上方一级菜单的变化匹配二级菜单,还需要根据当前路由判断哪个子菜单被选中;所有需要条件有:代表当前路由的响应式变量、代表当前一级路由的子路由的响应式变量、代表被选中的菜单的响应式变量,最终如下:
let selectedKeys = computed({get() { let current = route.fullPath; for (let i = 0; i < items.length; i++) { // 断言右边是空或者?或者 / // 完整匹配或者带参数匹配 let regexp=new RegExp(`(?:.*${items[i].path}$)|(?:.*${items[i].path}[?/].*)`); if(regexp.test(current)){return [i]; } } }, set(value) {return;} } );
考虑到参数路由和带有? 相关参数的路由,所以正则匹配的是
当前路由与对应的路由完全相等
以及部分相等的同时右边为 / 或者?
提示
由此还需考虑父路由存在相似的路由片段时,匹配的优先级的问题
2. 如何组织目录?
- 代表页面的组件一般以文件夹的形式通过 index.js 导出组件,方便观察层次结构,并且页面组件一般都会拆分 JS 模块,通过文件夹也更加方便文件的分类,保持目录的简洁。
- 其他的组件,如果设计到大量的逻辑,需要拆分 JS 模块,可以用文件夹,如何很简单的直接用.vue 文件即可。
- 如何让父子组件的层级更加清晰?首先名字可以按层级写;parent-children.vue。
- 名字较长的组件用“-”分割,更加友好。
3. 结构型的组件划分?
- 将布局看组架子(布局组件)、视图看做需要的内容(视图组件),布局承载内容;
- 通过全局状态的设置来动态调整布局组件的显示和隐藏。
4. 如何组织无限层级的子路由作为菜单?
模板方式实现起来非常的麻烦,JSX 的方式更加适合这种需求;
- 首先需要根据当前路由获取一个可以作为祖先的父级路由对象
5. 运行中的 router
getRoutes();
获取一个包括所有路由项的数组;不同层次的路由 path 属性都是从根节点开始的;路由的 children 属性则是内的子路由是相对路径
不管是 push、redirect、route-link,都可以进行相对路径(dynamic)或者绝对路径(/dynamic)跳转;
[
{
"path": "/spread/tencent",
"name": "腾讯广告",
"meta": {
"role": 0,
"icon": "icon-guangdiantong"
},
"props": {"default": false},
"children": [],
"instances": {},
"leaveGuards": {},
"updateGuards": {},
"enterCallbacks": {},
"components": {}}
]
正文完