前言

在日常开发中,我们经常需要新建项目,然而项目的初始化是比较繁琐的。因此开发一个公共项目模板,在创建新项目时基于此模板进行修改可以减少很多工作量

功能

vue3模板应具有以下功能

  • 国际化
  • typescript
  • 前端路由
  • 区分环境
  • 构建、打包
  • 代码检查 lint、husky
  • gitlab CI/CD
  • 数据 mock
  • 网络请求
  • 状态管理 store
  • 支持 ssr

实现

本项目中默认全部使用**Composition APIsetup语法糖语法,因此所安装的依赖包**都支持此写法

国际化

使用vue-i18n插件,官网

安装

需要安装最新版本

1
npm i vue-i18n@next --save

配置

src目录下创建i8n目录,在i18n目录中创建对应语言的文件夹,再在其中创建对应页面的翻译文件(以页面为单位进行翻译),最终的目录结构如下

1
2
3
4
5
6
7
8
9
src
- i18n
- en-US
- zh-CN
- about.js
- home.js
- index.js
- index.js
main.ts

最终i18n/index.js文件导出的数据格式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default {
// 英语的文案
en_US: {
// 英语首页的文案
home: {
title: "home",
},
about: {},
},

// 中文的文案
zh_CN: {
// 中文首页的文案
home: {
title: "首页",
},
about: {},
},
};

在入口文件main.ts中配置 i18n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createApp } from "vue";
import { createI18n } from "vue-i18n";
import i18nMessages from "./i18n";
import App from "./App.vue";

// 配置i18n
const i18n = createI18n({
// 默认语言
locale: "en_US",
messages: i18nMessages,
});

const app = createApp(App);
app.use(i18n).mount("#app");

使用

setup或者setup语法糖中使用i18n,需要使用useI18nhook 来实现

获取当前语言的翻译文案

1
2
3
4
5
6
7
8
<template>
<div>{{t('home.context')}}</div>
</template>

<script setup>
import { useI18n } from "vue-i18n";
const { t } = useI18n();
</script>

获取/切换当前语言

1
2
3
4
5
6
7
import { useI18n } from "vue-i18n";
// 获取全局i18n实例
const { locale } = useI18n({ useScope: "global" });
// 获取当前语言
const lang = locale.value;
// 切换当前语言
locale.value = "zh_CN";

前端路由

使用vue-router官网

安装

需要安装vue-router 4.x版本

1
npm i vue-router@4 --save

配置

创建router文件夹,在其中配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../pages/Home/index.vue";
import About from "../pages/About/index.vue";

// 定义路由
const routes = [
{
path: "/home",
component: Home,
},
{
path: "/about",
component: About,
},
];

// 创建路由实例
const router = createRouter({
history: createWebHashHistory(),
routes,
});

export default router;

main.ts文件引入配置后的router,并注册到全局

1
2
3
4
5
6
7
8
9
10
import { createApp } from "vue";
import i18n from "./i18n";
import router from "./router";
import App from "./App.vue";

const app = createApp(App);

app.use(i18n);
app.use(router);
app.mount("#app");

使用

router-link标签式路由跳转

router-view显示路由内容的区域,可以放在任意位置

setup中使用路由参考组合式 API

Typescript

vite项目原生支持使用typescript

配置 tsconfig 文件

1
2
3
4
5
6
7
{
"allowJs": true, // 允许使用js文件
"baseUrl": ".", // 配置路径别名,配合resolve.alias使用
"paths": {
"@/*": ["src/*"] // 这里必须写成 @/* 和 src/*的形式,少任意一个*都不行
}
}

代码检查

使用eslint prettier stylelint进行代码检查,以及husky lint-staged进行预提交检查

安装

安装husky lint-staged

1
npx mrm@2 lint-staged

安装eslint相关

1
npm i eslint eslint-plugin-vue vue-eslint-parser vite-plugin-eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser -D

安装prettier相关

1
npm i prettier eslint-config-prettier eslint-plugin-prettier -D

安装stylelint相关

1
npm i stylelint stylelint-config-standard postcss-html -D

配置

.stylelintrc.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"extends": "stylelint-config-standard",
"plugins": ["stylelint-scss"],
"rules": {
"no-empty-source": null,
"at-rule-no-unknown": null,
"no-invalid-position-at-import-rule": null
},
// 解析vue文件中的样式
"overrides": [
{
"files": ["*.vue", "**/*.vue"],
"customSyntax": "postcss-html"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
env: {
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier",
],
parser: "vue-eslint-parser", // 重要
plugins: ["@typescript-eslint"],
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
},
rules: {
// 关闭组件命名限制,但是除单个单次以外的组件,最好还是以multi-word的形式命名
"vue/multi-word-component-names": 0,
"@typescript-eslint/no-explicit-any": ["error", { ignoreRestArgs: true }],
"@typescript-eslint/no-explicit-any": 0,
},
};

状态管理

使用pinia进行状态管理,官网

安装

1
npm i pinia --save

使用

首先需要在main.ts文件中将pinia注册到全局

1
2
3
4
5
6
7
8
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "@/App.vue";

const app = createApp(App);
// 注册pinia,否则会报pinia未安装的错误
app.use(createPinia());
app.mount("#app");

src目录下创建store目录集中管理所有的store

pinia主要由三部分组成,分别是stategettersactions,它们相当于组件中的datacomputedmethod

store可以在组件中使用也可以在其他store中使用

创建 store

使用piniadefineStore方法创建store,该方法共有两个参数,第一个是store的 id,第二个是配置对象,导出的store最好以use开头,保持统一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineStore } from "pinia";

