[專案實戰] Webpack to Vite, 為開發提速!

2021-04-08 21:00:36

Webpack to Vite

背景

最近,就 前端開發過程中的痛點及可優化項 做了一次收集。 其中,構建耗時、專案編譯速度慢 的字眼出現了好幾次。

隨著業務的快速發展,我們很多專案的體積也快速膨脹。 隨之而來的, 就是打包變慢等問題。

提升研發效率,是技術人永恆的追求。

我們專案也有啟動慢的問題,同事也提到過幾次。 剛好我之前也做過類似的探索和優化, 於是就借這個機會,改造一下專案, 解決啟動耗時的問題

於昨天下午(2021.4.7 23:00), 成功嵌入 Vite, 專案啟動時間由約 190s => 20s, 熱更新時間縮短為 2s

中間踩了一些坑, 好在最後爬出來了, 相關技術要點都會在下文中呈現。

FBI Warning: 以下文字,只是我結合自己的實際專案, 總結出來的一些淺薄的經驗, 如有錯誤,歡迎指正 :)

今天的主要內容:

  • 為什麼 Vite 啟動這麼快
  • 我的專案如何植入 Vite
  • 我在改造過程中遇到的問題
  • 關於 Vite 開發、打包上線的一些思考
  • 相關程式碼和結論

正文

為什麼 Vite 啟動這麼快

底層實現上, Vite 是基於 esbuild 預構建依賴的。

esbuild 使用 go 編寫,並且比以 js 編寫的打包器預構建依賴, 快 10 - 100 倍。

因為 js 跟 go 相比實在是太慢了,js 的一般操作都是毫秒計,go 則是納秒。

另外, 兩者的啟動方式也有所差異。

webpack 啟動方式

image.png

Vite 啟動方式

image.png

Webpack 會先打包,然後啟動開發伺服器,請求伺服器時直接給予打包結果。

而 Vite 是直接啟動開發伺服器,請求哪個模組再對該模組進行實時編譯

由於現代瀏覽器本身就支援 ES Module,會自動向依賴的 Module 發出請求。

Vite 充分利用了這一點,將開發環境下的模組檔案,就作為瀏覽器要執行的檔案,而不是像 W ebpack 那樣進行打包合併

由於 Vite 在啟動的時候不需要打包,也就意味著不需要分析模組的依賴不需要編譯
因此啟動速度非常快。當瀏覽器請求某個模組時,再根據需要對模組內容進行編譯。

這種按需動態編譯的方式,極大的縮減了編譯時間,專案越複雜、模組越多,vite 的優勢越明顯。

在 HMR(熱更新)方面,當改動了一個模組後,僅需讓瀏覽器重新請求該模組即可,不像webpack那樣需要把該模組的相關依賴模組全部編譯一次,效率更高。

從實際的開發體驗來看, 在 Vite 模式下, 開發環境可以瞬間啟動, 但是等到頁面出來, 要等一段時間。

我的專案如何植入 Vite

新專案

建立一個 Vite 新專案就比較簡單:

yarn create @vitejs/app

image.png

image.png

生成好之後, 直接啟動就可以了:

image.png

已有專案

已有專案的遷移, 稍微繁瑣一些。

首先, 加入 Vite 的相關設定。 這裡我使用了一個 cli 工具: wp2vite.

安裝好之後, 直接執行:

image.png

這一步, 會自動生成 Vite 的組態檔,並引入相關的依賴。

把依賴安裝一下, 啟動就可以了。

如果沒有意外的話, 你會收穫一堆報錯

恭喜你,進入開心愉快的踩坑環節。

我在改造過程中遇到的問題

1. alias 錯誤

image.png

專案程式碼裡設定了一些別名,vite 無法識別,所以需要在vite 裡面也設定 alias:

  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },

2. 無法識別 less 全域性變數

image.png

解決辦法:

把自定義的全域性變數從外部注入即可, 直接在 vite.config.js 的 css 選項中加入:

  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          hack: `true;@import '${resolve('./src/vars.less')}';`,
          ...themeVariables,
        },
        javascriptEnabled: true,
      },
    },
  },

3. Uncaught Error: Target container is not a DOM element.

image.png

根元素未找到。

原因是: 預設生成的 index.html 中:

<div id="root"></div>

id 是 root, 而邏輯中的是#app, 這裡直接改成 id=app 即可。

4. typings 檔案找不到

image.png

typings 檔案未找到

這個錯誤, 乍一看, 一頭霧水。

進去看一下原始碼和編譯後的程式碼:

原始碼:

image.png

編譯後:

image.png

image.png

typings 檔案這不是好好的在這嗎, 怎麼就找不到?

想了一下: Vite 不知道 typeings 檔案是不需要被編譯的,需要告訴編譯器不編譯這個檔案。

最後在 TS 官方檔案裡找到了答案:

https://www.typescriptlang.or...

Type-Only Imports and Export

This feature is something most users may never have to think about; however, if you’ve hit issues under --isolatedModules, TypeScript’s transpileModule API, or Babel, this feature might be relevant.

TypeScript 3.8 adds a new syntax for type-only imports and exports.

import type { SomeThing } from "./some-module.js";
export type { SomeThing };

需要單獨引入types, 於是把程式碼改為:

image.png

同時要注意, 如果一個檔案有有多個匯出, 也要分開引入:

image.png

唯一痛苦的是: 全域性都需要改一遍, 體力活。

至此,typeings 問題完美解決。

5. 無法識別 svg

