大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以點此加我微信ruochuan12?進群參與,每周大家一起學習200行左右的源碼,共同進步。已進行三個月了,很多小伙伴表示收獲頗豐。
我們從 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
export default function App() {return (<AppProvider><BrowserRouter><ConfigProvider locale={currentLocale}><LocaleProvider><BasicLayout><Routes /></BasicLayout></LocaleProvider></ConfigProvider></BrowserRouter></AppProvider>);
}
根據之前約定式路由,添加相應的路由配置
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
const routes = basicRoutes.map(({ trunk, ...config }) => {const Trunk = React.lazy(() => trunk());return {...config,component: (<React.Suspense fallback={<Spinner />}><Trunk /></React.Suspense>),};
});export default function 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 才走插件。
很輕松,第一個頁面成功運行。
由于遷移之后需要使用微前端,因此我們將公共配置通過外置插件統一管理。
export default defineConfig({server: {// 每個項目配置不同的端口號port: 3001,},plugins: [reactRefresh(),// 公共配置插件baseConfigPlugin(),// AntD 插件antdPlugin(),],
});
遷移后發現 Vite
需要配置的其實很少,抽取的公共配置,封裝成 Vite
插件。
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
?相關的插件,請持續關注。
最近組建了一個湖南人的前端交流群,如果你是湖南人可以加我微信?ruochuan12?私信 湖南 拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我歷時3年才寫了10余篇源碼文章,但收獲了100w+閱讀
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》10余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助1000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~