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