做好這 16 個方向,逐步搭建出團隊的 vue3 前端架構
點擊上方 前端Q,關注公眾號
回復加群,加入前端Q技術交流群
前言
由于 vue3.2 版本的發(fā)布,<script setup> 的實驗性標志已經(jīng)去掉,這說明這個語法提案已經(jīng)正式開始使用,并且我個人對這個方案表示非常喜歡,其他的更新[1]請自行了解。到目前為止,我認為 vue3 已經(jīng)完全可以用于生產(chǎn)環(huán)境。在此將我的開發(fā)體驗,總結至此,分享給大家。
我認為前端架構核心工作是定制一套適合當前業(yè)務需求的解決方案,從而降低需求的增加而帶來的技術實現(xiàn)的復雜度。下面我將從 16 個方向,逐漸帶領大家搭建一套屬于你自己的腳手架,制定一套合理的解決方案,為項目打下良好的基礎,與同伴形成合適的開發(fā)習慣。
由于篇幅問題,以講解實現(xiàn)思路為主,希望大家友善發(fā)言,共同進步!
目錄
-
1.搭建腳手架[2] -
1.1 前端腳手架應具備哪些功能?[3] -
1.2 如何開發(fā)一款自己的腳手架?[4] -
1.3 如何根據(jù)選項生成文件?[5] -
2.基于 vite 的搭建基礎模板[6] -
2.1 創(chuàng)建基本模板項目[7] -
2.2 常用插件推薦[8] -
3.使用 Typescript[9] -
4.配置環(huán)境變量[10] -
4.1 配置模式[11] -
4.2 常用的環(huán)境變量[12] -
4.3 封裝靜態(tài)資源文件[13] -
4.4 封裝 `SVG` 的圖標組件[14] -
5.按需自動引入組件[15] -
5.1 安裝與配置[16] -
5.2 改變?nèi)纸M件注冊方式[17] -
5.3 自動引入組件庫[18] -
6.樣式[19] -
6.1 預設基礎樣式[20] -
6.2 CSS 預處理器[21] -
6.3 開啟 scoped[22] -
6.4 深度選擇器[23] -
7.布局[24] -
7.1 常規(guī)的布局[25] -
7.2 特殊的布局[26] -
8.集成 Tailwind.css[27] -
8.1 效率提升[28] -
8.2 JIT 模式[29] -
8.3 關于打包體積[30] -
9.vuex 替代方案 pinia[31] -
9.1 為什么采用 Pinia \?[32] -
9.2 創(chuàng)建 Store[33] -
9.3 State[34] -
9.4 Getters[35] -
9.5 Actions[36] -
9.6 Devtools[37] -
10.基于 mitt 處理組件間事件聯(lián)動[38] -
10.1 為什么選擇 mitt ?[39] -
10.2 嚴重警告[40] -
10.3 如何使用 mitt ?[41] -
11.異步請求[42] -
11.1 基于 axios 的封裝[43] -
11.2 為 axios 增加泛型的支持[44] -
11.3 封裝更方便的 useRequest[45] -
11.4 統(tǒng)一的 API 接口管理[46] -
11.5 mock[47] -
12.路由[48] -
12.1 創(chuàng)建路由三部曲[49] -
12.2 使用 meta 豐富你的路由[50] -
13.項目性能與細節(jié)優(yōu)化[51] -
13.1 開啟 gzip[52] -
13.2 頁面載入進度條[53] -
13.3 Title[54] -
13.4 解決移動端使用 vh 的問題[55] -
13.5 可以常駐的 JavaScript 庫[56] -
14.代碼風格與流程規(guī)范[57] -
14.1 ESLint[58] -
14.2 StyleLint[59] -
14.3 代碼提交規(guī)范[60] -
15.編寫使用文檔[61] -
15.1 使用 vitepress 搭建文檔[62] -
15.2 文檔部署[63] -
16.插件[64] -
16.1 VSCode 插件[65] -
16.2 Chrome 插件[66] -
源碼[67] -
參考[68]
1.搭建腳手架
使用 vue-cli 或 vite ,通過一系列的配置,初始化一個開發(fā)模板,無需從零開始搭建開發(fā)環(huán)境,可以有效的提升開發(fā)效率,相信也是大多數(shù)開發(fā)者接手一個新項目所使用的一種方式。盡管官方提供的腳手架已經(jīng)足夠優(yōu)秀,但未必是真正符合我們自己團隊的使用習慣,所以從官方的基礎上,開發(fā)一款屬于我們自己的腳手架,能更多的提升開發(fā)效率。
1.1 前端腳手架應具備哪些功能?
-
減少重復的初始化工作,不需要再復制其他類似的項目刪除無關代碼,或從零搭建一個項目。 -
可以根據(jù)團隊需求,使用簡單的交互操作生成相應的目錄結構和文件。 -
統(tǒng)一團隊的開發(fā)習慣、代碼風格,保證構建結果的一致性。 -
完整的使用文檔,降低新人上手、開發(fā)和后期維護成本。
1.2 如何開發(fā)一款自己的腳手架?
提到構建前端工程化中腳手架,相信大家已經(jīng)看過不少文章,幾年前我也曾經(jīng)寫過一篇關于腳手架構建的文章[69],隨便搜一下關鍵詞可以看到很多相關的文章,在這里不做太多的介紹,主要講一些這些文章中很少提到的如何根據(jù)選項生成文件。
1.3 如何根據(jù)選項生成文件?
說實話我也不知道大佬們是怎么根據(jù)各種配置編譯成相應的文件,這塊希望大家踴躍發(fā)言,尋求一種更佳高效簡潔的方式。在這里跟大家分享一下我的方案:
交互方面,搭建過腳手架的同學一定知道 inquirer[70],這個庫可以很方便的通過交互式操作獲取到我們選擇的一些自定義配置參數(shù)。那么問題來了,如何通過這些配置相應的創(chuàng)建對應的文件呢?
這里我推薦使用 EJS[71] + Prettier[72] 生成代碼,通過 fs-extra[73] 寫入最終的文件。
-
EJS
EJS 是一款 JavaScript 模板引擎,我們可以通過傳入?yún)?shù),生成對應的代碼串,例如創(chuàng)建一個 package.ejs 用來生成 package.json 中,如果我們選擇使用了 scss 作為 CSS 預處理器,然后將 sass 和 stylelint-scss 作為項目的安裝依賴:
<% if (precss === 'scss') { -%>
"sass": "1.26.5",
"stylelint-scss": "^3.20.1",
<% } -%>
復制代碼
模板引擎可以幫你通過參數(shù)生成代碼,它并不會限制你生成任何類型的代碼文件,因為我們生成的是純代碼,最后通過讀取 .ejs 文件對應生成相應的類型文件即可。
-
Prettier
Prettier 是一款代碼格式化工具,相信大家對它并不陌生。使用 EJS 生成的目的還是給開發(fā)人員閱讀和編輯,所以生成的代碼應該符合最終的格式要求,因為后續(xù)我們會為腳手架添加 ESLint 和 StyleLint 等工具,剛剛創(chuàng)建的項目里面一堆紅線報錯可是十分不友好的。
import prettier = require("prettier");
prettier.format(code, { parser: 'json' }))
復制代碼
parser 是 prettier 的解析器,常見的 typescript、css、less、json 等文件都可以進行格式化。
2.基于 vite 的搭建基礎模板
最早搭建 vue3 腳手架的時候,我選擇的用 vue/cli 搭建,因為生態(tài)不健全,有些基于 webpack 的功能無法使用,但現(xiàn)在 vite 生態(tài)已經(jīng)比較完善了,所以重構腳手架,由 webpack 轉向 vite,這一步極大的提升了開發(fā)體驗。
2.1 創(chuàng)建基本模板項目
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,后續(xù)會在相應的章節(jié)說明用法。
3.使用 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 的類型檢查意義了,當然寫類型的習慣是需要慢慢去養(yǎng)成的,不用急于一時。
4.配置環(huán)境變量
vite 提供了兩種模式:具有開發(fā)服務器的開發(fā)模式(development)和生產(chǎn)模式(production)。
這里我們可以建立 4 個 .env 文件,一個通用配置和三種環(huán)境:開發(fā)、測試、生產(chǎn)。
4.1 配置模式
NODE_ENV=development # 開發(fā)模式
NODE_ENV=production # 生產(chǎn)模式
復制代碼
-
.env 通用配置,我個人喜歡把他當作項目的配置文件,例如項目的 title,此文件不對應任何模式。 -
.env.development 開發(fā)環(huán)境,使用 development 模式。 -
.env.staging 測試環(huán)境,因為要部署到測試服務器,或本地使用 serve 命令預覽,所以使用 production 模式。 -
.env.production 生產(chǎn)環(huán)境,因為要部署到測試服務器,或本地使用 serve 命令預覽,所以使用 production 模式。
package.json 內(nèi) script 需要增加 staging 命令
"script": {
"build": "vue-tsc --noEmit && vite build",
"staging": "vue-tsc --noEmit && vite build --mode staging",
"serve": "vite preview --host"
}
復制代碼
4.2 常用的環(huán)境變量
推薦使用以下常見的三個變量:
-
VITE_APP_BASE_URL
接口請求地址。
通常后端會區(qū)分三種環(huán)境,部署在不同的地址下。
-
VITE_APP_STATIC_URL
靜態(tài)資源地址。
靜態(tài)資源我是不建議你直接放在項目中,這會導致項目倉庫變得巨大。
本地開發(fā)和測試環(huán)境我會選在使用本地搭建的靜態(tài)資源服務器,你可以找后端運維的同學幫你搭建,或者你使用 http-server 在本地啟動一個服務器也可以。生產(chǎn)環(huán)境建議上傳至 OSS。
-
VITE_PUBLIC_PATH
構建資源公共路徑。
這個與 vue/cli 中的 publicPath 同理,有的時候你構建的項目并不是存放在跟路徑下,例如 http://ip:port/{項目名}。
4.3 封裝靜態(tài)資源文件
如果你配置了 VITE_APP_STATIC_URL 靜態(tài)資源環(huán)境變量,那么你需要封裝以下兩個東西:
-
根據(jù)環(huán)境返回實際的資源地址函數(shù)。 -
方便使用的靜態(tài)資源組件。
baseStaticUrl.ts
// 處理靜態(tài)資源鏈接
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;
}
復制代碼
靜態(tài)資源組件
靜態(tài)資源主要有圖片、音頻和視頻三種常見的形式。
-
通過 src 寫入相對的路徑,使用上述的函數(shù)來補全完整的路徑,即可在不同的環(huán)境下使用不同地址的靜態(tài)資源。 -
通過 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();});
// 監(jiān)聽視頻 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 插件,實現(xiàn)自動引入 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="" color="" />
復制代碼
-
name 放置在 @/assets/icons 文件夾下的文件名。 -
color 顏色填充,使用此項會默認覆蓋圖標顏色。
5.按需自動引入組件
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 改變?nèi)纸M件注冊方式
我們通常將全局的組件封裝在 @/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 自動引入組件庫
在使用組件庫時,常規(guī)組件我們也會注冊到全局,如果使用局部注冊由于頁面中會使用到多個組件,會非常麻煩,所以這個功能絕佳,例如我們使用 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(),
]
})
],
})
復制代碼
目前支持的解析器,根據(jù)你的喜好去選擇:
-
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]
6.樣式
項目中最好使用通用樣式,可以創(chuàng)建 src/styles 目錄存放,這里推薦一些分類:
styles
├── antd # 組件庫樣式覆蓋,命名自取,這里以 ant design 為例
├── color.less # 顏色
├── index.less # 入口
├── global.less # 公共類
├── transition.less # 動畫相關
└── variable.less # 變量
復制代碼
6.1 預設基礎樣式
相信用過 normalize[92] 的同學不在少數(shù),它可以重置 css 樣式,使各瀏覽器效果保持一致。后面的章節(jié)會提到 tailwind.css,它內(nèi)置了預設樣式重置的功能,與 normalize 還是有一定的區(qū)別,有興趣的同學可以了解一下[93]。
6.2 CSS 預處理器
雖然 vite 原生支持 less/sass/scss/stylus,但是你必須手動安裝他們的預處理器依賴,例如:
npm install -D less
復制代碼
如何選擇預處理器?
推薦使用你是所使用的組件庫的樣式語言,因為 css 預處理器學會一種后,入手其他幾乎沒有學習成本。
6.3 開啟 scoped
沒有加 scoped 屬性,會編譯成全局樣式,造成全局污染。
<style scoped></style>
復制代碼
6.4 深度選擇器
有時我們可能想明確地制定一個針對子組件的規(guī)則。
如果你希望 scoped 樣式中的一個選擇器能夠作用得“更深”,例如影響子組件,你可以使用 >>> 操作符。有些像 Sass 之類的預處理器無法正確解析 >>>。這種情況下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——兩者都是 >>> 的別名,同樣可以正常工作。
7.布局
頁面整體布局是一個產(chǎn)品最外層的框架結構,往往會包含導航、頁腳、側邊欄等。在頁面之中,也有很多區(qū)塊的布局結構。在真實項目中,頁面布局通常統(tǒng)領整個應用的界面,有非常重要的作用,所以單獨拆分出來也是非常有必要的。
在腳手架中,所有的通用布局組件都應該放在 src/layouts 中,這種封裝比較簡單,這里就不貼代碼了,大家按照自己實際情況自行發(fā)揮,在此僅提供一下封裝思路。
7.1 常規(guī)的布局
BasicLayout
基礎頁面布局,包含了頭部導航,側邊欄等。
BlankLayout
空白的布局。
7.2 特殊的布局
RouteLayout
如果你的項目在路由切換中需要對某些二級頁面進行緩存,那么推薦你創(chuàng)建一個 RouteLayout,通過路由 meta 中的配置,返回 router-view 或者使用 keep-alive 包裹的 router-view。
UserLayout
用于用戶登錄注冊等頁面抽離出來。
PageLayout
基礎布局,包含了面包屑等信息,內(nèi)含 slot。
8.集成 Tailwind.css
Tailwind.css[94] 在我第一次看到它的時候,內(nèi)心是比較反感的,但實際上手之后又覺得真香。從 vue2 項目中,我已經(jīng)引入了 tailwind,整體的開發(fā)結果就是,基本很少再使用 <style> 標簽去轉本定義一些 class 和樣式,畢竟起名字這種事,一個是涉及到規(guī)范,一個是涉及到英語。如果你選擇 tailwind,CSS 預處理器的作用就會顯得微乎其微,因為你無需再自定定義各種變量和 mixins。
總體來說,學習成本并不高,花上兩個小時足夠上手,記住不用死記硬背那些類名。
8.1 效率提升
很多人總是說樣式要與 HTML 分離,現(xiàn)在為什么又要提倡 tailwind 這種與 HTML 緊密結合的工具?這是因為現(xiàn)在使用 vue 這類框架已經(jīng)高度組件化,樣式分離是為了方便復用和維護,但在組件化面前樣式分離只能是降低開發(fā)效率。
下面介紹一下 tailwind 提供了哪些提升效率的功能:
-
提供了大量的功能類,極大的提高了可維護性。 -
響應式設計,各種設備一把梭。 -
懸停、焦點和其它狀態(tài)。 -
深色模式。 -
支持配置,例如顏色方面很難做到跟你的設計師統(tǒng)一。 -
不用為起名字而糾結???
8.2 JIT 模式
如果你的環(huán)境支持 postcss8( vue/cli 構建的 vue2 項目是 postcss7 ),那么 JIT 模式直接帶你起飛。
-
超快的構建速度。 -
支持變體,你甚至可以這么寫 sm:hover:active:disabled:opacity-75。 -
支持任意樣式,例如 md:top-[-113px]。 -
開發(fā)和生產(chǎn)環(huán)境結果是一致的,(我在 vue2 項目中就遇到過組件庫構建結果不一致的問題)。
如果你使用 vscode 那你一定要安裝 Tailwind CSS IntelliSense[95] 插件,它可以自動補全類名,顯著降低學習成本。
8.3 關于打包體積
使用默認配置,未壓縮是 3739.4kB ,Gzip壓縮 是 293.9kB,Brotli壓縮 是 73.2kB。這似乎看起來很大,這是因為 tailwind 提供了成千上萬的功能類,其中絕大部分你不會使用到。
當構建生產(chǎn)時,你應該使用 purge 選項來 tree-shake 優(yōu)化未使用的樣式,并優(yōu)化您的最終構建大小當使用 Tailwind 刪除未使用的樣式時,很難最終得到超過 10kb 的壓縮 CSS。
還有一點,Atom CSS 極大的提升了樣式的復用程度,從而直接降低了構建體積。
9.vuex 替代方案 pinia
由于 vuex 4 對 typescript 的支持讓人感到難過,所以狀態(tài)管理棄用了 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 開發(fā)體驗。
下面簡單的介紹一下如何使用 Pinia,并對比 vuex 有哪些區(qū)別與注意事項,具體請參考官方文檔[99]。
9.2 創(chuàng)建 Store
Pinia 已經(jīng)內(nèi)置在腳手架中,并且與 vue 已經(jīng)做好了關聯(lián),你可以在任何位置創(chuàng)建一個 store:
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () =>({}),
getters: {},
actions: {}
})
復制代碼
這與 Vuex 有很大不同,它是標準的 Javascript 模塊導出,這種方式也讓開發(fā)人員和你的 IDE 更加清楚 store 來自哪里。
Pinia 與 Vuex 的區(qū)別:
-
id 是必要的,它將所使用 store 連接到 devtools。 -
創(chuàng)建方式: new Vuex.Store(...)(vuex3),createStore(...)(vuex4)。 -
對比于 vuex3 ,state 現(xiàn)在是一個函數(shù)返回對象。 -
沒有 mutations,不用擔心,state 的變化依然記錄在 devtools 中。
9.3 State
創(chuàng)建好 store 之后,可以在 state 中創(chuàng)建一些屬性了:
state: () => ({ name: 'codexu', age: 18 })
復制代碼
將 store 中的 state 屬性設置為一個函數(shù),該函數(shù)返回一個包含不同狀態(tài)值的對象,這與我們在組件中定義數(shù)據(jù)的方式非常相似。
在模板中使用 store:
現(xiàn)在我們想從 store 中獲取到 name 的狀態(tài),我們只需要使用以下的方式即可:
<h1>{{userStore.name}}</h1>
const userStore = useUserStore()
return { userStore }
復制代碼
注意這里并不需要 userStore.state.name。
雖然上面的寫法很舒適,但是你一定不要用解構的方式去提取它內(nèi)部的值,這樣做的話,會失去它的響應式:
const { name, email } = useUserStore()
復制代碼
9.4 Getters
Pinia 中的 getter 與 Vuex 中的 getter 、組件中的計算屬性具有相同的功能,傳統(tǒng)的函數(shù)聲明使用 this 代替了 state 的傳參方法,但箭頭函數(shù)還是要使用函數(shù)的第一個參數(shù)來獲取 state ,因為箭頭函數(shù)處理 this 的作用范圍:
getters: {
nameLength() {
return this.name.length
},
nameLength: state => state.name.length,
nameLength: ()=> this.name.length ?
}
復制代碼
9.5 Actions
這里與 Vuex 有極大的不同,Pinia 僅提供了一種方法來定義如何更改狀態(tài)的規(guī)則,放棄 mutations 只依靠 Actions,這是一項重大的改變。
Pinia 讓 Actions 更加的靈活:
-
可以通過組件或其他 action 調(diào)用 -
可以從其他 store 的 action 中調(diào)用 -
直接在商店實例上調(diào)用 -
支持同步或異步 -
有任意數(shù)量的參數(shù) -
可以包含有關如何更改狀態(tài)的邏輯(也就是 vuex 的 mutations 的作用) -
可以 $patch方法直接更改狀態(tài)屬性
actions: {
async insertPost(data){
await doAjaxRequest(data);
this.name = '...';
}
}
復制代碼
9.6 Devtools
腳手架已內(nèi)置下面的代碼,這將添加 devtools 支持:
import { createPinia, PiniaPlugin } from 'pinia'
Vue.use(PiniaPlugin)
const pinia = createPinia()
復制代碼
時間旅行功能貌似已經(jīng)可以使用了,這塊后續(xù)會關注。
10.基于 mitt 處理組件間事件聯(lián)動
如果你曾經(jīng)是 Vue2.x 的開發(fā)者,那么請閱讀下面引用官方文檔[100]的一段話:
我們從實例中完全移除了
$on、$off和$once方法。$emit仍然包含于現(xiàn)有的 API 中,因為它用于觸發(fā)由父組件聲明式添加的事件處理函數(shù)。在 Vue 3 中,已經(jīng)不可能使用這些 API 從組件內(nèi)部監(jiān)聽組件自己發(fā)出的事件了,該用例暫沒有遷移的方法。但是該 eventHub 模式可以被替換為實現(xiàn)了事件觸發(fā)器接口的外部庫,例如
mitt或tiny-emitter。
10.1 為什么選擇 mitt ?
-
足夠小,僅有 200bytes。 -
支持全部事件的監(jiān)聽和批量移除。 -
無依賴,不論是什么框架都可以直接使用。
10.2 嚴重警告
我們已經(jīng)無法在項目中使用 eventBus,僅推薦你在特殊場合下使用 mitt,它并不是開發(fā)的常態(tài),你一定要確保知道自己在做什么?否則你的項目將難以維護!!!
10.3 如何使用 mitt ?
在使用 mitt 前建議請閱讀官方文檔[101]:
腳手架默認提供一個可以直接使用的對象:
import emitter from '@/libs/emitter';
復制代碼
當然你也可以引入已經(jīng)安裝好的 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
復制代碼
11.異步請求
絕大多數(shù)項目想必逃脫不了接口的對接,如果你的項目存在大量的接口,我建議做到以下幾點:
-
封裝請求。 -
統(tǒng)一的 API 接口管理。 -
Mock 數(shù)據(jù)功能(根據(jù)需求斟酌使用)。
上述的主要目的就是在幫助我們簡化代碼和利于后期的更新維護。
11.1 基于 axios 的封裝
相信開發(fā)過 vue2 項目的同學已經(jīng)對 axios 非常熟悉的,在這里提供一些封裝的思路:
-
通過 import.meta.env.VITE_APP_BASE_URL獲取環(huán)境變量,配置baseURL,如果接口存在多個不同域名,可以通過 js 變量控制。 -
設置 timeout請求超時、斷網(wǎng)情況處理。 -
設置請求頭,攜帶 token。 -
異常攔截處理,后端通過你攜帶的 token判斷你是否過期,如果返回401你可能需要跳轉到登錄頁面,并提示需要重新登錄。 -
響應攔截,通常后端返回 code、data、msg,如果是請求正常,我們可以直接返回 data 數(shù)據(jù),如果是異常的 code,我們也可以在這里直接彈出報錯提示。 -
無感刷新 token,如果你的 token 過期,可以通過后端返回的 refreshToken 調(diào)用刷新接口,獲取新的 token。當然這里涉及到很多細節(jié),例如終端請求、重新發(fā)送請求、重新請求列隊。 -
中斷請求,例如頁面切換時,我們要中斷正在發(fā)生的請求。
相關代碼(僅供參考)[102]
11.2 為 axios 增加泛型的支持
到目前為止,axios 請求返回的類型是 any,這時我們對請求后的數(shù)據(jù)進行操作時,沒有享受到 ts 帶來的類型提示,這顯然不符合我們的預期。
這時我們要做的就是重新聲明 axios 模塊:新建一個 shims.d.ts,然后在調(diào)用時加上泛型。
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>;
}
}
復制代碼
做好這一步后,你就必須在創(chuàng)建接口時,聲明請求相應數(shù)據(jù)的類型。
11.3 封裝更方便的 useRequest
設想一下,編寫請求代碼時,我們通常會定義這么幾個變量:
-
data: 儲存請求數(shù)據(jù) -
loading: 請求加載狀態(tài)
尤其是 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 統(tǒng)一的 API 接口管理
自從前端和后端分家之后,前后端接口對接就成為了常態(tài),而對接接口的過程就離不開接口文檔,比較主流就是 Swagger,但是如何在前端項目中更好的去管理跟后端對接的接口呢?
在 src 目錄中 創(chuàng)建 api 目錄,內(nèi)部目錄應按照后端制定的模塊創(chuàng)建。
每個模塊中創(chuàng)建多個 ts 文件,一個接口應對應一個 ts 文件,其中包含了以下內(nèi)容:
-
請求參數(shù)的類型聲明。 -
響應數(shù)據(jù)的類型聲明。 -
返回定義好的請求函數(shù)(url、method、params、data 等)。
統(tǒng)一去定義和管理 API 接口,只要后端規(guī)范的命名和你認真的寫好類型聲明,對前端來說 typescript 就是最好的接口文檔。
11.5 mock
vite 使用 mock 數(shù)據(jù)非常簡單,你可以使用 vite-plugin-mock[105] 插件,如果你了解 mockjs,你可以快速上手。
12.路由
路由和菜單是組織起一個應用的關鍵骨架。
12.1 創(chuàng)建路由三部曲
通常一個項目需要做到這幾步:
-
使用 createRouter 創(chuàng)建路由,這時候根據(jù)需求選擇 Hash 路由或者 History 路由。 -
根據(jù)業(yè)務需求配置路由,注意這里很可能就用到前文提到過的布局組件。 -
如果有權限相關的業(yè)務,你需要創(chuàng)建 permission.ts 在路由鉤子觸發(fā)時做一些事情。
如果你的頁面比較多,建議你創(chuàng)建 routes 目錄,分模塊聲明路由。
參考代碼[106]
12.2 使用 meta 豐富你的路由
vue-router4.x 支持 typescript,配置路由的類型是 RouteRecordRaw,這里 meta 可以讓我們有更多的發(fā)揮空間,這里提供一些參考:
-
title: string; 頁面標題,通常必選。 -
icon?: string; 圖標,一般配合菜單使用。 -
auth?: boolean; 是否需要登錄權限。 -
ignoreAuth?: boolean; 是否忽略權限。 -
roles?: RoleEnum[]; 可以訪問的角色 -
keepAlive?: boolean; 是否開啟頁面緩存 -
hideMenu?: boolean; 有些路由我們并不想在菜單中顯示,比如某些編輯頁面。 -
order?: number; 菜單排序。 -
frameUrl?: string; 嵌套外鏈。
這里只提供一些思路,每個項目多多少少會涉及到這些問題,具體如何實現(xiàn)請查閱資料自行解決。
13.項目性能與細節(jié)優(yōu)化
13.1 開啟 gzip
開啟 gzip 可以極大的壓縮靜態(tài)資源,對頁面加載的速度起到了顯著的作用。
使用 vite-plugin-compression[107] 可以 gzip 或 brotli 的方式來壓縮資源,這一步需要服務器端的配合,vite 只能幫你打包出 .gz 文件。此插件使用簡單,你甚至無需配置參數(shù),引入即可。
13.2 頁面載入進度條
頁面路由切換時,附帶一個加載進度條會顯得非常友好,不至于白屏時間過長,讓用戶以為頁面假死。
這時候我們可以用到 nprogress[108],在路由切換時開啟和關閉:
import NProgress from 'nprogress';
router.beforeEach(async (to, from, next) => {
NProgress.start();
});
router.afterEach((to) => {
NProgress.done();
});
復制代碼
13.3 Title
在不同的路由下顯示不同的標題是常規(guī)的操作,我們可以通過路由鉤子獲取 meta 中的 title 屬性改變標簽頁上的 title。
你可以使用 vueuse 提供的 useTitle[109],或者 window.document.title 自行封裝。
你也可以通過環(huán)境變量將你的主標題拼接在路由標題的后面:
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.代碼風格與流程規(guī)范
14.1 ESLint
不管是多人合作還是個人項目,代碼規(guī)范都是很重要的。這樣做不僅可以很大程度地避免基本語法錯誤,也保證了代碼的可讀性。
這里推薦使用 airbnb 規(guī)范。
配置參考[113]
14.2 StyleLint
盡管前文提到過 tailwind,可以讓你幾乎不寫 css,但是涉及到團隊協(xié)作,這一點也要嚴謹。
StyleLint 是一個強大的、現(xiàn)代化的 CSS 檢測工具, 與 ESLint 類似, 是通過定義一系列的編碼風格規(guī)則幫助我們避免在樣式表中出現(xiàn)錯誤,配合編輯器的自動修復,可以很好的統(tǒng)一團隊項目 css 風格。
配置參考[114]
14.3 代碼提交規(guī)范
在多人協(xié)作的背景下,git 倉庫和 workflow 的作用很重要。而對于 commit 提交的信息說明存在一定規(guī)范,現(xiàn)使用 commitlint + husky 規(guī)范 git commit -m "" 中的描述信息。我們都知道,在使用 git commit 時,git 會提示我們填入此次提交的信息。可不要小看了這些 commit,團隊中規(guī)范了 commit 可以更清晰的查看每一次代碼提交記錄,還可以根據(jù)自定義的規(guī)則,自動生成 changeLog 文件。
提交格式(注意冒號后面有空格):
<type>[optional scope]: <description>
復制代碼
-
type :用于表明我們這次提交的改動類型。 -
optional scope:可選,用于標識此次提交主要涉及到代碼中哪個模塊。 -
description:一句話描述此次提交的主要內(nèi)容,做到言簡意賅。
Type 類型
-
build:編譯相關的修改,例如發(fā)布版本、對項目構建或者依賴的改動 -
chore:其他修改, 比如改變構建流程、或者增加依賴庫、工具等 -
ci:持續(xù)集成修改 -
docs:文檔修改 -
feat:新特性、新功能 -
fix:修改bug -
perf:優(yōu)化相關,比如提升性能、體驗 -
refactor:代碼重構 -
revert:回滾到上一個版本 -
style:代碼格式修改, 注意不是 css 修改 -
test:測試用例修改
關于 commitlint + husky 的配置文章有很多,大同小異,請根據(jù)自己的實際情況配置。
15.編寫使用文檔
做到這一步,你的整個腳手架開發(fā)已經(jīng)接近于尾聲,但是你做了這么多,你的同事并不知道如何使用,甚至你過一段時間也會忘記,所以你必須養(yǎng)成良好的編寫文檔習慣。
15.1 使用 vitepress 搭建文檔
這里我推薦使用 vuepress 或者 vitepress,說實話你只寫文檔 vitepress 會讓你更舒服,因為它很快。
vitepress[115] 很適合構建博客網(wǎng)站、技術文檔,就是因為它可以直接用 markdown 進行書寫,所有寫過博客的人,都應該對它不陌生。一個 .md 文件,即可生成一張頁面,十分方便。
創(chuàng)建一個 vitepress 文檔實在是太過于簡單,你可以參考官方文檔,或者參考我的文檔[116]。
15.2 文檔部署
如果你的團隊可以幫助你搭建 CI/CD 自動部署是再好不過了,如果沒有這個條件,你也可以通過 github 提供的 actions 功能,完成自動部署。
代碼參考[117]
16.插件
如果你想更痛快的用上述功能,建議你安裝下面的插件。
16.1 VSCode 插件
-
Vue Language Features \(Volar\)[118],你現(xiàn)在查 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 支持的也非常友好。
源碼
上述內(nèi)容,均可在我的開源項目 X-BUILD[125] 中找到相關源碼,如果可以幫到你,請給一顆 star 或點贊鼓勵我貢獻出更多的開源項目或文章。
參考
-
《基于Vue的前端架構,我做了這15點》[126] -
《搭建自己的腳手架—“優(yōu)雅”生成前端工程》[127] -
《Vuex4 對 TypeScript 并不友好,所以我選擇 Pinia》[128] -
《前端腳手架 webpack 遷移 Vite2 踩坑實踐》[129]
關于本文
作者:codexu
https://juejin.cn/post/7025524870842679310
往期推薦
最后
歡迎加我微信,拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...
