前言
在日常开发中,我们经常需要新建项目,然而项目的初始化是比较繁琐的。因此开发一个公共项目模板,在创建新项目时基于此模板进行修改可以减少很多工作量
功能
该vue3模板应具有以下功能
- 国际化
typescript
- 前端路由
- 区分环境
- 构建、打包
- 代码检查 lint、husky
- gitlab CI/CD
- 数据 mock
- 网络请求
- 状态管理 store
- 支持 ssr
实现
本项目中默认全部使用**Composition API和setup语法糖语法,因此所安装的依赖包**都支持此写法
国际化
使用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";
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";
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, "baseUrl": ".", "paths": { "@/*": ["src/*"] } }
|
代码检查
使用eslint prettier stylelint进行代码检查,以及husky lint-staged进行预提交检查
安装
安装husky 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 }, "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: { "vue/multi-word-component-names": 0, "@typescript-eslint/no-explicit-any": ["error", { ignoreRestArgs: true }], "@typescript-eslint/no-explicit-any": 0, }, };
|
状态管理
使用pinia进行状态管理,官网
安装
使用
首先需要在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);
app.use(createPinia()); app.mount("#app");
|
在src目录下创建store目录集中管理所有的store
pinia主要由三部分组成,分别是state、getters、actions,它们相当于组件中的data、computed、method
store可以在组件中使用也可以在其他store中使用
创建 store
使用pinia的defineStore方法创建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
注意
- 不能解构
store的返回值,否则会失去响应式功能,类似于setup组件中的props
- 如果实在想使用解构功能也可以,只是必须满足两个条件。首先是
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();
const { counter } = storeToRefs(store);
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.$patch({ counter: store.counter + 1, arr: [...arr, 4], });
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();
store.$reset();
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();
store.$subscribe( (mutation, state) => { console.log("state changed!"); }, { detached: true } ); </script>
|
getters
store中的getters属性相当于组件中的computed属性。getter是一个函数,传入的第一个参数是store的state
在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) { return state.counter * 2; },
getCounter() { return this.counter; },
plusCounter() { 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();
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();
const unsubscribe = store.$onAction( ({ name, store, args, after, onError }) => {} );
unsubscribe(); </script>
|
网络请求
使用axios,官网
安装
功能
- 分环境设置请求的
baseUrl。至少有两个环境development和production
- 支持
request / reponse拦截
- 支持设置请求代理。由于在本地开发时,服务端接口地址和前端本地项目地址不一致,会导致跨域,故使用请求代理实现跨域,该功能只有在
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;
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) { return response; }, function (error) { return Promise.reject(error); } );
export default request;
|
使用
在src目录下创建api目录统一管理项目接口,最好以页面为单位进行管理,一个文件表示一个页面,文件名最好就是页面名,统一直观
1 2 3 4 5 6 7 8
| import request from "@/utils/request";
export function getList(params: object) { return request.get("/", { params, }); }
|
区分环境(模式)
可以参考官网进行配置
默认情况下,项目中只有两种模式development和production。开发服务器(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); }
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