前言
在日常开发中,我们经常需要新建项目,然而项目的初始化是比较繁琐的。因此开发一个公共项目模板,在创建新项目时基于此模板进行修改可以减少很多工作量
功能
该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
,需要使用useI18n
hook 来实现
获取当前语言的翻译文案
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