實(shí)踐)
如果你和我一樣常年泡在 React 項(xiàng)目里應(yīng)該對(duì)下面這個(gè)場(chǎng)景特別熟悉項(xiàng)目用腳手架拉起來(lái)之后先裝路由、再配代理、補(bǔ) Mock、調(diào)打包配置、加狀態(tài)管理……業(yè)務(wù)一行代碼沒(méi)寫一上午已經(jīng)沒(méi)了。最近我把 Umi 從頭到尾認(rèn)真過(guò)了一遍最大的感受就是它把這些重復(fù)勞動(dòng)全部收編成“約定 插件”你要做的只是填業(yè)務(wù)。這篇初探記錄我會(huì)把選型理由、項(xiàng)目初始化、路由體系、聯(lián)調(diào)配置還有我實(shí)際踩過(guò)的坑一次說(shuō)清楚。適合剛接觸 Umi 的前端同學(xué)也適合在多套 React 項(xiàng)目之間反復(fù)搬運(yùn)配置的團(tuán)隊(duì)參考。1. 為什么要用 Umi先搞清楚它在解決什么問(wèn)題1.1 從“能用”到“好用”腳手架只是起點(diǎn)React 生態(tài)里能幫你把工程搭起來(lái)的工具其實(shí)不少但它們大多只解決“編譯能跑”這一個(gè)點(diǎn)。跑起來(lái)之后你會(huì)發(fā)現(xiàn)自己面前還有一排問(wèn)題排隊(duì)等著路由用哪個(gè)版本、請(qǐng)求庫(kù)怎么封裝、代理怎么配、Mock 要不要上、CSS 怎么處理、代碼分割怎么做、環(huán)境變量怎么區(qū)分……這些問(wèn)題單個(gè)看都不難但組合在一起就是一筆不小的維護(hù)成本。我見過(guò)很多項(xiàng)目光package.json里的依賴就有上百個(gè)其中一半是“為了把框架拼起來(lái)”而裝的。更頭疼的是這些依賴的版本和配置是強(qiáng)耦合的升級(jí)一個(gè)就可能連鎖炸一片。Umi 做的事情是把這些“組裝工作”從你手里拿走讓框架本身替你做出默認(rèn)選擇。它內(nèi)置了路由、打包優(yōu)化、代理、Mock、環(huán)境變量處理、SSR/預(yù)渲染等一堆能力你只需要聚焦業(yè)務(wù)代碼。用一句不太準(zhǔn)確但很好懂的話來(lái)說(shuō)普通腳手架給你一個(gè)毛坯房Umi 給你的是已經(jīng)走好水電的屋子你直接往里擺家具就行。1.2 插件化架構(gòu)把“框架”變成“積木”Umi 最核心的設(shè)計(jì)不是它自帶的某個(gè)功能而是它的插件機(jī)制。整個(gè)框架更像一個(gè)可插拔的底座路由、代碼分割、快速刷新、狀態(tài)管理、權(quán)限、布局這些能力本質(zhì)上都是一個(gè)個(gè)插件。這個(gè)設(shè)計(jì)的價(jià)值在于“按需使用”。你不用為了一個(gè)權(quán)限功能被迫引入一堆無(wú)關(guān)依賴反過(guò)來(lái)如果你需要某種團(tuán)隊(duì)內(nèi)部特有的能力也可以自己寫插件注入到框架的編譯流程里。項(xiàng)目在這個(gè)機(jī)制下像是一個(gè)集裝箱每塊功能都是標(biāo)準(zhǔn)卡槽需要哪塊就插哪塊。實(shí)際使用中你會(huì)發(fā)現(xiàn)Umi 的插件不是“能用就行”的程度而是可以深度定制編譯流程的。比如你想在構(gòu)建時(shí)自動(dòng)注入一段腳本、想統(tǒng)一修改產(chǎn)物文件名、想對(duì)某個(gè)路由做特殊處理都可以通過(guò)插件 API 去介入。對(duì)個(gè)人項(xiàng)目這可能是錦上添花但對(duì)一個(gè)長(zhǎng)期維護(hù)的團(tuán)隊(duì)項(xiàng)目來(lái)說(shuō)這是能不能形成“自己的工程體系”的分水嶺。1.3 哪些團(tuán)隊(duì)最適合接 Umi說(shuō)點(diǎn)掏心窩的話不是所有項(xiàng)目都適合上 Umi。我會(huì)優(yōu)先推薦給這幾種場(chǎng)景項(xiàng)目多、團(tuán)隊(duì)小團(tuán)隊(duì)同時(shí)維護(hù)好幾個(gè)中后臺(tái)系統(tǒng)如果每個(gè)項(xiàng)目的構(gòu)建配置都是復(fù)制粘貼出來(lái)的后期維護(hù)就是噩夢(mèng)。Umi 把公共能力收斂到框架里團(tuán)隊(duì)統(tǒng)一版本、統(tǒng)一約定成員在不同項(xiàng)目間流動(dòng)時(shí)幾乎沒(méi)有學(xué)習(xí)成本。前后端分離但聯(lián)調(diào)頻繁代理和 Mock 是 Umi 的開箱能力后端接口沒(méi)寫好、環(huán)境不穩(wěn)定的時(shí)候前端可以完全獨(dú)立跑通自己的流程。想快速出原型或做內(nèi)部工具約定式路由意味著“新建一個(gè)文件就是一個(gè)頁(yè)面”這種爽感在你趕工的時(shí)候會(huì)特別明顯。對(duì)框架有潔癖但又不想要黑盒插件機(jī)制給了一條出口團(tuán)隊(duì)可以把公共規(guī)范沉淀成內(nèi)部插件而不是散落在各個(gè)項(xiàng)目里的“復(fù)制粘貼文檔”。反過(guò)來(lái)說(shuō)如果你做的項(xiàng)目非常小只有一個(gè)頁(yè)面、一次交互那沒(méi)必要為了 Umi 多一層心智負(fù)擔(dān)如果你追求的完全是零依賴、手動(dòng)掌控一切的極簡(jiǎn)主義那 Umi 這種“全家桶”式的集成反而會(huì)讓你難受。工具沒(méi)有絕對(duì)的好只有匹配不匹配。2. 5 分鐘快速初始化 Umi 項(xiàng)目環(huán)境、命令與目錄拆解2.1 環(huán)境準(zhǔn)備Node 版本與包管理器Umi 4 對(duì) Node 版本有要求一般建議使用 16 及以上的版本我實(shí)際測(cè)試時(shí)在 18 和 20 的 LTS 上都很穩(wěn)。如果本機(jī)版本太舊第一步就會(huì)死在依賴安裝上。包管理器方面官方場(chǎng)景里 npm、yarn、pnpm 都支持但我個(gè)人更推薦 pnpm。原因很實(shí)在Umi 的依賴樹不小pnpm 的硬鏈接機(jī)制能讓安裝速度快不少磁盤占用也低。如果是團(tuán)隊(duì)協(xié)作一定要把包管理器鎖死在項(xiàng)目里最直接的辦法就是在根目錄放packageManager字段或者.npmrc避免有人用 npm 裝、有人用 yarn 裝最后 lock 文件互相打架。2.2 腳手架命令從空目錄到跑起來(lái)初始化 Umi 項(xiàng)目非常直接我習(xí)慣在一個(gè)空目錄里執(zhí)行# 確認(rèn) Node 版本 node -v # 用 pnpm 初始化 Umi 項(xiàng)目 pnpm dlx create-umilatest # 或者用 npm npx create-umilatest執(zhí)行之后會(huì)出現(xiàn)交互式提示讓你選擇應(yīng)用類型。初次上手建議選“簡(jiǎn)單應(yīng)用Simple App”它生成的項(xiàng)目結(jié)構(gòu)最精簡(jiǎn)適合用來(lái)理解 Umi 的核心機(jī)制。等你熟悉了再去嘗試那些帶更多內(nèi)置能力的模板。項(xiàng)目生成后按提示安裝依賴然后啟動(dòng)pnpm install pnpm dev不出意外的話編譯完成后終端會(huì)提示訪問(wèn)http://localhost:8000。第一次看到頁(yè)面秒開的時(shí)候我已經(jīng)對(duì)這個(gè)架子產(chǎn)生好感了——它沒(méi)有把我丟進(jìn)一堆配置里而是先給我一個(gè)能跑的最小閉環(huán)。2.3 目錄結(jié)構(gòu)幾個(gè)必須認(rèn)識(shí)的約定Umi 是“約定優(yōu)于配置”的典型代表所以目錄結(jié)構(gòu)本身就在表達(dá)規(guī)則。初始化出來(lái)的精簡(jiǎn)項(xiàng)目大致長(zhǎng)這樣├── src/ │ ├── pages/ # 約定式路由的頁(yè)面目錄 │ │ ├── index.tsx # 對(duì)應(yīng)路徑 / │ │ └── users/[id].tsx # 動(dòng)態(tài)路由對(duì)應(yīng) /users/:id │ ├── layouts/ # 全局布局可選 │ └── app.ts(x) # 運(yùn)行時(shí)擴(kuò)展登錄態(tài)、請(qǐng)求配置等 ├── config/ │ └── config.ts # 項(xiàng)目配置文件 ├── mock/ # 本地 Mock 數(shù)據(jù)目錄 ├── .env.local # 本地環(huán)境變量 └── package.json這里有兩個(gè)新手特別容易踩的點(diǎn)我提前幫你排一下雷。第一個(gè)是配置文件的位置Umi 允許.umirc.ts和config/config.ts二選一但不要兩個(gè)都放。我個(gè)人建議用config/config.ts因?yàn)轫?xiàng)目復(fù)雜后配置可以繼續(xù)拆分成多個(gè)模塊放進(jìn)config目錄而.umirc.ts只適合極簡(jiǎn)場(chǎng)景。第二個(gè)是pages目錄名的敏感度Umi 對(duì)頁(yè)面文件的大小寫、命名格式都有約定如果新增文件后發(fā)現(xiàn)路由沒(méi)生效先檢查文件名和路徑大概率是大小寫不一致。3. 路由體系詳解約定式路由與配置式路由的組合打法3.1 約定式路由新建文件就是新增頁(yè)面Umi 最讓人上頭的設(shè)計(jì)之一就是約定式路由。核心規(guī)則很簡(jiǎn)單src/pages目錄下的文件路徑就是頁(yè)面的路由地址。// src/pages/index.tsx export default function HomePage() { return div這里是首頁(yè)/div; }這個(gè)文件對(duì)應(yīng)的是/。如果再建一個(gè)src/pages/users.tsx那它就自動(dòng)映射到/users完全不需要去某個(gè)配置文件里聲明路由。文件路徑和路由的對(duì)應(yīng)關(guān)系看這張表就夠了文件位置對(duì)應(yīng)路由說(shuō)明src/pages/index.tsx/首頁(yè)src/pages/users.tsx/users普通頁(yè)面src/pages/users/[id].tsx/users/:id動(dòng)態(tài)路由src/pages/404.tsx自動(dòng)兜底未匹配路徑動(dòng)態(tài)路由是實(shí)際項(xiàng)目里很高頻的需求比如用戶詳情頁(yè)/users/123。在 Umi 4 里寫法是把動(dòng)態(tài)段用方括號(hào)包起來(lái)文件名叫[id].tsx然后頁(yè)面里通過(guò)useParams取參數(shù)import { useParams } from umi; export default function UserDetail() { const { id } useParams(); return div當(dāng)前用戶 ID{id}/div; }這種“文件即路由”的模式對(duì)團(tuán)隊(duì)協(xié)作特別友好。后端接口文檔里寫一個(gè)“新增用戶詳情頁(yè)面”你只需要往pages里丟一個(gè)文件路由、懶加載、代碼分割全自動(dòng)搞定。項(xiàng)目早期頁(yè)面增長(zhǎng)快的時(shí)候這個(gè)優(yōu)勢(shì)會(huì)被放大得很明顯。3.2 配置式路由復(fù)雜場(chǎng)景下的一頁(yè)紙方案約定式路由很好用但遇到權(quán)限控制、嵌套布局、需要?jiǎng)討B(tài)生成路由的場(chǎng)景它就顯得不夠靈活了。這時(shí)候可以切換成配置式路由把所有路由集中寫在config/config.ts里import { defineConfig } from umi; export default defineConfig({ routes: [ { path: /, component: /layouts/index }, { path: /login, component: ./login }, { path: /users, component: ./users/index, routes: [ { path: /users/list, component: ./users/list }, { path: /users/:id, component: ./users/[id] }, ], }, { path: *, component: ./404 }, ], });這里需要注意幾個(gè)點(diǎn)。一是配置式路由下component的路徑默認(rèn)相對(duì)src/pages也可以用/這種別名指向其他目錄比如上面的/layouts/index。二是路徑一定要寫完整子路由的path不要只寫相對(duì)片段否則很容易出現(xiàn)“頁(yè)面找得到但 URL 對(duì)不上”的詭異問(wèn)題。三是約定式路由和配置式路由不要混用一旦你聲明了routes字段約定式路由就會(huì)關(guān)閉兩邊同時(shí)存在只會(huì)讓你懷疑人生。什么時(shí)候該用配置式路由我的經(jīng)驗(yàn)是當(dāng)項(xiàng)目開始出現(xiàn)“這個(gè)頁(yè)面要登錄才能看”“這幾個(gè)頁(yè)面共用一套布局”“路由需要根據(jù)用戶角色動(dòng)態(tài)生成”這一類需求時(shí)就該切過(guò)去了。它是比約定式更重、但掌控力更強(qiáng)的方案。3.3 布局、權(quán)限與動(dòng)態(tài)路由的實(shí)戰(zhàn)結(jié)合先看布局。Umi 支持全局布局文件src/layouts/index.tsx它會(huì)在頁(yè)面外層自動(dòng)包裹一層結(jié)構(gòu)import { Outlet } from umi; export default function GlobalLayout() { return ( div header style{{ padding: 16, borderBottom: 1px solid #eee }} 這里是公共頭部 /header main style{{ padding: 16 }} Outlet / /main /div ); }注意子頁(yè)面通過(guò)Outlet /渲染這是路由嵌套的標(biāo)準(zhǔn)玩法。在配置式路由里直接在頂層路由的component指向這個(gè) Layout 即可。權(quán)限是另一個(gè)高頻訴求。Umi 提供wrappers機(jī)制可以給某個(gè)路由套一層“包裝組件”在渲染頁(yè)面之前做攔截{ path: /admin, component: ./admin/index, wrappers: [/wrappers/auth], }然后在src/wrappers/auth.tsx里寫校驗(yàn)邏輯import { Navigate } from umi; export default function AuthWrapper({ children }) { const isLogin localStorage.getItem(token); if (!isLogin) { return Navigate to/login replace /; } return children; }這套組合拳打下來(lái)布局和權(quán)限就從“散落在每個(gè)頁(yè)面里的重復(fù)代碼”變成了“路由層面的統(tǒng)一約束”。我在實(shí)際項(xiàng)目里最喜歡的是這種收攏感你不需要在每個(gè)頁(yè)面里復(fù)制一段判斷登錄的邏輯系統(tǒng)級(jí)的規(guī)則就應(yīng)該在系統(tǒng)級(jí)處理。4. 本地開發(fā)三件套Mock、代理與多環(huán)境配置4.1 代理配置前后端聯(lián)調(diào)不再改代碼前端開發(fā)最煩的一件事就是接口聯(lián)調(diào)時(shí)跨域。Umi 的 dev server 內(nèi)置了代理能力在配置文件里加一段proxy即可export default defineConfig({ proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true, pathRewrite: { ^/api: }, }, }, });解釋一下這三個(gè)關(guān)鍵配置的含義。target是后端真實(shí)地址changeOrigin表示把請(qǐng)求頭里的 Host 改成目標(biāo)地址很多后端會(huì)校驗(yàn) Host不設(shè)置就可能被拒pathRewrite是把瀏覽器端請(qǐng)求的路徑做一次改寫——比如瀏覽器請(qǐng)求/api/user經(jīng)過(guò)代理后會(huì)變成http://127.0.0.1:8080/user也就是把/api這個(gè)前綴去掉。這里有個(gè)新手容易迷糊的點(diǎn)代理只作用于本地 dev server跟你部署到服務(wù)器之后毫無(wú)關(guān)系。生產(chǎn)環(huán)境通常由網(wǎng)關(guān)或反向代理來(lái)轉(zhuǎn)發(fā)/api所以不要在代碼里寫死任何環(huán)境相關(guān)地址統(tǒng)一用相對(duì)路徑/api/xxx發(fā)起請(qǐng)求。4.2 Mock 數(shù)據(jù)后端沒(méi)寫好也能先跑頁(yè)面Umi 的 Mock 能力是我在所有前端框架里用下來(lái)最順手的。做法很簡(jiǎn)單在根目錄新建mock文件夾放一個(gè) TS 文件// mock/api.ts export default { GET /api/user: { name: 張三, id: 1 }, POST /api/login: (req, res) { const { username } req.body; if (username admin) { res.json({ token: mock-token }); } else { res.status(401).json({ message: 用戶名錯(cuò)誤 }); } }, };字符串形式的寫法適合直接返回靜態(tài)數(shù)據(jù)函數(shù)形式的寫法可以處理請(qǐng)求體、返回不同狀態(tài)碼。我在聯(lián)調(diào)階段最常用的姿勢(shì)是先用 Mock 把頁(yè)面交互完整跑通后端接口就緒后一鍵關(guān)掉 Mock 切換到真實(shí)接口業(yè)務(wù)代碼幾乎不用改。這里要提醒一點(diǎn)Mock 文件修改后不同版本 Umi 的熱更新行為不完全一樣如果你發(fā)現(xiàn)改了文件但接口返回沒(méi)變不用懷疑自己寫錯(cuò)了直接手動(dòng)重啟一下pnpm dev基本都能解決。另外Mock 目錄默認(rèn)只在開發(fā)環(huán)境下生效這正好符合我們的使用場(chǎng)景。4.3 環(huán)境變量與 UMI_ENV一套代碼多套環(huán)境多環(huán)境是每個(gè)正經(jīng)項(xiàng)目的必修課。Umi 約定根目錄下的.env、.env.local、.env.{UMI_ENV}會(huì)被自動(dòng)讀取。比如# .env UMI_APP_TITLE本地環(huán)境 API_BASE_URL/api # .env.prod UMI_APP_TITLE生產(chǎn)環(huán)境正常情況下process.env.UMI_APP_TITLE可以在代碼里直接使用。注意一個(gè)細(xì)節(jié)不是所有環(huán)境變量都會(huì)暴露到瀏覽器端只有以UMI_APP_開頭的變量會(huì)被安全注入其他變量?jī)H存在于 Node 側(cè)。這個(gè)設(shè)計(jì)是為了防止你不小心把密鑰之類的東西打進(jìn)前端包。那UMI_ENV怎么用它是 Umi 自己讀取的環(huán)境變量標(biāo)識(shí)用來(lái)決定加載哪個(gè).env文件。舉個(gè)例子# 開發(fā)時(shí)默認(rèn)加載 .env pnpm dev # 構(gòu)建生產(chǎn)包時(shí)加載 .env.prod UMI_ENVprod pnpm buildWindows 下設(shè)置環(huán)境變量不如 Linux 那么直接我一般用cross-env來(lái)抹平差異。這一整套組合下來(lái)一套代碼可以在本地、測(cè)試、生產(chǎn)三套環(huán)境間無(wú)縫切換部署腳本里只需要改UMI_ENV的值。5. 插件生態(tài)與常見問(wèn)題排查我的踩坑記錄5.1 常用官方插件先看看有沒(méi)有現(xiàn)成輪子Umi 生態(tài)里相當(dāng)一部分能力是通過(guò)插件提供的。初始化項(xiàng)目時(shí)交互式命令行就會(huì)讓你勾選需要的功能比如組件庫(kù)接入、全局布局、狀態(tài)管理、請(qǐng)求封裝、權(quán)限控制等。我的建議是不要嫌預(yù)置能力多但也不要在小項(xiàng)目里全選。這里分享一個(gè)選型原則**優(yōu)先使用官方維護(hù)的插件其次是社區(qū)高星插件最后才考慮自己造輪子。**原因很簡(jiǎn)單Umi 的插件和框架版本耦合比較緊官方插件會(huì)跟隨大版本及時(shí)適配社區(qū)插件則可能出現(xiàn)“框架升級(jí)了、插件沒(méi)人維護(hù)了”的尷尬情況。我自己就遇到過(guò)依賴的社區(qū)插件在 Umi 4 下不兼容最后不得不替換方案的情況。5.2 高頻報(bào)錯(cuò)與解決對(duì)照表整理一份我實(shí)測(cè)中最高頻的問(wèn)題對(duì)照表按“現(xiàn)象 → 可能原因 → 處理方式”的格式來(lái)希望能幫你少走點(diǎn)彎路現(xiàn)象可能原因處理方式啟動(dòng)時(shí)報(bào) Node 版本不支持本地 Node 過(guò)舊或過(guò)新切換到 LTS 版本18/20刪除 node_modules 重裝Mock 接口返回 404 或舊數(shù)據(jù)文件不在 mock 目錄 / 沒(méi)重啟確認(rèn)目錄名和導(dǎo)出格式手動(dòng)重啟 dev改了配置不生效同時(shí)存在 .umirc.ts 和 config/config.ts刪掉 .umirc.ts只保留一份配置代理請(qǐng)求 404pathRewrite 沒(méi)配或后端路徑不一致先用瀏覽器直連后端確認(rèn)地址再檢查 rewrite 規(guī)則新增頁(yè)面路由不出現(xiàn)文件名大小寫或路由緩存問(wèn)題檢查 pages 路徑刷新瀏覽器必要時(shí)重啟 dev這里面最容易被忽略的就是“同時(shí)存在兩份配置文件”的情況。Umi 會(huì)優(yōu)先讀取某一個(gè)但你可能會(huì)以為改的是生效的那份結(jié)果改了半小時(shí)發(fā)現(xiàn)一直在改一個(gè)“假配置”。遇到配置類問(wèn)題第一件事永遠(yuǎn)是確認(rèn)你改的文件確實(shí)被項(xiàng)目加載了。5.3 最后分享幾條避坑心得第一先跑通官方文檔的快速開始別一上來(lái)就照抄網(wǎng)上零散文章。Umi 2、3、4 之間的差異很大尤其是動(dòng)態(tài)路由寫法和部分配置項(xiàng)名稱都變過(guò)很多網(wǎng)上代碼放到 Umi 4 里就是跑不起來(lái)的。以你當(dāng)前所用大版本的官方文檔為準(zhǔn)這是最穩(wěn)的路徑。第二項(xiàng)目打算長(zhǎng)期維護(hù)的話盡早把config/config.ts里的公共配置抽成獨(dú)立函數(shù)或模塊。我實(shí)際用下來(lái)的體會(huì)是越是項(xiàng)目變大集中式配置越重要。散落在各個(gè)文件里的“臨時(shí)配置”是最難維護(hù)的因?yàn)樗鼈儧](méi)有規(guī)律可循。第三遇到“改了配置但表現(xiàn)完全沒(méi)變”的詭異問(wèn)題先別急著懷疑框架。多數(shù)情況下是緩存或者殘留的臨時(shí)文件在作怪。把項(xiàng)目根目錄下的臨時(shí)產(chǎn)物目錄刪掉再重新pnpm dev一次八成能解決。如果還不行跑一下umi setup重新生成項(xiàng)目依賴文件也能救回來(lái)很多莫名其妙的問(wèn)題。最后再分享一個(gè)我自己的直觀感受框架好不好用不在別人怎么說(shuō)而在于你寫業(yè)務(wù)時(shí)是不是真的少操了那份心。第一次跑通 Umi 的 dev、看著約定好的文件變成頁(yè)面自動(dòng)刷新的那一下我確實(shí)松了口氣。如果你在 Umi 里也折騰出過(guò)奇怪的坑或者發(fā)現(xiàn)了值得分享的玩法歡迎按著這套思路自己動(dòng)手搭一個(gè)項(xiàng)目試試。紙上得來(lái)終覺(jué)淺配置這種東西永遠(yuǎn)是自己跑通一遍才記得住。