从 0 搭建项目模板(vue3)

JeremyJone ... 2022-3-29 大约 28 分钟

# 从 0 搭建项目模板(vue3)

为简化每次初始化项目,最好的方法就是搭建一个项目模板,这样每次初始化的时候直接拉取就好,不用再一个一个进行配置,省去了大量时间。

该内容模板已经在 Github,如果需要,可以直接下载使用。具体使用方法可以查看 Github 或者 模板文档

# 项目技术方案

这套模板采用如下方案:

同时预装了如下内容:

# 项目结构

最终的目录文件结构如下图:

注:目录结构仅供参考,可以根据自行需求进行修改。

# 环境检测

因为采用了 vite 进行搭建,所以需要 Node.js 版本 >= 12.0.0。

查看版本的方法:

node -v
1

# 搭建项目结构

# 初始化项目

首先使用 vite 创建项目。根据 vite 官方文档,可以快速开始:

npm init vite@lastest
// or
yarn create vite
1
2
3

然后按照提示一步一步操作即可。

1、填写名称

这里填写 vite-vue3-ts-template,方便我们大致知道模板的架构。

2、选择框架

这里我们选择 vue

然后选择 vue-ts

这样,我们就成功初始化了一个项目。

此时根据提示,进入项目根目录,并执行 yarn 命令,会自动安装相应的内容。

cd vite-vue3-ts-template
yarn
1
2

在执行之后,启动项目,会看到已经成功运行了。

# 整理项目结构

src 文件夹下,新建如下文件/文件夹:

└── src/
    ├── assets/             # 静态资源目录
    ├── components/         # 公共组件目录
    ├── pages/              # 页面组件目录
    ├── router/             # 路由配置目录
    ├── store/              # 状态管理配置目录
    ├── typings/            # 类型文件目录
    ├── styles/             # 通用 CSS 目录
    ├── utils/              # 工具方法目录
    ├── App.vue             # 根组件
    ├── env.d.ts            # vue 环境声明,不同版本的 vite 文件可能会有所差异
    ├── main.ts             # 主文件
1
2
3
4
5
6
7
8
9
10
11
12

我们之后的内容,基本都是基于上面结构进行的。

# 修改 vite 的配置文件

vite 的配置在根目录下的 vite.config.ts 中,我们根据需要进行一些修改:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],

  resolve: {
    // 配置 @ 映射到 src 目录
    alias: {
      "@": resolve(__dirname, "src")
    }
  },
  base: "./", // 打包路径

  server: {
    port: 3000, // 端口
    open: true, // 启动打开浏览器
    cors: true, // 跨域
    proxy: {
      "/api": {
        target: "http://localhost:8080/api/", // 目标地址
        changeOrigin: true, // 修改源
        secure: false, // ssl
        rewrite: path => path.replace("/api/", "/")
      }
    }
  }
});
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

提示

在这里,当导入 "path" 时,可能会提示 找不到模块的类型声明

此时按照提示直接安装 @types/node 即可。

或者手动安装:

yarn add @types/node -D
1

# 安装网络工具 axios

1、直接命令安装最新版即可:

yarn add axios
1

2、配置

为了方便使用,我们需要进行简单配置。

utils 文件夹下创建一个 http 的文件夹,并在其中创建一个 index.ts 文件,结构如下:

src
└── utils/
    ├── http/
        ├── index.ts      # axios 配置文件
1
2
3
4

在文件中填入如下内容:

import Axios from "axios";

const baseURL = "";
const axios = Axios.create({
  baseURL, // 基础 url
  timeout: 10000 // 超时 10s
});

