我們從 UmiJS 遷移到了 Vite
我們從 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 中 @/* 路徑,代替方式
- defineConfig({
- resolve: {
- alias: {
- '@/': `${path.resolve(process.cwd(), 'src')}/`,
- },
- },
- });
遷移
Review 現有的代碼,找出可能出問題的點并統計。做前期準備。跑起來優先:
從頭 Vite 官方模板中創建一個項目,安裝所需依賴包。UmiJS 內置封裝了 react-router、antd react-intl,這里我們需要手動加上 BrowserRouter、ConfigProvider、LocaleProvider
- // App.tsx
- exportdefaultfunction App() {
- return (
- <AppProvider>
- <BrowserRouter>
- <ConfigProvider locale={currentLocale}>
- <LocaleProvider>
- <BasicLayout>
- <Routes />
- </BasicLayout>
- </LocaleProvider>
- </ConfigProvider>
- </BrowserRouter>
- </AppProvider>
- );
- }
根據之前約定式路由,添加相應的路由配置
- exportconst 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
- const routes = basicRoutes.map(({ trunk, ...config }) => {
- const Trunk = React.lazy(() => trunk());
- return {
- ...config,
- component: (
- <React.Suspense fallback={<Spinner />}>
- <Trunk />
- </React.Suspense>
- ),
- };
- });
- exportdefaultfunction Routes() {
- return (
- <Switch>
- {routes.map((route) => (
- <Route key={route.key || route.path} path={route.path} exact={route.exact} render={() => route.component} />
- ))}
- </Switch>
- );
- }
從原先的約定式路由遷移完成,項目中主要不兼容的地方就是從 umi 導入的成員
- 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 才走插件。
很輕松,第一個頁面成功運行。
由于遷移之后需要使用微前端,因此我們將公共配置通過外置插件統一管理。
- exportdefault defineConfig({
- server: {
- // 每個項目配置不同的端口號
- port: 3001,
- },
- plugins: [
- reactRefresh(),
- // 公共配置插件
- baseConfigPlugin(),
- // AntD 插件
- antdPlugin(),
- ],
- });
遷移后發現 Vite 需要配置的其實很少,抽取的公共配置,封裝成 Vite 插件。
- import path from'path';
- import LessPluginImportNodeModules from'less-plugin-import-node-modules';
- exportdefaultfunction 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 相關的插件,請持續關注。