我們從 UmiJS 遷移到了 Vite
theme: channing-cyan
我們從 UmiJS
遷移到 Vite
已經上線半年多了。遷移過程中也遇到了不少問題,好在 Vite
足夠優秀,繼承自 Rollup
的外掛系統,使我們有了自由發揮空間。目前很多人對 Vite
躍躍欲試,Vite
開發體驗到底怎麼樣,今天來敘敘遷移到 Vite
的親身經歷。
先說結論,Vite
已經很成熟,強烈建議有條件的可以從 webpack
遷移過來。
為什麼要放棄 UmiJS
2019 年底,在 Webpack
橫行霸道,各種腳手架琳琅滿目的時代選擇了 UmiJS
。它配置少、功能多、文件齊全、持續更新。一整套的解決方案,非常適合一個大部分非 React
技術棧的團隊。經過不斷地磨合,團隊很快適應了這種 React
開發模式,開發效率也是水漲船高。
凡事總有個原因,為什麼要遷移。2021 年初,為適應公司的發展,前端架構也需要做調整與升級。在專案日益增長的情況下,一次專案啟動需要耗費一分多鐘,熱更新也慢得基本無法使用。差點的機器配置啟動專案要麼好幾分鐘、要麼記憶體溢位。這種模式極大地降低了開發效率。無論是自定義修改內部 webpack
外掛、從各種角度如多核編譯、快取等方式優化,依然是杯水車薪。雖然 UmiJS
提供了 webpack5
外掛,不過在當時處於不可用的狀態。
我們主要的矛盾是:
- 啟動時間長
- 熱更新慢
- 太臃腫
- 框架 BUG 修復不及時
- 過度封裝,自定義外掛難度大
- 約定式功能太單一
適應業務的要求,我們也需要上微前端。UmiJS
也提供了微前端外掛 “乾坤”。但依然解決不了根本開發體驗問題。因此,在基礎腳手架上,我們尋求更多的是可控性及透明性。(儘管UmiJS
現在已經支援 Module Federation
的打包提速方案)
為什麼是 Vite
市面上的腳手架很多,陣營卻很少,大部分是基於 webpack
的上層封裝。webpack
的缺點很明顯,當冷啟動開發伺服器時,基於打包器的方式啟動必須優先抓取並構建你的整個應用,然後才能提供服務。
在瀏覽器 ESM 支援得很普遍得今天,Vite
這種可以稱得上是下一代前端開發與構建工具。在 Vite
中,HMR 是在原生 ESM 上執行的。當編輯一個檔案時,無論應用大小如何,HMR 始終能保持快速更新。
Vite
這種方式在我們習慣 webpack
的陰影下顯得尤為驚豔,可以說 Vite
完美地解決了我們所有的痛點。不過 Vite
也是剛釋出 2.0 不久,踩過坑的人也是相當少。我們便試試 Vite
。
前期調研
遷移的必要條件是在原有的功能下找到替代方案,我們便統計用到了 UmiJS
中的 API 及特性
UmiJS 配置
- alias - 配置別名(對應 resolve.alias)
- base - 設定路由字首(對應 base)
- define - 用於提供給程式碼中可用的變數(對應 define)
- outputPath - 指定輸出路徑(對應 build.outDir)
- hash - 配置是否讓生成的檔案包含 hash 字尾 (Vite 自帶)
- antd - 整合 antd 元件庫 (無需框架提供,Vite 中可自己引用)
- dva - 整合 dva 資料流(此庫已經很久沒有更新了,在 hooks 時代使用顯得格格不入。我們沒有大量使用,重寫一個檔案很輕鬆)
- locale - 國際化外掛,用於解決 i18n 問題(需要自己實現國際化邏輯,都是基於 react-intl 封裝,在 Vite 中實現無壓力)
- fastRefresh - 快速重新整理(對應 @vitejs/plugin-react-refresh 外掛)
- dynamicImport - 是否啟用按需載入(路由級的按需載入,在 Vite 中用 React.lazy 封裝)
- targets - 配置需要相容的瀏覽器最低版本(對應 @vitejs/plugin-legacy 外掛)
- theme - 配置 less 變數(對應 css.preprocessorOptions.less.modifyVars 配置)
- lessLoader - 設定 less-loader 配置項(與 theme 配置相同)
- ignoreMomentLocale - 忽略 moment 的 locale 檔案(可以通過 alias 設定別名方式解決)
- proxy - 配置代理能力(對應 server.proxy)
- externals - 設定哪些模組可以不被打包(對應 build.rollupOptions.external)
- copy - 設定要複製到輸出目錄的檔案或資料夾(對應 rollup-plugin-copy)
- mock - 配置 mock 屬性(對應 vite-plugin-mock)
- extraBabelPlugins - 配置額外的 babel 外掛(對應 @rollup/plugin-babel)
通過配置分析,基本上所有的 UmiJS
配置都可以在 Vite
中找到替代方案。除了配置還有一些約定
UmiJS 中 @/*
路徑,代替方式
js
defineConfig({
resolve: {
alias: {
'@/': `${path.resolve(process.cwd(), 'src')}/`,
},
},
});
遷移
Review 現有的程式碼,找出可能出問題的點並統計。做前期準備。跑起來優先:
從頭 Vite
官方模板中建立一個專案,安裝所需依賴包。UmiJS
內建封裝了 react-router
、antd
react-intl
,這裡我們需要手動加上 BrowserRouter
、ConfigProvider
、LocaleProvider
tsx
// App.tsx
export default function App() {
return (
<AppProvider>
<BrowserRouter>
<ConfigProvider locale={currentLocale}>
<LocaleProvider>
<BasicLayout>
<Routes />
</BasicLayout>
</LocaleProvider>
</ConfigProvider>
</BrowserRouter>
</AppProvider>
);
}
根據之前約定式路由,新增相應的路由配置
js
export const basicRoutes = [
{
path: '/',
exact: true,
trunk: () => import('@/pages/index'),
},
{
path: '/login',
exact: true,
trunk: () => import('@/pages/login'),
},
{
path: '/my-app',
trunk: () => import('@/pages/my-app'),
},
// ...
];
路由渲染元件,通過 React.lazy
實現 UmiJS
中的 dynamicImport
```jsx
const routes = basicRoutes.map(({ trunk, ...config }) => {
const Trunk = React.lazy(() => trunk());
return {
...config,
component: (
export default function Routes() {
return (
從原先的約定式路由遷移完成,專案中主要不相容的地方就是從 umi
匯入的成員
jsx
import { useIntl, history, useLocation, useSelector } from 'umi';
我們需要將所有 umi 中匯入的變數,通過編輯器的正則替換批量修改替換。
- 國際化的
useIntl
通過將語言檔案和react-intl
封裝,匯出一個全域性的formatMessage
方法 - 路由相關的 API 用
react-router-dom
匯出替換 Redux
相關的,用react-redux
匯出替換- 查詢專案中使用
require
的地方,替換為動態import
- 查詢專案中使用
process.env.NODE_ENV
,替換為import.meta.env.DEV
,因為再Vite
中不再有node.js
相關的 API
將 antd
新增進專案後,發現 babel-plugin-import
對應的 Vite
外掛似乎有問題,某些樣式在 dev 模式下缺失,打包後正常。排查發現是元件包裡面引用了 antd
,在 dev
模式下包名被“依賴預構建” 混淆,導致外掛無法正確插入 antd
的樣式。為此,我們自己寫了個外掛,在 dev 模式下全量引入樣式,prod 才走外掛。
很輕鬆,第一個頁面成功執行。
由於遷移之後需要使用微前端,因此我們將公共配置通過外接外掛統一管理。
tsx
export default defineConfig({
server: {
// 每個專案配置不同的埠號
port: 3001,
},
plugins: [
reactRefresh(),
// 公共配置外掛
baseConfigPlugin(),
// AntD 外掛
antdPlugin(),
],
});
遷移後發現 Vite
需要配置的其實很少,抽取的公共配置,封裝成 Vite
外掛。
```tsx import path from 'path'; import LessPluginImportNodeModules from 'less-plugin-import-node-modules';
export default function vitePluginBaseConfig(config: CustomConfig): Plugin {
return {
enforce: 'post',
name: 'base-config',
config() {
return {
cacheDir: '.vite',
resolve: {
alias: {
'@/': ${path.resolve(process.cwd(), 'src')}/
,
lodash: 'lodash-es',
'lodash.debounce': 'lodash-es/debounce',
'lodash.throttle': 'lodash-es/throttle',
},
},
server: {
host: '0.0.0.0',
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
'@primary-color': '#f99b0b',
...config.theme,
// 自定義 ant 字首
'@ant-prefix': config.antPrefix || 'ant',
},
plugins: [new LessPluginImportNodeModules()],
javascriptEnabled: true,
},
},
},
};
},
};
}
```
遷移的整個過程沒有想象中那麼繁雜,反而相對容易。幾乎常用的功能 Vite
都有方案支援,這也許是 Vite
的厲害之處吧。其實本質上的複雜度在於業務,專案的複雜度就是程式碼量的體現,通過 IDE 的搜尋替換,很快便完成了遷移併成功的執行。
現在,我們所有的專案都基於 Vite
,完全沒有了等待而摸魚的煩惱。
問題/解決
轉換 less
檔案 @import '~antd/es/style/themes/default.less'
中的 ~
別名報錯
配置 less
外掛less-plugin-import-node-modules
SyntaxError: The requested module 'xxx' does not provide an export named 'default'
我們將公共元件作為獨立的 npm 包之後使用時遇到的錯誤。本想著公共元件包自己不編譯,統一交給使用方編譯。所以匯出了 TS 原始檔。而這種情況常規下沒有問題,Vite
一旦遇到 CommonJS
或 UMD
的包才導致無法解析。雖然可以將無法解析的包放入 optimizeDeps.include
。但是架不住包的數量多啊,還是將它 tsc 轉譯為 JS 檔案再發布。
打包提速
首次打包發現需要 70 多秒,我們來優化打包結構
- 通過
build.minify
改為esbuild
(最新版Vite
已經預設esbuild
) 。Esbuild
比terser
快 20-40 倍,壓縮率只差 1%-2%。開啟後降低到 30 多秒 babel-plugin-import
的類似babel
外掛嚴重拖後腿,總共不到 40 秒的時間,它就要佔 10 秒。我們通過正則的方式做了個外掛,完美解決- 通過分析
rollup
對@ant-design/icons
、lodash
包的transform
數量非常多。我們將這些包也加入到剛剛做的外掛中
通過一頓操作下來,提速到 16 秒,先這樣吧。
為什麼將 cacheDir
放在根目錄
cacheDir
作為儲存快取檔案的目錄。此目錄下會儲存預打包的依賴項或 vite 生成的某些快取檔案,使用快取可以提高效能。在某些情況下需要聯調 node_modules
裡包,從而導致修改後未生效。這時需要使用 --force 命令列選項或手動刪除目錄,放在根目錄便於刪除。
相容性問題
Vite
的相容性可以通過官方的外掛 @vitejs/plugin-legacy
解決。我們已經放棄支援 IE 11,無限制在生產使用 ESM,羨慕嗎?
結語
如果你是新的專案,完全不必考慮 Webpack
了,Vite
及 rollup
的完全生態足夠支撐上生產。如果你是 Webpack
生態老專案,不忍體驗上的折磨,滿足遷移條件的話,不妨試試 Vite
,肯定會帶給你驚喜。
後面我會分享 Vite
和自己實現的微前端搭配組合,以及 Vite
相關的外掛,請持續關注。
傳送門
或獲取自己實現 vite-plugin-import 正則方式的外掛