Vite2+Vue3+TypeScript:搭建企業級輕量框架實踐

語言: CN / TW / HK

引言

隨著Vue3為廣大開發者所接受和自身生態逐漸完善,更多同學往vue3的工程化方向完善,本文恰好給大家介紹下如何更好使用vue3及其周邊外掛,以及讓他們組合到整個工程中去。

另外,Vue3支援Typescript語法程式設計也是其中一大亮點,為了探索新技術的工程化搭建,本文會把Typescript、vite、pinia等官方周邊整合到工程裡面。

接下來,為了讓大家更好理解本專案工程化的思路,本文會按照以下關鍵詞去逐步研讀(看專案程式碼可跳過前4步)

script setup

<script setup> 是在單檔案元件 (SFC) 中使用組合式 API 的編譯時語法糖。

搞個簡單demo對比script-setupscript區別:

```js // 單檔案元件script-setup編寫模式

```

```js // 普通script編寫模式

```

上述例子可以看出,script-setup弱化了vue模板式程式設計體驗,也使得程式碼更簡潔,開發者只需要引入正確的hooks後,把邏輯寫在script內就足以。

本專案所有元件都採用這種開發模式,相比於普通的 <script> 語法,vue官方肯定了它的優勢:

  • 更少的樣板內容,更簡潔的程式碼。
  • 能夠使用純 Typescript 宣告 props 和丟擲事件。
  • 更好的執行時效能 (其模板會被編譯成與其同一作用域的渲染函式,沒有任何的中間代理)。
  • 更好的 IDE 型別推斷效能 (減少語言伺服器從程式碼中抽離型別的工作)

最後筆者認為,從某方面講Vue3是一次vue-hooks的革命,通過compositionApi的引用使元件寫法更輕便簡潔;而script-setup正好使得這種體驗更加徹底,使單檔案元件寫法更接近函數語言程式設計,在react和vue之間無縫切換。


Typescript

近幾年前端對 TypeScript的呼聲越來越高,Typescript也成為了前端必備的技能。TypeScript 是 JS型別的超集,並支援了泛型、型別、名稱空間、列舉等特性,彌補了 JS 在大型應用開發中的不足。

在vue2版本時候,假如你要使用typescript,需要借用vue-class-component 、vue-property-decorator 等裝飾器加以判斷,而且要改成特定的程式碼結構讓vue去識別,並不方便。

到了Vue3的時代,框架已經完美相容了typescript,而且配置也簡單,對程式碼入侵也小,給開發者帶來了很大便利。


Vite

Vite是一種新型前端構建工具,能夠顯著提升前端開發體驗。比起webpack,vite還是有它很獨特的優勢,這裡推薦一篇文章《Vite 的好與壞》給大家參考下。

專案為什麼選vite代替webpack,結合社群和個人考慮,有幾點:(具體就不展開,推文已經分析的很細緻了) * Vite更加輕量,並且構建速度足夠快
webpack是使用nodejs去實現,而viite使用 esbuild 預構建依賴。Esbuild 使用 Go 編寫,並且比以 JavaScript 編寫的打包器預構建依賴快不是一個數量級。 * Vue官方出品,對vue專案相容性不錯 * 發展勢頭迅猛,未來可期

當然事物都有兩面性的,至目前為止,vite也有不少缺陷,例如:生態沒有webpack成熟、生產環境下隱藏的不穩定因素等都是它如今要面臨的問題。

但是,心懷夢想敢於向前,沒有新勢力的誕生,哪裡來的技術發展?相比之下,vite更像一個青年,並逐步前行。


Pinia

Pinia 是 Vue.js 的輕量級狀態管理庫,最近很受歡迎。它使用 Vue 3 中的新反應系統來構建一個直觀且完全型別化的狀態管理庫。

比起Vuex,Pinia具備以下優點:

  • 完整的 TypeScript 支援:與在 Vuex 中新增 TypeScript 相比,新增 TypeScript 更容易
  • 極其輕巧(體積約 1KB)
  • store 的 action 被排程為常規的函式呼叫,而不是使用 dispatch 方法或 MapAction 輔助函式,這在 Vuex 中很常見
  • 支援多個Store
  • 支援 Vue devtools、SSR 和 webpack 程式碼拆分

工程化搭建

言歸正傳,我們通過以上技術,整合到一個專案中去。一般用於企業級生產的專案,要具備以下能力: - 容錯性、可拓展性強 - 元件高內聚,減少模組之間耦合度 - 清晰的專案執行匯流排,方便增加插槽邏輯 - 高度抽象的全域性方法 - 資源壓縮+效能優化等

對照這些指標,我們來逐步搭建一個初步的工程框架。

備註:關於vue3語法、pinia使用等程式設計知識不會在這裡細述了,大家可以到網上檢索或者直接在專案裡面尋找。

1. 技術棧

程式設計: Vue3.x + Typescript
構建工具:Vite
路由 | 狀態管理:vue-router + Pinia
UI Element:nutui

2. 工程結構

. ├── README.md ├── index.html 專案入口 ├── mock mock目錄 ├── package.json ├── public ├── src │   ├── App.vue 主應用 │   ├── api 請求中心 │   ├── assets 資源目錄(圖片、less、css等) │   ├── components 專案元件 │   ├── constants 常量 │   ├── env.d.ts 全域性宣告 │   ├── main.ts 主入口 │   ├── pages 頁面目錄 │   ├── router 路由配置 │   ├── types ts型別定義 │   ├── store pinia狀態管理 │   └── utils 基礎工具包 ├── test 測試用例 ├── tsconfig.json ts配置 ├── .eslintrc.js eslint配置 ├── .prettierrc.json prettier配置 ├── .gitignore git忽略配置 └── vite.config.ts vite配置 其中,src/utils裡面放置全域性方法,供整個工程範圍的檔案呼叫,當然工程初始化的事件匯流排也放在這裡「下面會細述」。src/typessrc/constants分別存放專案的型別定義和常量,以頁面結構來劃分目錄。