我們在使用 svg 作為圖示元件的時候, 一般是:

import Icon from '@ant-design/icons';
import ErrorSvg from '@/assets/ico_error.svg';

const ErrorIcon = (props: any) => <Icon component={ErrorSvg} />;

// ...
<ErrorIcon />

瀏覽器報錯:

image.png

error occurred in the </src/assets/ico_error.svg> component

很明顯的看到, 這裡是把檔案路徑作為元件了。

現在要做的是:把這個檔案路徑, 換成可以識別的元件。

搜尋一番, 找到了個外掛: vite-plugin-react-svg

加入設定:

const reactSvgPlugin = require('vite-plugin-react-svg');

plugins: [
  reactSvgPlugin(),
],
import MyIcon from './svgs/my-icon.svg?component';

function App() {
  return (
    <div>
      <MyIcon />
    </div>
  );
}

需要注意的是: 引入的 svg 檔案需要加 ?component 作為字尾。

看了一下原始碼, 這個字尾是用來作為識別符號的,

image.png

如果字尾匹配上是component, 就解析檔案, 並快取, 最後返回結果:

image.png

知道原理之後, 就需要把全部的 .svg => .svg?component

vscode 一鍵替換就可以, 不過注意別把 node_module 裡面的也替換了。

6. global 未定義

image.png

global 是 Node裡面的變數, 會在使用者端報錯 ?

一層層看下去, 原來是引入的第三方包使用了global。

看 vite 檔案裡提到了 Client Types:

image.png

追加到 tsconfig 裡面:

 "compilerOptions": {
    "types": ["node", "jest", "vite/client"],
 }

然後, 並沒有什麼亂用。。。

image.png

沒辦法, 只得祭出 window 大法。

在入口index.tsx 裡面加上:

(window as any).global = window;

重新整理, 好了。

image.png

7. [未解決] 替代HtmlWebpackPlugin

還需要注入一些外部變數, 修改入口html, favicon, title 之類。

找到一個外掛: vite-plugin-singlefile

不過並沒有什麼用。

有了解的同學請留言賜教。

至此, 整個app 已經能在本地跑起來了, build 也沒問題。

7. 線上打包構建時, 記憶體溢位

本地能跑起來, 打包也沒問題, 後面當然是放到線上跑一跑啦。

立刻安排!

image.png

記憶體不足, 我就給你加點:

image.png

image.png

搞定!

unnamed.gif

關於 Vite 開發、打包上線的一些思考

從實際使用來看, vite 在一些功能上還是無法完全替代 webpack。

畢竟是後起之秀, 相關的生態還需要持續完善。

個人認為,目前一種比較穩妥的方式是:

  • 保留 webpack dev & build 的能力, vite 僅作為開發的輔助

等相關工具再完善一些, 再考慮完全遷移過來。

相關程式碼和結論

一個完整的 Vite demo

倉庫地址: https://github.com/beMySun/re...

image.png

業務專案的 vite.config.js 完整設定

import { defineConfig } from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
import legacyPlugin from '@vitejs/plugin-legacy';
import { resolve } from 'path';

const fs = require('fs');
const lessToJS = require('less-vars-to-js');
const themeVariables = lessToJS(fs.readFileSync(resolve(__dirname, './src/antd-custom.less'), 'utf8'));
const reactSvgPlugin = require('vite-plugin-react-svg');

// https://cn.vitejs.dev/config/
export default defineConfig({
  base: './',
  root: './',
  resolve: {
    alias: {
      'react-native': 'react-native-web',
      '@': resolve(__dirname, 'src'),
    },
  },
  define: {
    'process.env.REACT_APP_IS_LOCAL': '\'true\'',
    'window.__CID__': JSON.stringify(process.env.cid || 'id'),
  },
  server: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'https://stoku.test.shopee.co.id/',
        changeOrigin: true,
        cookieDomainRewrite: {
          'stoku.test.shopee.co.id': 'localhost',
        },
      },
    },
  },
  build: {
    target: 'es2015',
    minify: 'terser',
    manifest: false,
    sourcemap: false,
    outDir: 'build',
    rollupOptions: {},
  },
  esbuild: {},
  optimizeDeps: {},
  plugins: [
    // viteSingleFile({
    //   title: 'dynamic title', // doesn't work
    // }),
    reactSvgPlugin(),
    reactRefresh(),
    legacyPlugin({
      targets: [
        'Android > 39',
        'Chrome >= 60',
        'Safari >= 10.1',
        'iOS >= 10.3',
        'Firefox >= 54',
        'Edge >= 15',
      ],
    }),
    // vitePluginImp({
    //   libList: [
    //     {
    //       libName: 'antd',
    //       style: (name) => `antd/es/${name}/style`,
    //     },
    //   ],
    // }),
  ],
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          hack: `true;@import '${resolve('./src/vars.less')}';`,
          ...themeVariables,
        },
        javascriptEnabled: true,
      },
    },
  },
});

最後

使用 Vite 能大幅縮短專案構建時間,提升開發效率。

不過也要結合專案的實際情況,合理取捨。

對於我的這個專案而言,把 Vite 作為輔助開發的一種方式,還是挺有用的。

期待 Vite 能繼續完善,為研發提效。

好了, 內容大概就這麼多, 希望對大家有所幫助。

才疏學淺,如有錯誤, 歡迎指正。

謝謝。

最後,如果覺得內容有幫助, 可以關注下我的公眾號,掌握最新動態,一起學習!

image.png