export const useStore = defineStore("store", {
state: () => {
return {
counter: 1,
arr: [1, 2, 3],
};
},

getters: {
getCounter(state) {
return state.counter;
},
},

actions: {
increment() {
this.counter++;
},
},
});
state

state类似组件中的data,也是一个函数,并且返回一个对象,在对象中定义所有的响应属性

使用state

注意

  1. 不能解构store的返回值,否则会失去响应式功能,类似于setup组件中的props
  2. 如果实在想使用解构功能也可以,只是必须满足两个条件。首先是store中没有定义action,第二是必须使用storeToRefs方法将store包裹起来,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { storeToRefs } from "pinia";
import { useStore } from "@/store";

const store = useStore();

// 将store中的值解构出来
const { counter } = storeToRefs(store);

// 使用state的counter属性
store.counter;
</script>
更新state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
import { useStore } from "@/store";

const store = useStore();

// 方法一:直接更新属性
store.count++; // store.count = 2

// 方法二:使用$patch更新state属性,优势:可一次更新多个属性
store.$patch({
counter: store.counter + 1, // 要更新的state属性
arr: [...arr, 4], //每次都会赋值一个新数组
});

// 方法三:对于引用类型数据,每次赋值都相当于创建了一个新的数据,使用回调函数的形式可以直接修改state
store.$patch((state) => {
state.arr.push(4); // 直接在原本的数组上进行修改,不会创建新的数组
});
</script>
更新整个state
1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { useStore } from "@/store";

const store = useStore();

// 重置state
store.$reset();

// 直接更新整个state的值
store.$state = { counter: 12, name: "css" };
</script>

监听state变化

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { useStore } from "@/store";

const store = useStore();

// 监听整个state的变化而不是某个属性的变化
store.$subscribe(
(mutation, state) => {
console.log("state changed!");
},
{ detached: true }
);
</script>
getters

store中的getters属性相当于组件中的computed属性。getter是一个函数,传入的第一个参数是storestate

getter中可以通过使用state参数和this的方式获取state中的属性

getter中使用this调用其他getter

如果需要给getter传参数的话,可以让getter返回一个函数

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
import { defineStore } from "pinia";

export const useStore = defineStore("store", {
state: () => {
return {
counter: 1,
};
},

getters: {
doubleCounter(state) {
// 通过state参数获取属性
return state.counter * 2;
},

getCounter() {
// 通过this获取属性
return this.counter;
},

plusCounter() {
// 通过this使用其他getter
return this.getCounter + 1;
},

increment(state) {
// 通过返回一个函数来实现传参
return (num) => state.counter + num;
},
},
});
actions

store中的actions相当于组件中的method,在action函数中可以通过this访问和修改state中的属性

actions中可以定义同步异步方法

调用action

1
2
3
4
5
6
7
8
<script setup>
import { useStore } from "@/store";

const store = useState();

// 就像调用method一样调用action,也可以传递参数
store.increment(params);
</script>

监听action

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { useStore } from "@/store";

const store = useState();

// 监听action,返回一个函数用于取消监听
const unsubscribe = store.$onAction(
({ name, store, args, after, onError }) => {}
);

// 取消监听action
unsubscribe();
</script>

网络请求

使用axios官网

安装

1
npm i axios --save

功能

  1. 分环境设置请求的baseUrl。至少有两个环境developmentproduction
  2. 支持request / reponse拦截
  3. 支持设置请求代理。由于在本地开发时,服务端接口地址和前端本地项目地址不一致,会导致跨域,故使用请求代理实现跨域,该功能只有在development环境有效

配置

src/utils目录中创建request.js文件,实例化一个axios实例,并进行 1 和 2 两个功能的配置,如下:

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
import axios from "axios";

// 服务端接口地址
const baseURL = import.meta.env.VITE_SERVER_URL;

// 创建一个axios实例,实现自定义
const request = axios.create({
baseURL,
timeout: 10000,
});

// 拦截请求
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 拦截响应
request.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
return response;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
return Promise.reject(error);
}
);

export default request;

使用

src目录下创建api目录统一管理项目接口,最好以页面为单位进行管理,一个文件表示一个页面,文件名最好就是页面名,统一直观

1
2
3
4
5
6
7
8
// home页面用到的接口
import request from "@/utils/request";

export function getList(params: object) {
return request.get("/", {
params,
});
}

区分环境(模式)

可以参考官网进行配置

默认情况下,项目中只有两种模式developmentproduction。开发服务器(dev serve命令)运行在development(开发)模式,而build命令则运行在production(生产)模式。

我们可以在项目根目录下创建.env文件来创建更多的环境(模式),并且可以在文件中创建环境变量,这些环境变量必须是以VITE开头的,否则无效。

1
2
# .env.development,定义开发模式下的环境变量
VITE_SERVER_URL=127.0.0.1:3000

可以通过以下方式使用环境变量

1
const baseURL = import.meta.env.VITE.SERVER_URL;

可以在运行命令时添加--mode参数指定运行模式。例如vite serve --mode staging命令就会读取根目录下的.env.staging文件

1
2
3
4
{
"dev": "vite",
"dev:staging": "vite serve --mode staging"
}

vite 配置

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
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import ElementPlus from "unplugin-element-plus/vite";
import path from "path";
import devConfig from "./config/vite.config.dev";
import stagingConfig from "./config/vite.config.staging";
import prodConfig from "./config/vite.config.prod";

function resolve(dir) {
return path.resolve(__dirname, dir);
}

// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
return {
plugins: [vue(), ElementPlus()],
resolve: {
alias: {
"@": resolve("./src"),
},
},
server: {
host: true,
// 配置代理
proxy: {
api: {
target: "127.0.0.1:3000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
};
});

数据 mock