3. 工程配置

搭建Vite + Vue專案

```bash

npm 6.x

npm init [email protected] my-vue-app --template vue

npm 7+, 需要額外的雙橫線:

npm init [email protected] my-vue-app -- --template vue

yarn

yarn create vite my-vue-app --template vue

pnpm

pnpm create vite my-vue-app -- --template vue ``` 然後按照提示操作即可!

Vite配置

```typescript 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, // 開發打包開關 prodEnabled: !!process.env.USE_CHUNK_MOCK, // 生產打包開關 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前處理器 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' //指定生成靜態資源的存放路徑 } }; }); `` 工程添加了mock模式供開發者在沒有服務端情況下模擬資料請求,通過vite-plugin-mock外掛全域性配置到vite中,mock介面返回在mock目錄下增加,mock模式啟動命令:npm run dev:mock`。

FYI:vite-plugin-mock外掛在vite腳手架下提供devtools network攔截能力,假如你要實現更多mock場景,請使用mockjs「專案已安裝,直接可用」

編碼規範

tsconfig
eslint
prettier

事件匯流排

為了規範專案的初始化流程,方便在流程中插入自定義邏輯,在main.ts入口呼叫initialize(app)方法,initialize程式碼如下: ```typescript /* * 專案初始化匯流排 /

// 初始化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狀態管理庫 app.use(createPinia()); registerStore(); console.trace('pinia狀態庫已註冊...'); }; `` 在方法裡面,分別完成頁面的rem自適應佈局初始化、UI元件按需載入、路由、狀態庫初始化等操作,另外initialize`支援非同步邏輯注入,需要的自行新增並使用Promise包裹返回即可。

ps:initialize方法執行時機在主App掛載之前,請勿將dom操作邏輯放置此處

4. 請求中心

src/api包含每個頁面的非同步請求,也是通過頁面結構來劃分目錄。src/api/index.ts是其入口檔案,用來聚合每個請求模組,程式碼如下:

```typescript 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程式碼如下: ```typescript 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; (url: string, config?: AxiosRequestConfig): Promise; }

export class Request { public static axiosInstance: MyAxiosInstance;

public static init() { // 建立axios例項 this.axiosInstance = axios.create({ baseURL: '/api', timeout: 10000 }); // 初始化攔截器 this.initInterceptors(); }

// 初始化攔截器 public static initInterceptors() { // 設定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); } );

// 響應攔截器
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) {
      // 請求已發出,但是不在2xx的範圍
      Request.errorHandle(response);
    } else {
      Toast.fail('網路連線異常,請稍後再試!');
    }
    return Promise.reject(response?.data);
  }
);

}

/* * http握手錯誤 * @param res 響應回撥,根據不同響應進行不同操作 * @param message / private static errorHandle(res: IRequestResponse, message?: string) { // 狀態碼判斷 switch (res.status) { case 401: break; case 403: break; case 404: Toast.fail('請求的資源不存在'); break; default: // 錯誤資訊判斷 message && Toast.fail(message); } } } ```

這裡面做了幾件事情:
1. 配置axios例項,在攔截器設定請求和相應攔截操作,規整服務端返回的retcodemessage; 2. 改寫AxiosInstance的ts型別(由AxiosPromisePromise<any>),矯正呼叫方能正確判斷返回資料的型別; 3. 設定1個初始化函式init(),生成一個axios的例項供專案呼叫; 4. 配置errorHandle控制代碼,處理錯誤;

當然在第2步,你可以新增額外的請求攔截,例如RSA加密,本地快取策略等,當邏輯過多時,建議通過函式引入。

至此,我們就能愉快使用axios去請求資料了。 ```typescript // api模組→請求中心 import { Request } from './request';

userInfo: (options?: IRequestParams): Promise => Request.axiosInstance({ url: '/userInfo', method: 'post', desc: '獲取使用者資訊', isJSON: true, ...options })

// 業務模組→api模組 import request from '@/api/index';

request.user .userInfo({ data: { token } }) .then(res => { // do something... }); ```

5. SSR

待補充...


效能測試

開發環境啟動

圖中可以看出,Vite在冷啟動時對6項依賴進行Pre-Bundling後注入主應用中,整個專案啟動時間只花了738ms,效能相當快,這裡不由感嘆尤大對工程研究確實有一套😆。

另外,本專案也使用vite-plugin-style-import外掛對nutui檢視框架的樣式按需引入,在資源節省也起到正向作用。

構建後的資源包

分包策略是依據路由頁面來切割,對js和css單獨分離。

Lighthouse測試

以上為本地測試,首屏大約1000ms~1500ms,壓力主要來源vendor.js的載入以及首屏圖片資源拉取(首屏圖片資源來源於網路)。其實通過模組分割載入後,首頁的js包通過gzip壓縮到4.3kb。
當然真實場景是,專案部署上雲伺服器後肯定達不到本地資源載入速度,但可以通過CDN來加速優化,其效果也比較顯著。

Performance



參考文章

《組合式API》
《Vite 的好與壞》
《Vite和Webpack的核心差異》


寫在最後

感謝大家閱覽並歡迎糾錯。
GitHub專案傳送門