UmiJS 中后臺(tái)項(xiàng)目實(shí)踐
背景
中后臺(tái)項(xiàng)目一般都有較強(qiáng)的頁(yè)面結(jié)構(gòu)或者邏輯一致性,頁(yè)面比如像搜索、表格、導(dǎo)航菜單、布局,邏輯方面比如像數(shù)據(jù)流,權(quán)限。如果基于 Webpack 封裝這些功能就需要比較大的前期工作,Umi 則以路由為基礎(chǔ),并以此進(jìn)行功能擴(kuò)展,包含微前端、組件打包、請(qǐng)求庫(kù)、hooks 庫(kù)、數(shù)據(jù)流等。基于此在公司內(nèi)落地 umi 的實(shí)踐。
目錄結(jié)構(gòu)
基于 umi 的項(xiàng)目整體目錄結(jié)構(gòu)說(shuō)明,對(duì)項(xiàng)目能有個(gè)大致的了解
├──?package.json
├──?config
??└──?config.js
├──?dist
├──?mock
├──?public
└──?src
????├──?.umi
????├──?layouts/index.js
????├──?locales
????├──?models
????├──?pages
????????├──?index.less
????????└──?index.js
????├──?services
????├──?wrappers
????├──?global.js
????└──?app.js
config.js — 主要是路由配置,插件配置, webpack配置layouts — 布局相關(guān) locales — 國(guó)際化 models — dva數(shù)據(jù)流方案或者plugin-modelwrappers — 配置路由的高階組件封裝,比如路由級(jí)別的權(quán)限校驗(yàn) app.js — 運(yùn)行時(shí)配置,比如需要?jiǎng)討B(tài)修改路由,覆蓋渲染 render,監(jiān)聽(tīng)路由變化global.js — 全局執(zhí)行入口,比如可以放置 sentry等
路由
路由可以說(shuō)是前端項(xiàng)目的基石,下面談?wù)劼酚上嚓P(guān)的配置
//?config/route.js
export?default?[{
??path:?'/merchant',
??name:?'商戶管理',
??routes:?[
????{
??????path:?'/merchant/list',
??????name:?'商戶列表'
??????component:?'./list'
????},
????{
??????path:?'/merchant/detail',
??????name:?'商戶詳情',
??????hideInMenu:?true,
??????component:?'./detail'
????}
??]
}]
路由配置除了常規(guī)的 name,path,component 也可以支持配置 umi 插件的配置選項(xiàng),比如pro-layout的 hideInMenu 來(lái)隱藏路由對(duì)應(yīng)導(dǎo)航菜單項(xiàng)
路由組件按需加載可以在 config.js 中配置開(kāi)啟
//?config/config.js
export?default?{
??dynamicImport:?{}
}
路由也支持 hook 鉤子操作,比如登錄后再訪問(wèn)登錄頁(yè)面就重定向到首頁(yè)
//?config/route.js
{
??path:?'/login',
?wrappers:?[
????'@/wrappers/checkLogin',
??],
??component:?'./Login'
}
某些項(xiàng)目的路由可能是數(shù)據(jù)庫(kù)配置的,這個(gè)時(shí)候就需要?jiǎng)討B(tài)路由,從接口獲取數(shù)據(jù)創(chuàng)建路由
//?src/app.js
let?extraRoutes;
export?function?patchRoutes({?routes?})?{
??merge(routes,?extraRoutes);
}
export?function?render()?{
??fetch('/api/routes').then((res)?=>?{?extraRoutes?=?res.routes?})
}
數(shù)據(jù)流方案選擇
使用 @umijs/plugin-dva,開(kāi)發(fā)方式類似 redux
//?config/config.js
export?default?{
??dva:?{
????immer:?true,
????hmr:?false,
??}
}
約定是到 model 組織方式,不用手動(dòng)注冊(cè) model文件名即 namespace, model內(nèi)如果沒(méi)有聲明namespace,會(huì)以文件名作為namespace內(nèi)置 dva-loading,直接 connect loading字段使用即可
使用 @umijs/plugin-model
一種基于 hooks 范式的簡(jiǎn)易數(shù)據(jù)管理方案(部分場(chǎng)景可以取代 dva),通常用于中臺(tái)項(xiàng)目的全局共享數(shù)據(jù)。
//?src/models/useAuthModel.js
import?{?useState,?useCallback?}?from?'react'
export?default?function?useAuthModel()?{
??const?[user,?setUser]?=?useState(null)
??const?signin?=?useCallback((account,?password)?=>?{
????//?signin?implementation
????//?setUser(user?from?signin?API)
??},?[])
??const?signout?=?useCallback(()?=>?{
????//?signout?implementation
????//?setUser(null)
??},?[])
??return?{
????user,
????signin,
????signout
??}
}
使用 Model
import?{?useModel?}?from?'umi';
export?default?()?=>?{
??const?{?user,?fetchUser?}?=?useModel('user',?model?=>?({?user:?model.user,?fetchUser:?model.fetchUser?}));
??return?<>hello>
};
從使用體驗(yàn)來(lái)講,中臺(tái)項(xiàng)目基本就是表單和表格,跨頁(yè)面共享數(shù)據(jù)場(chǎng)景并不是很多,使用 dva 有點(diǎn)過(guò)重,因此推薦使用第 2 種 plugin-model 這種輕量級(jí)的
布局

