Files
blog-press/docs/Practice/Common/Vue3-Common.md
2026-06-05 15:53:29 +08:00

580 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Vue3 Common
date: 2025-12-15 23:25
---
# 一、依赖管理
  本项目会传递安装的依赖有:
| 包名称 | 版本 | 含义和用途说明 |
|-------|------|---------------|
| `@fortawesome/fontawesome-free` | ^6.7.2 | FontAwesome 图标库的免费版本 |
| `@popperjs/core` | ^2.11.8 | 工具提示和弹出框定位引擎 |
| `axios` | ^1.4.0 | 基于 Promise 的 HTTP 客户端 |
| `compressorjs` | ^1.2.1 | 纯 JavaScript 图片压缩库 |
| `crypto-js` | ^4.2.0 | JavaScript 加密算法库 |
| `dayjs` | ^1.11.13 | 轻量级的日期处理库 |
| `echarts` | ^5.5.1 | 百度开源的数据可视化图表库 |
| `element-plus` | ^2.6.0 | 基于 Vue 3 的桌面端 UI 组件库 |
| `js-cookie` | ^3.0.5 | JavaScript Cookie 操作库 |
| `lunar-calendar` | ^0.1.4 | 农历日历转换库 |
| `lunar-javascript` | ^1.6.13 | 农历日期处理的 JavaScript 库 |
| `path-browserify` | ^1.0.1 | Node.js path 模块的浏览器版本兼容实现 |
| `qs` | ^6.13.0 | URL 查询字符串解析和序列化库 |
| `v-calendar` | ^3.1.2 | Vue.js 的日历和日期选择器组件 |
::: tip
如果使用pnpm安装会传递peerDependencies部分。
:::
::: warning
建议将Vite、Typescript、@types等构建工具依赖放在devDependencies中
:::
# 二、vite.config.ts配置
## 2.1 生成ts类型文件
  安装vite-plugin-dts插件
```cmd
pnpm add vite-plugin-dts -D
```
```typescript
import dts from 'vite-plugin-dts'
// 打包输出文件夹
const outDirPath = 'dist'
// 需要打包的类型文件
const TARGET_TYPE_FOLDERS = ['src/components', 'src/utils', 'src/types', 'src/vue3-common.ts']
export default defineConfig({
plugins: [
// 生成类型文件
dts({
// 需要处理的文件
include: TARGET_TYPE_FOLDERS,
// 使用特定的 tsconfig 配置
tsconfigPath: path.resolve(__dirname, 'tsconfig.app.json'),
// 类型文件输出目录
outDir: path.resolve(__dirname, outDirPath)
})
],
})
```
  打包后会在dist目录生成.d.ts文件提供完整的Typescript类型支持。