// 请求拦截器
axios.interceptors.request.use(
  config => {
    // TODO: 配置请求内容

    // config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 响应拦截器
axios.interceptors.response.use(
  response => {
    // TODO: 配置对响应内容的处理

    return response;
  },
  error => {
    let { response } = error;
    if (error && error.response) {
      switch (error.response.status) {
        case 400:
          response.message = "未知错误";
          break;
        case 401:
          response.message = "未授权";
          break;
        case 403:
          response.message = "权限不足";
          break;
        case 404:
          response.message = "数据不存在";
          break;
        case 405:
          response.message = "不允许的请求方法";
          break;
        case 408:
          response.message = "请求超时";
          break;
        case 415:
          response.message = "不支持的媒体类型";
          break;
        case 500:
          response.message = "服务器出现异常";
          break;
        case 501:
          response.message = "网络未实现";
          break;
        case 502:
          response.message = "网络错误";
          break;
        case 503:
          response.message = "服务不可用";
          break;
        case 504:
          response.message = "网络超时";
          break;
        case 505:
          response.message = "http版本不支持该请求";
          break;
        default:
          response.message = `其他错误。错误代码:${error.response.status}`;
      }
    } else {
      response = { message: "无法连接到服务器!" };
    }
    return Promise.reject(response);
  }
);

export default axios;
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
81
82

3、封装使用方法

为了方便使用,我们最好将其常用的 getpostputdelete 进行二次封装。

index.ts 同级目录下创建一个 requests.ts 文件,结构如下:

src
└── utils/
    ├── http/
        ├── index.ts      # axios 配置文件
        ├── requests.ts   # 方法封装
1
2
3
4
5

在文件中键入如下内容:

import axios from ".";

/**
 * @param  promise
 * @param errorExt - Additional Information you can pass to the err object
 */
function to<T, U = unknown>(
  promise: Promise<T>,
  errorExt?: object
): Promise<[U | null, T | undefined]> {
  return promise
    .then<[null, T]>((data: T) => [null, data])
    .catch<[U, undefined]>(err => {
      if (errorExt) {
        Object.assign(err, errorExt);
      }

      return [err, undefined];
    });
}

/**
 * GET methods
 * @param url
 * @param data
 * @returns {Promise}
 */
export function get<T>(url: string, params = {}): TO<T> {
  return to(
    new Promise((resolve, reject) => {
      axios
        .get(url, {
          params
        })
        .then(result => {
          resolve(result.data as T);
        })
        .catch(err => {
          reject(err);
        });
    })
  );
}

/**
 * POST methods
 * @param url
 * @param data
 * @returns {Promise}
 */
export function post<T>(url: string, data?: Record<string, unknown>): TO<T> {
  return to(
    new Promise((resolve, reject) => {
      axios
        .post(url, data)
        .then(result => {
          resolve(result.data as T);
        })
        .catch(err => {
          reject(err);
        });
    })
  );
}

/**
 * PUT methods
 * @param url
 * @param data
 * @returns {Promise}
 */
export function put<T>(url: string, data?: Record<string, unknown>): TO<T> {
  return to(
    new Promise((resolve, reject) => {
      axios
        .put(url, data)
        .then(result => {
          resolve(result.data as T);
        })
        .catch(err => {
          reject(err);
        });
    })
  );
}

/**
 * DELETE methods
 * @param url
 * @param data
 * @returns {Promise}
 */
export function del<T>(url: string, data?: Record<string, unknown>): TO<T> {
  return to(
    new Promise((resolve, reject) => {
      axios
        .delete(url, data)
        .then(result => {
          resolve(result.data as T);
        })
        .catch(err => {
          reject(err);
        });
    })
  );
}
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

4、类型声明

注意到我这里为了更好的使用 Promise,对其返回值进行了一个封装,但是其返回类型 TO 并没有定义,编译器同时会报错。我们在 typings 文件夹中创建一个 http.d.ts 的文件,并进行声明:

declare type TO<T = unknown> = Promise<[unknown, T | undefined]>;
1

此时声明好了,同时错误也没有了。

# 安装路由工具 vue-router

1、安装 vue-router

因为我们用的是 vue3,所以需要 vue-router 4.x,直接带版本安装即可:

yarn add vue-router@4
1

router 文件夹下创建 index.ts 文件,结构如下:

src
└── router/
    ├── index.ts      # vue-router 配置文件
1
2
3

并键入如下内容:

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import Home from "@/pages/Home.vue";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
  // TODO: 填写其他页面路由内容
  {
    // 匹配全部其他内容
    path: "/:pathMatch(.*)*",
    component: () => import(/* webpackChunkName: "404" */ "@/pages/404.vue")
  }
];

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

// 路由拦截器
router.beforeEach((to, from, next) => {
  // TODO: 自定义拦截内容

  next();
});

export default router;
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

2、挂载路由

打开 main.ts 文件,并修改挂载路由:



 


 


import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";

createApp(App)
  .use(router)
  .mount("#app");
1
2
3
4
5
6
7

3、配置路由页面

为了测试它的效果,我们需要添加几个简单页面,同时配置路由页面。

打开 App.vue 文件,删除 HelloWorld 组件,并在模板中替换为 <router-view /> 组件:





 













<script setup lang="ts"></script>

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <router-view />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后在 pages 文件夹中创建两个文件,Home.vue404.vue

src
└── pages/
    ├── 404.vue
    ├── Home.vue
1
2
3
4

内容分别如下:

  • Home.vue
<template>
  <h1>Home Page</h1>

  <HelloWorld msg="Welcome to Vite + Vue3 + TypeScript Template" />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";

export default defineComponent({
  name: "HomePage",

  components: { HelloWorld },

  setup() {}
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 404.vue
<template>
  <h1>页面不存在</h1>
  <div>对不起,您访问的页面不存在。</div>
  <button @click="onBack">返回</button>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  setup() {
    return {};
  },

  methods: {
    onBack() {
      this.$router.go(-1);
    }
  }
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

现在启动项目,会打开 Home 页,随意输入一个路由,它会匹配到 404 页面,点击 返回 按钮,又回到了 Home 页面。

到这里,路由就配置完成。

# 安装状态管理工具 vuex

1、安装 vuex

vue-router 一样,因为我们用的是 vue3,所以需要 vuex 4.x,直接带版本安装即可:

yarn add vuex@next --save
1

store 文件夹下创建 index.ts 文件,结构如下:

src
└── store/
    ├── index.ts      # vuex 配置文件
1
2
3

并键入如下内容:

import { createStore } from "vuex";

export default createStore({
  state: {
    // TODO: 根据实际内容修改
    count: 0
  },
  mutations: {},
  actions: {},
  modules: {}
});
1
2
3
4
5
6
7
8
9
10
11

2、全局挂载 vuex

打开 main.ts 文件,并添加 vuex,挂载到全局中:




 



 


import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";
import store from "./store/index";

createApp(App)
  .use(router)
  .use(store)
  .mount("#app");
1
2
3
4
5
6
7
8
9

3、在组件中使用 vuex

此时可以使用。为了方便测试,我们在 store/index.ts 文件中添加修改方法:

export default createStore({
  state: {
    // TODO: 根据实际内容修改
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment: ({ commit }) => {
      commit("increment");
    }
  },
  modules: {}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

同时修改 HelloWorld.vue 中的内容,删除原来的 count 属性,使用 store 中的属性:


 



 
 

 
 
 







 
 
 
 


<script setup lang="ts">
import { useStore } from "vuex";

defineProps<{ msg: string }>();

// const count = ref(0)
const store = useStore();

function increment() {
  store.dispatch("increment");
}
</script>

<template>
  <h1>{{ msg }}</h1>

  <!-- ... other code ... -->

  <!-- <button type="button" @click="count++">count is: {{ count }}</button> -->
  <button type="button" @click="increment">
    count is: {{ store.state.count }}
  </button>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行项目测试,可以正常运行。

4、类型声明

此时虽然可以正常,但是我们可以看到,当我们使用 store.state 时,类型是任意的,我们看不到内部任何属性,当然就更看不到属性值的类型了,这显然不符合我们使用 TypeScript 的需求。

为解决这个问题,我们需要对 store/index.ts 进行一些改造。

首先声明一个 State 的类型:

export interface State {
  // TODO: 根据 state 实际内容修改
  count: number;
}
1
2
3
4

然后声明一个 Key,这里 vue 已经帮我们提供了一个解决方案:

import { InjectionKey } from "vue";
export const key: InjectionKey<Store<State>> = Symbol("store_key"); // key 内容随便填
1
2

现在我们可以通过传入对应的 key 来访问对应的 State

再次打开 HelloWorld.vue,现在我们把 key 值传入 useStore 中,即:


 
 

import { useStore } from "vuex";
import { key } from "../store";
const store = useStore(key);
1
2
3

现在就可以看到类型已经可以正常显示了:

到目前,它好像看起来很正常了。但是随着页面变多,我们需要在每一个页面都传入 key 值,这是一个相当繁琐的事情,我们需要一个更简便的方法,有没有呢?答案是肯定的。

5、改造类型声明,简化它

store/index.ts 中简单封装一个 useStore


 


















 
 
 

import { InjectionKey } from "vue";
import { createStore, useStore as baseUseStore, Store } from "vuex";

export interface State {
  // TODO: 根据 state 实际内容修改
  count: number;
}

export const key: InjectionKey<Store<State>> = Symbol("store_key"); // key 内容随便填

export default createStore({
  state: {
    // TODO: 根据实际内容修改
    count: 0
  },
  mutations: {},
  actions: {},
  modules: {}
});

export function useStore() {
  return baseUseStore(key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

然后在 main.ts 文件中挂载这个 key 值即可:




 



 


import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";
import store, { key } from "./store/index";

createApp(App)
  .use(router)
  .use(store, key)
  .mount("#app");
1
2
3
4
5
6
7
8
9

现在我们每次导入 useStore 时,不需要再从 vuex 中导入,而是通过 @/store 导入即可,使用上没有其他区别。

这样,我们就将 vuex 完整的配置好了。

# 安装 CSS 预编译器

根据项目需要添加对应的预编译器:

yarn add stylus -D  # stylus
yarn add sass -D    # sass / scss
yarn add less -D    # less
1
2
3

这个根据需要对应安装即可。

# 安装 husky

husky 是一个钩子工具,它可以帮助我们在一些特定时候触发一些特定的功能。比如我们在提交代码的时候,需要验证代码是否符合规则,这时候可以通过 husky 很方便的实现。

因为后面的内容基本都需要用到 husky,所以我们首先把它添加到模板中。

1、通过脚本直接安装并配置 husky

npx husky-init
1

它会在项目根目录下创建一个名为 .husky 的文件夹,同时在里面生成相关文件。

2、然后执行:

yarn
# or
npm install
1
2
3

安装 husky 到项目中。

husky 包含很多钩子,我们常用的有 pre-commitpre-push 等,从名字就很清晰的知道它应该在什么时候调用。我们在后面需要经常用到这些钩子还帮助我们规范项目。

# husky 的错误解决方案

  • Yarn on Windows

对于 Windows 10 的用户,在使用 yarn 的时候,可能存在执行 pre-push 的时候出现 stdin is not a tty 的错误。

这时需要在 .husky 文件夹下创建一个名为 common.sh 的文件,并键入如下内容:

command_exists () {
  command -v "$1" >/dev/null 2>&1
}

# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1; then
  exec < /dev/tty
fi
1
2
3
4
5
6
7
8

然后就可以在钩子文件中使用 yarn 命令了。参考

# 添加代码规范

随着代码越来越复杂,写代码的人越来越多,代码的格式可能会越来越眼花缭乱。为避免这些问题,代码规范就必不可少。

良好的代码规范可以有效避免团队之间的代码风格差异化,提高审查代码效率等等。

通常情况下,使用简单的 eslint + prettier 就可以保持大部分规范,同时 vue 也提供了对应的 vue-eslint,也可以查看 vue风格指南

# 配置 EditorConfig

EditorConfig 是编辑器风格配置,可以帮助多人或者多设备统一编辑器风格。

提示

VS Code 中需要插件 EditorConfig for VS Code

JetBrains 系列如 WebStorm 中,则默认支持。

在根目录下创建一个名为 .editorconfig 的配置文件:

# Editor configuration, see http://editorconfig.org

root = true                        # 是否为根目录,支持不同层级单独配置

[*]                                # 适用所有文件
indent_style = space               # tab | space
indent_size = 4                    # 缩进
end_of_line = crlf                 # lf | cr | crlf
charset = utf-8                    # 字符集
trim_trailing_whitespace = true    # 自动切掉首尾空格
insert_final_newline = true        # 末尾空行

[*.{js,ts,vue,jsx,tsx}]            # 如何处理这些文件
indent_size = 2                    # 缩进为 2

[*.md]                             # 如何处理这些文件
trim_trailing_whitespace = false   # 不自动切掉首尾空格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 配置 Prettier

Prettier 是一个格式化文档工具,可以快速将我们的代码格式化为统一风格。

提示

VS Code 中需要插件 Prettier - Code formatter

JetBrains 系列如 WebStorm 中,则默认支持。

1、安装 Prettier

yarn add prettier -D
1

2、配置 Prettier

在根目录下创建名为 .prettierrc 的文件,并键入如下内容:

{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "none",
  "bracketSpacing": true,
  "semi": true,
  "arrowParens": "avoid"
}
1
2
3
4
5
6
7
8
9
10

具体内容可以根据需要调整,具体可以参考 官方文档

3、使用 Prettier

通过命令可以快速对指定文档进行格式化,也可以通过右键或者快捷方式(Alt+Shift+F)进行格式化。

格式化命令:

npx prettier --write .    # . 表示所有文档
1

# 配置 ESLint

ESLint 可以查找并提示代码中的问题,并支持自动修复(如果可以),通过严格的规范检查来控制代码质量。

相信不少人都被 ESLint 虐过,初接触时,满屏的红色,那叫一个酸爽,但是随着使用的深入,它一定会给你带来质的变化。

提示

VS Code 中需要插件 ESLint

JetBrains 系列如 WebStorm 中,则默认支持。

1、安装 ESLint

yarn add eslint -D
1

2、配置 ESLint

安装后,通过 npx eslint --init 可以快速创建配置:

这里我们直接依次照图选择即可,并选择一个最流行的规则,即 Airbnb

最后,如果不选择使用 npm 安装,可以手动进行安装:

yarn add eslint-plugin-vue eslint-config-airbnb-base eslint-plugin-import @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
1

执行完成,会在根目录下创建一个名为 .eslintrc.js 的配置文件:










 






module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: ["plugin:vue/essential", "airbnb-base"],
  parserOptions: {
    ecmaVersion: 13,
    parser: "@typescript-eslint/parser",
    sourceType: "module"
  },
  plugins: ["vue", "@typescript-eslint"],
  rules: {}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以根据实际情况增删内容。

然后添加针对 vue 的规则配置:

  • 添加高亮行内容到文件中:




 



module.exports = {
  extends: [
    "plugin:vue/essential",
    "airbnb-base",
    "plugin:vue/vue3-recommended" // 添加该行内容,针对 vue3 的规则。如果是 vue2,则是 plugin:vue/recommended
  ]
};
1
2
3
4
5
6
7
  • 添加 parser

 






module.exports = {
  parser: "vue-eslint-parser",
  parserOptions: {
    parser: "@typescript-eslint/parser",
    sourceType: "module"
  }
};
1
2
3
4
5
6
7

此时如果文件有错误,则会显示出来。如果没显示出来,重启编译器再试。

3、添加 TypeScript 支持

对于 airbnb-base 来说,需要单独添加 TypeScript 的支持。

安装插件:

yarn add eslint-config-airbnb-typescript -D
1

.eslintrc.js 中添加如下内容:





 




module.exports = {
  extends: [
    "plugin:vue/essential",
    "airbnb-base",
    "airbnb-typescript/base", // 添加 typescript 支持
    "plugin:vue/vue3-recommended"
  ]
};
1
2
3
4
5
6
7
8

4、使用 ESLint

package.json 文件的 scripts 字段中添加如下内容:

"scripts": {
    "lint": "eslint src/**/*.{js,vue,ts,jsx,tsx}",
    "lint:fix": "eslint src/**/*.{js,vue,ts,jsx,tsx} --fix",
    "lint:create": "eslint --init"
}
1
2
3
4
5

然后执行:yarn lint:fix 即可修复所有内容,如果此时报错,则需要手动修复。

5、常用的 vue 规则

module.exports = {
  rules: {
    "vue/no-multiple-template-root": "off", // 启用根层级多个标签
    "vue/script-setup-uses-vars": "error" // 标记 setup 中的变量为 used
  }
};
1
2
3
4
5
6

# 在 husky 中集成样式规范

我们不想每次都手动执行命令来修正代码样式,那么可以通过 husky 的钩子,在每次提交代码之前首先修改样式即可。

只需要在 .husky/pre-commit 文件中修改命令:

# 如果是全局安装 eslint,则可以直接使用 eslint 命令
yarn eslint --fix ./src --ext .js,.jsx,.ts,.tsx,.vue
1
2

在每次预提交的时候,都会触发该方法进行检查并修复内容,同时如果出现错误,不会继续。

但是这样会有一个问题,每次都会检查所有文件,这在开发中并不常用,因为通常代码不是我们一个人写的,同时项目可能包含远古代码,修改格式会导致未知错误。这时候就需要仅仅针对当前我们修改过的文件进行检查即可。

# 使用 lint-staged

想要做到上述要求,我们需要使用 lint-staged 工具帮助我们仅仅检查暂存区的内容。

# 安装 lint-staged

yarn add lint-staged -D
1

# 配置 lint-staged

package.json 中增加配置内容:

{
  "lint-staged": {
    "*.{vue,js,ts,jsx,tsx}": "eslint --fix"
  }
}
1
2
3
4
5

提示

有时候我们看到有些项目或者一些文章,在该字段中同时添加 eslint --fixgit add,也就是 "*.{vue,js,ts,jsx,tsx}": ["eslint --fix", "git add"],这是 lint-staged v10 以下版本的写法,在 v10 及以上版本不再需要 git add

这表示检查并修复暂存区中所有的 vuejstsjsxtsx 文件。

# 修改 husky 的钩子内容

将之前在 .husky/pre-commit 中的指令修改为:npx lint-staged

这样就配置好了。当我们提交一个有错误的文件时,它会报错并拒绝提交:

# 解决样式配置后出现的问题

# 解决 Prettier 行尾换行符报错

有时候当我们安装了 Prettier 之后,发现每一行的行尾都会报错,这是因为行尾换行符的冲突配置所致。此时我们需要在 .eslintrc.js 中的 rules 字段中添加一条规则:

rules: {
    // 解决 prettier 行尾报错
    "prettier/prettier": ["error", { endOfLine: "auto" }],
}
1
2
3
4

所有文件的结尾错误就可以解决。

# 解决 Prettier 与 ESLint 的冲突问题

这两个的规则有时存在冲突,一般情况下以 Prettier 为优先。

安装插件:

yarn add eslint-plugin-prettier eslint-config-prettier -D
1

.eslintrc.js 中添加:






 


extends: [
  'plugin:vue/essential',
  'airbnb-base',
  "airbnb-typescript/base",
  "plugin:vue/vue3-recommended",
  'plugin:prettier/recommended' // 添加解决冲突插件
],
1
2
3
4
5
6
7

# 解决通过 @ 引入的路径问题

如果在使用了 @ 字符的路径中出现该问题,则可以按如下方法解决。

安装工具:

yarn add eslint-import-resolver-alias eslint-import-resolver-typescript -D
1

tsconfig.json 中添加:



 
 
 
 
 



{
  "compilerOptions": {
    "baseUrl": ".",
    "types": ["vite/client", "node"],
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
1
2
3
4
5
6
7
8
9

.eslintrc.js 中添加:






 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


module.exports = {
  parserOptions: {
    ecmaVersion: 12,
    parser: "@typescript-eslint/parser",
    sourceType: "module",
    project: ["./tsconfig.json"]
  },
  settings: {
    "import/parsers": {
      "@typescript-eslint/parser": [".ts", ".tsx"]
    },
    "import/resolver": {
      alias: {
        map: [["@", "./src"]],
        extensions: [".js", ".jsx"]
      },
      typescript: {
        alwaysTryTypes: true,
        project: "./tsconfig.json"
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 尝试保存文件时自动修复错误

在当前项目的根目录下创建文件夹 .vscode,在其中新建文件 settings.json 并键入:

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
1
2
3
4
5

如果需要在全局生效,则进入设置页面(文件 -> 首选项 -> 设置),搜索 editor.codeActionsOnSave,打开 settings.json 配置上述内容即可。

# 修复所有 vue 文件第一行首字符处出现 Parsing error: "parserOptions.project"... 的错误

如遇到上面问题,配置 .eslintrc.js 中的 extraFileExtensions 字段即可:



 



module.exports = {
  parserOptions: {
    extraFileExtensions: [".vue"]
  }
};
1
2
3
4
5

如果配置后未生效,重启编辑器即可。

# 修复 defineProps 等未定义的错误

使用 <script setup> 标签时,常用的 defineProps 等属于全局宏定义,不需要二次引入,但是可能会导致规则报错,需要在 .eslintrc.js 中添加配置:

module.exports = {
  globals: {
    defineProps: "readonly",
    defineEmits: "readonly",
    defineExpose: "readonly",
    withDefaults: "readonly"
  }
};
1
2
3
4
5
6
7
8

# 修复 [plugin:vite:eslint] 的相关错误

有时候浏览器会报 [plugin:vite:eslint] 的相关错误,此时可以通过安装 vite-plugin-eslint 并配置来解决。

  • 安装:
yarn add vite-plugin-eslint -D
1
  • 配置

vite.config.ts 文件中配置:

import eslintPlugin from "vite-plugin-eslint";

export default defineConfig({
  plugins: [vue(), eslintPlugin()]
});
1
2
3
4
5

如果你需要指定文件,可以添加参数:



 
 
 


plugins: [
  vue(),
  eslintPlugin({
    include: ["src/**/*.js", "src/**/*.vue", "src/**/*.ts"]
  })
];
1
2
3
4
5
6

此时如果还报错,可以重启编译器和服务器尝试,通常会好。

# 添加提交规范

GIT 的具体提交规范,可以参考 提交规范,这里只涉及规范工具的使用。

# 集成 Commitizen

Commitizen 是一个撰写规范提交信息的工具。

我们可以全局安装该工具:

npm install -g commitizen
1

提示

Commitizen 最好是全局安装,否则不可以在全局使用 git cz 命令,而是需要在项目中使用 yarn git-cz

然后在项目中初始化:

npx commitizen init cz-conventional-changelog --save-dev --save-exact
1

这样就可以通过命令行直接使用 Commitizen 了。以后但凡使用 git commit 命令,一律改为使用 git cz 即可,根据 cli 的提示一步步选择就可以生成符合规范的提交消息了。

注意:要先使用 git add 添加要提交的文件。

按照提示依次填写完成,就可以成功提交了。

# 自定义提交配置

因为提交的提示都是英文的,也没有配置内容,这时我们就需要 cz-customizable 来个性化。

在项目中初始化它:

npx commitizen init cz-customizable --save-dev --save-exact --force
1

注意上面的 --force 参数,因为之前已经初始化了 cz-conventional-changelog,我们在这里需要覆盖它。

执行完成后,我们在根目录下创建一个 .cz-config.js 的文件,然后根据 官方文档 进行配置即可。

可以参考下面的中文示例进行配置:

// 官方示例:https://github.com/leoforfree/cz-customizable/blob/master/cz-config-EXAMPLE.js
module.exports = {
  types: [
    { value: "feat", name: "feat:\t\t新增功能" },
    { value: "fix", name: "fix:\t\t修复 bug" },
    { value: "docs", name: "docs:\t\t文档变更" },
    {
      value: "style",
      name: "style:\t代码格式(不影响功能,例如空格、分号等格式修正)"
    },
    {
      value: "refactor",
      name: "refactor:\t代码重构(不包括 bug 修复、功能新增)"
    },
    { value: "perf", name: "perf:\t\t性能优化" },
    { value: "test", name: "test:\t\t添加、修改测试用例" },
    {
      value: "build",
      name:
        "build:\t构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)"
    },
    { value: "ci", name: "ci:\t\t修改 CI 配置、脚本" },
    {
      value: "chore",
      name: "chore:\t对构建过程或辅助工具和库的更改(不影响源文件、测试用例)"
    },
    { value: "revert", name: "revert:\t回滚 commit" }
  ],

  scopes: [
    { value: "components", name: "components:\t组件相关" },
    { value: "composables", name: "composables:\tcomposables相关" },
    { value: "utils", name: "utils:\tutils相关" },
    { value: "styles", name: "styles:\t样式相关" },
    { value: "dependencies", name: "dependencies:\t依赖相关" },
    { value: "other", name: "other:\t其他" },
    // 选中自定义可以自行填写,或者开启 allowCustomScopes 亦可
    { value: "custom", name: "custom:\t自定义" }
  ],

  // allowCustomScopes: true,
  // allowTicketNumber: false,
  // isTicketNumberRequired: false,
  // ticketNumberPrefix: 'TICKET-',
  // ticketNumberRegExp: '\\d{1,5}',

  scopeOverrides: {
    fix: [
      { name: "merge" },
      { name: "style" },
      { name: "e2eTest" },
      { name: "unitTest" }
    ]
  },

  messages: {
    type: "确保本次提交遵循 Angular 规范!\n选择你要提交的类型:",
    scope: "\n选择一个 scope(可选):",
    // 选择 scope: custom 时会出下面的提示
    customScope: "请输入自定义的 scope:",
    subject: "填写本次提交的主题:\n",
    body: '填写详细的变更描述(可选)。使用 "|" 换行:\n',
    breaking: "是否存在非兼容性的变更。如果有,请填写:\n",
    footer: "请填写相应的 ISSUES(可选)。 例如: #1, #2:\n",
    confirmCommit: "确认提交?"
  },

  // 设置只有 type 选择了 feat 或 fix,才询问 breaking message
  allowBreakingChanges: ["feat", "fix"],
  // askForBreakingChangeFirst : true,

  // 跳过要询问的步骤
  // skipQuestions: ["body", "footer"],

  // subject 限制长度
  subjectLimit: 100,
  breaklineChar: "|" // 支持 body 和 footer
  // footerPrefix : 'ISSUES CLOSED:'
};
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

此时再运行 git cz 的时候,就可以看到中文了。

现在的提示,对于我这种英语学渣来说,就友好了很多~ 😄

# 集成 commitlint

尽管我们添加了 Commitizen,但是我们仍然可以通过 git commit 填写任意内容进行提交。为了保证规范,我们需要添加一个限制,就是 如果没有按照规范填写提交信息,就不可以提交成功,此时就需要通过 commitlint 来实现。

# 安装 commitlint

yarn add @commitlint/config-conventional @commitlint/cli -D
1

# 配置 commitlint

在根目录下创建一个名为 commitlint.config.js 的文件,并填写如下内容:

module.exports = {
  extends: ["@commitlint/config-conventional"]
};
1
2
3

配置好 commitlint,我们使用 husky 的钩子方法来验证提交信息。执行下面命令:

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
1

如果没有安装 husky,请参考前面的内容 安装 husky

我们现在再来试一下通过 git commit 命令随意填写提交信息:

可以看到已经不能正常提交了,同时给出错误的提示。

当我们按照要求填写内容的时候,它就可以正常提交了:

毕竟我们手写很可能出错,所以还是尽量选用 git cz 命令来按照提示选择最为稳妥。

# 集成 Standard-Version

生成 CHANGELOG.md 的方式有很多种,我们选择 standard-version 来帮助我们实现。

它是一个命令行工具,可以通过我们提交的内容,自动更新项目的版本,同时添加 git tag 和更新 CHANGELOG.md,很方便。

我们的版本号分为 majorminorpatch,分别是主版本次版本补丁版本,它们分别对应项目的重构级别、新功能的增加、bug 的修复等。这在 语义化版本 中有详细的解释。

# 安装 Standard-Version

npm install -g standard-version  # 推荐全局安装
# or
yarn add standard-version -D
1
2
3

# 配置 Standard-Version

package.json 中添加如下脚本:

{
  "scripts": {
    "release": "standard-version"
  }
}
1
2
3
4
5

# 使用 Standard-Version

# 1、自动生成版本号

默认情况下,工具会根据规则自动生成版本号:

# 全局安装执行
standard-version

# 脚本运行则执行
yarn release
# or
npm run release
1
2
3
4
5
6
7
  • 如果提交的 typefeat,则 minor +1
  • 如果提交的 typefix,则 patch +1
# 2、指定版本号

我们可以通过参数,指定我们需要的版本号,但是这仍然需要遵循 语义化版本

假如当前版本号为 v1.0.0

standard-version -r major            # 2.0.0
standard-version -r 2.0.0-t          # 2.0.0-t
standard-version -r minor            # 1.1.0
standard-version -r 1.1.0-t          # 1.1.0-t
standard-version -r patch            # 1.0.1
standard-version -r 1.0.1-t          # 1.0.1-t

# 脚本运行
yarn release -- --release-as minor   # 1.1.0
1
2
3
4
5
6
7
8
9
  • -r 表示 --release-as,指定版本号
# 3、预发布版本

预发布版本用来发布预发版本,通常在版本号之后添加 alphabeta 等字样。

假如当前版本为 v1.0.0

standard-version --prerelease         # 1.0.1-0
standard-version --prerelease alpha   # 1.0.1-alpha.0

# 脚本运行
yarn release -- -p beta               # 1.0.1-beta.0
1
2
3
4
5
# 4、添加 tag 前缀

有时候我们需要为 tag 添加一些前缀内容,如 @scope/package@2.0.0,则需要:

standard-version --tag-prefix @scope/package@  # @scope/package@2.0.1

# 脚本运行
yarn release -- -t @scope/package@             # @scope/package@2.0.1
1
2
3
4

# Standard-Version 的一些问题

如果版本为 0.x.x 时,例如 0.1.1,当我们的提交中包含 feat 特性时,执行 yarn release 不会变成 0.2.1,而是 0.1.2,这是因为没有主版本号。

这时需要手动给定版本号,或者修改主版本号后再执行。

# 添加单元测试

对于任何项目,尤其是大型项目,单元测试是一个非常重要的环节。完整的测试可以有效保证代码质量。

# 添加测试依赖

yarn add jest@26 ts-jest@26 @vue/test-utils@next vue-jest@next @types/jest eslint-plugin-jest -D
1

安装提示

错误日期:2021.11.8

jestts-jest 由于版本冲突,最新如果直接安装,则为版本 27,此时运行测试内容会报错:

需要指定版本 26,会解决该问题。

详见 ISSUE #351

同时测试需要 babel,所以再安装 babel 依赖:

yarn add @babel/core @babel/preset-env @babel/preset-typescript @vue/babel-plugin-jsx -D
1
yarn add babel-jest@26
1

安装提示

错误日期:2021.11.8

同样需要注意,babel-jest 由于版本问题,如果安装最新版 ^27,会引起 TypeError: babelJest.getCacheKey is not a function 的错误,所以需要安装 ^26 版本。

详见 ISSUE 344

# 配置 jest

  • 在根目录下添加 jest.config.js 文件,并键入如下内容:
module.exports = {
  clearMocks: true,
  coverageDirectory: "coverage",
  coverageProvider: "v8",
  moduleFileExtensions: ["vue", "js", "json", "jsx", "ts", "tsx", "node"],
  testMatch: ["**/tests/units/**/?(*.)+(unit|test|spec).[jt]s?(x)"],
  testPathIgnorePatterns: ["/node_modules/"],
  transform: {
    "^.+\\.jsx?$": "babel-jest",
    "^.+\\.vue?$": "vue-jest",
    "^.+\\.tsx$": "ts-jest"
  },
  moduleNameMapper: {
    // 支持源代码中相同的 `@` -> `src` 别名
    "^@/(.*)$": "<rootDir>/src/$1"
  },
  preset: "ts-jest",
  testEnvironment: "jsdom",
  collectCoverage: true,
  collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx,vue}", "!**/node_modules/**"]
  // coverageReporters: ["text", "text-summary", "html"],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 在根目录下添加 babel.config.js 文件,并键入:
module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }],
    "@babel/preset-typescript"
  ],
  plugins: ["@vue/babel-plugin-jsx"]
};
1
2
3
4
5
6
7
  • tsconfig.json 中添加:


 






 



{
  "compilerOptions": {
    "types": ["vite/client", "node", "jest"]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts"
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

.eslintrc.js 中添加:



 






extends: [
    'plugin:vue/essential',
    'plugin:jest/recommended',
    'airbnb-base',
    'airbnb-typescript/base',
    'plugin:vue/vue3-recommended',
    'plugin:prettier/recommended' // 添加 prettier 插件
],
1
2
3
4
5
6
7
8

# 执行测试

package.json 中添加脚本:

{
  "scripts": {
    "test": "jest"
  }
}
1
2
3
4
5

执行 yarn test,会根据 jest.config.js 的配置内容进行测试:

# 将测试集成到 husky 中

默认情况下,husky 提供的代码是每次提交都进行测试,这样对于开发来说比较麻烦,我们之前也删除了对应的代码。我们希望在 push 的时候测试代码,这样可以减少测试数量,同时也保证了代码的有效性。

使用 pre-push 钩子就可以实现。

npx husky add .husky/pre-push "yarn test $1"
1

该命令会在 .husky 文件夹下创建一个 pre-push 的钩子文件。

现在,每次我们提交的时候都会执行测试,只有当测试通过才会进行 push 操作。

# 测试常见问题

# 配置全局 key 之后的 vuex 如何测试

在测试中,可以发现,当页面中调用了之前我们配置的 ./useStore() 之后,会报找不到 key 的错误:

此时我们需要对 store 进行一个全局注入,将 key 一并注入到测试组件中。



 





 
 
 








import { shallowMount } from "@vue/test-utils";
import WelcomeComponent from "@/components/HelloWorld.vue";
import store, { key } from "@/store/index";

describe("Welcome Component Test", () => {
  const wrapper = shallowMount(WelcomeComponent, {
    props: { msg: "Hello Test!" },

    global: {
      plugins: [[store, key]]
    }
  });

  it("load compoent", () => {
    const html = wrapper.text();
    expect(html).toContain("Hello Test!");
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

需要注意的是,JSSymbol 是唯一的,所以需要从 store 中导出。但是我们定义的 keyInjectionKey 类型,不能用作键,故不能使用:

global: {
  provide: {
    [key]: store
  },
}
1
2
3
4
5

的方式,所以我们选择通过 plugins 的方式添加。更多参见 文档


  1. 使用时注意使用一个即可。请添加后自行删除 package.jsondevDependencies 字段中的多余内容即可。 ↩︎