一、代码规范
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
npm install prettier -D
-D 就是npm install --save-dev 表示改依赖只在开发环境中
-
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检测
如果使用
vue cli
创建项目的时候选择了ESLint
,则vue会默认配置好ESLint
所需的环境vscode安装
ESLint
插件-
解决
ESLint
和prettier
之间的冲突vue cli
创建的项目中,ESLint
的规范是vue
团队的,如果我们想要用自己的ESLint
配置,则会和他们的规范冲突,这样一来prettier
格式化后就会和ESLint
的不一致,为了解决这个问题,需要安装如下两个插件npm i eslint-plugin-prettier eslint-config-prettier -D
这两个插件如果在
vue cli
创建项目时选择了ESLint + prettier
,则会默认帮我们装上的将插件添加到.eslintrc.js
extends: [ 'plugin:vue/vue3-essential', 'eslint:recommended', '@vue/typescript/recommended', '@vue/prettier', '@vue/prettier/@typescript-eslint', 'plugin:prettier/recommended' ],
即在最后一行加上
'plugin:prettier/recommended'
即可
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
npx commitizen init cz-conventional-changelog --save-dev --save-exact
该命令会安装
cz-conventional-changelog
并在package.json
中进行配置 -
现在提交代码就可以使用
npx cz
提交,提交的message
就是规范的了-
type
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.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')
}
}
遇到明确没问题的ESLint提示
比如这里的const path = require('path')
,ESLint会提示使用ES风格的import替代,但是由于是给node用的配置文件,必须是commonJS风格的,这个时候我们需要把这一条ESLint提示禁用掉
vscode中将鼠标悬停在提示的代码处,会弹出对应的ESLint提示项,比如这里的提示就是@typescript-eslint/no-var-requires
将它复制下来,打开.eslintrc.js
,在rules
中添加该配置项,并且值设为off
即可关闭
rules: {
'@typescript-eslint/no-var-requires': 'off'
}
2.2 vue-router集成
-
安装
vue-router
npm install vue-router@4
-
创建文件 --
src/router/index.ts
index.ts
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集成
-
安装vuex
npm install vuex@next --save
-
创建文件 --
src/store/index.ts
index.ts
import { createStore } from 'vuex' const store = createStore({ state() { return { name: 'plasticine' } } }) export default store
-
在
main.ts
中注册app.use(store)
-
在
App.vue
中使用<h1>{{ $store.state.name }}</h1>
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按需引入
-
安装两个插件
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()] }) ] } }
-
在
main.ts
中注册全局组件main.ts
import { ElButton, ElTable, ElAlert, ElAside, ElAutocomplete, ElAvatar, ElBacktop, ElBadge } from 'element-plus' const app = createApp(App) const components = [ ElButton, ElTable, ElAlert, ElAside, ElAutocomplete, ElAvatar, ElBacktop, ElBadge ] for (const cpn of components) { app.component(cpn.name, cpn) }
这样的话虽然能用,但是随着开发进度的进行,引用的组件越来越多,会导致
main.ts
文件过于臃肿,作为入口文件,应当尽量只包括主要逻辑,不应该包含过多的逻辑代码,因此现在我们对其进行抽离 -
创建文件 --
src/global/index.ts
index.ts
import { ElButton, ElTable, ElAlert, ElAside, ElAutocomplete, ElAvatar, ElBacktop, ElBadge } from 'element-plus' import { App } from 'vue' const components = [ ElButton, ElTable, ElAlert, ElAside, ElAutocomplete, ElAvatar, ElBacktop, ElBadge ] export function registerApp(app: App): void { for (const cpn of components) { app.component(cpn.name, cpn) } }
导出一个函数
registerApp
,在main.ts
中只用调用该函数即可main.ts
import { createApp } from 'vue' import App from './App.vue' import { registerApp } from '@/global' const app = createApp(App) registerApp(app) app.mount('#app')
其实还可以进一步抽离,因为以后可能还要注册别的组件,这时候如果全部注册逻辑写在单个
registerApp
函数里就又会变得臃肿了,因此将每个组件库的组件注册再次抽离成一个文件,如现在要注册element-plus
,那么我们就在global
目录下创建一个register-element.ts
,然后把注册逻辑写在里面,registerApp
去调用即可
`register-element.ts`
```typescript
import { App } from 'vue'
import {
ElButton,
ElTable,
ElAlert,
ElAside,
ElAutocomplete,
ElAvatar,
ElBacktop,
ElBadge
} from 'element-plus'
const components = [
ElButton,
ElTable,
ElAlert,
ElAside,
ElAutocomplete,
ElAvatar,
ElBacktop,
ElBadge
]
export default function (app: App): void {
for (const cpn of components) {
app.component(cpn.name, cpn)
}
}
```
`src/global/index.ts`
```typescript
import { App } from 'vue'
import registerElement from './register-element'
export function registerApp(app: App): void {
registerElement(app)
}
```
-
App.vue
中直接使用<el-button>element-plus 按钮</el-button>
-
更优雅地注册
Vue的
app.use()
会默认传入app
,因此可以进行如下调整,让代码风格更加统一
`main.ts`
```typescript
import { createApp } from 'vue'
import App from './App.vue'
import { globalRegister } from '@/global'
const app = createApp(App)
app.use(globalRegister)
app.mount('#app')
```
`src/global/index.ts`
```typescript
import { App } from 'vue'
import registerElement from './register-element'
export function globalRegister(app: App): void {
app.use(registerElement)
}
```
2.5 axios集成
2.5.1 安装axios
npm install axios
2.5.2 创建文件 -- src/service/request/config.ts
该文件用于存放一些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
实例对象,主要是添加对各种拦截器的支持,拦截器的粒度细致到以下三个阶段:
- 全局请求响应拦截,对所有的请求都生效
- 实例请求响应拦截,针对不同的实例可以设置不同的请求响应拦截
- 单独请求响应拦截,针对具体接口设置相应的请求响应拦截
要实现上述拦截器,需要自己封装一个拦截器类型接口,分别对应请求成功处理、请求失败处理、响应成功处理、响应失败处理
因此再创建一个文件,用于存放用到的接口类型 -- src/service/request/type.ts
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
}
export interface WFRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: WFRequestInterceptors<T>
showLoading?: boolean
}
创建一个类用于封装AxiosInstance
实例,这个类存放在 src/service/request/index.ts
中并默认导出
src/service/request/index.ts
import axios from 'axios'
import { ElLoading } from 'element-plus'
import type { AxiosInstance } from 'axios'
import { WFRequestConfig, WFRequestInterceptors } from './type'
import { DEAFAULT_LOADING } from './config'
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
class WFRequest {
instance: AxiosInstance
interceptors?: WFRequestInterceptors
showLoading: boolean // 处理请求时是否要显示加载动画
loading?: LoadingInstance
constructor(config: WFRequestConfig) {
// 创建 axios 实例
this.instance = axios.create(config)
// 保存基本信息
this.showLoading = config.showLoading ?? DEAFAULT_LOADING
this.interceptors = config.interceptors
// ================== 属于实例的拦截器 ==================
// 将请求拦截器注册到 axios 实例中
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
)
// 将请求拦截器注册到 axios 实例中
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
)
// ================== 所有实例的拦截器 ==================
// 所有实例的请求拦截器
this.instance.interceptors.request.use(
(config) => {
console.log('所有的实例都有的拦截器: 请求成功拦截')
// 处理加载动画
if (this.showLoading) {
this.loading = ElLoading.service({
lock: true,
text: '正在请求数据......',
background: 'rgba(0, 0, 0, 0.5)'
})
}
return config
},
(err) => {
console.log('所有的实例都有的拦截器: 请求成功拦截')
return err
}
)
// 所有实例的响应拦截器
this.instance.interceptors.response.use(
(res) => {
console.log('所有的实例都有的拦截器: 响应成功拦截')
// 如果有加载动画则将加载动画移除
this.loading?.close()
// 从 res 中提出 data 返回 因为 data 才是前端真正需要的,其他的东西是 axios 自己封装的 基本用不到
const data = res.data
return data
},
(err) => {
console.log('所有的实例都有的拦截器: 响应失败拦截')
this.loading?.close()
// HTTP 的状态码要在失败响应拦截器中拦截
if (err.response.status === 404) {
console.log('404 not found...')
}
return err
}
)
}
request<T>(config: WFRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 单个请求如果配置了请求拦截器 则先执行其配置的请求拦截器 再执行全局的请求拦截器
if (config.interceptors?.requestInterceptor) {
config = config.interceptors.requestInterceptor(config)
}
// 判断是否要显示 loading
if (config.showLoading === false) {
this.showLoading = config.showLoading
}
this.instance
.request<any, T>(config)
.then((res) => {
// 如果单次请求配置了响应拦截器 则执行实例的响应拦截器
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res)
}
// 将 showLoading 设置为 true -- 这样就不会影响下一个请求了
this.showLoading = DEAFAULT_LOADING
resolve(res)
})
.catch((err) => {
// 将 showLoading 设置为 true -- 这样就不会影响下一个请求了
this.showLoading = DEAFAULT_LOADING
reject(err)
return err
})
})
}
get<T>(config: WFRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'GET' })
}
post<T>(config: WFRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'POST' })
}
put<T>(config: WFRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'PUT' })
}
delete<T>(config: WFRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'DELETE' })
}
patch<T>(config: WFRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'PATCH' })
}
}
export default WFRequest
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<any, T>
,目的是不让axios帮我们封装成AxiosResponse
实例,而是直接返回我们需要的泛型对象T(会由Promise封装)
因此then
的res
类型就是T
,然后再在调用单个请求的拦截器的时候:
export interface WFRequestInterceptors<T = AxiosResponse> {
...
responseInterceptor?: (res: T) => T
}
此时响应拦截器接收到的参数类型就是T,T默认就是AxiosResponse
,而一旦我们更改为自己想要的T类型,则不会再传入和返回AxiosResponse
类型的对象
之所以要折腾这么一大长串代码,是因为要实现一个功能:让调用接口的时候得到的返回值是预先定义好的后端接口会返回的数据格式的接口对象,这样在调用者看来,就能有如下体验:
// 可以指定接口返回的对象类型
interface DataType {
data: any
returnCode: string
success: boolean
}
wfRequest
.get<DataType>({
url: '/home/multidata'
})
.then((res) => {
console.log(res)
console.log(res.data)
console.log(res.returnCode)
console.log(res.success)
})
调用者在then
中拿到的不再是AxiosResponse
对象,而是调用get方法时传入的DataType泛型对象
2.5.3.2 service中实例化封装好的类
在src/service/index.ts
中实例化一个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
中使用体验一下
import wfRequest from './service'
// 可以指定接口返回的对象类型
interface DataType {
data: any
returnCode: string
success: boolean
}
wfRequest.get<DataType>({
url: '/home/multidata',
interceptors: {
requestInterceptor: (config) => {
console.log('单独请求的拦截器')
return config
},
responseInterceptor: (res) => {
console.log('单独响应的拦截器')
console.log(res.returnCode)
console.log(res.success)
console.log(res.data)
return res
}
}
})