1. 前端工程化常用工具总结

210 阅读7分钟

1. 代码规范

1.1 vscode集成editorconfig

安装editorconfig插件后,在项目根目录下生成如下配置

.editorconfig

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

1.2 使用prettier格式化工具

Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。

  1. 安装prettier -D 就是npm install --save-dev 表示改依赖只在开发环境中
npm install prettier -D
  1. prettier配置文件.prettierrc
  • useTabs:使用tab缩进还是空格缩进,选择false;
  • tabWidth:tab是空格的情况下,是几个空格,选择2个;
  • printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
  • singleQuote:使用单引号还是双引号,选择true,使用单引号;
  • trailingComma:在多行输入的尾逗号是否添加,设置为 none
  • semi:语句末尾是否要加分号,默认值true,选择false表示不加;

.prettierrc

{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "none",
  "semi": false
}
  1. prettier忽略文件.prettierignore
/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*
  1. package.json中添加格式化所有文件的脚本
"prettier": "prettier --write ."

1.3 使用ESLint检测

1.3.1 安装配置ESLint

  1. 如果使用vue cli创建项目的时候选择了ESLint,则vue会默认配置好ESLint所需的环境
  2. vscode安装ESLint插件
  3. 解决ESLintprettier之间的冲突 vue cli创建的项目中,ESLint的规范是vue团队的,如果我们想要用自己的ESLint配置,则会和他们的规范冲突,这样一来prettier格式化后就会和ESLint的不一致,为了解决这个问题,需要安装如下两个插件 这两个插件如果在vue cli创建项目时选择了ESLint + prettier,则会默认帮我们装上的 将插件添加到.eslintrc.js 即在最后一行加上'plugin:prettier/recommended'即可
npm i eslint-plugin-prettier eslint-config-prettier -D
extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/typescript/recommended',
    '@vue/prettier',
    '@vue/prettier/@typescript-eslint',
    'plugin:prettier/recommended'
],

1.3.2 忽略ESLint警告

有时候会遇到一些警告,但如果我们能够明确是没有问题的警告的话,可以选择将其忽略,在ESLint的配置文件.eslintrc中配置。 vscode中将鼠标悬停在提示的代码处,会弹出对应的ESLint提示项,比如遇到一个提示为@typescript-eslint/no-var-requires,将它复制下来,打开.eslintrc.js,在rules中添加该配置项,并且值设为off即可关闭

rules: {
  '@typescript-eslint/no-var-requires': 'off'
}

注意:添加了忽略项后,最好重新启动一下开发环境服务器,因为热更新对配置修改是无效的。


1.4 git Husky保证提交代码的规范

虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:

  • 也就是我们希望保证代码仓库中的代码都是符合eslint规范的;
  • 那么我们需要在组员执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;

husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push

这里我们可以使用自动配置命令:

npx husky-init && npm install

该命令会做三件事:

  1. 添加husky项目依赖到package.json中的devDependencies
  2. 在项目目录下创建 .husky 文件夹,该文件夹中存放hook配置,也可以手动执行下面的命令进行创建
npx huksy install
  1. package.json中添加一个脚本
"prepare": "husky install"

接下来,我们需要去完成一个操作:在进行commit时,执行package.json中的lint脚本,这时候就需要修改hook配置了

打开.hucky中的pre-commit配置文件,将原本的npm test改成npm run lint即可


1.5 git commit规范

  • Commitizen用于编写规范的commit message
  • commitlint用于检查提交的信息是否符合规范,用于避免提交的时候是直接git commit -m "xxx",而不是通过Commitizen时的情况

1.5.1 commitizen

  1. 安装commitizen
npm install commitizen -D
  1. 安装cz-conventional-changelog,并且初始化cz-conventional-changelog 该命令会安装cz-conventional-changelog并在package.json中进行配置
npx commitizen init cz-conventional-changelog --save-dev --save-exact
  1. 现在提交代码就可以使用npx cz提交,提交的message就是规范的了,还可以在package.json中添加脚本来提交,这样就能用npm run commit提交代码了
"scripts": {
  "commit": "cz"
}

提交代码的类型

Type作用
feat新增特性 (feature)
fix修复 Bug(bug fix)
docs修改文档 (documentation)
style代码格式修改(white-space, formatting, missing semi colons, etc)
refactor代码重构(refactor)
perf改善性能(A code change that improves performance)
test测试(when adding missing tests)
build变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)
ci更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
chore变更构建流程或辅助工具(比如更改测试环境)
revert代码回退

1.5.2 commitlint

  1. 安装 @commitlint/config-conventional 和 @commitlint/cli
npm i @commitlint/config-conventional @commitlint/cli -D
  1. 在根目录创建commitlint.config.js文件,配置commitlint
