逐步搭建出團隊的 vue3 前端架構
作者:codexu
https://juejin.cn/post/7025524870842679310
前言
由於 vue3.2 版本的發佈,<script setup>
的實驗性標誌已經去掉,這說明這個語法提案已經正式開始使用,並且我個人對這個方案表示非常喜歡,其他的更新 [1] 請自行了解。到目前爲止,我認爲 vue3 已經完全可以用於生產環境。在此將我的開發體驗,總結至此,分享給大家。
我認爲前端架構核心工作是定製一套適合當前業務需求的解決方案,從而降低需求的增加而帶來的技術實現的複雜度。下面我將從 16 個方向,逐漸帶領大家搭建一套屬於你自己的腳手架,制定一套合理的解決方案,爲項目打下良好的基礎,與同伴形成合適的開發習慣。
由於篇幅問題,以講解實現思路爲主,希望大家友善發言,共同進步!
目錄
-
- 搭建腳手架 [2]
-
1.1 前端腳手架應具備哪些功能?[3]
-
1.2 如何開發一款自己的腳手架?[4]
-
1.3 如何根據選項生成文件?[5]
-
- 基於 vite 的搭建基礎模板 [6]
-
2.1 創建基本模板項目 [7]
-
2.2 常用插件推薦 [8]
-
- 使用 Typescript[9]
-
- 配置環境變量 [10]
-
4.1 配置模式 [11]
-
4.2 常用的環境變量 [12]
-
4.3 封裝靜態資源文件 [13]
-
4.4 封裝
SVG
的圖標組件 [14] -
- 按需自動引入組件 [15]
-
5.1 安裝與配置 [16]
-
5.2 改變全局組件註冊方式 [17]
-
5.3 自動引入組件庫 [18]
-
- 樣式 [19]
-
6.1 預設基礎樣式 [20]
-
6.2 CSS 預處理器 [21]
-
6.3 開啓 scoped[22]
-
6.4 深度選擇器 [23]
-
- 佈局 [24]
-
7.1 常規的佈局 [25]
-
7.2 特殊的佈局 [26]
-
- 集成 Tailwind.css[27]
-
8.1 效率提升 [28]
-
8.2 JIT 模式 [29]
-
8.3 關於打包體積 [30]
-
9.vuex 替代方案 pinia[31]
-
9.1 爲什麼採用 Pinia ?[32]
-
9.2 創建 Store[33]
-
9.3 State[34]
-
9.4 Getters[35]
-
9.5 Actions[36]
-
9.6 Devtools[37]
-
- 基於 mitt 處理組件間事件聯動 [38]
-
10.1 爲什麼選擇 mitt ?[39]
-
10.2 嚴重警告 [40]
-
10.3 如何使用 mitt ?[41]
-
- 異步請求 [42]
-
11.1 基於 axios 的封裝 [43]
-
11.2 爲 axios 增加泛型的支持 [44]
-
11.3 封裝更方便的 useRequest[45]
-
11.4 統一的 API 接口管理 [46]
-
11.5 mock[47]
-
- 路由 [48]
-
12.1 創建路由三部曲 [49]
-
12.2 使用 meta 豐富你的路由 [50]
-
- 項目性能與細節優化 [51]
-
13.1 開啓 gzip[52]
-
13.2 頁面載入進度條 [53]
-
13.3 Title[54]
-
13.4 解決移動端使用 vh 的問題 [55]
-
13.5 可以常駐的 JavaScript 庫 [56]
-
- 代碼風格與流程規範 [57]
-
14.1 ESLint[58]
-
14.2 StyleLint[59]
-
14.3 代碼提交規範 [60]
-
- 編寫使用文檔 [61]
-
15.1 使用 vitepress 搭建文檔 [62]
-
15.2 文檔部署 [63]
-
- 插件 [64]
-
16.1 VSCode 插件 [65]
-
16.2 Chrome 插件 [66]
-
源碼 [67]
-
參考 [68]
- 搭建腳手架
使用 vue-cli
或 vite
,通過一系列的配置,初始化一個開發模板,無需從零開始搭建開發環境,可以有效的提升開發效率,相信也是大多數開發者接手一個新項目所使用的一種方式。儘管官方提供的腳手架已經足夠優秀,但未必是真正符合我們自己團隊的使用習慣,所以從官方的基礎上,開發一款屬於我們自己的腳手架,能更多的提升開發效率。
1.1 前端腳手架應具備哪些功能?
-
減少重複的初始化工作,不需要再複製其他類似的項目刪除無關代碼,或從零搭建一個項目。
-
可以根據團隊需求,使用簡單的交互操作生成相應的目錄結構和文件。
-
統一團隊的開發習慣、代碼風格,保證構建結果的一致性。
-
完整的使用文檔,降低新人上手、開發和後期維護成本。
1.2 如何開發一款自己的腳手架?
提到構建前端工程化中腳手架,相信大家已經看過不少文章,幾年前我也曾經寫過一篇關於腳手架構建的文章 [69],隨便搜一下關鍵詞可以看到很多相關的文章,在這裏不做太多的介紹,主要講一些這些文章中很少提到的如何根據選項生成文件。
1.3 如何根據選項生成文件?
說實話我也不知道大佬們是怎麼根據各種配置編譯成相應的文件,這塊希望大家踊躍發言,尋求一種更佳高效簡潔的方式。在這裏跟大家分享一下我的方案:
交互方面,搭建過腳手架的同學一定知道 inquirer[70],這個庫可以很方便的通過交互式操作獲取到我們選擇的一些自定義配置參數。那麼問題來了,如何通過這些配置相應的創建對應的文件呢?
這裏我推薦使用 EJS[71] + Prettier[72] 生成代碼,通過 fs-extra[73] 寫入最終的文件。
- EJS
EJS 是一款 JavaScript 模板引擎,我們可以通過傳入參數,生成對應的代碼串,例如創建一個 package.ejs
用來生成 package.json
中,如果我們選擇使用了 scss
作爲 CSS 預處理器,然後將 sass
和 stylelint-scss
作爲項目的安裝依賴:
<% if (precss === 'scss') { -%>
"sass": "1.26.5",
"stylelint-scss": "^3.20.1",
<% } -%>
複製代碼
模板引擎可以幫你通過參數生成代碼,它並不會限制你生成任何類型的代碼文件,因爲我們生成的是純代碼,最後通過讀取 .ejs
文件對應生成相應的類型文件即可。
- Prettier
Prettier 是一款代碼格式化工具,相信大家對它並不陌生。使用 EJS 生成的目的還是給開發人員閱讀和編輯,所以生成的代碼應該符合最終的格式要求,因爲後續我們會爲腳手架添加 ESLint 和 StyleLint 等工具,剛剛創建的項目裏面一堆紅線報錯可是十分不友好的。
import prettier = require("prettier");
prettier.format(code, { parser: 'json' }))
複製代碼
parser
是 prettier 的解析器,常見的 typescript、css、less、json 等文件都可以進行格式化。
- 基於 vite 的搭建基礎模板
最早搭建 vue3 腳手架的時候,我選擇的用 vue/cli 搭建,因爲生態不健全,有些基於 webpack 的功能無法使用,但現在 vite 生態已經比較完善了,所以重構腳手架,由 webpack 轉向 vite,這一步極大的提升了開發體驗。
2.1 創建基本模板項目
npm init vite@latest
yarn create vite
pnpm create vite
複製代碼
然後按照提示操作即可,vite 提供的選項很少,只有 vue 或 vue + ts,不像 vue/cli 提供那麼多的配置方式,所以剩下的東西需要我們手動配置。
當然 vite 也提供了很多模板,但是我認爲做加法比做減法更加容易,在衆多的模板中很難找到適合我們自己的。
2.2 常用插件推薦
這裏先簡單瞭解幾個好用的 vite 插件:
-
unplugin-vue-components[74]:組件的按需自動導入。
-
vite-plugin-svg-icons[75]:用於生成 svg 雪碧圖。
-
vite-plugin-compression[76]:使用 gzip 或者 brotli 來壓縮資源。
爲什麼只推薦這麼幾個插件?因爲 vite
對許多 webpack
需要安裝的 loader
或 plugin
都有着天生的支持,比如 less、sass、typescript,後續會在相應的章節說明用法。
- 使用 Typescript
vue2.x 版本對 TypeScript 的支持是硬傷,而 TypeScript 對大型項目的保障能力是被普遍認可的。這一點在 vue3.x 版本中得到了非常友好的支持。
Vite 天然支持引入
.ts
文件。
這裏對 tsconfig.json 做了一些修改:
{
"compilerOptions": {
"types": ["vite/client"],
"baseUrl": "src",
"paths": {
"@/*": ["./*"]
}
},
"exclude": ["node_modules"]
}
複製代碼
在初期使用 typeScript 的時候,很多人都很喜歡使用 any 類型,把 typeScript 寫成了 anyScript ,雖然使用起來很方便,但是這就失去了 typeScript 的類型檢查意義了,當然寫類型的習慣是需要慢慢去養成的,不用急於一時。
- 配置環境變量
vite 提供了兩種模式:具有開發服務器的開發模式(development)和生產模式(production)。
這裏我們可以建立 4 個 .env
文件,一個通用配置和三種環境:開發、測試、生產。
4.1 配置模式
NODE_ENV=development # 開發模式
NODE_ENV=production # 生產模式
複製代碼
-
.env 通用配置,我個人喜歡把他當作項目的配置文件,例如項目的 title,此文件不對應任何模式。
-
.env.development 開發環境,使用 development 模式。
-
.env.staging 測試環境,因爲要部署到測試服務器,或本地使用 serve 命令預覽,所以使用 production 模式。
-
.env.production 生產環境,因爲要部署到測試服務器,或本地使用 serve 命令預覽,所以使用 production 模式。
package.json 內 script 需要增加 staging 命令
"script": {
"build": "vue-tsc --noEmit && vite build",
"staging": "vue-tsc --noEmit && vite build --mode staging",
"serve": "vite preview --host"
}
複製代碼
4.2 常用的環境變量
推薦使用以下常見的三個變量:
- VITE_APP_BASE_URL
接口請求地址。
通常後端會區分三種環境,部署在不同的地址下。
- VITE_APP_STATIC_URL
靜態資源地址。
靜態資源我是不建議你直接放在項目中,這會導致項目倉庫變得巨大。
本地開發和測試環境我會選在使用本地搭建的靜態資源服務器,你可以找後端運維的同學幫你搭建,或者你使用 http-server 在本地啓動一個服務器也可以。生產環境建議上傳至 OSS。
- VITE_PUBLIC_PATH
構建資源公共路徑。
這個與 vue/cli 中的 publicPath 同理,有的時候你構建的項目並不是存放在跟路徑下,例如 http://ip:port/{項目名}
。
4.3 封裝靜態資源文件
如果你配置了 VITE_APP_STATIC_URL 靜態資源環境變量,那麼你需要封裝以下兩個東西:
-
根據環境返回實際的資源地址函數。
-
方便使用的靜態資源組件。
baseStaticUrl.ts
// 處理靜態資源鏈接
export default function baseStaticUrl(src = '') {
const { VITE_APP_STATIC_URL } = import.meta.env;
if (src) {
return `${VITE_APP_STATIC_URL}${src}`;
}
return VITE_APP_STATIC_URL as string;
}
複製代碼
靜態資源組件
靜態資源主要有圖片、音頻和視頻三種常見的形式。
-
通過 src 寫入相對的路徑,使用上述的函數來補全完整的路徑,即可在不同的環境下使用不同地址的靜態資源。
-
通過 type 傳入圖片、音頻和視頻的類型。
-
autoplay 是解決以視頻爲背景的情況下,視頻無法自動播放的問題。
<script lang="ts" setup>
import { computed, ref, Ref, withDefaults, onMounted, watch } from 'vue';
import { baseStaticUrl } from '@/libs/utils';
import useDevice from '@/hooks/useDevice';
interface Props {
src?: string;
type?: string;
autoplay?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
src: '',
type: 'img',
autoplay: true,
});
const envSrc = computed(() => baseStaticUrl(props.src));
// 處理視頻自動播放(解決 chrome 無法自動播放的問題)
const { deviceType } = useDevice();
const poster = computed(() =>
deviceType.value === 'desktop' ? '' : baseStaticUrl(props.src),
);
const videoRef: Ref<HTMLVideoElement | null> = ref(null);
// 解決移動端視頻無法自動播放的問題
function videoAutoPlay() {
if (props.type === 'video' && videoRef.value !== null) {
videoRef.value.src = baseStaticUrl(props.src);
}
if (props.autoplay && videoRef.value) {
videoRef.value.oncanplay = () => {
if (videoRef.value) videoRef.value.play();
};
}
}
// 自動播放視頻
onMounted(() => { videoAutoPlay();});
// 監聽視頻 src,如果存在則自動播放
watch(envSrc, () => { if (videoRef.value) videoRef.value.play(); });
</script>
<script lang="ts">
export default { name: 'StaticFile' };
</script>
<template>
<img v-if="type === 'img'" :src="envSrc" />
<video ref="videoRef" v-else-if="type === 'video'" muted :poster="poster" />
<audio v-else :src="envSrc" />
</template>
複製代碼
4.4 封裝 SVG
的圖標組件
svg 圖標比較小,而且都是可讀的 xml 文本,所以我們把它直接放在項目中即可,通過 vite-plugin-svg-icons
插件,實現自動引入 svg 圖標。
配置 vite.config.ts:
plugins: [
viteSvgIcons({
iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]',
}),
]
複製代碼
封裝一個 vue 組件:
<script setup lang="ts">
import { computed, withDefaults } from 'vue';
interface Props {
prefix?: string;
name?: string;
color?: string;
}
const props = withDefaults(defineProps<Props>(), {
prefix: 'icon',
name: '',
color: '#000',
});
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
</script>
<template>
<svg aria-hidden="true">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
複製代碼
首先將下載的 .svg 圖標放入 @/assets/icons 文件夾下
<svg-icon />
複製代碼
-
name 放置在 @/assets/icons 文件夾下的文件名。
-
color 顏色填充,使用此項會默認覆蓋圖標顏色。
- 按需自動引入組件
unplugin-vue-components[77] 是一款非常強大的插件(極力推薦),核心功能就是幫助你自動按需引入組件,Tree-shakable,只註冊你使用的組件。這裏說一下他的兩個核心使用方式和配置方式。
此插件不僅支持 vue3,同時也支持 vue2,並且支持 Vite、Webpack、Vue CLI、Rollup。
5.1 安裝與配置
安裝:
npm i unplugin-vue-components -D
複製代碼
配置:
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
Components({ /* options */ }),
],
})
複製代碼
這裏的 options 可以配置一些選項,後面提到的組件庫註冊會使用到。
5.2 改變全局組件註冊方式
我們通常將全局的組件封裝在 @/src/components
中,然後通過 app.component()
註冊全局組件。使用此插件後,無需手寫註冊,直接在模板中使用組件即可:
這裏引入官方的示例:
<template>
<div>
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
複製代碼
自動編譯爲:
<template>
<div>
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</div>
</template>
<script>
import HelloWorld from './src/components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
複製代碼
5.3 自動引入組件庫
在使用組件庫時,常規組件我們也會註冊到全局,如果使用局部註冊由於頁面中會使用到多個組件,會非常麻煩,所以這個功能絕佳,例如我們使用 ant-design-vue 組件庫。
直接在模板中使用即可,無需手動註冊或局部引用:
<template>
<a-button>按鈕</a-button>
</template>
複製代碼
當然,你還需要在 vite 中引入它的解析器:
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
Components({
resolvers: [
AntDesignVueResolver(),
]
})
],
})
複製代碼
目前支持的解析器,根據你的喜好去選擇:
-
Ant Design Vue[78]
-
Element Plus[79]
-
Element UI[80]
-
Headless UI[81]
-
IDux[82]
-
Naive UI[83]
-
Prime Vue[84]
-
Vant[85]
-
VEUI[86]
-
Varlet UI[87]
-
View UI[88]
-
Vuetify[89]
-
VueUse Components[90]
-
Quasar[91]
- 樣式
項目中最好使用通用樣式,可以創建 src/styles
目錄存放,這裏推薦一些分類:
styles
├── antd # 組件庫樣式覆蓋,命名自取,這裏以 ant design 爲例
├── color.less # 顏色
├── index.less # 入口
├── global.less # 公共類
├── transition.less # 動畫相關
└── variable.less # 變量
複製代碼
6.1 預設基礎樣式
相信用過 normalize[92] 的同學不在少數,它可以重置 css 樣式,使各瀏覽器效果保持一致。後面的章節會提到 tailwind.css,它內置了預設樣式重置的功能,與 normalize 還是有一定的區別,有興趣的同學可以瞭解一下 [93]。
6.2 CSS 預處理器
雖然 vite 原生支持 less/sass/scss/stylus,但是你必須手動安裝他們的預處理器依賴,例如:
npm install -D less
複製代碼
如何選擇預處理器?
推薦使用你是所使用的組件庫的樣式語言,因爲 css 預處理器學會一種後,入手其他幾乎沒有學習成本。
6.3 開啓 scoped
沒有加 scoped 屬性,會編譯成全局樣式,造成全局污染。
<style scoped></style>
複製代碼
6.4 深度選擇器
有時我們可能想明確地制定一個針對子組件的規則。
如果你希望 scoped 樣式中的一個選擇器能夠作用得 “更深”,例如影響子組件,你可以使用 >>> 操作符。有些像 Sass 之類的預處理器無法正確解析 >>>。這種情況下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——兩者都是 >>> 的別名,同樣可以正常工作。
- 佈局
頁面整體佈局是一個產品最外層的框架結構,往往會包含導航、頁腳、側邊欄等。在頁面之中,也有很多區塊的佈局結構。在真實項目中,頁面佈局通常統領整個應用的界面,有非常重要的作用,所以單獨拆分出來也是非常有必要的。
在腳手架中,所有的通用佈局組件都應該放在 src/layouts 中,這種封裝比較簡單,這裏就不貼代碼了,大家按照自己實際情況自行發揮,在此僅提供一下封裝思路。
7.1 常規的佈局
BasicLayout
基礎頁面佈局,包含了頭部導航,側邊欄等。
BlankLayout
空白的佈局。
7.2 特殊的佈局
RouteLayout
如果你的項目在路由切換中需要對某些二級頁面進行緩存,那麼推薦你創建一個 RouteLayout,通過路由 meta
中的配置,返回 router-view
或者使用 keep-alive
包裹的 router-view
。
UserLayout
用於用戶登錄註冊等頁面抽離出來。
PageLayout
基礎佈局,包含了麪包屑等信息,內含 slot。
- 集成 Tailwind.css
Tailwind.css[94] 在我第一次看到它的時候,內心是比較反感的,但實際上手之後又覺得真香。從 vue2 項目中,我已經引入了 tailwind,整體的開發結果就是,基本很少再使用 <style>
標籤去轉本定義一些 class 和樣式,畢竟起名字這種事,一個是涉及到規範,一個是涉及到英語。如果你選擇 tailwind,CSS 預處理器的作用就會顯得微乎其微,因爲你無需再自定定義各種變量和 mixins。
總體來說,學習成本並不高,花上兩個小時足夠上手,記住不用死記硬背那些類名。
8.1 效率提升
很多人總是說樣式要與 HTML 分離,現在爲什麼又要提倡 tailwind 這種與 HTML 緊密結合的工具?這是因爲現在使用 vue 這類框架已經高度組件化,樣式分離是爲了方便複用和維護,但在組件化面前樣式分離只能是降低開發效率。
下面介紹一下 tailwind 提供了哪些提升效率的功能:
-
提供了大量的功能類,極大的提高了可維護性。
-
響應式設計,各種設備一把梭。
-
懸停、焦點和其它狀態。
-
深色模式。
-
支持配置,例如顏色方面很難做到跟你的設計師統一。
-
不用爲起名字而糾結???
8.2 JIT 模式
如果你的環境支持 postcss8( vue/cli 構建的 vue2 項目是 postcss7 ),那麼 JIT 模式直接帶你起飛。
-
超快的構建速度。
-
支持變體,你甚至可以這麼寫
sm:hover:active:disabled:opacity-75
。 -
支持任意樣式,例如
md:top-[-113px]
。 -
開發和生產環境結果是一致的,(我在 vue2 項目中就遇到過組件庫構建結果不一致的問題)。
如果你使用 vscode 那你一定要安裝 Tailwind CSS IntelliSense[95] 插件,它可以自動補全類名,顯著降低學習成本。
8.3 關於打包體積
使用默認配置,未壓縮是 3739.4kB ,Gzip 壓縮 是 293.9kB,Brotli 壓縮 是 73.2kB。這似乎看起來很大,這是因爲 tailwind 提供了成千上萬的功能類,其中絕大部分你不會使用到。
當構建生產時,你應該使用 purge 選項來 tree-shake 優化未使用的樣式,並優化您的最終構建大小當使用 Tailwind 刪除未使用的樣式時,很難最終得到超過 10kb 的壓縮 CSS。
還有一點,Atom CSS
極大的提升了樣式的複用程度,從而直接降低了構建體積。
9.vuex 替代方案 pinia
由於 vuex 4
對 typescript 的支持讓人感到難過,所以狀態管理棄用了 vuex 而採取了 pinia[96]。
忘記在哪看到,尤大好像說 pinia[97] 可能會代替 vuex,所以請放心使用。
9.1 爲什麼採用 Pinia ?
-
Pinia 的 API 設計非常接近
Vuex 5
的提案 [98]。(作者是 Vue 核心團隊成員) -
無需像
Vuex 4
自定義複雜的類型來支持 typescript,天生具備完美的類型推斷。 -
模塊化設計,你引入的每一個 store 在打包時都可以自動拆分他們。
-
無嵌套結構,但你可以在任意的 store 之間交叉組合使用。
-
Pinia 與 Vue devtools 掛鉤,不會影響 Vue 3 開發體驗。
下面簡單的介紹一下如何使用 Pinia,並對比 vuex 有哪些區別與注意事項,具體請參考官方文檔 [99]。
9.2 創建 Store
Pinia 已經內置在腳手架中,並且與 vue 已經做好了關聯,你可以在任何位置創建一個 store:
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () =>({}),
getters: {},
actions: {}
})
複製代碼
這與 Vuex 有很大不同,它是標準的 Javascript 模塊導出,這種方式也讓開發人員和你的 IDE 更加清楚 store 來自哪裏。
Pinia 與 Vuex 的區別:
-
id 是必要的,它將所使用 store 連接到 devtools。
-
創建方式:
new Vuex.Store(...)
(vuex3),createStore(...)
(vuex4)。 -
對比於 vuex3 ,state 現在是一個函數返回對象。
-
沒有 mutations,不用擔心,state 的變化依然記錄在 devtools 中。
9.3 State
創建好 store 之後,可以在 state 中創建一些屬性了:
state: () => ({ name: 'codexu', age: 18 })
複製代碼
將 store 中的 state 屬性設置爲一個函數,該函數返回一個包含不同狀態值的對象,這與我們在組件中定義數據的方式非常相似。
在模板中使用 store:
現在我們想從 store 中獲取到 name 的狀態,我們只需要使用以下的方式即可:
<h1>{{userStore.name}}</h1>
const userStore = useUserStore()
return { userStore }
複製代碼
注意這裏並不需要 userStore.state.name
。
雖然上面的寫法很舒適,但是你一定不要用解構的方式去提取它內部的值,這樣做的話,會失去它的響應式:
const { name, email } = useUserStore()
複製代碼
9.4 Getters
Pinia 中的 getter 與 Vuex 中的 getter 、組件中的計算屬性具有相同的功能,傳統的函數聲明使用 this 代替了 state 的傳參方法,但箭頭函數還是要使用函數的第一個參數來獲取 state ,因爲箭頭函數處理 this 的作用範圍:
getters: {
nameLength() {
return this.name.length
},
nameLength: state => state.name.length,
nameLength: ()=> this.name.length ❌
}
複製代碼
9.5 Actions
這裏與 Vuex 有極大的不同,Pinia 僅提供了一種方法來定義如何更改狀態的規則,放棄 mutations 只依靠 Actions,這是一項重大的改變。
Pinia 讓 Actions 更加的靈活:
-
可以通過組件或其他 action 調用
-
可以從其他 store 的 action 中調用
-
直接在商店實例上調用
-
支持同步或異步
-
有任意數量的參數
-
可以包含有關如何更改狀態的邏輯(也就是 vuex 的 mutations 的作用)
-
可以
$patch
方法直接更改狀態屬性
actions: {
async insertPost(data){
await doAjaxRequest(data);
this.name = '...';
}
}
複製代碼
9.6 Devtools
腳手架已內置下面的代碼,這將添加 devtools 支持:
import { createPinia, PiniaPlugin } from 'pinia'
Vue.use(PiniaPlugin)
const pinia = createPinia()
複製代碼
時間旅行功能貌似已經可以使用了,這塊後續會關注。
- 基於 mitt 處理組件間事件聯動
如果你曾經是 Vue2.x 的開發者,那麼請閱讀下面引用官方文檔 [100] 的一段話:
我們從實例中完全移除了
$on
、$off
和$once
方法。$emit
仍然包含於現有的 API 中,因爲它用於觸發由父組件聲明式添加的事件處理函數。在 Vue 3 中,已經不可能使用這些 API 從組件內部監聽組件自己發出的事件了,該用例暫沒有遷移的方法。但是該 eventHub 模式可以被替換爲實現了事件觸發器接口的外部庫,例如
mitt
或tiny-emitter
。
10.1 爲什麼選擇 mitt ?
-
足夠小,僅有 200bytes。
-
支持全部事件的監聽和批量移除。
-
無依賴,不論是什麼框架都可以直接使用。
10.2 嚴重警告
我們已經無法在項目中使用 eventBus,僅推薦你在特殊場合下使用 mitt,它並不是開發的常態,你一定要確保知道自己在做什麼?否則你的項目將難以維護!!!
10.3 如何使用 mitt ?
在使用 mitt 前建議請閱讀官方文檔 [101]:
腳手架默認提供一個可以直接使用的對象:
import emitter from '@/libs/emitter';
複製代碼
當然你也可以引入已經安裝好的 mitt:
import mitt from 'mitt'
const emitter = mitt()
複製代碼
mitt 提供了非常簡單的 API,下面代碼是官方演示:
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
emitter.emit('foo', { a: 'b' })
// clearing all events
emitter.all.clear()
// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
複製代碼
- 異步請求
絕大多數項目想必逃脫不了接口的對接,如果你的項目存在大量的接口,我建議做到以下幾點:
-
封裝請求。
-
統一的 API 接口管理。
-
Mock 數據功能(根據需求斟酌使用)。
上述的主要目的就是在幫助我們簡化代碼和利於後期的更新維護。
11.1 基於 axios 的封裝
相信開發過 vue2 項目的同學已經對 axios 非常熟悉的,在這裏提供一些封裝的思路:
-
通過
import.meta.env.VITE_APP_BASE_URL
獲取環境變量,配置baseURL
,如果接口存在多個不同域名,可以通過 js 變量控制。 -
設置
timeout
請求超時、斷網情況處理。 -
設置請求頭,攜帶
token
。 -
異常攔截處理,後端通過你攜帶的
token
判斷你是否過期,如果返回401
你可能需要跳轉到登錄頁面,並提示需要重新登錄。 -
響應攔截,通常後端返回 code、data、msg,如果是請求正常,我們可以直接返回 data 數據,如果是異常的 code,我們也可以在這裏直接彈出報錯提示。
-
無感刷新 token,如果你的 token 過期,可以通過後端返回的 refreshToken 調用刷新接口,獲取新的 token。當然這裏涉及到很多細節,例如終端請求、重新發送請求、重新請求列隊。
-
中斷請求,例如頁面切換時,我們要中斷正在發生的請求。
相關代碼(僅供參考)[102]
11.2 爲 axios 增加泛型的支持
到目前爲止,axios 請求返回的類型是 any,這時我們對請求後的數據進行操作時,沒有享受到 ts 帶來的類型提示,這顯然不符合我們的預期。
這時我們要做的就是重新聲明 axios 模塊:新建一個 shims.d.ts,然後在調用時加上泛型。
import { AxiosRequestConfig } from 'axios';
declare module 'axios' {
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<T>;
request<T = any>(config: AxiosRequestConfig): Promise<T>;
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
}
}
複製代碼
做好這一步後,你就必須在創建接口時,聲明請求相應數據的類型。
11.3 封裝更方便的 useRequest
設想一下,編寫請求代碼時,我們通常會定義這麼幾個變量:
-
data: 儲存請求數據
-
loading: 請求加載狀態
尤其是 loading,我們需要在請求前設置爲 true,請求結束後設置爲 false。
上面的封裝方式,是對基礎的功能封裝,因爲我們在使用 vue3,所以可以進行再一次的封裝成爲 hook,我們使用起來會更加方便。
例如下面這個樣子:
使用 useRequest 定義一個接口:
export default getUserInfo(id) {
return useRequest({
method: 'get',
url: '/api/user',
params: { id }
})
}
複製代碼
使用此接口:
const { data, loading } = getUserInfo();
複製代碼
注意這裏的 data 是響應式的。
這是我想到的一種思路,目前還沒有做很好的封裝,相關代碼僅供參考 [103],你也可以借鑑一些成熟方案,比如 vueuse 中的 useFetch[104],但是他是基於 Fetch API 設計的,並不符合我的預期要求,有更好的方案請大家在下面留言。
11.4 統一的 API 接口管理
自從前端和後端分家之後,前後端接口對接就成爲了常態,而對接接口的過程就離不開接口文檔,比較主流就是 Swagger,但是如何在前端項目中更好的去管理跟後端對接的接口呢?
在 src 目錄中 創建 api 目錄,內部目錄應按照後端制定的模塊創建。
每個模塊中創建多個 ts 文件,一個接口應對應一個 ts 文件,其中包含了以下內容:
-
請求參數的類型聲明。
-
響應數據的類型聲明。
-
返回定義好的請求函數(url、method、params、data 等)。
統一去定義和管理 API 接口,只要後端規範的命名和你認真的寫好類型聲明,對前端來說 typescript 就是最好的接口文檔。
11.5 mock
vite 使用 mock 數據非常簡單,你可以使用 vite-plugin-mock[105] 插件,如果你瞭解 mockjs,你可以快速上手。
- 路由
路由和菜單是組織起一個應用的關鍵骨架。
12.1 創建路由三部曲
通常一個項目需要做到這幾步:
-
使用 createRouter 創建路由,這時候根據需求選擇 Hash 路由或者 History 路由。
-
根據業務需求配置路由,注意這裏很可能就用到前文提到過的佈局組件。
-
如果有權限相關的業務,你需要創建 permission.ts 在路由鉤子觸發時做一些事情。
如果你的頁面比較多,建議你創建 routes 目錄,分模塊聲明路由。
參考代碼 [106]
12.2 使用 meta 豐富你的路由
vue-router4.x 支持 typescript,配置路由的類型是 RouteRecordRaw,這裏 meta 可以讓我們有更多的發揮空間,這裏提供一些參考:
-
title: string; 頁面標題,通常必選。
-
icon?: string; 圖標,一般配合菜單使用。
-
auth?: boolean; 是否需要登錄權限。
-
ignoreAuth?: boolean; 是否忽略權限。
-
roles?: RoleEnum[]; 可以訪問的角色
-
keepAlive?: boolean; 是否開啓頁面緩存
-
hideMenu?: boolean; 有些路由我們並不想在菜單中顯示,比如某些編輯頁面。
-
order?: number; 菜單排序。
-
frameUrl?: string; 嵌套外鏈。
這裏只提供一些思路,每個項目多多少少會涉及到這些問題,具體如何實現請查閱資料自行解決。
- 項目性能與細節優化
13.1 開啓 gzip
開啓 gzip 可以極大的壓縮靜態資源,對頁面加載的速度起到了顯著的作用。
使用 vite-plugin-compression[107] 可以 gzip 或 brotli 的方式來壓縮資源,這一步需要服務器端的配合,vite 只能幫你打包出 .gz
文件。此插件使用簡單,你甚至無需配置參數,引入即可。
13.2 頁面載入進度條
頁面路由切換時,附帶一個加載進度條會顯得非常友好,不至於白屏時間過長,讓用戶以爲頁面假死。
這時候我們可以用到 nprogress[108],在路由切換時開啓和關閉:
import NProgress from 'nprogress';
router.beforeEach(async (to, from, next) => {
NProgress.start();
});
router.afterEach((to) => {
NProgress.done();
});
複製代碼
13.3 Title
在不同的路由下顯示不同的標題是常規的操作,我們可以通過路由鉤子獲取 meta 中的 title 屬性改變標籤頁上的 title。
你可以使用 vueuse 提供的 useTitle[109],或者 window.document.title 自行封裝。
你也可以通過環境變量將你的主標題拼接在路由標題的後面:
const { VITE_APP_TITLE } = import.meta.env;
複製代碼
13.4 解決移動端使用 vh 的問題
有興趣的同學可以嘗試一下 chrome 移動端瀏覽器上的 100vh,是真正的視口高度的 100% 嘛。
爲了解決這一問題,我們可以通過 postCss 插件解決。
安裝 postcss-viewport-height-correction[110] 插件:
npm install -D postcss-viewport-height-correction
複製代碼
在 postcss.config.js 中增加 plugin:
module.exports = {
plugins: {
'postcss-viewport-height-correction': {},
},
}
複製代碼
添加這一段 js 代碼在全局,你可以直接添加在 index.html 上即可:
const customViewportCorrectionVariable = 'vh';
function setViewportProperty(doc) {
let prevClientHeight;
const customVar = `--${customViewportCorrectionVariable || 'vh'}`;
function handleResize() {
const { clientHeight } = doc;
if (clientHeight === prevClientHeight) return;
requestAnimationFrame(function updateViewportHeight() {
doc.style.setProperty(customVar, `${clientHeight * 0.01}px`);
prevClientHeight = clientHeight;
});
}
handleResize();
return handleResize;
}
window.addEventListener('resize', setViewportProperty(document.documentElement));
複製代碼
13.5 可以常駐的 JavaScript 庫
-
前文提到過的 vueuse[111],非常強大,強烈建議嘗試。
-
lodash[112],用了都說好,早用早下班。
- 代碼風格與流程規範
14.1 ESLint
不管是多人合作還是個人項目,代碼規範都是很重要的。這樣做不僅可以很大程度地避免基本語法錯誤,也保證了代碼的可讀性。
這裏推薦使用 airbnb 規範。
配置參考 [113]
14.2 StyleLint
儘管前文提到過 tailwind,可以讓你幾乎不寫 css,但是涉及到團隊協作,這一點也要嚴謹。
StyleLint 是一個強大的、現代化的 CSS 檢測工具, 與 ESLint 類似, 是通過定義一系列的編碼風格規則幫助我們避免在樣式表中出現錯誤,配合編輯器的自動修復,可以很好的統一團隊項目 css 風格。
配置參考 [114]
14.3 代碼提交規範
在多人協作的背景下,git 倉庫和 workflow 的作用很重要。而對於 commit 提交的信息說明存在一定規範,現使用 commitlint + husky 規範 git commit -m "" 中的描述信息。我們都知道,在使用 git commit 時,git 會提示我們填入此次提交的信息。可不要小看了這些 commit,團隊中規範了 commit 可以更清晰的查看每一次代碼提交記錄,還可以根據自定義的規則,自動生成 changeLog 文件。
提交格式(注意冒號後面有空格):
<type>[optional scope]: <description>
複製代碼
-
type :用於表明我們這次提交的改動類型。
-
optional scope:可選,用於標識此次提交主要涉及到代碼中哪個模塊。
-
description:一句話描述此次提交的主要內容,做到言簡意賅。
Type 類型
-
build:編譯相關的修改,例如發佈版本、對項目構建或者依賴的改動
-
chore:其他修改, 比如改變構建流程、或者增加依賴庫、工具等
-
ci:持續集成修改
-
docs:文檔修改
-
feat:新特性、新功能
-
fix:修改 bug
-
perf:優化相關,比如提升性能、體驗
-
refactor:代碼重構
-
revert:回滾到上一個版本
-
style:代碼格式修改, 注意不是 css 修改
-
test:測試用例修改
關於 commitlint + husky 的配置文章有很多,大同小異,請根據自己的實際情況配置。
- 編寫使用文檔
做到這一步,你的整個腳手架開發已經接近於尾聲,但是你做了這麼多,你的同事並不知道如何使用,甚至你過一段時間也會忘記,所以你必須養成良好的編寫文檔習慣。
15.1 使用 vitepress 搭建文檔
這裏我推薦使用 vuepress 或者 vitepress,說實話你只寫文檔 vitepress 會讓你更舒服,因爲它很快。
vitepress[115] 很適合構建博客網站、技術文檔,就是因爲它可以直接用 markdown 進行書寫,所有寫過博客的人,都應該對它不陌生。一個 .md 文件,即可生成一張頁面,十分方便。
創建一個 vitepress 文檔實在是太過於簡單,你可以參考官方文檔,或者參考我的文檔 [116]。
15.2 文檔部署
如果你的團隊可以幫助你搭建 CI/CD 自動部署是再好不過了,如果沒有這個條件,你也可以通過 github 提供的 actions 功能,完成自動部署。
代碼參考 [117]
- 插件
如果你想更痛快的用上述功能,建議你安裝下面的插件。
16.1 VSCode 插件
-
Vue Language Features (Volar)[118],你現在查 Volar 可能找不到,你需要的是這個。
-
Vue 3 Snippets[119],vue3 快捷輸入。
-
Tailwind CSS IntelliSense[120],tailwind 代碼提示。
-
Stylelint[121]
-
Prettier - Code formatter[122]
-
ESLint[123]
16.2 Chrome 插件
- Vue.js devtools[124],你當然要安裝支持 vue3 的版本,而且此版本對 pinia 支持的也非常友好。
源碼
上述內容,均可在我的開源項目 X-BUILD[125] 中找到相關源碼,如果可以幫到你,請給一顆 star 或點贊鼓勵我貢獻出更多的開源項目或文章。
參考
-
《基於 Vue 的前端架構,我做了這 15 點》[126]
-
《搭建自己的腳手架—“優雅” 生成前端工程》[127]
-
《Vuex4 對 TypeScript 並不友好,所以我選擇 Pinia》[128]
-
《前端腳手架 webpack 遷移 Vite2 踩坑實踐》[129]
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/cfw1KFdrwV8GzDN1pnu_kQ