## 2.2 build打包配置
```typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
import path from 'path'
import fs from 'fs'
import { ICommonObj } from './src/types'
// 打包输出文件夹
const outDirPath = 'dist'
// 需要单个打包的文件夹(组件和工具类)
const TARGET_LIB_FOLDERS = ['src/components', 'src/utils']
// 需要排除的第三方依赖
const EXTERNAL = ['vue', 'element-plus', 'echarts', 'axios', 'moment', 'crypto-js', 'spark-md5', 'path-browserify']
/**
* 递归读取文件夹下的所有文件
* @param folderPath 文件夹
*/
const getFilesFromFolder = (folderPath: string) => {
const files: string[] = []
// 读取文件夹中的内容
fs.readdirSync(folderPath).forEach((item: string) => {
const fullPath = path.join(folderPath, item)
// 读取文件状态 如果是文件夹,递归读取
if (fs.statSync(fullPath).isDirectory()) {
files.push(...getFilesFromFolder(fullPath))
} else {
// 否则添加至文件列表
files.push(fullPath)
}
})
return files
}
// 动态生成入口文件
const inputEntries = TARGET_LIB_FOLDERS.reduce((entries: ICommonObj, folder) => {
// 获取文件夹内的所有文件
const files = getFilesFromFolder(path.resolve(__dirname, folder))
files.forEach((filePath) => {
// 判断文件是否是 .vue 或 .ts 文件
if (filePath.endsWith('.vue') || filePath.endsWith('.ts')) {
// 获取相对路径并保持目录结构
const relativePath = path.relative('src', filePath)
const entryName = path.join('lib', relativePath.replace(/\.(vue|ts)$/, ''))
entries[entryName] = filePath // 为每个文件创建一个入口
}
})
return entries
}, {})
// https://vitejs.dev/config/
export default defineConfig({
build: {
// lib文件配置
lib: {
// 入口文件
entry: path.resolve(__dirname, 'src/vue3-common.ts'),
formats: ['es'],
name: 'vue3-common',
// 文件名
fileName: (format) => `vue3-common.${format}.js`
},
// 输出文件路径
outDir: outDirPath,
// 是否将css文件分割
cssCodeSplit: true,
// Rollup打包配置
rollupOptions: {
// 需要排除的依赖 通常为第三方库
external: EXTERNAL,
// 输入配置
input: {
// 入口文件
'vue3-common': path.resolve(__dirname, 'src/vue3-common.ts'),
// 其余需要单独打包的文件
...inputEntries
},
// 输出配置
output: {
dir: path.resolve(__dirname, outDirPath),
// 入口文件
entryFileNames: '[name].js',
// chunk文件
chunkFileNames: 'lib/[name].js',
// 资源文件
assetFileNames: 'styles/[name].[ext]'
}
},
// 是否压缩代码
minify: false
}
})
```
  打包后会在dist目录生成源码文件。
