经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
最佳实践:基于vite3的monorepo前端工程搭建
来源:cnblogs  作者:京东云开发者  时间:2023/5/29 10:52:00  对本文有异议

一、技术栈选择

1.代码库管理方式-Monorepo: 将多个项目存放在同一个代码库中

?选择理由1:多个应用(可以按业务线产品粒度划分)在同一个repo管理,便于统一管理代码规范、共享工作流

?选择理由2:解决跨项目/应用之间物理层面的代码复用,不用通过发布/安装npm包解决共享问题

2.依赖管理-PNPM: 消除依赖提升、规范拓扑结构

?选择理由1:通过软/硬链接方式,最大程度节省磁盘空间

?选择理由2:解决幽灵依赖问题,管理更清晰

3.构建工具-Vite:基于ESM和Rollup的构建工具

?选择理由:省去本地开发时的编译过程,提升本地开发效率

4.前端框架-Vue3:Composition API

?选择理由:除了组件复用之外,还可以复用一些共同的逻辑状态,比如请求接口loading与结果的逻辑

5.模拟接口返回数据-Mockjs

?选择理由:前后端统一了数据结构后,即可分离开发,降低前端开发依赖,缩短开发周期

二、目录结构设计:重点关注src部分

1.常规/简单模式:根据文件功能类型集中管理

mesh-fe
├── .husky #git提交代码触发
│ ├── commit-msg
│ └── pre-commit
├── mesh-server #依赖的node服务
│ ├── mock
│ │ └── data-service #mock接口返回结果
│ └── package.json
├── README.md
├── package.json
├── pnpm-workspace.yaml #PNPM工作空间
├── .eslintignore #排除eslint检查
├── .eslintrc.js #eslint配置
├── .gitignore
├── .stylelintignore #排除stylelint检查
├── stylelint.config.js #style样式规范
├── commitlint.config.js #git提交信息规范
├── prettier.config.js #格式化配置
├── index.html #入口页面
└── mesh-client #不同的web应用package
├── vite-vue3
├── src
├── api #api调用接口层
├── assets #静态资源相关
├── components #公共组件
├── config #公共配置,如字典/枚举等
├── hooks #逻辑复用
├── layout #router中使用的父布局组件
├── router #路由配置
├── stores #pinia全局状态管理
├── types #ts类型声明
├── utils
│ ├── index.ts
│ └── request.js #Axios接口请求封装
├── views #主要页面
├── main.ts #js入口
└── App.vue

2.基于domain领域模式:根据业务模块集中管理

mesh-fe
├── .husky #git提交代码触发
│ ├── commit-msg
│ └── pre-commit
├── mesh-server #依赖的node服务
│ ├── mock
│ │ └── data-service #mock接口返回结果
│ └── package.json
├── README.md
├── package.json
├── pnpm-workspace.yaml #PNPM工作空间
├── .eslintignore #排除eslint检查
├── .eslintrc.js #eslint配置
├── .gitignore
├── .stylelintignore #排除stylelint检查
├── stylelint.config.js #style样式规范
├── commitlint.config.js #git提交信息规范
├── prettier.config.js #格式化配置
├── index.html #入口页面
└── mesh-client #不同的web应用package
├── vite-vue3
├── src #按业务领域划分
├── assets #静态资源相关
├── components #公共组件
├── domain #领域
│ ├── config.ts
│ ├── service.ts
│ ├── store.ts
│ ├── type.ts
├── hooks #逻辑复用
├── layout #router中使用的父布局组件
├── router #路由配置
├── utils
│ ├── index.ts
│ └── request.js #Axios接口请求封装
├── views #主要页面
├── main.ts #js入口
└── App.vue

可以根据具体业务场景,选择以上2种方式其中之一。

三、搭建部分细节

1.Monorepo+PNPM集中管理多个应用(workspace)

?根目录创建pnpm-workspace.yaml,mesh-client文件夹下每个应用都是一个package,之间可以相互添加本地依赖:pnpm install

  1. packages:
  2. # all packages in direct subdirs of packages/
  3. - 'mesh-client/*'
  4. # exclude packages that are inside test directories
  5. - '!**/test/**'

?pnpm install #安装所有package中的依赖

?pnpm install -w axios #将axios库安装到根目录

?pnpm --filter | -F <name> <command> #执行某个package下的命令

?与NPM安装的一些区别:

?所有依赖都会安装到根目录node_modules/.pnpm下;

?package中packages.json中下不会显示幽灵依赖(比如tslib@types/webpack-dev),需要显式安装,否则报错

?安装的包首先会从当前workspace中查找,如果有存在则node_modules创建软连接指向本地workspace

?"mock": "workspace:^1.0.0"

2.Vue3请求接口相关封装

?request.ts封装:主要是对接口请求和返回做拦截处理,重写get/post方法支持泛型

  1. import axios, { AxiosError } from 'axios'
  2. import type { AxiosRequestConfig, AxiosResponse } from 'axios'
  3. // 创建 axios 实例
  4. const service = axios.create({
  5. baseURL: import.meta.env.VITE_APP_BASE_URL,
  6. timeout: 1000 * 60 * 5, // 请求超时时间
  7. headers: { 'Content-Type': 'application/json;charset=UTF-8' },
  8. })
  9. const toLogin = (sso: string) => {
  10. const cur = window.location.href
  11. const url = `${sso}${encodeURIComponent(cur)}`
  12. window.location.href = url
  13. }
  14. // 服务器状态码错误处理
  15. const handleError = (error: AxiosError) => {
  16. if (error.response) {
  17. switch (error.response.status) {
  18. case 401:
  19. // todo
  20. toLogin(import.meta.env.VITE_APP_SSO)
  21. break
  22. // case 404:
  23. // router.push('/404')
  24. // break
  25. // case 500:
  26. // router.push('/500')
  27. // break
  28. default:
  29. break
  30. }
  31. }
  32. return Promise.reject(error)
  33. }
  34. // request interceptor
  35. service.interceptors.request.use((config) => {
  36. const token = ''
  37. if (token) {
  38. config.headers!['Access-Token'] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
  39. }
  40. return config
  41. }, handleError)
  42. // response interceptor
  43. service.interceptors.response.use((response: AxiosResponse<ResponseData>) => {
  44. const { code } = response.data
  45. if (code === '10000') {
  46. toLogin(import.meta.env.VITE_APP_SSO)
  47. } else if (code !== '00000') {
  48. // 抛出错误信息,页面处理
  49. return Promise.reject(response.data)
  50. }
  51. // 返回正确数据
  52. return Promise.resolve(response)
  53. // return response
  54. }, handleError)
  55. // 后端返回数据结构泛型,根据实际项目调整
  56. interface ResponseData<T = unknown> {
  57. code: string
  58. message: string
  59. result: T
  60. }
  61. export const httpGet = async <T, D = any>(url: string, config?: AxiosRequestConfig<D>) => {
  62. return service.get<ResponseData<T>>(url, config).then((res) => res.data)
  63. }
  64. export const httpPost = async <T, D = any>(
  65. url: string,
  66. data?: D,
  67. config?: AxiosRequestConfig<D>,
  68. ) => {
  69. return service.post<ResponseData<T>>(url, data, config).then((res) => res.data)
  70. }
  71. export { service as axios }
  72. export type { ResponseData }

?useRequest.ts封装:基于vue3 Composition API,将请求参数、状态以及结果等逻辑封装复用

  1. import { ref } from 'vue'
  2. import type { Ref } from 'vue'
  3. import { ElMessage } from 'element-plus'
  4. import type { ResponseData } from '@/utils/request'
  5. export const useRequest = <T, P = any>(
  6. api: (...args: P[]) => Promise<ResponseData<T>>,
  7. defaultParams?: P,
  8. ) => {
  9. const params = ref<P>() as Ref<P>
  10. if (defaultParams) {
  11. params.value = {
  12. ...defaultParams,
  13. }
  14. }
  15. const loading = ref(false)
  16. const result = ref<T>()
  17. const fetchResource = async (...args: P[]) => {
  18. loading.value = true
  19. return api(...args)
  20. .then((res) => {
  21. if (!res?.result) return
  22. result.value = res.result
  23. })
  24. .catch((err) => {
  25. result.value = undefined
  26. ElMessage({
  27. message: typeof err === 'string' ? err : err?.message || 'error',
  28. type: 'error',
  29. offset: 80,
  30. })
  31. })
  32. .finally(() => {
  33. loading.value = false
  34. })
  35. }
  36. return {
  37. params,
  38. loading,
  39. result,
  40. fetchResource,
  41. }
  42. }