module.exports = {
  extends: ['@commitlint/config-conventional']
}
  1. 使用husky生成commit-msg文件,验证提交信息:
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

2. 第三方集成

2.1 vue.config.js配置

vue.config.js有三种配置方式:

  • 方式一:直接通过CLI提供给我们的选项来配置:
    • 比如publicPath:配置应用程序部署的子目录(默认是 /,相当于部署在 https://www.my-app.com/);
    • 比如outputDir:修改输出的文件夹;
  • 方式二:通过configureWebpack修改webpack的配置:
    • 可以是一个对象,直接会被合并;
    • 可以是一个函数,会接收一个config,可以通过config来修改配置;
  • 方式三:通过chainWebpack修改webpack的配置:
    • 是一个函数,会接收一个基于  webpack-chain 的config对象,可以对配置进行修改;

示例

const path = require('path')

module.exports = {
  // 配置方式一
  outputDir: './build',
  
  // 配置方式二:对象形式
  configureWebpack: {
    resolve: {
      alias: {
        views: '@/views'
      }
    }
  }
  
  // 配置方式三:函数形式
  configureWebpack: (config) => {
    config.resolve.alias = {
      '@': path.resolve(__dirname, 'src'),
      views: '@/views'
    }
  },
      
  // 配置方式四:链式调用形式
  chainWebpack: (config) => {
    config.resolve.alias.set('@', path.resolve(__dirname, 'src')).set('views', '@/views')
  }
}

2.2 vue-router集成

  1. 安装vue-router
npm install vue-router@4
  1. vue-router入口文件
import { createRouter, createWebHashHistory } from 'vue-router'
import { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/main'
  },
  {
    path: '/main',
    component: () => import('@/views/main/main.vue')
  },
  {
    path: '/login',
    component: () => import('@/views/login/login.vue')
  }
]

const router = createRouter({
  routes,
  history: createWebHashHistory()
})

export default router
  1. main.ts中注册 main.ts
import { createApp } from 'vue'
import App from './App.vue'

import router from '@/router'

const app = createApp(App)
app.use(router)
app.mount('#app')
  1. App.vue中配置路由跳转
<template>
  <div class="app">
    <router-link to="/login">登录</router-link>
    <router-link to="/main">首页</router-link>
    <router-view></router-view>
  </div>
</template>

2.3 vuex集成

2.3.1 安装vuex

npm install vuex@next --save

2.3.2 声明state类型和$store

  1. 创建**src/store/type.ts**声明项目中的**state**的类型
export declare interface IState {
  login: ILoginState
}
  1. 创建src/store/vuex.d.ts声明$store的类型
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
import { IState } from 'store'

declare module '@vue/runtime-core' {
  // 为 `this.$store` 提供类型声明
  interface ComponentCustomProperties {
    $store: Store<IState>
  }
}

2.3.3 useStore中注入类型

src/store/type.ts中定义injection key的类型

import { InjectionKey } from 'vue'
import { Store } from 'vuex'

export declare interface IState {
  login: ILoginState
}

// 定义 injection key
export declare const key: InjectionKey<Store<IState>> = Symbol()

2.3.4 vuex入口文件

import { createStore, useStore as baseUseStore } from 'vuex'

import loginModule from './login'
import { IState, key } from './type'

const store = createStore<IState>({
  modules: { login: loginModule }
})

export function useStore() {
  return baseUseStore(key)
}

export default store

这里自己封装了一次useStore,传入了前面定义的key,这样外面调用的时候直接调用useStore()就可以获得类型声明的store对象了


2.3.5 main.ts中注册store

注册时把定义好的key也传入,就可以将store类型化,在使用的时候获得类型提示

import store from './store'
import { key } from './store/type'

app.use(store, key)

2.4 element-plus集成

  1. 安装element-plus
npm install element-plus --save
  1. 引入element-plus

2.4.1 完整引入

// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

2.4.2按需引入

  1. 安装两个插件:unplugin-vue-componentsunplugin-auto-import
npm install -D unplugin-vue-components unplugin-auto-import
  1. 修改vue.config.js中的Webpack配置
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ]
  }
}
  1. App.vue中直接使用
<el-button>按钮</el-button>

2.5 axios集成

2.5.1 安装axios

npm install axios

2.5.2 axios配置项文件

该文件用于存放一些axios用到的配置项,如BASE_URL

/**
 * 生产环境 -- production
 * 开发环境 -- development
 * 测试环境 -- test
 */

let BASE_URL = ''
const TIME_OUT = 10000

switch (process.env.NODE_ENV) {
  case 'development':
    BASE_URL = 'http://123.207.32.32:8000'
    break
  case 'production':
    BASE_URL = 'https://www.baidu.com/'
    break
  case 'test':
    BASE_URL = 'https://www.baidu.com/'
    break
}

export { BASE_URL, TIME_OUT }