# 三、package.json配置
## 3.1 导出路径
```json
{
"exports": {
".": {
"import": "./dist/vue3-common.js",
"require": "./dist/vue3-common.js",
"types": "./dist/vue3-common.d.ts"
},
"./components/SvgIcon.vue": {
"import": "./dist/lib/components/SvgIcon.js",
"types": "./dist/components/SvgIcon.vue.d.ts"
},
"./styles/SvgIcon.css": "./dist/styles/SvgIcon.css",
"./components/LoginForm.vue": {
"import": "./dist/lib/components/LoginForm.js",
"types": "./dist/components/LoginForm.vue.d.ts"
},
"./styles/LoginForm.css": "./dist/styles/LoginForm.css",
"./components/MenuItem.vue": {
"import": "./dist/lib/components/MenuItem.js",
"types": "./dist/components/MenuItem.vue.d.ts"
},
"./components/MenuList.vue": {
"import": "./dist/lib/components/MenuList.js",
"types": "./dist/components/MenuList.vue.d.ts"
},
"./components/Hamburger.vue": {
"import": "./dist/lib/components/Hamburger.js",
"types": "./dist/components/Hamburger.vue.d.ts"
},
"./styles/Hamburger.css": "./dist/styles/Hamburger.css",
"./components/MultiInput.vue": {
"import": "./dist/lib/components/MultiInput.js",
"types": "./dist/components/MultiInput.vue.d.ts"
},
"./styles/MultiInput.css": "./dist/styles/MultiInput.css",
"./types": {
"types": "./dist/types/index.d.ts"
},
"./utils/axiosUtil": {
"import": "./dist/lib/utils/axiosUtil.js",
"types": "./dist/types/utils/axiosUtil.d.ts"
},
"./utils/cryptoUtil": {
"import": "./dist/lib/utils/cryptoUtil.js",
"types": "./dist/types/utils/cryptoUtil.d.ts"
},
"./utils/dataUtil": {
"import": "./dist/lib/utils/dataUtil.js",
"types": "./dist/types/utils/dataUtil.d.ts"
},
}
}
```
  在项目中引入common包后通过配置tsconfig.json即可实现按需引入功能`import { fun1 } from 'vue3-common/utils/index'`
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"vue3-common/*": ["node_modules/vue3-common/dist/*"]
}
}
}
```
# 四、发布框架
1. 发布到本地
执行`npm pack`命令,会在项目文件夹下生成`.tgz`文件,其他项目通过文件路径形式引入:
```json
"vue3-common": "file:../vue3-common/vue3-common-1.0.0.tgz"
```
2. 发布到git
将dist文件夹发布到git仓库其他项目通过git形式引入
```json
"vue3-common": "git+https://gitee.com/Cxx0822/vue3-common#master"
```
# 五、组件使用说明
## 5.1 菜单组件
1. 安装Vue Router
```cmd
pnpm install vue-router
```
2. 在src/views文件夹中新建vue文件例如
```cmd
views
viewA
index.vue
viewB
index.vue
```
3. 在src/views文件夹下新建meta.ts配置文件
```typescript
import type { IRouteMetaConfig } from 'vue3-common/types'
export const metaList: IRouteMetaConfig = {
'/viewA': {
title: 'viewA',
icon: 'viewA',
order: 1,
redirect: '/viewA/index'
},
'/viewA/index': {
title: 'viewA-Index',
icon: ''
},
'/viewB': {
title: 'viewB',
icon: 'viewB',
order: 2,
redirect: '/viewB/index'
},
'/viewB/index': {
title: 'viewB-index',
icon: ''
}
}
```
  order对应的显示顺序关系icon对应的为src/icons/svg中的svg图标文件参考下文图标组件
4. 在src目录新建router文件夹新建menu.ts文件
```typescript
import { getRoutersByModules, sortRoutesByOrder } from 'vue3-common/utils/routerUtil'
import Layout from '@/layout/index.vue'
import { metaList } from '@/views/meta'
const modules = {
...import.meta.glob('@/views/viewA/**/*.vue'),
...import.meta.glob('@/views/viewB/**/*.vue'),
}
const menuRoutes = getRoutersByModules(modules, Layout, metaList)
export default sortRoutesByOrder(menuRoutes)
```
  导入刚才的views文件夹并生成路由菜单。
5. 如果还有其他的常量路由可以在src/router中新建constant.ts文件
```typescript
import type { RouteRecordRaw } from 'vue-router'
const constantRoutes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/login'
},
// 主页
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
}
]
export default constantRoutes
```
  该部分即Vue Router中的路由定义。
6. 在src/router中新建index.ts
```typescript
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { setupRouteGuard } from 'vue3-common/utils/permissionUtil'
// 使用 import.meta.glob 自动导入所有 src/router 目录下的 .ts 文件
const routeModules = import.meta.glob('./*.ts', { eager: true })
// 将所有模块的默认导出(即路由配置)合并成一个路由数组
const routes: RouteRecordRaw[] = Object.values(routeModules)
.map((module: any) => module.default) // 获取每个模块的默认导出
.flat() // 扁平化数组,确保所有路由项都在一个数组中
const router = createRouter({
history: createWebHashHistory(),
scrollBehavior: () => ({ top: 0 }),
routes
})
// 设置路由守卫
setupRouteGuard(router)
export default router
```
  遍历src/router文件夹下所有的路由文件并添加路由守卫。
7. 在main.ts中配置路由
```ts
// 引入路由
import router from './router'
// 创建Vue3实例
const app = createApp(App)
// 使用路由
app.use(router)
```
8. 在layout中使用
```vue
<template>
<aside class="layout-sidebar">
<menu-list
:route="useRoute()"
:routers="menuRoutes"
:collapse="isCollapse"
class="layout-menu"/>
</aside>
</template>
<script lang="ts" setup>
import { MenuList } from 'vue3-common'
import menuRoutes from '@/router/menu'
import { useRoute } from 'vue-router'
</script>
```
&emsp;&emsp;如果需要对路由菜单进行权限处理,可以使用计算属性过滤一遍:
```ts
const routers = computed(() => {
const menuPathList = menuRoutes.map((item) => item.path)
const removePathList = menuPathList.filter((item) => !authStore.currentUser.menu.includes(item))
removePathList.forEach((item) => {
router.removeRoute(capitalizeStr(item.replace('/', '')))
})
return menuRoutes.filter((route) => !removePathList.includes(route.path))
})
```
::: tip
实现原理:
```ts
import type { RouteComponent, RouteRecordRaw, RouteMeta } from 'vue-router'
/**
* 任意对象类型
*/
interface ICommonObj {
[key: string]: any;
}
/**
* 路由meta配置
*/
interface IRouteMetaConfig {
[key: string]: RouteMeta;
}
/**
* 路由路径转为名称 /admin/dashboard -> AdminDashboard
* @param path
*/
export const convertPath2Name = (path: string): string => {
// 移除路径开头和结尾的斜杠(如果存在)
const newPath = path.replace(/^\/|\/$/g, '')
// 将路径按斜杠分割成多个部分
const parts = newPath.split('/')
// 处理每个部分:将连字符后的字母大写,并移除连字符;处理动态参数(如 :id
return parts.map((part) =>
part.split('-').map((subPart) => {
// 处理动态参数(如 :id
if (subPart.startsWith(':')) {
return subPart.slice(1).charAt(0).toUpperCase() + subPart.slice(2)
}
// 普通部分
return subPart.charAt(0).toUpperCase() + subPart.slice(1)
}).join('')).join('')
}
/**
* 根据meta的order排序
* @param routes 路由列表
*/
export const sortRoutesByOrder = (routes: RouteRecordRaw[]): RouteRecordRaw[] => {
// 排序当前路由的子路由
routes.forEach((route) => {
if (route.children && route.children.length > 0) {
// 如果有子路由,递归排序子路由
route.children = sortRoutesByOrder(route.children)
}
})
// 对当前路由按 meta.order 排序
return routes.sort((a, b) => (a.meta?.order as number || 0) - (b.meta?.order as number || 0))
}
/**
* 获取路由
* @param modules 通过import.meta.glob导入的模块
* @param layout 布局组件
* @param metaConfig 路由meta配置
*/
export const getRoutersByModules = (modules: ICommonObj,
layout: RouteComponent, metaConfig?: IRouteMetaConfig): RouteRecordRaw[] => {
// const modules = import.meta.glob('@/views/admin/**/*.vue')
const routes: RouteRecordRaw[] = []
Object.keys(modules).forEach((filePath) => {
// 获取文件的模块路径和文件名
const parentPath = filePath.substring(0, filePath.lastIndexOf('/')).replace('/src/views', '')
const path = filePath.replace('.vue', '')
.replace('/src/views', '')
.replace(/\[([^\]]+)]/g, '/:$1')
// 为每个文件生成对应的路由
const route: RouteRecordRaw = {
path,
name: convertPath2Name(path),
component: modules[filePath]
}
// 如果存在meta配置
if (metaConfig) {
route.meta = metaConfig[path]
// 设置重定向属性
if (route.meta?.redirect) {
route.redirect = route.meta?.redirect as any
}
}
// 如果该路径已经存在(嵌套路由),则将其作为子路由
let parentRoute = routes.find((item) => item.path === parentPath)
if (!parentRoute) {
parentRoute = {
path: parentPath,
name: convertPath2Name(parentPath),
component: layout,
children: []
}
// 如果存在meta配置
if (metaConfig) {
parentRoute.meta = metaConfig[parentPath]
// 设置重定向属性
if (parentRoute.meta?.redirect) {
parentRoute.redirect = parentRoute.meta?.redirect
}
}
routes.push(parentRoute)
}
// 如果是子路由,加入到父路由的 `children` 数组
parentRoute.children?.push(route)
})
return routes
}
```
&emsp;&emsp;核心原理就是利用`import.meta.glob`创建路径->组件的映射,然后结合路由配置表,转为路由对象并添加到`vue router`中。
:::
## 5.2 图标组件
1. 安装vite-plugin-svg-icons依赖
```cmd
pnpm install vite-plugin-svg-icons -D
```
2. 配置vite.config.ts
```typescript
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
// 指定 SVG图标 保存的文件夹路径
iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
// 指定 使用svg图标的格式
symbolId: 'icon-[dir]-[name]'
})
],
})
```
3. 在main.ts中注册
```typescript
// 注册svg-icon
import 'virtual:svg-icons-register'
```
4. 在src/icons/svg目录中添加svg图标
5. 在组件中使用
```vue
<template>
<svg-icon name="user"/>
</template>
<script setup>
import { SvgIcon } from 'vue3-common'
</script>
```