Best practice: front-end engineering construction of monorepo based on vite3 | JD Cloud technical team

1. Technology stack selection

1. Code base management method - Monorepo: store multiple projects in the same code base

▪ Reason for choosing 1: Multiple applications (which can be divided according to the granularity of business line products) are managed in the same repo, which is convenient for unified management of code specifications and shared workflow

▪ Reason for choosing 2: Solve the code reuse at the physical level between cross-projects/applications, without publishing/installing npm packages to solve the sharing problem

2. Dependency management - PNPM: Eliminate dependency promotion, standardize topology

▪Reason 1: Save disk space to the greatest extent through soft/hard links

▪ Reason for choosing 2: Solve the problem of ghost dependency and make management clearer

3. Construction tool-Vite: a construction tool based on ESM and Rollup

▪Reason for choosing: Eliminate the compilation process during local development and improve the efficiency of local development

4. Front-end framework-Vue3: Composition API

▪ Reason for selection: In addition to component reuse, some common logic states can also be reused, such as the logic of request interface loading and results

5. Mock interface returns data - Mockjs

▪Reason for choosing: After the front-end and back-end data structures are unified, development can be separated, reducing front-end development dependencies and shortening the development cycle

2. Directory structure design: focus on the src part

1. Regular/simple mode: centralized management according to file function type