?API接口层

  1. import { httpGet } from '@/utils/request'
  2. const API = {
  3. getLoginUserInfo: '/userInfo/getLoginUserInfo',
  4. }
  5. type UserInfo = {
  6. userName: string
  7. realName: string
  8. }
  9. export const getLoginUserInfoAPI = () => httpGet<UserInfo>(API.getLoginUserInfo)

?页面使用:接口返回结果userInfo,可以自动推断出UserInfo类型,

  1. // 方式一:推荐
  2. const {
  3. loading,
  4. result: userInfo,
  5. fetchResource: getLoginUserInfo,
  6. } = useRequest(getLoginUserInfoAPI)
  7. // 方式二:不推荐,每次使用接口时都需要重复定义type
  8. type UserInfo = {
  9. userName: string
  10. realName: string
  11. }
  12. const {
  13. loading,
  14. result: userInfo,
  15. fetchResource: getLoginUserInfo,
  16. } = useRequest<UserInfo>(getLoginUserInfoAPI)
  17. onMounted(async () => {
  18. await getLoginUserInfo()
  19. if (!userInfo.value) return
  20. const user = useUserStore()
  21. user.$patch({
  22. userName: userInfo.value.userName,
  23. realName: userInfo.value.realName,
  24. })
  25. })

3.Mockjs模拟后端接口返回数据

  1. import Mock from 'mockjs'
  2. const BASE_URL = '/api'
  3. Mock.mock(`${BASE_URL}/user/list`, {
  4. code: '00000',
  5. message: '成功',
  6. 'result|10-20': [
  7. {
  8. uuid: '@guid',
  9. name: '@name',
  10. tag: '@title',
  11. age: '@integer(18, 35)',
  12. modifiedTime: '@datetime',
  13. status: '@cword("01")',
  14. },
  15. ],
  16. })

四、统一规范

1.ESLint

注意:不同框架下,所需要的preset或plugin不同,建议将公共部分提取并配置在根目录中,package中的eslint配置设置extends。

  1. /* eslint-env node */
  2. require('@rushstack/eslint-patch/modern-module-resolution')
  3. module.exports = {
  4. root: true,
  5. extends: [
  6. 'plugin:vue/vue3-essential',
  7. 'eslint:recommended',
  8. '@vue/eslint-config-typescript',
  9. '@vue/eslint-config-prettier',
  10. ],
  11. overrides: [
  12. {
  13. files: ['cypress/e2e/**.{cy,spec}.{js,ts,jsx,tsx}'],
  14. extends: ['plugin:cypress/recommended'],
  15. },
  16. ],
  17. parserOptions: {
  18. ecmaVersion: 'latest',
  19. },
  20. rules: {
  21. 'vue/no-deprecated-slot-attribute': 'off',
  22. },
  23. }

2.StyleLint

  1. module.exports = {
  2. extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
  3. plugins: ['stylelint-order'],
  4. customSyntax: 'postcss-html',
  5. rules: {
  6. indentation: 2, //4空格
  7. 'selector-class-pattern':
  8. '^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:\[.+\])?$',
  9. // at-rule-no-unknown: 屏蔽一些scss等语法检查
  10. 'at-rule-no-unknown': [true, { ignoreAtRules: ['mixin', 'extend', 'content', 'export'] }],
  11. // css-next :global
  12. 'selector-pseudo-class-no-unknown': [
  13. true,
  14. {
  15. ignorePseudoClasses: ['global', 'deep'],
  16. },
  17. ],
  18. 'order/order': ['custom-properties', 'declarations'],
  19. 'order/properties-alphabetical-order': true,
  20. },
  21. }

3.Prettier

  1. module.exports = {
  2. printWidth: 100,
  3. singleQuote: true,
  4. trailingComma: 'all',
  5. bracketSpacing: true,
  6. jsxBracketSameLine: false,
  7. tabWidth: 2,
  8. semi: false,
  9. }

4.CommitLint

  1. module.exports = {
  2. extends: ['@commitlint/config-conventional'],
  3. rules: {
  4. 'type-enum': [
  5. 2,
  6. 'always',
  7. ['build', 'feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert'],
  8. ],
  9. 'subject-full-stop': [0, 'never'],
  10. 'subject-case': [0, 'never'],
  11. },
  12. }

五、附录:技术栈图谱

原文链接:https://www.cnblogs.com/Jcloud/p/17439817.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号