前言
完善一下项目初始化配置,分为store配置、初始化内部系统配置、注册全局组件、配置路由、路由守卫、注册全局指令、配置全局错误处理这些内容。
1、配置 store
在src/stores文件夹下新建一个index.ts,顺便删除原有的counter.ts
// src/stores/index.ts
import type { App } from 'vue';
import { createPinia } from 'pinia';
const store = createPinia();
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };
更改一下main.ts里有关store的部分
// main.ts
import './style/tailwind.scss';
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setupStore } from '/@/stores';
async function bootstrap() {
const app = createApp(App);
// 配置 store
setupStore(app);
app.use(router);
app.mount('#app');
}
bootstrap();
注意这里的bootstrap函数,后面几步的setupXXX都会在这里调用 接着在stores文件夹下新建modules文件夹,里面新建app.ts文件用来存放项目配置
// app.ts
import { defineStore } from 'pinia';
import { store } from '/@/stores';
import { ProjectConfig } from '/#/config';
import { PROJ_CFG_KEY } from '/@/enums/cacheEnum';
import { Persistent } from '/@/utils/cache/persistent';
import { deepMerge } from '/@/utils';
// app基本配置
interface AppState {
// 页面加载状态
pageLoading: boolean;
// 项目配置
projectConfig: ProjectConfig | null;
}
export const useAppStore = defineStore({
id: 'app',
state: (): AppState => ({
pageLoading: false,
projectConfig: Persistent.getLocal(PROJ_CFG_KEY),
}),
getters: {
getPageLoading(state): boolean {
return state.pageLoading;
},
getProjectConfig(state): ProjectConfig {
return state.projectConfig || ({} as ProjectConfig);
},
},
actions: {
setPageLoading(loading: boolean): void {
this.pageLoading = loading;
},
setProjectConfig(config: DeepPartial<ProjectConfig>): void {
this.projectConfig = deepMerge(this.projectConfig || {}, config) as ProjectConfig;
Persistent.setLocal(PROJ_CFG_KEY, this.projectConfig);
},
},
});
// 需要在设置之外使用
export function useAppStoreWithOut() {
return useAppStore(store);
}
这里面用到的utils函数在上一篇utils配置中有提到 使用场景
// demo
import { useAppStoreWithOut } from '/@/stores/modules/app';
const appStore = useAppStoreWithOut();
appStore.setPageLoading(false);
2、初始化内部系统配置
新建/@/logics/initAppConfig.ts文件
// initAppConfig.ts
/**
* 应用程序配置
*/
import type { ProjectConfig } from '/#/config';
import { PROJ_CFG_KEY } from '/@/enums/cacheEnum';
import projectSetting from '/@/settings/projectSetting';
import { useAppStore } from '/@/stores/modules/app';
import { getCommonStoragePrefix, getStorageShortName } from '/@/utils/env';
import { Persistent } from '/@/utils/cache/persistent';
import { deepMerge } from '/@/utils';
// 初始项目配置
export function initAppConfigStore() {
const appStore = useAppStore();
let projCfg: ProjectConfig = Persistent.getLocal(PROJ_CFG_KEY) as ProjectConfig;
projCfg = deepMerge(projectSetting, projCfg || {});
appStore.setProjectConfig(projCfg);
setTimeout(() => {
clearObsoleteStorage();
}, 16);
}
/**
* 随着版本的不断迭代,localStorage中存储的缓存密钥将越来越多
* 此方法用于删除无用的密钥
*/
export function clearObsoleteStorage() {
const commonPrefix = getCommonStoragePrefix();
const shortPrefix = getStorageShortName();
[localStorage, sessionStorage].forEach((item: Storage) => {
Object.keys(item).forEach((key) => {
if (key && key.startsWith(commonPrefix) && !key.startsWith(shortPrefix)) {
item.removeItem(key);
}
});
});
}
/@/settings/projectSetting这个文件需要新建一下。
// projectSetting.ts
import type { ProjectConfig } from '/#/config';
import { SessionTimeoutProcessingEnum } from '/@/enums/appEnum';
// ! 更改后需要清除浏览器缓存
const setting: ProjectConfig = {
// 会话超时处理
sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP,
// 使用错误处理程序插件
useErrorHandle: false,
// 切换接口时是否删除未关闭的消息并通知
closeMessageOnSwitch: true,
// 切换路由时是否取消已发送但未响应的http请求
// 如果启用了它,我想覆盖单个接口。可以在单独的界面中设置
removeAllHttpPending: false,
};
export default setting;
/@/enums/appEnum这个文件需要新建一下。
// appEnum.ts
// 会话超时处理
export enum SessionTimeoutProcessingEnum {
ROUTE_JUMP,
PAGE_COVERAGE,
}
更改一下main.ts文件。
// main.ts
import './style/tailwind.scss';
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setupStore } from '/@/stores';
import { initAppConfigStore } from './logics/initAppConfig';
async function bootstrap() {
const app = createApp(App);
// 配置 store
setupStore(app);
// 初始化内部系统配置
initAppConfigStore();
app.use(router);
app.mount('#app');
}
bootstrap();
3、注册全局组件
这部分我采用的全局引入的方式,如果需要按需引入,按照组件库官网提供的方式修改即可,也可以用我自己常用的方法。 新建/@/components/registerGlobComp.ts文件。
// registerGlobComp.ts
import type { App } from 'vue';
import VXETable from 'vxe-table';
import ElementPlus from 'element-plus';
import 'xe-utils';
export function registerGlobComp(app: App) {
app.use(VXETable).use(ElementPlus);
}
安装组件库
// package.json
"vxe-table": "^4.3.12",
"element-plus": "^2.3.4",
"xe-utils": "^3.5.7",
如果需要按需引入,registerGlobComp.ts文件可以这样写。
// registerGlobComp.ts
import type { App } from 'vue';
import VXETable from 'vxe-table';
import { ElButton, ElAlert } from 'element-plus';
import 'xe-utils';
export function registerGlobComp(app: App) {
app.use(VXETable).use(ElButton).use(ElAlert);
}
修改一下main.ts
// main.ts
import './style/tailwind.scss';
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { setupStore } from '/@/stores';
import { initAppConfigStore } from './logics/initAppConfig';
import { registerGlobComp } from './components/registerGlobComp';
async function bootstrap() {
const app = createApp(App);
// 配置 store
setupStore(app);
// 初始化内部系统配置
initAppConfigStore();
// 注册全局组件
registerGlobComp(app);
app.use(router);
app.mount('#app');
}
bootstrap();
4、配置路由
把原有的文件删掉,重新按照下面的结构新建一个。
直接上代码。
// /@/router/routes/modules/about.ts
// 路由示例
const abouteDemo = {
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('/@/views/AboutView.vue'),
};
export default abouteDemo;
// /@/router/routes/modules/home.ts
import HomeView from '/@/views/HomeView.vue';
// 路由示例
const homeDemo = {
path: '/home',
name: 'home',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('/@/views/HomeView.vue'),
};
export default homeDemo;
// /@/router/routes/index.ts
import { AppRouteModule, AppRouteRecordRaw } from '/@/router/types';
import { PageEnum } from '/@/enums/pageEnum';
const modules = import.meta.glob('./modules/**/*.ts', { import: 'default', eager: true });
const routeModuleList: AppRouteModule[] = [];
Object.keys(modules).forEach((key) => {
const mod = modules[key] || {};
const modList = Array.isArray(mod) ? [...mod] : [mod];
routeModuleList.push(...modList);
});
// 根路由
export const RootRoute: AppRouteRecordRaw = {
path: '/',
name: 'Root',
redirect: PageEnum.BASE_HOME,
meta: {
title: 'Root',
},
};
// 未经许可的基本路由
export const basicRoutes = [RootRoute, ...routeModuleList];
/@/enums/pageEnum这个文件需要新建一下。
// /@/enums/pageEnum
export enum PageEnum {
// basic home path
BASE_HOME = '/home',
}
// /@/router/index.ts
import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router';
import { basicRoutes } from './routes';
import type { App } from 'vue';
// 白名单应该包含基本静态路由
const WHITE_NAME_LIST: string[] = [];
const getRouteNames = (array: any[]) =>
array.forEach((item) => {
WHITE_NAME_LIST.push(item.name);
getRouteNames(item.children || []);
});
getRouteNames(basicRoutes);
export const router = createRouter({
// hash 历史记录
// history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH),
// 应该添加到路由的初始路由列表。
routes: basicRoutes as unknown as RouteRecordRaw[],
// 是否应该禁止尾部斜杠。默认为假
strict: true,
// 路由滚动行为
scrollBehavior: () => ({ left: 0, top: 0 }),
});
// 重置路由
export function resetRouter() {
router.getRoutes().forEach((route) => {
const { name } = route;
if (name && !WHITE_NAME_LIST.includes(name as string)) {
router.hasRoute(name) && router.removeRoute(name);
}
});
}
// 配置路由器
export function setupRouter(app: App<Element>) {
app.use(router);
}
// /@/router/types.ts
import type { RouteRecordRaw, RouteMeta } from 'vue-router';
import { defineComponent } from 'vue';
export type Component<T = any> =
| ReturnType<typeof defineComponent>
| (() => Promise<typeof import('*.vue')>)
| (() => Promise<T>);
// @ts-ignore
export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string;
meta: RouteMeta;
component?: Component | string;
components?: Component;
children?: AppRouteRecordRaw[];
props?: Recordable;
fullPath?: string;
}
export type AppRouteModule = AppRouteRecordRaw;
5、路由导航守卫
因为是模板项目,所以导航守卫简单做一下。 新建/@/router/guard/index.ts文件。
// /@/router/guard/index.ts
import type { RouteLocationNormalized, Router } from 'vue-router';
import projectSetting from '/@/settings/projectSetting';
import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel';
// 注意,这里不要更改创建顺序
export function setupRouterGuard(router: Router) {
createPageGuard(router);
createHttpGuard(router);
createScrollGuard(router);
}
/**
* 用于处理页面状态的挂钩
*/
function createPageGuard(router: Router) {
const loadedPageMap = new Map<string, boolean>();
router.beforeEach(async (to) => {
// 页面已经加载,再次打开会更快,不需要进行加载和其他处理
to.meta.loaded = !!loadedPageMap.get(to.path);
return true;
});
router.afterEach((to) => {
loadedPageMap.set(to.path, true);
});
}
/**
* 路由切换时用于关闭当前页面已完成请求的接口
* @param router
*/
function createHttpGuard(router: Router) {
const { removeAllHttpPending } = projectSetting;
let axiosCanceler: Nullable<AxiosCanceler>;
if (removeAllHttpPending) {
axiosCanceler = new AxiosCanceler();
}
router.beforeEach(async () => {
// 切换路由将删除以前的请求
axiosCanceler?.removeAllPending();
return true;
});
}
// 回顶部的路由开关
// 只在hash路由模式下触发
function createScrollGuard(router: Router) {
const isHash = (href: string) => {
// 匹配hash路由的#
// 替换createWebHistory为createWebHashHistory即可更换为hash路由
return /^#/.test(href);
};
const body = document.body;
router.afterEach(async (to) => {
// 滚动到顶部
isHash((to as RouteLocationNormalized & { href: string })?.href) && body.scrollTo(0, 0);
return true;
});
}
新建/@/utils/http/axios/axiosCancel,后面封装axios也会用到。
// /@/utils/http/axios/axiosCancel
import type { AxiosRequestConfig, Canceler } from 'axios';
import axios from 'axios';
import { isFn } from '/@/utils/is';
// 用于存储每个请求的识别和取消功能
let pendingMap = new Map<string, Canceler>();
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
export class AxiosCanceler {
/**
* Add request
* @param {Object} config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pendingMap.has(url)) {
// 如果当前没有挂起的请求,请添加它
pendingMap.set(url, cancel);
}
});
}
/**
* @description: clear所有在pending的请求
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
cancel && isFn(cancel) && cancel();
});
pendingMap.clear();
}
/**
* 删除请求
* @param {Object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 如果挂起中有当前请求标识符,
// 需要取消并删除当前请求
const cancel = pendingMap.get(url);
cancel && cancel(url);
pendingMap.delete(url);
}
}
/**
* @description: reset
*/
reset(): void {
pendingMap = new Map<string, Canceler>();
}
}
添加axios依赖
// package.json
"axios": "^1.4.0",
导航守卫里还可以使用先前封装的mitt对route进行监听、处理页面加载状态、用户权限、进入login页清除用户信息等,后续根据需要再加,现在是模板版本不需要那么复杂。 修改main.ts。
// main.ts
import './style/tailwind.scss';
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import { router, setupRouter } from './router';
import { setupStore } from '/@/stores';
import { initAppConfigStore } from './logics/initAppConfig';
import { registerGlobComp } from './components/registerGlobComp';
import { setupRouterGuard } from '/@/router/guard';
async function bootstrap() {
const app = createApp(App);
// 配置 store
setupStore(app);
// 初始化内部系统配置
initAppConfigStore();
// 注册全局组件
registerGlobComp(app);
// 配置路由
setupRouter(app);
// 路由守卫
setupRouterGuard(router);
app.mount('#app');
}
bootstrap();
6、注册全局指令
新建/@/directives/index.ts文件。
// /@/directives/index.ts
/**
* 配置和注册全局指令
*/
import type { App } from 'vue';
import { setupRepeatDirective } from './repeatClick';
import { setupRippleDirective } from './ripple';
import { setupClickOutsideDirective } from './clickOutside';
export function setupGlobDirectives(app: App) {
// 防止重复点击
setupRepeatDirective(app);
// 水波纹
setupRippleDirective(app);
// 点内外部触发不同事件
setupClickOutsideDirective(app);
}
这三个是常用的自定义指令,下面直接上代码。
// repeatClick.ts
/**
* 防止重复点击,注意和防抖节流区分开
* @Example v-repeat-click="()=>{}"
*/
import { on, once } from '/@/utils/domUtils';
import type { App, Directive, DirectiveBinding } from 'vue';
const RepeatDirective: Directive = {
beforeMount(el: Element, binding: DirectiveBinding<any>) {
let interval: Nullable<IntervalHandle> = null;
let startTime = 0;
const handler = (): void => binding?.value();
const clear = (): void => {
if (Date.now() - startTime < 100) {
handler();
}
interval && clearInterval(interval);
interval = null;
};
on(el, 'mousedown', (e: MouseEvent): void => {
if ((e as any).button !== 0) return;
startTime = Date.now();
once(document as any, 'mouseup', clear);
interval && clearInterval(interval);
interval = setInterval(handler, 100);
});
},
};
export function setupRepeatDirective(app: App) {
app.directive('repeat-click', RepeatDirective);
}
export default RepeatDirective;
// ripple/index.ts
import type { App, Directive } from 'vue';
import './index.scss';
export interface RippleOptions {
event: string;
transition: number;
}
export interface RippleProto {
background?: string;
zIndex?: string;
}
export type EventType = Event & MouseEvent & TouchEvent;
const options: RippleOptions = {
event: 'mousedown',
transition: 400,
};
const RippleDirective: Directive & RippleProto = {
beforeMount: (el: HTMLElement, binding) => {
if (binding.value === false) return;
const bg = el.getAttribute('ripple-background');
setProps(Object.keys(binding.modifiers), options);
const background = bg || RippleDirective.background;
const zIndex = RippleDirective.zIndex;
el.addEventListener(options.event, (event: EventType) => {
rippler({
event,
el,
background,
zIndex,
});
});
},
updated(el, binding) {
if (!binding.value) {
el?.clearRipple?.();
return;
}
const bg = el.getAttribute('ripple-background');
el?.setBackground?.(bg);
},
};
function rippler({
event,
el,
zIndex,
background,
}: { event: EventType; el: HTMLElement } & RippleProto) {
const targetBorder = parseInt(getComputedStyle(el).borderWidth.replace('px', ''));
const clientX = event.clientX || event.touches[0].clientX;
const clientY = event.clientY || event.touches[0].clientY;
const rect = el.getBoundingClientRect();
const { left, top } = rect;
const { offsetWidth: width, offsetHeight: height } = el;
const { transition } = options;
const dx = clientX - left;
const dy = clientY - top;
const maxX = Math.max(dx, width - dx);
const maxY = Math.max(dy, height - dy);
const style = window.getComputedStyle(el);
const radius = Math.sqrt(maxX * maxX + maxY * maxY);
const border = targetBorder > 0 ? targetBorder : 0;
const ripple = document.createElement('div');
const rippleContainer = document.createElement('div');
// Styles for ripple
ripple.className = 'ripple';
Object.assign(ripple.style ?? {}, {
marginTop: '0px',
marginLeft: '0px',
width: '1px',
height: '1px',
transition: `all ${transition}ms cubic-bezier(0.4, 0, 0.2, 1)`,
borderRadius: '50%',
pointerEvents: 'none',
position: 'relative',
zIndex: zIndex ?? '9999',
backgroundColor: background ?? 'rgba(0, 0, 0, 0.12)',
});
// Styles for rippleContainer
rippleContainer.className = 'ripple-container';
Object.assign(rippleContainer.style ?? {}, {
position: 'absolute',
left: `${0 - border}px`,
top: `${0 - border}px`,
height: '0',
width: '0',
pointerEvents: 'none',
overflow: 'hidden',
});
const storedTargetPosition =
el.style.position.length > 0 ? el.style.position : getComputedStyle(el).position;
if (storedTargetPosition !== 'relative') {
el.style.position = 'relative';
}
rippleContainer.appendChild(ripple);
el.appendChild(rippleContainer);
Object.assign(ripple.style, {
marginTop: `${dy}px`,
marginLeft: `${dx}px`,
});
const {
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
} = style;
Object.assign(rippleContainer.style, {
width: `${width}px`,
height: `${height}px`,
direction: 'ltr',
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
});
setTimeout(() => {
const wh = `${radius * 2}px`;
Object.assign(ripple.style ?? {}, {
width: wh,
height: wh,
marginLeft: `${dx - radius}px`,
marginTop: `${dy - radius}px`,
});
}, 0);
function clearRipple() {
setTimeout(() => {
ripple.style.backgroundColor = 'rgba(0, 0, 0, 0)';
}, 250);
setTimeout(() => {
rippleContainer?.parentNode?.removeChild(rippleContainer);
}, 850);
el.removeEventListener('mouseup', clearRipple, false);
el.removeEventListener('mouseleave', clearRipple, false);
el.removeEventListener('dragstart', clearRipple, false);
setTimeout(() => {
let clearPosition = true;
for (let i = 0; i < el.childNodes.length; i++) {
if ((el.childNodes[i] as Recordable).className === 'ripple-container') {
clearPosition = false;
}
}
if (clearPosition) {
el.style.position = storedTargetPosition !== 'static' ? storedTargetPosition : '';
}
}, options.transition + 260);
}
if (event.type === 'mousedown') {
el.addEventListener('mouseup', clearRipple, false);
el.addEventListener('mouseleave', clearRipple, false);
el.addEventListener('dragstart', clearRipple, false);
} else {
clearRipple();
}
(el as Recordable).setBackground = (bgColor: string) => {
if (!bgColor) {
return;
}
ripple.style.backgroundColor = bgColor;
};
}
function setProps(modifiers: Recordable, props: Recordable) {
modifiers.forEach((item: Recordable) => {
if (isNaN(Number(item))) props.event = item;
else props.transition = item;
});
}
export function setupRippleDirective(app: App) {
app.directive('ripple', RippleDirective);
}
export default RippleDirective;
// ripple/index.scss
.ripple-container {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
overflow: hidden;
pointer-events: none;
}
.ripple-effect {
position: relative;
z-index: 9999;
width: 1px;
height: 1px;
margin-top: 0;
margin-left: 0;
pointer-events: none;
border-radius: 50%;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
// clickOutside.ts
import { on } from '/@/utils/domUtils';
import { isServer } from '/@/utils/is';
import type { App, ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
type FlushList = Map<
HTMLElement,
{
documentHandler: DocumentHandler;
bindingFn: (...args: unknown[]) => unknown;
}
>;
const nodeList: FlushList = new Map();
let startClick: MouseEvent;
if (!isServer) {
on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
on(document, 'mouseup', (e: MouseEvent) => {
for (const { documentHandler } of nodeList.values()) {
documentHandler(e, startClick);
}
});
}
function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
let excludes: HTMLElement[] = [];
if (Array.isArray(binding.arg)) {
excludes = binding.arg;
} else {
// due to current implementation on binding type is wrong the type casting is necessary here
excludes.push(binding.arg as unknown as HTMLElement);
}
return function (mouseup, mousedown) {
const popperRef = (
binding.instance as ComponentPublicInstance<{
popperRef: Nullable<HTMLElement>;
}>
).popperRef;
const mouseUpTarget = mouseup.target as Node;
const mouseDownTarget = mousedown.target as Node;
const isBound = !binding || !binding.instance;
const isTargetExists = !mouseUpTarget || !mouseDownTarget;
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
const isSelf = el === mouseUpTarget;
const isTargetExcluded =
(excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
const isContainedByPopper =
popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
if (
isBound ||
isTargetExists ||
isContainedByEl ||
isSelf ||
isTargetExcluded ||
isContainedByPopper
) {
return;
}
binding.value();
};
}
const ClickOutsideDirective: ObjectDirective = {
beforeMount(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
updated(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
unmounted(el) {
nodeList.delete(el);
},
};
export function setupClickOutsideDirective(app: App) {
app.directive('click-outside', ClickOutsideDirective);
}
export default ClickOutsideDirective;
clickOutside.ts里面有一个isServer函数,去utils里面添加上
// /@/utils/is.ts
...
export const isServer = typeof window === 'undefined';
...
最后修改一下main.ts。
// main.ts
import './style/tailwind.scss';
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import { router, setupRouter } from './router';
import { setupStore } from '/@/stores';
import { initAppConfigStore } from './logics/initAppConfig';
import { registerGlobComp } from './components/registerGlobComp';
import { setupRouterGuard } from '/@/router/guard';
import { setupGlobDirectives } from '/@/directives';
async function bootstrap() {
const app = createApp(App);
// 配置 store
setupStore(app);
// 初始化内部系统配置
initAppConfigStore();
// 注册全局组件
registerGlobComp(app);
// 配置路由
setupRouter(app);
// 路由守卫
setupRouterGuard(router);
// 注册全局指令
setupGlobDirectives(app);
app.mount('#app');
}
bootstrap();
7、配置全局错误处理
新建/@/logics/error-handle/index.ts文件。
// /@/logics/error-handle/index.ts
/**
* 用于配置全局错误处理功能,可以监控vue错误、脚本错误、静态资源错误和Promise错误
*/
import type { ErrorLogInfo } from '/#/store';
import { useErrorLogStoreWithOut } from '/@/stores/modules/errorLog';
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { App } from 'vue';
import projectSetting from '/@/settings/projectSetting';
/**
* 处理错误堆栈信息
* @param error
*/
function processStackMsg(error: Error) {
if (!error.stack) {
return '';
}
let stack = error.stack
.replace(/\n/gi, '') // 删除换行符以保存传输内容的大小
.replace(/\bat\b/gi, '@') // At in chrome, @ in ff
.split('@') // 用@拆分信息
.slice(0, 9) // 最大堆栈长度(Error.stackTraceLimit=10),因此只取前10个
.map((v) => v.replace(/^\s*|\s*$/g, '')) // 删除多余的空格
.join('~') // 手动添加分隔符以便稍后显示
.replace(/\?[^:]+/gi, ''); // 删除js文件链接的冗余参数(?x=1等
const msg = error.toString();
if (stack.indexOf(msg) < 0) {
stack = msg + '@' + stack;
}
return stack;
}
/**
* 获取模块名称
* @param vm
*/
function formatComponentName(vm: any) {
if (vm.$root === vm) {
return {
name: 'root',
path: 'root',
};
}
const options = vm.$options as any;
if (!options) {
return {
name: 'anonymous',
path: 'anonymous',
};
}
const name = options.name || options._componentTag;
return {
name: name,
path: options.__file,
};
}
/**
* 配置Vue错误处理功能
*/
function vueErrorHandler(err: Error, vm: any, info: string) {
const errorLogStore = useErrorLogStoreWithOut();
const { name, path } = formatComponentName(vm);
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.VUE,
name,
file: path,
message: err.message,
stack: processStackMsg(err),
detail: info,
url: window.location.href,
});
}
/**
* 配置脚本错误处理功能
*/
export function scriptErrorHandler(
event: Event | string,
source?: string,
lineno?: number,
colno?: number,
error?: Error,
) {
if (event === 'Script error.' && !source) {
return false;
}
const errorInfo: Partial<ErrorLogInfo> = {};
colno = colno || (window.event && (window.event as any).errorCharacter) || 0;
errorInfo.message = event as string;
if (error?.stack) {
errorInfo.stack = error.stack;
} else {
errorInfo.stack = '';
}
const name = source ? source.substr(source.lastIndexOf('/') + 1) : 'script';
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.SCRIPT,
name: name,
file: source as string,
detail: 'lineno' + lineno,
url: window.location.href,
...(errorInfo as Pick<ErrorLogInfo, 'message' | 'stack'>),
});
return true;
}
/**
* 配置Promise错误处理功能
*/
function registerPromiseErrorHandler() {
window.addEventListener(
'unhandledrejection',
function (event) {
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.PROMISE,
name: 'Promise Error!',
file: 'none',
detail: 'promise error!',
url: window.location.href,
stack: 'promise error!',
message: event.reason,
});
},
true,
);
}
/**
* 配置监控资源加载错误处理功能
*/
function registerResourceErrorHandler() {
// 配置监控资源加载错误处理功能
window.addEventListener(
'error',
function (e: Event) {
const target = e.target ? e.target : (e.srcElement as any);
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.RESOURCE,
name: 'Resource Error!',
file: (e.target || ({} as any)).currentSrc,
detail: JSON.stringify({
tagName: target.localName,
html: target.outerHTML,
type: e.type,
}),
url: window.location.href,
stack: 'resource is not found',
message: (e.target || ({} as any)).localName + ' is load error',
});
},
true,
);
}
/**
* 配置全局错误处理
* @param app
*/
export function setupErrorHandle(app: App) {
const { useErrorHandle } = projectSetting;
if (!useErrorHandle) {
return;
}
// Vue异常监视;
app.config.errorHandler = vueErrorHandler;
// 脚本错误
window.onerror = scriptErrorHandler;
// promise异常
registerPromiseErrorHandler();
// 静态资源异常
registerResourceErrorHandler();
}
这部分数据会存储在store里面,所以需要在store新建errorLog模块,顺便在enums和types里补充所需类型。
// /@/stores/modules/errorLog.ts
import type { ErrorLogInfo } from '/#/store';
import { defineStore } from 'pinia';
import { store } from '/@/stores';
import { formatToDateTime } from '/@/utils/dateUtils';
import projectSetting from '/@/settings/projectSetting';
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
export interface ErrorLogState {
errorLogInfoList: Nullable<ErrorLogInfo[]>;
errorLogListCount: number;
}
export const useErrorLogStore = defineStore({
id: 'app-error-log',
state: (): ErrorLogState => ({
// 异常列表
errorLogInfoList: null,
// 异常次数
errorLogListCount: 0,
}),
getters: {
getErrorLogInfoList(state): ErrorLogInfo[] {
return state.errorLogInfoList || [];
},
getErrorLogListCount(state): number {
return state.errorLogListCount;
},
},
actions: {
addErrorLogInfo(info: ErrorLogInfo) {
const item = {
...info,
time: formatToDateTime(new Date()),
};
this.errorLogInfoList = [item, ...(this.errorLogInfoList || [])];
this.errorLogListCount += 1;
},
setErrorLogListCount(count: number): void {
this.errorLogListCount = count;
},
/**
* ajax请求错误后触发
* @param error
* @returns
*/
addAjaxErrorInfo(error) {
const { useErrorHandle } = projectSetting;
if (!useErrorHandle) {
return;
}
const errInfo: Partial<ErrorLogInfo> = {
message: error.message,
type: ErrorTypeEnum.AJAX,
};
if (error.response) {
const {
config: { url = '', data: params = '', method = 'get', headers = {} } = {},
data = {},
} = error.response;
errInfo.url = url;
errInfo.name = 'Ajax Error!';
errInfo.file = '-';
errInfo.stack = JSON.stringify(data);
errInfo.detail = JSON.stringify({ params, method, headers });
}
this.addErrorLogInfo(errInfo as ErrorLogInfo);
},
},
});
// 需要在设置之外使用
export function useErrorLogStoreWithOut() {
return useErrorLogStore(store);
}
新建/@/types/store.d.ts和/@/enums/exceptionEnum.ts文件。
// /@/enums/exceptionEnum.ts
/**
* @description: 异常相关枚举
*/
export enum ErrorTypeEnum {
VUE = 'vue',
SCRIPT = 'script',
RESOURCE = 'resource',
AJAX = 'ajax',
PROMISE = 'promise',
}
// /@/types/store.d.ts
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
// 错误日志信息
export interface ErrorLogInfo {
// 错误类型
type: ErrorTypeEnum;
// 错误文件
file: string;
// 错误名称
name?: string;
// 错误消息
message: string;
// 错误堆栈
stack?: string;
// 错误详细信息
detail: string;
// 错误url
url: string;
// 错误时间
time?: string;
}
修改main.ts。
// main.ts
import './style/tailwind.scss';
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import { router, setupRouter } from './router';
import { setupStore } from '/@/stores';
import { initAppConfigStore } from './logics/initAppConfig';
import { registerGlobComp } from './components/registerGlobComp';
import { setupRouterGuard } from '/@/router/guard';
import { setupGlobDirectives } from '/@/directives';
import { setupErrorHandle } from '/@/logics/error-handle';
async function bootstrap() {
const app = createApp(App);
// 配置 store
setupStore(app);
// 初始化内部系统配置
initAppConfigStore();
// 注册全局组件
registerGlobComp(app);
// 配置路由
setupRouter(app);
// 路由守卫
setupRouterGuard(router);
// 注册全局指令
setupGlobDirectives(app);
// 配置全局错误处理,useErrorHandle为false时不启用
setupErrorHandle(app);
app.mount('#app');
}
bootstrap();
注意全局错误处理是受ProjectConfig里的useErrorHandle变量控制,默认false。
结语
作为一个前端通用模板项目,这些配置只多不少,下一篇是axios封装。未完待续...