MeterSphere/frontend/README.md

806 lines
25 KiB
Markdown
Raw 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.

## 提交规范
### 1. 提交消息格式
```bin
<Type>[optional Scope]: <Description>
[optional body]
[optional footer]
```
#### Type 提交的类型
> 表示提交的目的或影响的范围
- feat: 新功能A new feature
- fix: 修复 bugA bug fix
- docs: 文档变更Documentation changes
- style: 代码样式调整Code style changes
- refactor: 代码重构Code refactoring
- test: 测试相关的变更Test-related changes
- chore: 构建过程或工具变动Build process or tooling changes
- perf: 性能优化Performance optimization
- ci: CI/CD 相关的变动Changes to the CI/CD configuration or scripts
- revert: 撤销之前的提交Revert a previous commit
#### Scope 作用域
提交的作用域,表示本次提交影响的部分代码或模块,可以根据项目的需要选择性地添加。
#### Description 描述
简明扼要地描述本次提交的内容
#### body 正文
可选的详细描述,可以包含更多的信息和上下文
#### footer 脚注
可选的脚注,通常用于引用相关的问题编号或关闭问题。
## 示例提交消息:
```
feat(user): add login functionality
- Add login form and authentication logic
- Implement user authentication API endpoints
Closes #123
```
在这个示例中,提交类型为 feat新功能作用域为 user描述了添加登录功能的内容。正文部分提供了更详细的说明并引用了问题编号。
## 架构总览
```
├── babel.config.js // babel配置支持JSX
├── commitlint.config.js // commitlint配置校验commit信息
├── components.d.ts // 组件注册TS声明
├── config // 项目构建配置
│ ├── plugin // 构建插件
│ ├── utils // 构建工具方法
│ ├── vite.config.base.ts // vite基础配置
│ ├── vite.config.dev.ts // vite开发环境配置
│ └── vite.config.prod.ts // vite 生产配置
├── index.html // 单页面html模板
├── src
│ ├── App.vue // 应用入口vue
│ ├── api // 项目请求api封装
│ │ ├── http // axios封装
│ │ ├── modules // 各业务模块的请求方法
│ │ ├── requrls // 按业务模块划分的接口地址
│ ├── assets // 全局静态资源
│ │ ├── images
- svg
│ │ ├── logo.svg
│ │ ├── style
│ ├── components // 组件
│ ├── config // 全局配置常量类、JSON
│ ├── directive // 自定义指令集
│ ├── enums // 全局枚举定义
│ ├── hooks // 全局hooks集
│ ├── layout // 应用布局组件
│ ├── locale // 国际化配置
│ ├── main.ts // 项目主入口
│ ├── mock // mock数据配置
│ ├── models // 全局数据模型定义
│ ├── router // 路由
│ ├── store // pinia状态库
│ ├── types // 全局TS声明
│ ├── utils // 公共工具方法
│ └── views
│ ├── modules // 页面模块
│ └── base // 公共页面403、404等
│ ├── env.d.ts // 环境信息TS类型声明
└── .env.development // 开发环境变量声明
└── .env.production // 生产环境变量声明
└── .eslintrc.js // eslint配置
└── .prettierrc.js // prettier配置
└── tsconfig.json // 全局TS配置
```
<a name="dd14f832"></a>
## -状态管理模块设计
Vue3 状态管理方案采用的是`pinia`API 风格与`Redux`类的状态管理库类似,也是通过模块化的方式注册`模块store`,每个`store`中包含数据仓库`state`、数据包装过滤`getter`、同步/异步操作`action`。与`Vuex`相比,`pinia`提供了`compasition-API`、完整支持 TS 以及可扩展的`Plugin`功能。<br />整体模块划分为业务模块`modules/*`、注册入口`index`以及插件`plugins/*`。<br />首先,在`store/index.ts`中声明注册`pinia`并引入自定义的插件:
```typescript
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { debouncePlugin } from './plugins';
import useXXStore from './modules/xx';
const pinia = createPinia();
// 插件会在实例创建后应用因此插件不能在pinia实例创建前使用
pinia.use(debouncePlugin).use(piniaPluginPersistedstate);
export { useXXStore }; // 导出模块store
export default pinia;
```
然后,在项目入口`mian.ts`中使用:
```typescript
import { createApp } from 'vue';
import store from '@/store';
import app from '@/App.vue';
createApp(app).use(store).mount('#app');
```
<a name="piniaPlugins.ts"></a>
### plugins
在此文件内编写`pinia`插件,并在注册的时候引入使用即可:
```typescript
import { debounce, isObject } from 'lodash-es';
// 首先得声明插件使用到的额外属性因为pinia的TS类型声明中每个store只有三个原生属性state、getters、actions若没有使用到额外属性则无需声明
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
cache?: Partial<CacheType<S, Store>>; // 缓存配置
debounce?: Partial<Record<keyof StoreActions<Store>, number>>; // 节流配置
}
}
// 基于lodash的防抖函数封装读取store中cache配置的属性针对已配置的属性更改操作进行防抖处理
export const debouncePlugin = ({ options, store }: PiniaPluginContext) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((debounceActions: debounceAction, action) => {
debounceActions[action] = debounce(store[action], options.debounce![action]);
return debounceActions;
}, {});
}
};
```
上述自定义插件,在 store 中如下配置:
```typescript
export const useStore = defineStore('demo', {
state: () => ({
testDebounce: '',
}),
getters: {},
actions: {},
debounce: {
// 防抖设置
testDebounce: 500, // 值为防抖缓冲时间
},
});
```
<a name="520bb47a"></a>
## -网络模块设计
网络模块包含:请求 url 封装`api/requrls/*`、请求方法封装`api/modules/*`、请求工具封装`api/http/*`。
<a name="slaQE"></a>
### requrls
将项目接口地址收敛至此文件夹下管理,避免出现一个项目多个重复接口 url、方便接口地址复用且方便统一处理
```typescript
export const LoginUrl = '/api/user/login';
export const LogoutUrl = '/api/user/logout';
export const GetUserInfoUrl = '/api/user/info';
export const GetMenuListUrl = '/api/user/menu';
```
<a name="y561q"></a>
### modules
将项目实际请求方法按业务模块划分,统一管理实际业务请求,将请求方法与接口地址解耦
```typescript
import axios from 'axios';
import { LoginUrl, LogoutUrl, GetUserInfoUrl, GetMenuListUrl } from '@/api/requrls/user';
import type { RouteRecordNormalized } from 'vue-router';
import type { UserState } from '@/store/modules/user/types';
import type { LoginData, LoginRes } from '@/models/user';
export function login(data: LoginData) {
return axios.post<LoginRes>(LoginUrl, data);
}
export function logout() {
return axios.post<LoginRes>(LogoutUrl);
}
export function getUserInfo() {
return axios.post<UserState>(GetUserInfoUrl);
}
export function getMenuList() {
return axios.post<RouteRecordNormalized[]>(GetMenuListUrl);
}
```
最终通过`index.ts`将请求方法暴露出去
```typescript
export * from './modules/user';
export * from './modules/dashboard';
export * from './modules/message';
```
<a name="0f0a9627"></a>
#### 请求工具封装
基于`axios`封装请求方法,提供`form-data/json/urlencoded`格式的数据处理、自定义头部处理、响应拦截错误处理、分级提示modal、message、none、get 请求防缓存。
```typescript
// 分级错误信息提示none为静默模式即不提示、modal为对话框提示、message为tips消息提示
// ErrorMessageMode默认为message模式通过传入请求方法的options.errorMessageMode入参判断
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
// 三种入参数据格式
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data qs序列化处理
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload文件流处理
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}
// 对上传文件请求入参格式额外定义
export interface UploadFileParams {
// 除文件流外的参数
data?: Recordable;
// 文件流字段名
name?: string;
// 文件内容
file: File | Blob;
// 文件名
filename?: string;
// 其他parmas
[key: string]: any;
}
// 基准返回数据格式
export interface Result<T = any> {
code: number;
type: 'success' | 'error' | 'warning';
message: string;
result: T;
}
```
<a name="c53518d5"></a>
## -directive 指令集
指令入口`index.ts`导入并注册定义的全部指令
```typescript
import { App } from 'vue';
import permission from './permission';
export default {
install(Vue: App) {
Vue.directive('permission', permission);
},
};
```
权限指令(或其他自定义指令)在同级目录下创建,以指令名称命名文件夹,下面是示例:
```typescript
import { DirectiveBinding } from 'vue';
import { useUserStore } from '@/store';
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
const userStore = useUserStore();
const { role } = userStore;
if (Array.isArray(value)) {
if (value.length > 0) {
const permissionValues = value;
const hasPermission = permissionValues.includes(role);
if (!hasPermission && el.parentNode) {
el.parentNode.removeChild(el);
}
}
} else {
throw new Error(`need roles! Like v-permission="['admin','user']"`);
}
}
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding);
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding);
},
};
```
<a name="Kw0kR"></a>
## -hooks
全局抽象钩子集(与 Vue2 的 mixins 类似),这里只写业务逻辑的钩子!!!通过`@vueuse/core`我们已经可以得到非常多实用的钩子函数,没必要重复造轮子,在编写钩子功能前先去[Function List](https://vueuse.org/functions.html)查看是否已经有相同功能的钩子了,没有再自己写。导出钩子`export default function useXxx`,以权限钩子示例:
```typescript
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/store';
export default function usePermission() {
const userStore = useUserStore();
return {
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
// do something
},
findFirstPermissionRoute(_routers: any, role = 'admin') {
// do something
},
// You can add any rules you want
};
}
```
<a name="In07K"></a>
## -layout
项目布局设计模块,将最上层布局组件放置此文件夹内,统一管理项目各类上层布局(若存在,例如左侧菜单-右侧内容布局、顶层菜单-中间内容-底部页脚布局等等),示例:
```vue
<template>
<!-- router-view内为实际路由切换变化的内容视为路由页面容器 -->
<router-view v-slot="{ Component, route }">
<!-- transition为页面录音切换时提供进出动画平滑过渡页面渲染 -->
<transition name="fade" mode="out-in" appear>
<component :is="Component" v-if="route.meta.ignoreCache" :key="route.fullPath" />
<!-- keep-alive提供组件状态缓存以便快速渲染组件内容 -->
<keep-alive v-else :include="cacheList">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useTabBarStore } from '@/store';
const tabBarStore = useTabBarStore();
const cacheList = computed(() => tabBarStore.getCacheList);
</script>
```
<a name="KZkaZ"></a>
## -models
全局数据模型,将涉及请求、组件 props、状态库的公共属性抽象为数据模型在此文件夹内声明保证全局数据的类型统一、方便维护。下面是请求模型示例
```typescript
export interface HttpResponse<T = unknown> {
status: number;
msg: string;
code: number;
data: T;
}
```
也可以是业务相关模型:
```typescript
export interface LoginData {
username: string;
password: string;
}
export interface LoginRes {
token: string;
}
```
<a name="d3Gdu"></a>
## -mock
提供全局接口数据 mock 功能,避免因为调试而修改请求代码导致出现问题,按功能模块划分文件,通过`index.ts`暴露,示例如下:
```typescript
import Mock from 'mockjs';
import './user';
import '@/views/dashboard/workplace/mock';
Mock.setup({
timeout: '600-1000',
});
```
```typescript
import Mock from 'mockjs';
import setupMock, { successResponseWrap, failResponseWrap } from '@/utils/setup-mock';
import { isLogin } from '@/utils/auth';
import { MockParams } from '@/types/mock';
setupMock({
setup() {
// 用户信息
Mock.mock(new RegExp('/api/user/info'), () => {
if (isLogin()) {
const role = window.localStorage.getItem('userRole') || 'admin';
return successResponseWrap({
name: '王立群',
avatar: '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
email: 'wangliqun@email.com',
job: 'frontend',
jobName: '前端艺术家',
organization: 'Frontend',
organizationName: '前端',
location: 'beijing',
locationName: '北京',
introduction: '人潇洒,性温存',
personalWebsite: 'https://www.arco.design',
phone: '150****0000',
registrationDate: '2013-05-10 12:10:00',
accountId: '15012312300',
certification: 1,
role,
});
}
return failResponseWrap(null, '未登录', 50008);
});
// 登出
Mock.mock(new RegExp('/api/user/logout'), () => {
return successResponseWrap(null);
});
},
});
```
<a name="QgxDQ"></a>
## -router
项目路由管理模块,入口文件`index.ts`注册并暴露全部路由,以模块命名文件夹划分模块路由放置在`routes/*`下;`guard/*`下放置路由导航控制,包含权限、登录重定向等;`app-menus/index.ts`为菜单相关的路由信息;`constants.ts`定义路由常量,包括路由白名单、重定向路由名、默认主页路由信息等
<a name="YZWcg"></a>
## -enums
全局的枚举类型声明模块,将可枚举的类型统一管理,以`类型+Enum`命名,方便维护。示例如下:
```typescript
/**
* @description: Request result set
*/
export enum ResultEnum {
SUCCESS = 0,
ERROR = 1,
TIMEOUT = 401,
TYPE = 'success',
}
/**
* @description: request method
*/
export enum RequestEnum {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
}
/**
* @description: contentTyp
*/
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}
```
<a name="I0KEf"></a>
## -locale
国际化模块,存放项目声明的国际化配置,按语种划分模块。模块入口文件为`index.ts`,负责定义菜单、导航栏等公共的国际化配置,其他的按系统功能声明并导入`index.ts`,还有项目内页面组件的国际化配置也需要导入至`index.ts`,页面组件的国际化配置在页面的目录下声明,如:`views/dashboard/workplace/locale`。
```typescript
import { createI18n } from 'vue-i18n';
import en from './en-US';
import cn from './zh-CN';
export const LOCALE_OPTIONS = [
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
];
const defaultLocale = localStorage.getItem('MS-locale') || 'zh-CN';
const i18n = createI18n({
locale: defaultLocale,
fallbackLocale: 'en-US',
legacy: false,
allowComposition: true,
messages: {
'en-US': en,
'zh-CN': cn,
},
});
export default i18n;
```
<a name="fZfbw"></a>
## -types
项目级别的类型声明,与业务无关的类型声明,与`models`、`enums`不同的是,这里声明的是项目模块级别的类型,或者工具模块的类型声明,例如:`axios`的配置声明、第三方插件不提供 TS 支持但需要我们自定义的声明等
<a name="hvu5O"></a>
## -utils
公共方法、工具,按功能类型命名文件,单个文件内工具方法的功能要对应命名
<a name="FFTqz"></a>
## -views
页面模块,按功能模块划分,公共模块有`login`、`base`,其中`base`模块内包含 404、403 页面等
<a name="RwVcu"></a>
## -theme 主题配置
1. 去Desing Lab 创建主题 https://arco.design/themes/home
2. 以`ms-theme-` 命名为开头
3. 点击页面的配置主题
**“CSS 变量” + “Tailwind 配置变量” + “基于 css 变量自行计算混合色覆盖 arco-theme 变量”**
## -.env.\*环境变量配置
在`vite`中内置了环境变量配置功能,只需要在项目根目录下创建以`.env.*`开头的文件,在内部写入配置的变量即可,默认`.env`为生产环境、`.env.development`为开发环境、`.env.XXX`为自定义`XXX`环境(注意:自定义环境需要在`package.json`的项目运行命令后加入`--mode XXX`),各类环境变量配置示例如下( ⚠️ 注意!!环境变量文件内使用注释要用`#`,不能使用`//`:
```bash
# .env.production 为生产环境配置
NODE_ENV=production # 代码中通过import.meta.env.NODE_ENV访问
VITE_STG=1 # 自定义变量必须用VITE_开头代码中通过import.meta.env.VITE_XXX访问
# .env.development 为开发环境配置
NODE_ENV=development
VITE_APP_ENV = dev
VITE_APP_TITLE = 我是标题
# .env.XXX 为自定义环境配置
VITE_MYENV = 1 # 代码中通过import.meta.env.VITE_MYENV访问
```
⚠️ 上述环境变量在代码中正常都可以用`import.meta.env.VITE_XXX`访问,但是在`vite.config.ts`配置文件中无法使用此方法访问,原因是此访问链是 vite 在初始化后通过读取本地`.env.XXX`文件并注入到`import.meta`中的,但是在`vite.config.ts`中 vite 此时还未初始化,所以无法通过上述方法访问,应通过下面方法访问:
```typescript
import { defineConfig, loadEnv } from 'vite'; // 导入loadEnv方法
loadEnv(mode, process.cwd()).VITE_XXX; // 在需要访问env里变量的地方使用此方法访问即可
```
<a name="c61428f2"></a>
## -TailwindCSS 配置
```javascript
module.exports = {
content: ['./index.html', './src/**/*.{html,js,vue}', './src/*.{html,js,vue}'], // 需要解析的文件路径
theme: {
// 自定义主题配置
backgroundColor: {
// 自定义背景色
menuHover: '#272D39',
headerBg: '#191E29',
},
textColor: (theme) => ({
// 自定义字体颜色
...theme('colors'), // 这里必须解构原本有的颜色不然会导致在页面style中使用@apply应用内部字体颜色类的时候报错找不到内部字体颜色类
'40Gray': 'rgba(255,255,255,0.40)',
'65Gray': 'rgba(255,255,255,0.65)',
}),
extend: {},
},
plugins: [],
};
```
<a name="c7baab5c"></a>
## -TS 配置-tsconfig.json
```json
{
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/components.d.ts",
"auto-imports.d.ts"
], // TS解析路径配置
"exclude": ["node_modules"],
"compilerOptions": {
"allowJs": true, // 允许编译器编译JSJSX文件
"noEmit": true,
"target": "esnext", // 使用es最新语法
"useDefineForClassFields": true,
"allowSyntheticDefaultImports": true, // 允许异步导入模块,配合自动导入插件使用
"module": "esnext", // 使用ES模块语法
"moduleResolution": "node",
"strict": true, // 严格模式
"jsx": "preserve",
"sourceMap": true, // 代码映射
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"skipLibCheck": true, // 跳过node依赖包语法检查
"types": [
// "vitest/globals",
// "vite-plugin-svg-icons/client"
], // 手动导入TS类型声明文件
"baseUrl": ".",
"paths": {
// 路径映射
"@/*": ["./src/*"],
"#/*": ["types/*"]
}
}
}
```
<a name="50a6c7f6"></a>
## -vite 配置-插件详解
`vite`构建生产资源使用`rollup`打包,所以可以在`vite`中使用`rollup`支持的所有插件。
- 组件自动导入,使用`unplugin-auto-import/vite`插件实现自动导入组件、`unplugin-vue-components/vite`插件按需导入自定义组件,具体用法如下:
```typescript
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ArcoResolver } from 'unplugin-vue-components/resolvers'
import { vitePluginForArco } from '@arco-plugins/vite-vue'
export default () => defineConfig({
plugins: [
// 自定义组件自动引入
AutoImport({å
dts: 'src/auto-import.d.ts', // 输出声明文件地址(在使用typescript时必须配置不然会导致页面使用未导入的组件时报错)
resolvers: [ElementPlusResolver()], // ElementPlus自动导入
}),
// 自定义组件按需引入
Components({
dts: 'src/components.d.ts', // 输出声明文件地址(在使用typescript时必须配置不然会导致页面使用未导入的组件时报错)
dirs: ['src/components'], // 按需加载的文件夹
resolvers: [
ArcoResolver(), // ArcoDesignVue按需加载
],
}),
// 样式自动导入
vitePluginForArco({})
]
})
```
- 使用`vite-plugin-svg-icons`插件实现自动加载 svg 图片,并通过封装 svg 组件的方式,一行代码使用 svg配置如下
```typescript
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export default () =>
defineConfig({
plugins: [
createSvgIconsPlugin({
// 指定svg读取的文件夹
iconDirs: [resolve(process.cwd(), 'src/assets/icons/svg')],
// 指定icon的读取名字使用svg文件名为icon名字
symbolId: 'icon-[dir]-[name]',
}),
],
});
```
- 使用`vite`自带插件`loadEnv`读取环境信息,可作环境判断,使用`rollup-plugin-visualizer`插件可分析打包后的文件体积,配置如下:
```typescript
import { defineConfig, loadEnv } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
// 这里的mode参数为package.json文件配置的环境参数使用`--mode XXX`,如"report: rimraf dist && vite build --mode analyze"
export default ({ mode }) =>
defineConfig({
plugins: [
// 这里通过--mode analyze配置为分析模式使用visualizer插件分析方法输出report.html分析报告
loadEnv(mode, process.cwd()).VITE_ANALYZE === 'Y'
? visualizer({ open: true, brotliSize: true, filename: 'report.html' })
: null,
],
});
```
<a name="0c0978d0"></a>
## -vite 配置-build 详解
上面讲述了插件应用,下面讲解除了插件外,在`vite`中的 build 构建生产资源时,`rollupOptions`的配置:
- `output`为输出产物配置,我们可以通过`manualChunks`去配置分包策略(与`webpack`分包机制类似),配置如下:
```typescript
import { mergeConfig } from 'vite';
import baseConfig from './vite.config.base';
import configCompressPlugin from './plugin/compress';
import configVisualizerPlugin from './plugin/visualizer';
import configArcoResolverPlugin from './plugin/arcoResolver';
import configImageminPlugin from './plugin/imagemin';
export default mergeConfig(
{
mode: 'production',
plugins: [
configCompressPlugin('gzip'),
configVisualizerPlugin(),
configArcoResolverPlugin(),
configImageminPlugin(),
],
build: {
rollupOptions: {
output: {
manualChunks: {
arco: ['@arco-design/web-vue'],
chart: ['echarts', 'vue-echarts'],
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
},
},
},
chunkSizeWarningLimit: 2000,
},
},
baseConfig
);
```
## 单元测试
测试框架vitest
单元测试实用工具库: @vue/test-utils
测试环境: js-dom
报告: @vitest/coverage-c8
```bash
pnpm add -D vitest @vue/test-utils js-dom @vitest/coverage-c8
```
配置
根目录新建 vitest.config.ts
```typescript
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './config/vite.config.dev';
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
},
})
);
```