
背景
中后台項目一般都有較強的頁面結構或者邏輯一致性,頁面比如像搜索、表格、導航菜單、布局,邏輯方面比如像數據流,權限。如果基於 Webpack 封裝這些功能就需要比較大的前期工作,Umi 則以路由為基礎,並以此進行功能擴展,包含微前端、組件打包、請求庫、hooks 庫、數據流等。基於此在公司內落地 umi
的實踐。
目錄結構
基於 umi
的項目整體目錄結構說明,對項目能有個大致的了解
├── 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 — 布局相關
- locales — 國際化
- models —
dva
數據流方案或者plugin-model
- wrappers — 配置路由的高階組件封裝,比如路由級別的權限校驗
- app.js — 運行時配置,比如需要動態修改路由,覆蓋渲染
render
,監聽路由變化 - global.js — 全局執行入口,比如可以放置
sentry
等
路由
路由可以說是前端項目的基石,下面談談路由相關的配置
// config/route.js
export default [{
path: '/merchant',
name: '商戶管理',
routes: [
{
path: '/merchant/list',
name: '商戶列表'
component: './list'
},
{
path: '/merchant/detail',
name: '商戶詳情',
hideInMenu: true,
component: './detail'
}
]
}]
路由配置除了常規的 name
,path
,component
也可以支持配置 umi
插件的配置選項,比如pro-layout的 hideInMenu 來隱藏路由對應導航菜單項
路由組件按需加載可以在 config.js
中配置開啟
// config/config.js
export default {
dynamicImport: {}
}
路由也支持 hook 鈎子操作,比如登錄后再訪問登錄頁面就重定向到首頁
// config/route.js
{
path: '/login',
wrappers: [
'@/wrappers/checkLogin',
],
component: './Login'
}
某些項目的路由可能是數據庫配置的,這個時候就需要動態路由,從接口獲取數據創建路由
// src/app.js
let extraRoutes;
export function patchRoutes({ routes }) {
merge(routes, extraRoutes);
}
export function render() {
fetch('/api/routes').then((res) => { extraRoutes = res.routes })
}
數據流方案選擇
- 使用 @umijs/plugin-dva,開發方式類似
redux
// config/config.js
export default {
dva: {
immer: true,
hmr: false,
}
}
- 約定是到 model 組織方式,不用手動注冊
model
- 文件名即 namespace,
model
內如果沒有聲明namespace
,會以文件名作為namespace
- 內置 dva-loading,直接 connect
loading
字段使用即可
- 使用 @umijs/plugin-model
一種基於 hooks
范式的簡易數據管理方案(部分場景可以取代 dva
),通常用於中台項目的全局共享數據。
// 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</>
};
從使用體驗來講,中台項目基本就是表單和表格,跨頁面共享數據場景並不是很多,使用 dva
有點過重,因此推薦使用第 2 種 plugin-model
這種輕量級的
布局
@umijs/plugin-layout
插件提供了更加方便的布局
- 默認為 Ant Design 的 Layout @ant-design/pro-layout[1],支持它全部配置項。
- 側邊欄菜單數據根據路由中的配置自動生成。
- 默認支持對路由的 403/404 處理和 Error Boundary。
- 搭配 @umijs/plugin-access 插件一起使用,可以完成對路由權限的控制。
// src/app.js
export const layout = {
logout: () => {}, // do something
rightRender:(initInfo)=> { return 'hahah'; },// return string || ReactNode;
};
權限
一般項目離不開權限的管理, umi 使用 @umijs/plugin-access 來提供權限設置
// src/access.js
export default function(initialState) {
const { permissions } = initialState; // getInitialState方法執行后
return {
canAccessMerchant: true,
...permissions
}
}
- 對路由頁面的權限控制,在路由配置中新增
access
屬性
// config/route.js
export default [{
path: '/merchant',
name: '商戶管理',
routes: [
{
path: '/merchant/list',
name: '商戶列表'
component: './list',
access: 'canAccessMerchant'
}
]
}]
- 當然也可以在頁面或組件內用
useAccess
獲取到權限相關信息
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
- 實際業務開發中,權限需要從接口動態獲取,就需要使用 @umijs/plugin-initial-state和 @umijs/plugin-model
// src/app.js
/**
getInitialState會在整個應用最開始執行,返回值會作為全局共享的數據。Layout 插件、Access 插件以及用戶都可以通過 useModel('@@initialState') 直接獲取到這份數據
*/
export async function getInitialState() {
const permissions = await fetchUserPermissions()
return { permissions }
}
國際化
@umijs/plugin-locale 國際化插件,用於解決 i18n
問題
使用 antd
開發,默認是英文,顯示中文就需要開啟國際化配置
// config/config.js
export default {
locale: {
default: 'zh-CN',
antd: true,
baseNavigator: true,
}
}
在路由中的 title
或者 name
可直接使用國際化 key
,自動被轉成對應語言的文案
// src/locales/zh-CN.js
export default {
'about.title': '關於 - 標題',
}
// src/locales/en-US.js
export default {
'about.title': 'About - Title',
}
項目配置如下
export default {
routes: [
{
path: '/about',
component: 'About',
title: 'about.title',
}
]
}
集成 redux 插件
如果開啟 dva
,也就是使用 redux
來集中管理數據流,那么使用 redux-persist 插件持久化 redux
數據到 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)
})
插件開發
umi
實現了完整的生命周期,並使其插件化,這樣就為使用者提供了擴展入口。比如設置默認配置插件
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 升級到 Umi3 的優勢
組內電商項目在升級之前使用的是內嵌 umi2
的 antd-design-pro4
, 雖然可以滿足業務開發,但是模板依然還是有較多不符合業務的部分,比如權限校驗這塊。
Umi3 的發布也帶來更好的架構和開發體驗
- 配置層做了大量精簡
- 最新的 Umi3 插件提供了 Layout, 數據流,權限等新方案
- 終於把模板內的權限相關代碼內置化了
基於 Umi 搭建腳手架模板
基於 Umi 搭建內部中台腳手架模板如下圖顯示
基於 Umi 此腳手架模板擴展了如下能力
- 編譯打包符合公司 beetle(內部 CI/CD 平台)部署規范的 dist 目錄
- 自定義默認配置插件,減少配置項配置
eslint
校驗prettier
格式化代碼- git 提交規范
- 結合
pro-layout
實現更加方便的布局 - 利用運行時配置
app.js
動態生成本地和遠程相結合的配置式導航菜單 - 結合
plugin-access
插件和內部權限系統實現頁面或按鈕級別權限控制
新建項目根據公司內的腳手架工具選擇中台模板可快速創建帶有權限、布局、代碼規范、通用頁面等功能的初始項目,可以很大的避免重復工作。
總結
Umi 提供了開箱即用能力, 你不需要配置 webpack,babel 這些,最佳實踐配置已內置化。當然也可以自定義開發插件擴展。Umi 在性能上做了很多努力,這些對於開發者是無感知的。
稍有不足的是 Umi 對 webpack-dev-server
配置開放較少,如果有對 webpack-dev-server
有比較大配置需求則需要考量一下~~
