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 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。
- 安装prettier -D 就是npm install --save-dev 表示改依赖只在开发环境中
npm install prettier -D
- 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
}
- prettier忽略文件
.prettierignore
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
- 在
package.json
中添加格式化所有文件的脚本
"prettier": "prettier --write ."
1.3 使用ESLint检测
1.3.1 安装配置ESLint
- 如果使用
vue cli
创建项目的时候选择了ESLint
,则vue会默认配置好ESLint
所需的环境 - vscode安装
ESLint
插件 - 解决
ESLint
和prettier
之间的冲突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
该命令会做三件事:
- 添加
husky
项目依赖到package.json
中的devDependencies
中 - 在项目目录下创建
.husky
文件夹,该文件夹中存放hook配置,也可以手动执行下面的命令进行创建
npx huksy install
- 在
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
- 安装
commitizen
npm install commitizen -D
- 安装
cz-conventional-changelog
,并且初始化cz-conventional-changelog
该命令会安装cz-conventional-changelog
并在package.json
中进行配置
npx commitizen init cz-conventional-changelog --save-dev --save-exact
- 现在提交代码就可以使用
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
- 安装 @commitlint/config-conventional 和 @commitlint/cli
npm i @commitlint/config-conventional @commitlint/cli -D
- 在根目录创建commitlint.config.js文件,配置commitlint
module.exports = {
extends: ['@commitlint/config-conventional']
}
- 使用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集成
- 安装
vue-router
npm install vue-router@4
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
- 在
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')
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
- 创建
**src/store/type.ts**
声明项目中的**state**
的类型
export declare interface IState {
login: ILoginState
}
- 创建
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集成
- 安装
element-plus
npm install element-plus --save
- 引入
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按需引入
- 安装两个插件:
unplugin-vue-components
和unplugin-auto-import
npm install -D unplugin-vue-components unplugin-auto-import
- 修改
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()]
})
]
}
}
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
实例对象,主要是添加对各种拦截器的支持,拦截器的粒度细致到以下三个阶段:
- 全局请求响应拦截,对所有的请求都生效
- 实例请求响应拦截,针对不同的实例可以设置不同的请求响应拦截
- 单独请求响应拦截,针对具体接口设置相应的请求响应拦截
要实现上述拦截器,需要自己封装一个拦截器类型接口,分别对应请求成功处理、请求失败处理、响应成功处理、响应失败处理
因此再创建一个文件,用于存放用到的接口类型
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
接口文档的信息可以知道请求的参数类型和响应的数据类型
那么就可以用
ts
的interface
特性,定义请求体的接口类型以及响应体的类型
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
}
}
})
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
的如下报错:
点进源码发现,
AxiosRequestConfig
中的headers
是一个可选属性,于是我就改成了config?.headers.Authorization = token
,但是又遇到了新的报错:
然后我就仔细看了一下
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;
};
那很明显,这个类型就是用来表示一个对象的属性的key
和value
应当是什么类型的,由AxiosRequestHeaders
的类型声明可以知道,key
是string
类型,value
是string
、number
或者boolean
,那就简单了,我直接显式给headers
指定一个对象,然后在里面写key
和value
不就行了吗?
requestInterceptor: (config) => {
console.log('实例拦截器 -- 请求拦截器')
// 1. 实例请求拦截器 -- 发送请求的时候带上 token
const token = 'test token'
if (token) {
config.headers = {
Authorization: `Bearer ${token}`
}
}
return config
}
3.1 封装axios时,ElLoading样式有问题
因为在
src/service/request/index.ts
中,使用ElLoading
时,是手动导入的,因此需要按照element-plus官方文档的手动导入方式去安装相应插件,让它在导入组件的同时自动导入相关样式,阅读element-plus官方文档可以发现
只要安装
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. 小结
- 使用
prettier
和eslint
工具使编码规范化 - 使用
commitizen
规范化git commit
信息 - 使用
git husky
搭配commitlint
防止直接git commit
提交不规范的信息 - 使用类封装
AxiosInstance
,并使用一个子类继承AxiosRequestConfig
,实现对全局拦截器、实例拦截器、单个请求拦截器的封装 - 灵活抽离全局拦截器到单独的文件中导出,并在注册时使用JS的
bind
特性绑定this
,使全局拦截器即使不在类的内部编写也能正常使用 - 遇到问题时能够查看源码找到原因,并自主思考解决方案