```
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-based mode: centralized management according to business modules

```
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
```

You can choose one of the above two methods according to the specific business scenario.

3. Construction details

1. Monorepo+PNPM centrally manages multiple applications (workspace)

▪Create pnpm-workspace.yaml in the root directory, and each application under the mesh-client folder is a package, and local dependencies can be added to each other: pnpm install

packages:
  # all packages in direct subdirs of packages/
  - 'mesh-client/*'
  # exclude packages that are inside test directories
  - '!**/test/**'

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

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

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

▪ Some differences from NPM install:

▪All dependencies will be installed to the root directory node_modules/.pnpm;

▪The packages.json in the package will not display ghost dependencies (such as tslib@types/webpack-dev), and need to be explicitly installed, otherwise an error will be reported

▪The installed package will first be searched from the current workspace, and if it exists, node_modules will create a soft connection pointing to the local workspace

▪"mock": “workspace:^1.0.0”

2. Vue3 request interface related encapsulation

▪request.ts encapsulation: mainly to intercept interface requests and returns, rewrite get/post methods to support generics

import axios, { AxiosError } from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_URL,
  timeout: 1000 * 60 * 5, // 请求超时时间
  headers: { 'Content-Type': 'application/json;charset=UTF-8' },
})

const toLogin = (sso: string) => {
  const cur = window.location.href
  const url = `${sso}${encodeURIComponent(cur)}`
  window.location.href = url
}

// 服务器状态码错误处理
const handleError = (error: AxiosError) => {
  if (error.response) {
    switch (error.response.status) {
      case 401:
        // todo
        toLogin(import.meta.env.VITE_APP_SSO)
        break
      // case 404:
      //   router.push('/404')
      //   break
      // case 500:
      //   router.push('/500')
      //   break
      default:
        break
    }
  }
  return Promise.reject(error)
}

// request interceptor
service.interceptors.request.use((config) => {
  const token = ''
  if (token) {
    config.headers!['Access-Token'] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
  }
  return config
}, handleError)

// response interceptor
service.interceptors.response.use((response: AxiosResponse<ResponseData>) => {
  const { code } = response.data
  if (code === '10000') {
    toLogin(import.meta.env.VITE_APP_SSO)
  } else if (code !== '00000') {
    // 抛出错误信息,页面处理
    return Promise.reject(response.data)
  }
  // 返回正确数据
  return Promise.resolve(response)
  // return response
}, handleError)

// 后端返回数据结构泛型,根据实际项目调整
interface ResponseData<T = unknown> {
  code: string
  message: string
  result: T
}

export const httpGet = async <T, D = any>(url: string, config?: AxiosRequestConfig<D>) => {
  return service.get<ResponseData<T>>(url, config).then((res) => res.data)
}

export const httpPost = async <T, D = any>(
  url: string,
  data?: D,
  config?: AxiosRequestConfig<D>,
) => {
  return service.post<ResponseData<T>>(url, data, config).then((res) => res.data)
}

export { service as axios }

export type { ResponseData }

▪useRequest.ts encapsulation: Based on vue3 Composition API, logic encapsulation and reuse of request parameters, status and results

import { ref } from 'vue'
import type { Ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { ResponseData } from '@/utils/request'
export const useRequest = <T, P = any>(
  api: (...args: P[]) => Promise<ResponseData<T>>,
  defaultParams?: P,
) => {
  const params = ref<P>() as Ref<P>
  if (defaultParams) {
    params.value = {
      ...defaultParams,
    }
  }
  const loading = ref(false)
  const result = ref<T>()
  const fetchResource = async (...args: P[]) => {
    loading.value = true
    return api(...args)
      .then((res) => {
        if (!res?.result) return
        result.value = res.result
      })
      .catch((err) => {
        result.value = undefined
        ElMessage({
          message: typeof err === 'string' ? err : err?.message || 'error',
          type: 'error',
          offset: 80,
        })
      })
      .finally(() => {
        loading.value = false
      })
  }
  return {
    params,
    loading,
    result,
    fetchResource,
  }
}

▪API interface layer

import { httpGet } from '@/utils/request'

const API = {
  getLoginUserInfo: '/userInfo/getLoginUserInfo',
}
type UserInfo = {
  userName: string
  realName: string
}
export const getLoginUserInfoAPI = () => httpGet<UserInfo>(API.getLoginUserInfo)

▪Page use: the interface returns the result userInfo, which can automatically infer the UserInfo type,

// 方式一:推荐
const {
  loading,
  result: userInfo,
  fetchResource: getLoginUserInfo,
} = useRequest(getLoginUserInfoAPI)

// 方式二:不推荐,每次使用接口时都需要重复定义type
type UserInfo = {
  userName: string
  realName: string
}
const {
  loading,
  result: userInfo,
  fetchResource: getLoginUserInfo,
} = useRequest<UserInfo>(getLoginUserInfoAPI)

onMounted(async () => {
  await getLoginUserInfo()
  if (!userInfo.value) return
  const user = useUserStore()
  user.$patch({
    userName: userInfo.value.userName,
    realName: userInfo.value.realName,
  })
})

3. Mockjs simulates the back-end interface to return data

import Mock from 'mockjs'
const BASE_URL = '/api'
Mock.mock(`${BASE_URL}/user/list`, {
  code: '00000',
  message: '成功',
  'result|10-20': [
    {
      uuid: '@guid',
      name: '@name',
      tag: '@title',
      age: '@integer(18, 35)',
      modifiedTime: '@datetime',
      status: '@cword("01")',
    },
  ],
})

4. Unified specification

1. ESLint

Note: Different frameworks require different presets or plugins. It is recommended to extract and configure the common parts in the root directory, and set the eslint configuration in the package to extends.

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier',
  ],
  overrides: [
    {
      files: ['cypress/e2e/**.{cy,spec}.{js,ts,jsx,tsx}'],
      extends: ['plugin:cypress/recommended'],
    },
  ],
  parserOptions: {
    ecmaVersion: 'latest',
  },
  rules: {
    'vue/no-deprecated-slot-attribute': 'off',
  },
}

2.StyleLint

module.exports = {
  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
  plugins: ['stylelint-order'],
  customSyntax: 'postcss-html',
  rules: {
    indentation: 2, //4空格
    'selector-class-pattern':
      '^(?:(?: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]+)*)?(?:\[.+\])?$',
    // at-rule-no-unknown: 屏蔽一些scss等语法检查
    'at-rule-no-unknown': [true, { ignoreAtRules: ['mixin', 'extend', 'content', 'export'] }],
    // css-next :global
    'selector-pseudo-class-no-unknown': [
      true,
      {
        ignorePseudoClasses: ['global', 'deep'],
      },
    ],
    'order/order': ['custom-properties', 'declarations'],
    'order/properties-alphabetical-order': true,
  },
}

3.Prettier

module.exports = {
  printWidth: 100,
  singleQuote: true,
  trailingComma: 'all',
  bracketSpacing: true,
  jsxBracketSameLine: false,
  tabWidth: 2,
  semi: false,
}

4.CommitLint

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['build', 'feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert'],
    ],
    'subject-full-stop': [0, 'never'],
    'subject-case': [0, 'never'],
  },
}

5. Appendix : Technology Stack Map

Author: JD Technology Niu Zhiwei

Source: JD Cloud Developer Community

Guess you like

Origin blog.csdn.net/JDDTechTalk/article/details/130924850