本文方法只在相关版本下测试使用,未做全面测试,也不保证其他版本可用。

VSCode: 1.67.2
vue: 3.2.33
vue-router: 4.0.15
vuex: 4.0.2
webpack: 5.72.0
vue/cli: 5.0.4
node: 16.14.0
npm: 8.3.1
axios: 0.27.2
element-plus: 2.2.0
vant: 4.0.0-alpha.3


Vue3 项目开发基本流程(一) 项目框架搭建

安装 VueCli

1
npm install -g @vue/cli

项目框架搭建

创建项目

1
2
3
4
5
6
7
8
9
vue create vue3-project
# 选择babel、typescript、Vue-Router、Vuex、postcss(less)、lint(eslint-prettier)
# class-style component syntax --- No
# Babel alongside TypeScript --- Yes
# router mode --- history
# css ---less
# ESlint --- ESlint + Prettier
# Lint on save
# dedicated config files

配置 .editorconfig

    1. VSCode 安装 EditorConfig for VS Code 插件
    1. 项目根目录右键点击 Generate .editorconfig,生成.editorconfig 文件
    1. 配置.editorconfig 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true

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

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

配置 prettier

安装 prettier
  • prettier,强大的代码格式化工具。
1
npm install prettier -D --save-exact
prettier 相关配置
    1. 新建.prettierrc 文件
    1. 配置.prettierrc
      1
      2
      3
      4
      5
      6
      7
      8
      {
      "useTabs": false, //使用tab缩进还是空格缩进
      "tabWidth": 2, //tab空格的字符长度为2
      "printWidth": 80, //每一行字符最大的长度
      "singleQuote": false, //单引号or双引号
      "trailingComma": "none", //多行输入的尾逗号是否添加
      "semi": true //语句末尾是否加分号
      }
    1. 新建.prettierignore 文件,忽略某个文件使用 prettier 进行代码格式化
    1. 配置.prettierignore 文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /dist/*
    .local
    .output.js
    /node_modules/**

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

    /public/*
    1. VSCode 安装 prettier 插件
    1. 手动配置 package.json prettier 脚本
      1
      2
      3
      "scripts": {
      "prettier": "prettier --write"
      }
    1. 在新建的项目启动前,执行 prettier 代码格式化命令
      1
      npm run prettier

配置 ESlint

    1. VueClii 项目已经默认配置 ESlint 环境
    1. VSCode 安装 ESlint 插件
    1. 解决 ESlint 和 prettier 冲突问题
      1
      2
      #安装两个插件(vuecli创建项目时,选择prettier就会自动安装)
      npm i eslint-plugin-prettier eslint-config-prettier -D
    1. .eslintrc.js 配置
      1
      2
      3
      4
      5
      6
      7
      8
      extends: [
      "plugin:vue/vue3-essential",
      "eslint:recommended",
      "@vue/typescript/recommended",
      "plugin:prettier/recommended",
      "@vue/prettier",
      "@vue/prettier/@typescript-eslint"
      ]
    1. 配置 git 提交规范,使其符合 ESlint 要求
      1. git Husky 相关配置
      • 安装 husky,husky 是一个 git hook 工具,可以触发 git 提交的各个阶段,pre-commit、commit-msg、pre-push
        1
        npx husky-init && npm install
      • 安装完成后会自动在 package.json 脚本中生成 husky 脚本
        1
        2
        3
        "scripts": {
        "prepare": "husky install"
        }
      • 安装完成后会自动在项目根目录生成.husky 文件夹;需要手动在 .husky/pre-commit 文件中配置
        1
        2
        npm run lint
        # 这样配置完成后,在执行git commit时 会自动进行ESlint校验
      1. npx cz 提交规范配置
      • 安装 commitizen,commitizen 是在 git commit 提交 meaasge 的文本风格规范
        1
        npm i commitizen -D
      • 安装 cz-conventional-changelog,初始化 cz-conventional-changelog
        1
        npx commitizen init cz-conventional-changelog --save-dev --save-exact
      • 安装和初始化 cz-conventional-changelog 之后:
        • 自动在 package.json 脚本中生成
          1
          2
          3
          4
          5
          "config": {
          "commitizen": {
          "path": "./node_modules/cz-conventional-changelog"
          }
          }
      • 安装完成后,需要手动配置 package.json commit 脚本,代码提交时在终端执行 npx cz 或者 npm run commmit
        1
        2
        3
        "scripts": {
        "commit": "cz"
        }
      1. git commit 提交规范配置
      • 安装@commitlint/config-conventional 和 @commitlint/cli
        1
        npm install --save-dev @commitlint/config-conventional @commitlint/cli -D
      • 项目根目录创建 commitlint.config.js,配置
        1
        2
        3
        module.exports = {
        extends: ["@commitlint/config-conventional"]
        }
      • 在终端执行以下命令,使用 husky 生成提交信息 commit-msg,验证提交信息规范
        1
        npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
      • 上述命令执行完成后,项目根目录自动生成.husky/commit-msg 文件,在 commit-msg 中配置
        1
        npx --no-install commitlint --edit

配置 vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const path = require("path")
const { defineConfig } = require("@vue/cli-service")
module.exports = defineConfig({
transpileDependencies: true,
outputDir: "./build",
// configureWebpack: {
// resolve: {
// alias: {
// assets: "@/assets",
// components: "@/components",
// common: "@/common",
// network: "@/network",
// views: "@/views"
// }
// }
// }
// configureWebpack: (config) => {
// config.resolve.alias = {
// '@': path.resolve(__dirname, 'src'),
// views: '@/views'
// }
// },
chainWebpack: (config) => {
//配置别名
config.resolve.alias
.set("@", path.resolve(__dirname, "src"))
.set("assets", "@/assets")
.set("common", "@/common")
.set("components", "@/components")
.set("network", "@/network")
.set("views", "@/views")
}
})

配置 vue-router

安装 vue-router
1
npm i vue-router
新建 router/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//创建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")
},
{
path: "*",
component: () => import("../views/error/ERR404.vue")
}
]

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

export default router
挂载 vue-router
1
2
3
4
# 在main.ts中挂载router
import router from './router'

createApp(App).use(router).mount('#app')

配置 vuex

安装 vuex
1
npm i vuex
新建 store/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
//创建store实例
import { createStore } from "vuex"

const store = createStore({
state() {
return {
name: ""
}
}
})

export default store
挂载 vuex
1
2
3
import store from "./store"

createApp(App).use(store).mount("#app")

配置全局 CSS

安装 normalize.css
配置 CSS
  • 手动创建 src/assets/css/base.less
  • 手动创建 src/assets/css/index.less
1
@import url("./base.less");
  • 在 main.ts 中引用 css
1
2
import "normalize.css"
import "assets/css/index.less"
POSTCSS 浏览器适配
  • 安装 postcss-px-to-viewport-8-plugin
1
2
3
# 将px转换为vw/vh
npm i postcss-px-to-viewport-8-plugin
# 基于postcss-px-to-viewport开发的插件,直接使用postcss-px-to-viewport会报错
  • 项目根目录创建 postcss.config.js,配置
1
2
3
4
5
6
7
module.exports = {
plugins: {
"postcss-px-to-viewport": {
viewportWidth: 375
}
}
}

第三方 API 接口跨域代理配置

vue.config.js 配置代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { defineConfig } = require("@vue/cli-service")

module.exports = defineConfig({
devServer: {
proxy: {
"/api": {
//代理 访问接口,仅用于本地测试
target: "https://api.bilibili.com/x/web-interface",
pathRewrite: {
"^/api": ""
},
changeOrigin: true
}
}
}
})
接口跨域测试错误处理
  • 404
1
2
3
4
5
6
7
8
9
10
//以axios网络请求为例,需要配置
import axios from "axios"
const resquest = axios.create({
baseURL: "/api", //非常关键,搭配vue.config.js / devServer / proxy配置
timeout: 10000
})
request
.get("/test", {})
.then((res) => console.log(res))
.catch((err) => console.log(err))
  • 403
1
2
<!-- 在public/index.html配置,所有请求不发送 referrer,绕过防盗链 -->
<meta name="referrer" content="never" />

配置环境

  • 创建 .env.development .env.production 文件
1
2
3
4
5
6
7
8
9
10
11
# .env.development文件
VUE_APP_BASE_URL = http://127.0.0.1/dev
VUE_APP_BASE_NAME = HELLO VUE
# 获取VUE_APP_BASE_URL,process.env.VUE_APP_BASE_URL
# 获取VUE_APP_BASE_NAME,process.env.VUE_APP_BASE_NAME

# .env.production文件
VUE_APP_BASE_URL = http://127.0.0.1/prod
VUE_APP_BASE_NAME = HELLO VUE
# 获取VUE_APP_BASE_URL,process.env.VUE_APP_BASE_URL
# 获取VUE_APP_BASE_NAME,process.env.VUE_APP_BASE_NAME

axios 封装

安装 axios
1
npm i axios
配置与封装 axios
  • 新建 axios 配置文件, src/service/request/config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let BASE_URL = ""
let BASE_NAME = ""
if (process.env.NODE_ENV === "development") {
BASE_URL = "http://127.0.0.1/dev"
BASE_NAME = "Hello Vue"
} else if (process.env.NODE_ENV === "production") {
BASE_URL = "http://127.0.0.1/prod"
BASE_NAME = "Hello World"
} else {
BASE_URL = "http://127.0.0.1/test"
BASE_NAME = "Hello Test"
}

export { BASE_URL, BASE_NAME }
  • 新建类型文件 src/service/request/type.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let BASE_URL = ""
let BASE_NAME = ""
if (process.env.NODE_ENV === "development") {
BASE_URL = "http://127.0.0.1/dev"
BASE_NAME = "Hello Vue"
} else if (process.env.NODE_ENV === "production") {
BASE_URL = "http://127.0.0.1/prod"
BASE_NAME = "Hello World"
} else {
BASE_URL = "http://127.0.0.1/test"
BASE_NAME = "Hello Test"
}

export { BASE_URL, BASE_NAME }
  • 新建类型文件 src/service/request/type.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { AxiosRequestConfig, AxiosResponse } from "axios"

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

export interface MyRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: MyRequestInterseptors<T>
}
  • 新建 axios 请求文件, src/service/request/request.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import axios from "axios"
import { AxiosInstance } from "axios"

import { MyRequestInterseptors, MyRequestConfig } from "./type"

class MyRequest {
instance: AxiosInstance
interceptors?: MyRequestInterseptors

constructor(config: MyRequestConfig) {
this.instance = axios.create(config)
this.interceptors = config.interceptors

//实例自定义拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requesInterceptorCatch
)
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
)

//所有实例的拦截器
this.instance.interceptors.request.use(
(config) => {
return config
},
(err) => {
return err
}
)
this.instance.interceptors.response.use(
(res) => {
return res
},
(err) => {
return err
}
)
}

request<T>(config: MyRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
if (config.interceptors?.requestInterceptor) {
config = config.interceptors.requestInterceptor(config)
}
this.instance
.request<any, T>(config)
.then((res) => {
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res)
}
resolve(res)
})
.catch((err) => {
reject(err)
return err
})
})
}

get<T>(config: MyRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "GET" })
}

post<T>(config: MyRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "POST" })
}

delete<T>(config: MyRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "DELETE" })
}

patch<T>(config: MyRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "PATCH" })
}
}

export default MyRequest
  • 新建 axios 封装导出文件, src/service/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import MyRequest from "./request/request"
import { BASE_URL } from "./request/config"

const myRequestInstance = new MyRequest({
baseURL: BASE_URL,
timeout: 10000,
interceptors: {
requestInterceptor: (config) => {
//请求拦截器,验证token
const token = ""
if (token && config.headers && config.headers.Authorization) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
requesInterceptorCatch: (err) => {
return err
},
responseInterceptor: (res) => {
//响应拦截,处理数据
const data = res.data
return data
},
responseInterceptorCatch: (err) => {
if (err.response.status === 404) {
console.log("404.")
}
return err
}
}
})

export default myRequestInstance
  • 项目引用 axios
1
2
3
4
5
6
7
8
import { myRequestInstance } from "../index"

const someDataRequest = (url, params) => {
return myRequestInstance.get({
url: url,
params: params
})
}

配置 element-plus

安装 element-plus
1
npm i element-plus
全局引用 element-plus
1
2
3
4
import ElementPlus from "element-plus"
import "element-plus/dist/index.css"

app.use(ElementPlus)
局部引用 element-plus
  • 单组件文件中按需引用
1
2
3
4
5
6
7
8
9
10
<script lang="ts">
import { ElButton } from "elemeny-plus"
import "element-plus/dist/index.css"

export default defineComponent({
components: {
ElButton
}
})
</script>
  • 按需引用多个组件,需要自动配置

    • 安装 bable-plugin-import
    1
    npm i babel-plugin-import -D
    • 配置 babel.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    plugins: [
    "import",
    {
    libraryName: "element-plus",
    customStyleName: (name) => {
    return `element-plus/lib/theme-chalk/${name}.css`
    }
    }
    ]
    }
    • 单组件文件中配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script lang="ts">
    import { ElButton } from "elemeny-plus"
    import "element-plus/dist/index.css"

    export default defineComponent({
    components: {
    ElButton
    }
    })
    </script>
  • 按需引用,全局注册

    • 安装 bable-plugin-import 配置 babel.config.js,跟自动配置一样
    • main.ts 文件中引用
      1
      2
      3
      import { ElButton } from "elemeny-plus"
      import "element-plus/dist/index.css"
      app.component(ElButton.name, ElButton)
    • 全局注册多个组件
      1
      2
      3
      4
      5
      6
      import { ElButton, ElForm, ElInput } from "elemeny-plus"
      import "element-plus/dist/index.css"
      const components = [ElButton, ElForm, ElInput]
      for (const component of components) {
      app.component(component.name, component)
      }
  • 按需引用,全局注册,抽取封装

    • 安装 bable-plugin-import 配置 babel.config.js,跟自动配置一样

    • 创建文件 src/global/register-element.ts

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import { App } from "vue"
      import { ElButton } from "elemeny-plus"
      import "element-plus/dist/index.css"
      const components = [ElButton, ElForm, ElInput]
      export default function (app: App): void {
      for (const component of components) {
      app.component(component.name, component)
      }
      }
    • 创建文件 src/global/index.ts

      1
      2
      3
      4
      5
      6
      7
      import { App } from "vue"
      import registerElement from "./register-element"

      export function registerApp(app: App): void {
      //registerElement(app)
      app.use(registerElement)
      }
    • main.ts 文件中引用

      1
      2
      3
      import { registerApp } from "./global"
      //registerApp(app)
      app.use(registerApp)

配置 vant

安装 vant
1
npm i vant
按需引用
  • 安装 babel.config.js
1
npm i babel-plugin-import -D
  • 配置 babel.config.js
1
2
3
4
5
6
7
8
9
10
11
12
{
"plugins": [
[
"import",
{
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}
]
]
}
  • 单文件组件引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<NavBar>
<template #left>
<div>
<Icon
name="arrow-left"
size="20"
color="#000"
class="navbar_arrow_left"
/>
<span class="navbar_title">123</span>
</div>
</template>
</NavBar>
</template>

<script setup lang="ts">
import { NavBar, Icon } from "vant"
</script>

<style scoped>
.navbar_arrow_left {
font-weight: bold;
margin-right: 10px;
-webkit-tap-highlight-color: transparent;
}
.navbar_title {
font-size: 20px;
font-weight: bold;
-webkit-tap-highlight-color: transparent;
}
</style>

项目 package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
"name": "vue3-project",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"prettier": "prettier --write .",
"prepare": "husky install",
"commit": "cz"
},
"dependencies": {
"@vant/use": "^1.3.6",
"axios": "^0.27.2",
"core-js": "^3.8.3",
"normalize.css": "^8.0.1",
"vant": "^4.0.0-alpha.3",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@commitlint/cli": "^16.2.4",
"@commitlint/config-conventional": "^16.2.4",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"babel-plugin-import": "^1.13.5",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"husky": "^7.0.0",
"less": "^4.0.0",
"less-loader": "^8.0.0",
"postcss-px-to-viewport-8-plugin": "^1.1.3",
"prettier": "2.6.2",
"typescript": "~4.5.5"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}