2.5.3 封装AxiosInstance

封装AxiosInstance实例对象,主要是添加对各种拦截器的支持,拦截器的粒度细致到以下三个阶段:

  1. 全局请求响应拦截,对所有的请求都生效
  2. 实例请求响应拦截,针对不同的实例可以设置不同的请求响应拦截
  3. 单独请求响应拦截,针对具体接口设置相应的请求响应拦截

要实现上述拦截器,需要自己封装一个拦截器类型接口,分别对应请求成功处理、请求失败处理、响应成功处理、响应失败处理

因此再创建一个文件,用于存放用到的接口类型

import { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface WFRequestInterceptors<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

/**
 * T 是接口返回的数据的类型
 * D 是请求时携带的 data 类型,比如 post 请求的 data
 */
export interface WFRequestConfig<T = AxiosResponse, D = any>
  extends AxiosRequestConfig<D> {
  interceptors?: WFRequestInterceptors<T>
}

创建一个ts文件专门用于存放全局拦截器 注意:由于全局拦截器被抽离到单独的文件中了,this需要显式绑定才能正常工作,因为我们是希望this指向**WFRequest**实例的,如果是js的话其实在函数中直接用this是没问题的,但是如果是ts的话,需要在函数的第一个参数中显式指明this会指向谁,否则会报错。

import { AxiosResponse } from 'axios'
import WFRequest from '.'
import { WFRequestConfig } from './type'

/**
 * 全局请求拦截器
 */
function requestInterceptor(
  this: WFRequest,
  config: WFRequestConfig
): WFRequestConfig {
  console.log('全局拦截器 -- 请求拦截器')
  return config
}

/**
 * 全局请求异常拦截器
 */
function requestInterceptorCatch(this: WFRequest, error: any): any {
  console.log('全局拦截器 -- 请求异常拦截器')
  return error
}

/**
 * 全局响应拦截器
 */
function responseInterceptor<T extends AxiosResponse>(
  this: WFRequest,
  res: T
): T {
  console.log('全局拦截器 -- 响应拦截器', this)
  // 从 res 中提出 data 返回 因为 data 才是前端真正需要的,其他的东西是 axios 自己封装的 基本用不到
  const data = res.data

  return data
}

/**
 * 全局响应异常拦截器
 */
function responseInterceptorCatch(this: WFRequest, error: any): any {
  console.log('全局拦截器 -- 响应异常拦截器', this)
  return error
}

export default {
  requestInterceptor,
  requestInterceptorCatch,
  responseInterceptor,
  responseInterceptorCatch
}

创建一个类用于封装AxiosInstance实例,这个类存放在 src/service/request/index.ts中并默认导出

import axios, { AxiosInstance, AxiosResponse } from 'axios'

import globalInterceptors from './global-interceptors'
import { WFRequestConfig, WFRequestInterceptors } from './type'

// T 是响应体的数据类型
class WFRequest<T = any> {
  instance: AxiosInstance
  interceptors?: WFRequestInterceptors

  constructor(config: WFRequestConfig<AxiosResponse<T>>) {
    // 1. 创建 axios 实例
    this.instance = axios.create(config)

    // 2. 保存基本信息
    this.interceptors = config.interceptors

    // 3. 注册从 config 中取得的属于实例的拦截器

    // 注册 请求拦截器 和 请求异常拦截器
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )

    // 注册 响应拦截器 和 响应异常拦截器
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )

    // 4. 注册全局拦截器

    // 注册全局 请求拦截器 和 请求异常拦截器
    this.instance.interceptors.request.use(
      globalInterceptors.requestInterceptor.bind(this),
      globalInterceptors.requestInterceptorCatch.bind(this)
    )

    // 注册全局 响应拦截器 和 响应异常拦截器
    this.instance.interceptors.response.use(
      globalInterceptors.responseInterceptor.bind(this),
      globalInterceptors.responseInterceptorCatch.bind(this)
    )
  }

  request<T, D = any>(config: WFRequestConfig<T, D>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 1. 如果单个请求中配置了请求拦截器则优先执行它的请求拦截器
      if (config.interceptors?.requestInterceptor) {
        config = config.interceptors.requestInterceptor(config)
      }

      // 2. 调用 axios 实例的 request 发送请求
      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 3. 如果单个请求中配置了响应拦截器则优先执行它的响应拦截器
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }

          resolve(res)
        })
        .catch((err) => {
          reject(err)

          return err
        })
    })
  }

  get<T, D = any>(config: WFRequestConfig<T, D>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T, D = any>(config: WFRequestConfig<T, D>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  put<T, D = any>(config: WFRequestConfig<T, D>): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT' })
  }

  patch<T, D = any>(config: WFRequestConfig<T, D>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }

  delete<T, D = any>(config: WFRequestConfig<T, D>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }
}

export default WFRequest

