引言
隨著Vue3
為廣大開發(fā)者所接受和自身生態(tài)逐漸完善,更多同學(xué)往vue3的工程化方向完善,本文恰好給大家介紹下如何更好使用vue3及其周邊插件,以及讓他們組合到整個工程中去。
另外,Vue3支持Typescript
語法編程也是其中一大亮點,為了探索新技術(shù)的工程化搭建,本文會把Typescript、vite、pinia等官方周邊整合到工程里面。
接下來,為了讓大家更好理解本項目工程化的思路,本文會按照以下關(guān)鍵詞去逐步研讀(看項目代碼可跳過前4步):
script setup
<script setup>
是在單文件組件 (SFC) 中使用組合式 API 的編譯時語法糖。
搞個簡單demo對比script-setup
和script
區(qū)別:
// 單文件組件script-setup編寫模式
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
// 普通script編寫模式
<script>
import { ref } from 'vue'
export default {
setup(props) {
const count = ref(0)
// 暴露給 template
return {
count
}
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
上述例子可以看出,script-setup
弱化了vue模板式編程體驗,也使得代碼更簡潔,開發(fā)者只需要引入正確的hooks后,把邏輯寫在script內(nèi)就足以。
本項目所有組件都采用這種開發(fā)模式,相比于普通的 <script>
語法,vue官方肯定了它的優(yōu)勢:
- 更少的樣板內(nèi)容,更簡潔的代碼。
- 能夠使用純 Typescript 聲明 props 和拋出事件。
- 更好的運行時性能 (其模板會被編譯成與其同一作用域的渲染函數(shù),沒有任何的中間代理)。
- 更好的 IDE 類型推斷性能 (減少語言服務(wù)器從代碼中抽離類型的工作)
最后筆者認(rèn)為,從某方面講Vue3
是一次vue-hooks的革命,通過compositionApi的引用使組件寫法更輕便簡潔;而script-setup
正好使得這種體驗更加徹底,使單文件組件寫法更接近函數(shù)式編程,在react和vue之間無縫切換。
Typescript
近幾年前端對 TypeScript的呼聲越來越高,Typescript也成為了前端必備的技能。TypeScript 是 JS類型的超集,并支持了泛型、類型、命名空間、枚舉等特性,彌補了 JS 在大型應(yīng)用開發(fā)中的不足。
在vue2版本時候,假如你要使用typescript,需要借用vue-class-component
、vue-property-decorator
等裝飾器加以判斷,而且要改成特定的代碼結(jié)構(gòu)讓vue去識別,并不方便。
到了Vue3的時代,框架已經(jīng)完美兼容了typescript,而且配置也簡單,對代碼入侵也小,給開發(fā)者帶來了很大便利。
Vite
Vite是一種新型前端構(gòu)建工具,能夠顯著提升前端開發(fā)體驗。比起webpack,vite還是有它很獨特的優(yōu)勢,這里推薦一篇文章《Vite 的好與壞》給大家參考下。
項目為什么選vite代替webpack,結(jié)合社區(qū)和個人考慮,有幾點:(具體就不展開,推文已經(jīng)分析的很細(xì)致了)
- Vite更加輕量,并且構(gòu)建速度足夠快
webpack是使用nodejs去實現(xiàn),而viite使用 esbuild 預(yù)構(gòu)建依賴。Esbuild 使用 Go 編寫,并且比以 JavaScript 編寫的打包器預(yù)構(gòu)建依賴快不是一個數(shù)量級。 - Vue官方出品,對vue項目兼容性不錯
- 發(fā)展勢頭迅猛,未來可期
當(dāng)然事物都有兩面性的,至目前為止,vite也有不少缺陷,例如:生態(tài)沒有webpack成熟、生產(chǎn)環(huán)境下隱藏的不穩(wěn)定因素等都是它如今要面臨的問題。
但是,心懷夢想敢于向前,沒有新勢力的誕生,哪里來的技術(shù)發(fā)展?相比之下,vite更像一個青年,并逐步前行。
Pinia
Pinia 是 Vue.js 的輕量級狀態(tài)管理庫,最近很受歡迎。它使用 Vue 3 中的新反應(yīng)系統(tǒng)來構(gòu)建一個直觀且完全類型化的狀態(tài)管理庫。
比起Vuex,Pinia具備以下優(yōu)點:
- 完整的 TypeScript 支持:與在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
- 極其輕巧(體積約 1KB)
- store 的 action 被調(diào)度為常規(guī)的函數(shù)調(diào)用,而不是使用 dispatch 方法或 MapAction 輔助函數(shù),這在 Vuex 中很常見
- 支持多個Store
- 支持 Vue devtools、SSR 和 webpack 代碼拆分
工程化搭建
言歸正傳,我們通過以上技術(shù),整合到一個項目中去。一般用于企業(yè)級生產(chǎn)的項目,要具備以下能力:
- 容錯性、可拓展性強
- 組件高內(nèi)聚,減少模塊之間耦合度
- 清晰的項目執(zhí)行總線,方便增加插槽邏輯
- 高度抽象的全局方法
- 資源壓縮+性能優(yōu)化等
對照這些指標(biāo),我們來逐步搭建一個初步的工程框架。
備注:關(guān)于vue3語法、pinia使用等編程知識不會在這里細(xì)述了,大家可以到網(wǎng)上檢索或者直接在項目里面尋找。
1. 技術(shù)棧
編程: Vue3.x
+ Typescript
構(gòu)建工具:Vite
路由 | 狀態(tài)管理:vue-router
+ Pinia
UI Element:nutui
2. 工程結(jié)構(gòu)
.
├── README.md
├── index.html 項目入口
├── mock mock目錄
├── package.json
├── public
├── src
│ ├── App.vue 主應(yīng)用
│ ├── api 請求中心
│ ├── assets 資源目錄(圖片、less、css等)
│ ├── components 項目組件
│ ├── constants 常量
│ ├── env.d.ts 全局聲明
│ ├── main.ts 主入口
│ ├── pages 頁面目錄
│ ├── router 路由配置
│ ├── types ts類型定義
│ ├── store pinia狀態(tài)管理
│ └── utils 基礎(chǔ)工具包
├── test 測試用例
├── tsconfig.json ts配置
├── .eslintrc.js eslint配置
├── .prettierrc.json prettier配置
├── .gitignore git忽略配置
└── vite.config.ts vite配置
其中,src/utils
里面放置全局方法,供整個工程范圍的文件調(diào)用,當(dāng)然工程初始化的事件總線也放在這里「下面會細(xì)述」。src/types
和src/constants
分別存放項目的類型定義和常量,以頁面結(jié)構(gòu)來劃分目錄。
3. 工程配置
搭建Vite + Vue項目
# npm 6.x
npm init vite@latest my-vue-app --template vue
# npm 7+, 需要額外的雙橫線:
npm init vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
# pnpm
pnpm create vite my-vue-app -- --template vue
然后按照提示操作即可!
Vite配置
import { defineConfig, ConfigEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import styleImport from 'vite-plugin-style-import';
import { viteMockServe } from 'vite-plugin-mock';
const path = require('path')
// https://vitejs.dev/config/
export default defineConfig(({ command }: ConfigEnv) => {
return {
base: './',
plugins: [
vue(),
// mock
viteMockServe({
mockPath: 'mock', //mock文件地址
localEnabled: !!process.env.USE_MOCK, // 開發(fā)打包開關(guān)
prodEnabled: !!process.env.USE_CHUNK_MOCK, // 生產(chǎn)打包開關(guān)
logger: false, //是否在控制臺顯示請求日志
supportTs: true
}),
styleImport({
libs: [
// nutui按需加載配置,詳見 https://nutui.jd.com/#/start
{
libraryName: '@nutui/nutui',
libraryNameChangeCase: 'pascalCase',
resolveStyle: name => {
return `@nutui/nutui/dist/packages/${name}/index.scss`;
}
}
]
})
],
resolve: {
alias: [
{
find: '@',
replacement: '/src'
}
]
},
css: {
// css預(yù)處理器
preprocessorOptions: {
scss: {
// 配置 nutui 全局 scss 變量
additionalData: `@import "@nutui/nutui/dist/styles/variables.scss";`
},
less: {
charset: false,
additionalData: '@import "./src/assets/less/common.less";'
}
}
},
build: {
terserOptions: {
compress: {
drop_console: true
}
},
outDir: 'dist', //指定輸出路徑
assetsDir: 'assets' //指定生成靜態(tài)資源的存放路徑
}
};
});
工程添加了mock模式供開發(fā)者在沒有服務(wù)端情況下模擬數(shù)據(jù)請求,通過vite-plugin-mock
插件全局配置到vite中,mock接口返回在mock
目錄下增加,mock模式啟動命令:npm run dev:mock
。
FYI:
vite-plugin-mock
插件在vite腳手架下提供devtools network攔截能力,假如你要實現(xiàn)更多mock場景,請使用mockjs「項目已安裝,直接可用」。
編碼規(guī)范
事件總線
為了規(guī)范項目的初始化流程,方便在流程中插入自定義邏輯,在main.ts
入口調(diào)用initialize(app)
方法,initialize代碼如下:
/**
* 項目初始化總線
*/
// 初始化nutui樣式
import '@nutui/nutui/dist/style.css';
import { initRem } from '@/utils/calcRem';
import nutUiList from '@/utils/nutuiImport';
import router from '@/router';
import { createPinia } from 'pinia';
import { registerStore } from '@/store';
export const initialize = async (app: any) => {
// 初始化rem
initRem(window, document.documentElement);
window.calcRem(1080);
console.trace('rem初始化完成...');
// 按需加載nutui組件
Object.values(nutUiList).forEach(co => {
app.use(co);
});
console.trace('nutui組件加載完成...');
// 掛載路由
app.use(router);
console.trace('router已掛載...');
// 注冊pinia狀態(tài)管理庫
app.use(createPinia());
registerStore();
console.trace('pinia狀態(tài)庫已注冊...');
};
在方法里面,分別完成頁面的rem自適應(yīng)布局初始化、UI組件按需加載、路由、狀態(tài)庫初始化等操作,另外initialize
支持異步邏輯注入,需要的自行添加并使用Promise包裹返回即可。
ps:
initialize
方法執(zhí)行時機在主App掛載之前,請勿將dom操作邏輯放置此處
4. 請求中心
src/api
包含每個頁面的異步請求,也是通過頁面結(jié)構(gòu)來劃分目錄。src/api/index.ts
是其入口文件,用來聚合每個請求模塊,代碼如下:
import { Request } from './request';
import box from './box';
import user from './user';
// 初始化axios
Request.init();
export default {
box,
user
// ...其他請求模塊
};
這里的Request是請求中心的類對象,返回1個axios實例,src/api/request.ts
代碼如下:
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import {
IRequestParams,
IRequestResponse,
TBackData
} from '@/types/global/request';
import { Toast } from '@nutui/nutui';
interface MyAxiosInstance extends AxiosInstance {
(config: AxiosRequestConfig): Promise<any>;
(url: string, config?: AxiosRequestConfig): Promise<any>;
}
export class Request {
public static axiosInstance: MyAxiosInstance;
public static init() {
// 創(chuàng)建axios實例
this.axiosInstance = axios.create({
baseURL: '/api',
timeout: 10000
});
// 初始化攔截器
this.initInterceptors();
}
// 初始化攔截器
public static initInterceptors() {
// 設(shè)置post請求頭
this.axiosInstance.defaults.headers.post['Content-Type'] =
'application/x-www-form-urlencoded';
/**
* 請求攔截器
* 每次請求前,如果存在token則在請求頭中攜帶token
*/
this.axiosInstance.interceptors.request.use(
(config: IRequestParams) => {
const token = localStorage.getItem('ACCESS_TOKEN');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
(error: any) => {
Toast.fail(error);
}
);
// 響應(yīng)攔截器
this.axiosInstance.interceptors.response.use(
// 請求成功
(response: IRequestResponse): TBackData => {
const {
data: { code, message, data }
} = response;
if (response.status !== 200 || code !== 0) {
Request.errorHandle(response, message);
}
return data;
},
// 請求失敗
(error: AxiosError): Promise<any> => {
const { response } = error;
if (response) {
// 請求已發(fā)出,但是不在2xx的范圍
Request.errorHandle(response);
} else {
Toast.fail('網(wǎng)絡(luò)連接異常,請稍后再試!');
}
return Promise.reject(response?.data);
}
);
}
/**
* http握手錯誤
* @param res 響應(yīng)回調(diào),根據(jù)不同響應(yīng)進行不同操作
* @param message
*/
private static errorHandle(res: IRequestResponse, message?: string) {
// 狀態(tài)碼判斷
switch (res.status) {
case 401:
break;
case 403:
break;
case 404:
Toast.fail('請求的資源不存在');
break;
default:
// 錯誤信息判斷
message && Toast.fail(message);
}
}
}
這里面做了幾件事情:
- 配置axios實例,在攔截器設(shè)置請求和相應(yīng)攔截操作,規(guī)整服務(wù)端返回的
retcode
和message
; - 改寫
AxiosInstance
的ts類型(由AxiosPromise
→Promise<any>
),矯正調(diào)用方能正確判斷返回數(shù)據(jù)的類型; - 設(shè)置1個初始化函數(shù)
init()
,生成一個axios的實例供項目調(diào)用; - 配置
errorHandle
句柄,處理錯誤;
當(dāng)然在第2步,你可以添加額外的請求攔截,例如RSA加密,本地緩存策略等,當(dāng)邏輯過多時,建議通過函數(shù)引入。
至此,我們就能愉快使用axios去請求數(shù)據(jù)了。
// api模塊→請求中心
import { Request } from './request';
userInfo: (options?: IRequestParams): Promise<TUser> =>
Request.axiosInstance({
url: '/userInfo',
method: 'post',
desc: '獲取用戶信息',
isJSON: true,
...options
})
// 業(yè)務(wù)模塊→api模塊
import request from '@/api/index';
request.user
.userInfo({
data: {
token
}
})
.then(res => {
// do something...
});
5. SSR
待補充...
性能測試
開發(fā)環(huán)境啟動
圖中可以看出,Vite在冷啟動時對6項依賴進行Pre-Bundling后注入主應(yīng)用中,整個項目啟動時間只花了738ms,性能相當(dāng)快,這里不由感嘆尤大對工程研究確實有一套??。
另外,本項目也使用vite-plugin-style-import
插件對nutui視圖框架的樣式按需引入,在資源節(jié)省也起到正向作用。
構(gòu)建后的資源包
分包策略是依據(jù)路由頁面來切割,對js和css單獨分離。
Lighthouse測試
以上為本地測試,首屏大約1000ms~1500ms,壓力主要來源vendor.js的加載以及首屏圖片資源拉取(首屏圖片資源來源于網(wǎng)絡(luò))。其實通過模塊分割加載后,首頁的js包通過gzip壓縮到4.3kb。
當(dāng)然真實場景是,項目部署上云服務(wù)器后肯定達不到本地資源加載速度,但可以通過CDN來加速優(yōu)化,其效果也比較顯著。
Performance
參考文章
《組合式API》
《Vite 的好與壞》
《Vite和Webpack的核心差異》
寫在最后
感謝大家閱覽并歡迎糾錯。
GitHub項目傳送門