@umijs/plugin-layout 插件提供了更加方便的布局
默認(rèn)為 Ant Design 的 Layout @ant-design/pro-layout[1],支持它全部配置項(xiàng)。 側(cè)邊欄菜單數(shù)據(jù)根據(jù)路由中的配置自動(dòng)生成。 默認(rèn)支持對(duì)路由的 403/404 處理和 Error Boundary。 搭配 @umijs/plugin-access 插件一起使用,可以完成對(duì)路由權(quán)限的控制。
//?src/app.js
export?const?layout?=?{
??logout:?()?=>?{},?//?do?something
??rightRender:(initInfo)=>?{?return?'hahah';?},//?return?string?||?ReactNode;
};
權(quán)限
一般項(xiàng)目離不開(kāi)權(quán)限的管理, umi 使用 @umijs/plugin-access 來(lái)提供權(quán)限設(shè)置
//?src/access.js
export?default?function(initialState)?{
??const?{?permissions?}?=?initialState;?//?getInitialState方法執(zhí)行后
??return?{
????canAccessMerchant:?true,
????...permissions
??}
}
對(duì)路由頁(yè)面的權(quán)限控制,在路由配置中新增 access屬性
//?config/route.js
export?default?[{
??path:?'/merchant',
??name:?'商戶管理',
??routes:?[
????{
??????path:?'/merchant/list',
??????name:?'商戶列表'
??????component:?'./list',
??????access:?'canAccessMerchant'
????}
??]
}]
當(dāng)然也可以在頁(yè)面或組件內(nèi)用 useAccess獲取到權(quán)限相關(guān)信息
import?React?from?'react'
import?{?useAccess?}?from?'umi'
const?PageA?=?props?=>?{
??const?{?foo?}?=?props;
??const?access?=?useAccess();
??if?(access.canReadFoo)?{
????//?如果可以讀取?Foo,則...
??}
??return?<>TODO>
}
export?default?PageA
實(shí)際業(yè)務(wù)開(kāi)發(fā)中,權(quán)限需要從接口動(dòng)態(tài)獲取,就需要使用 @umijs/plugin-initial-state 和 @umijs/plugin-model
//?src/app.js
/**
getInitialState會(huì)在整個(gè)應(yīng)用最開(kāi)始執(zhí)行,返回值會(huì)作為全局共享的數(shù)據(jù)。Layout 插件、Access 插件以及用戶都可以通過(guò) useModel('@@initialState')?直接獲取到這份數(shù)據(jù)
*/
export?async?function?getInitialState()?{
??const?permissions?=?await?fetchUserPermissions()
??return?{?permissions?}
}
國(guó)際化
@umijs/plugin-locale 國(guó)際化插件,用于解決 i18n 問(wèn)題
使用 antd 開(kāi)發(fā),默認(rèn)是英文,顯示中文就需要開(kāi)啟國(guó)際化配置
//?config/config.js
export?default?{
??locale:?{
????default:?'zh-CN',
????antd:?true,
????baseNavigator:?true,
??}
}
在路由中的 title 或者 name 可直接使用國(guó)際化 key,自動(dòng)被轉(zhuǎn)成對(duì)應(yīng)語(yǔ)言的文案
//?src/locales/zh-CN.js
export?default?{
??'about.title':?'關(guān)于?-?標(biāo)題',
}
//?src/locales/en-US.js
export?default?{
??'about.title':?'About?-?Title',
}
項(xiàng)目配置如下
export?default?{
??routes:?[
????{
??????path:?'/about',
??????component:?'About',
??????title:?'about.title',
????}
??]
}
集成 redux 插件
如果開(kāi)啟 dva,也就是使用 redux 來(lái)集中管理數(shù)據(jù)流,那么使用 redux-persist 插件持久化 redux 數(shù)據(jù)到 localStorage 里,大致使用如下
//?src/app.js
import?{?getDvaApp?}?from?'umi'
import?{?persistStore,?persistReducer?}?from?'redux-persist'
import?storage?from?'redux-persist/lib/storage'
import?autoMergeLevel2?from?'redux-persist/lib/stateReconciler/autoMergeLevel2'
import?createFilter?from?'redux-persist-transform-filter'
export?const?dva?=?{
??config:?{
????onError(e)?{
??????e.preventDefault()
????},
????onReducer(reducer)?{
??????const?globalCollapsedFilter?=?createFilter('global',?['collapsed'])
??????const?persistConfig?=?{
????????key:?'root',
????????storage,
????????whitelist:?['global'],
????????transforms:?[globalCollapsedFilter],
????????stateReconciler:?autoMergeLevel2
??????}
??????return?persistReducer(persistConfig,?reducer)
????}
??}
}
window.addEventListener('DOMContentLoaded',?()?=>?{
??const?app?=?getDvaApp()
??persistStore(app._store)
})
插件開(kāi)發(fā)
umi 實(shí)現(xiàn)了完整的生命周期,并使其插件化,這樣就為使用者提供了擴(kuò)展入口。比如設(shè)置默認(rèn)配置插件
export?default?api?=>?{
??api.modifyDefaultConfig(config?=>?{
????return?Object.assign({},?config,?{
??????title:?false,
??????history:?{
????????type:?'hash'
??????},
??????hash:?true,
??????antd:?{},
??????dva:?{
????????hmr:?true
??????},
??????dynamicImport:?{
????????loading:?'@/components/PageLoading'
??????},
??????targets:?{
????????ie:?10
??????},
??????runtimePublicPath:?true,
??????terserOptions:?{
????????compress:?{
??????????drop_console:?true
????????}
??????}
????});
??});
}
Umi2 升級(jí)到 Umi3 的優(yōu)勢(shì)
組內(nèi)電商項(xiàng)目在升級(jí)之前使用的是內(nèi)嵌 umi2 的 antd-design-pro4, 雖然可以滿足業(yè)務(wù)開(kāi)發(fā),但是模板依然還是有較多不符合業(yè)務(wù)的部分,比如權(quán)限校驗(yàn)這塊。
Umi3 的發(fā)布也帶來(lái)更好的架構(gòu)和開(kāi)發(fā)體驗(yàn)
配置層做了大量精簡(jiǎn) 最新的 Umi3 插件提供了 Layout, 數(shù)據(jù)流,權(quán)限等新方案 終于把模板內(nèi)的權(quán)限相關(guān)代碼內(nèi)置化了
基于 Umi 搭建腳手架模板
基于 Umi 搭建內(nèi)部中臺(tái)腳手架模板如下圖顯示