注意:为了保证能够访问到**WFRequest**实例,**this**应当指向该实例,因此注册全局拦截器的时候要用**bind**显式绑定**this**


2.5.3.1使用泛型T的原因

这里使用到的泛型T,意思是在调用request方法后返回的对象类型是由axios的AxiosResponse封装好的T,即调用返回对象的data属性拿到的就是T类型的对象,这点可以通过源码验证:

request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;

而这里调用request时,传入的泛型为request<T, D = any>。再将这两个泛型传递给WFRequestConfig,T是返回的数据的类型,D是请求体的数据的类型,这样就能够获得类型提示了。这样子做的好处在后面的体验一节中能够感受到


2.5.3.2 service中实例化封装好的类

实例化一个WFRequest的对象,并将其导出以供使用

import WFRequest from './request'
import { BASE_URL, TIME_OUT } from './request/config'

const wfRequest = new WFRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  interceptors: {
    requestInterceptor: (config) => {
      // 给该实例发起的所有请求携带上 token
      const token = 'temp_token'
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }
      console.log('单个实例请求成功的拦截')

      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('单个实例请求失败的拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('响应成功的拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('响应失败的拦截')
      return err
    }
  }
})

export default wfRequest

2.5.3.3 体验

在项目的main.ts中使用体验一下,比如现在我们有一个登录接口,根据Apifox接口文档的信息可以知道请求的参数类型和响应的数据类型 image.png 那么就可以用tsinterface特性,定义请求体的接口类型以及响应体的类型

interface IAccountLoginRequestData {
  username: string
  password: string
}
interface IAccountLoginResponseData {
  id: number
  name: string
  token: string
}

interface IDataType<T> {
  code: number
  message: string
  data: T
}
wfRequest.post<IDataType<IAccountLoginResponseData>, IAccountLoginRequestData>({
  url: '/login',
  data: {
    username: 'admin',
    password: '123456'
  },
  interceptors: {
    requestInterceptor: (config) => {
      console.log('单个请求拦截器 -- 请求拦截器')

      return config
    },
    responseInterceptor: (res) => {
      console.log('单个请求拦截器 -- 响应拦截器')
      console.log('请求结果 -- ', res)

      return res
    }
  }
})

image.png

image.png


3. 遇到的问题

3.1 实例请求拦截器添加token有问题

起初我给请求携带token的时候,是这样添加的

requestInterceptor: (config) => {
  console.log('实例拦截器 -- 请求拦截器')
  // 1. 实例请求拦截器 -- 发送请求的时候带上 token
  const token = 'test token'
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }

  return config
}

于是遇到了ts的如下报错: image.png 点进源码发现,AxiosRequestConfig中的headers是一个可选属性,于是我就改成了config?.headers.Authorization = token,但是又遇到了新的报错: image.png 然后我就仔细看了一下headers的类型,是AxiosRequestHeaders类型

export type AxiosRequestHeaders = Record<string, string | number | boolean>;

点进源码后发现是Record类型,再次点进去看看Record的类型

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

那很明显,这个类型就是用来表示一个对象的属性的keyvalue应当是什么类型的,由AxiosRequestHeaders的类型声明可以知道,keystring类型,valuestringnumber或者boolean,那就简单了,我直接显式给headers指定一个对象,然后在里面写keyvalue不就行了吗?

requestInterceptor: (config) => {
  console.log('实例拦截器 -- 请求拦截器')
  // 1. 实例请求拦截器 -- 发送请求的时候带上 token
  const token = 'test token'
  if (token) {
    config.headers = {
      Authorization: `Bearer ${token}`
    }
  }

  return config
}

3.1 封装axios时,ElLoading样式有问题

image.png 因为在src/service/request/index.ts中,使用ElLoading时,是手动导入的,因此需要按照element-plus官方文档的手动导入方式去安装相应插件,让它在导入组件的同时自动导入相关样式,阅读element-plus官方文档可以发现 image.png 只要安装unplugin-element-plus插件即可

npm i unplugin-element-plus -D

然后修改vue.config.js中的webpack配置

const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      }),
      require('unplugin-element-plus/webpack')({}) // 自动导入 element-plus 组件样式
    ]
  }
}

4. 小结

  1. 使用prettiereslint工具使编码规范化
  2. 使用commitizen规范化git commit信息
  3. 使用git husky搭配commitlint防止直接git commit提交不规范的信息
  4. 使用类封装AxiosInstance,并使用一个子类继承AxiosRequestConfig,实现对全局拦截器、实例拦截器、单个请求拦截器的封装
  5. 灵活抽离全局拦截器到单独的文件中导出,并在注册时使用JS的bind特性绑定this,使全局拦截器即使不在类的内部编写也能正常使用
  6. 遇到问题时能够查看源码找到原因,并自主思考解决方案