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

image.png

引言

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

另外,Vue3支持Typescript語法編程也是其中一大亮點,為了探索新技術(shù)的工程化搭建,本文會把Typescript、vite、pinia等官方周邊整合到工程里面。

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

script setup

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

搞個簡單demo對比script-setupscript區(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-componentvue-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/typessrc/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ī)范

tsconfig

eslint

prettier

事件總線

為了規(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);
    }
  }
}

這里面做了幾件事情:

  1. 配置axios實例,在攔截器設(shè)置請求和相應(yīng)攔截操作,規(guī)整服務(wù)端返回的retcodemessage
  2. 改寫AxiosInstance的ts類型(由AxiosPromisePromise<any>),矯正調(diào)用方能正確判斷返回數(shù)據(jù)的類型;
  3. 設(shè)置1個初始化函數(shù)init(),生成一個axios的實例供項目調(diào)用;
  4. 配置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)境啟動

image.png

圖中可以看出,Vite在冷啟動時對6項依賴進行Pre-Bundling后注入主應(yīng)用中,整個項目啟動時間只花了738ms,性能相當(dāng)快,這里不由感嘆尤大對工程研究確實有一套??。

另外,本項目也使用vite-plugin-style-import插件對nutui視圖框架的樣式按需引入,在資源節(jié)省也起到正向作用。

構(gòu)建后的資源包

image.png

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

Lighthouse測試

image.png

以上為本地測試,首屏大約1000ms~1500ms,壓力主要來源vendor.js的加載以及首屏圖片資源拉取(首屏圖片資源來源于網(wǎng)絡(luò))。其實通過模塊分割加載后,首頁的js包通過gzip壓縮到4.3kb。

當(dāng)然真實場景是,項目部署上云服務(wù)器后肯定達不到本地資源加載速度,但可以通過CDN來加速優(yōu)化,其效果也比較顯著。

Performance

image.png




參考文章

《組合式API》

《Vite 的好與壞》

《Vite和Webpack的核心差異》


寫在最后

感謝大家閱覽并歡迎糾錯。

GitHub項目傳送門

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容