基于 Umi 此腳手架模板擴(kuò)展了如下能力
編譯打包符合公司 beetle(內(nèi)部 CI/CD 平臺(tái))部署規(guī)范的 dist 目錄 自定義默認(rèn)配置插件,減少配置項(xiàng)配置 eslint校驗(yàn)prettier格式化代碼git 提交規(guī)范 結(jié)合 pro-layout實(shí)現(xiàn)更加方便的布局利用運(yùn)行時(shí)配置 app.js動(dòng)態(tài)生成本地和遠(yuǎn)程相結(jié)合的配置式導(dǎo)航菜單結(jié)合 plugin-access插件和內(nèi)部權(quán)限系統(tǒng)實(shí)現(xiàn)頁(yè)面或按鈕級(jí)別權(quán)限控制
新建項(xiàng)目根據(jù)公司內(nèi)的腳手架工具選擇中臺(tái)模板可快速創(chuàng)建帶有權(quán)限、布局、代碼規(guī)范、通用頁(yè)面等功能的初始項(xiàng)目,可以很大的避免重復(fù)工作。
總結(jié)
Umi 提供了開(kāi)箱即用能力, 你不需要配置 webpack,babel 這些,最佳實(shí)踐配置已內(nèi)置化。當(dāng)然也可以自定義開(kāi)發(fā)插件擴(kuò)展。Umi 在性能上做了很多努力,這些對(duì)于開(kāi)發(fā)者是無(wú)感知的。
稍有不足的是 Umi 對(duì) webpack-dev-server 配置開(kāi)放較少,如果有對(duì) webpack-dev-server 有比較大配置需求則需要考量一下~~
·END·
匯聚精彩的免費(fèi)實(shí)戰(zhàn)教程
喜歡本文,點(diǎn)個(gè)“在看”告訴我


