項目地址:前端
medux-react-adminreact
點擊在線預覽git
本項目主要用來展現如何將 @medux 應用於 web 後臺管理系統,你可能看不到豐富的後臺 UI 控件及界面,由於這不是重點,網上這樣的輪子已經不少了,本項目想着重表達的是:通用化解題思路github
一般追求極致用戶體驗的UI/UE設計師
可能會讓前端開發者定製各類 UI,你可能會抱怨說:這樣的設計將會打亂你的模塊化思想,或者讓問題變得複雜化,或者失去代碼重用性...然而在他們看來或許你只是想偷懶而已...無語...web
用戶體驗固然重要,但程序的健壯性與可維護性一樣重要,離開了它們,再好的用戶體驗都只是空中花園。別忘了人類工業革命的大爆發就是從大量製造標準件開始的,勞斯萊斯永遠成不了消費的主流。ajax
因此,咱們須要在定製化和標準化之間作個妥協權衡,既保持很好的用戶體驗,又可以面向更多的通用業務場景。一個思路是將絕大多數場景與極少數場景分而治之,若是某個 UI 方案能切合 90%的業務場景,何須爲了兼容少數場景而扭曲變形呢?typescript
說了這麼多,只是想說明本項目的立意是爲了提供一套適合大多業務場景的通用後臺。redux
本項目之開發目錄主要結構以下:api
src
├── assets // 存放公共靜態資源
├── entity // 存放業務實體類型定義
├── common // 存放公共代碼
├── components // 存放UI公共組件
├── modules
│ ├── app //主Module
│ │ ├── components
│ │ ├── views
│ │ │ ├── Main
│ │ │ ├── LoginForm
│ │ │ └── ...
│ │ ├── model.ts
│ │ └── index.ts
│ ├── admin //module分類,須要提早登陸的Module
│ │ ├── adminHome
│ │ ├── adminLayout
│ │ ├── adminMember
│ │ ├── adminPost
│ │ └── adminRole
│ ├── article //module分類,遊客可訪問的Module
│ │ ├── articleHome
│ │ ├── articleLayout
│ │ ├── articleAbout
│ │ └── articleService
│ └── index.ts //模塊配置、路由配置
└──Global.ts //將一些經常使用變量提高至全局,方便使用
└──index.ts 啓動入口
複製代碼
首先咱們要發現並定義各類業務實體的類型與數據結構,能夠把它們稱之爲 entity,並將他們放在 src/entity 下瀏覽器
組件一般分 2 類:
靜態資源與以上 component 同樣,分爲全局公用和 Module 內部公用 2 類:
從用戶可訪問性能夠把頁面分爲 2 類:
src/modules/admin
下src/modules/article
下,固然這裏只是說不須要提早登陸,裏面部分功能仍是須要「按需登陸」,好比 幫助中心 - Banner 中的「立刻諮詢」按鈕若是細心的話,登陸界面也應當分 2 種:
/login
,顯然會丟失當前表單內容(固然你也能夠編碼保存),此時比較好的用戶體驗是保持當前頁面狀態,而後直接 Pop 登陸彈窗,讓用戶登陸後還能夠繼續以前的操做流,以下圖所示
登陸/登出以後要不要刷新頁面?
Pop登陸彈窗
讓用戶從新登陸,從新登陸後判斷一下 session 過時若是隻是在短期內一般不會引發用戶數據失效,此時能夠不刷新頁面,從而讓用戶填寫的表單數據不至於丟失。如何保持 client 和 server 中用戶狀態的同步,一般須要一個 socket 長鏈接推送或是 ajax 輪詢,爲了減小併發的壓力一般使用一個 channel 就夠了,能夠本身定義這個 channel 的數據結構,一般只是用來推送增量差別化的數據
。
在 singlePage 單頁應用中,一般上一個頁面會直接覆蓋下一個頁面內容,沒有所謂在新窗口中打開
這個用戶體驗,那麼當我想比較 2 個頁面時變得很難作到。
好比我想快速的比較一下不一樣搜索條件的列表結果,當你用第 2 個搜索條件從新搜索時,發現直接把原來的結果覆蓋了...
固然你能夠設計成相似於瀏覽器同樣的多 Tab 窗口,可是那樣會讓問題複雜化,好比 Dom 要銷燬嗎?考慮到此需求不必定是很是高頻,因此本項目利用相似瀏覽器收藏夾
的功能來變相實現多窗口,如圖
從 Restful 獲得啓發,現實中紛繁複雜的業務規則其實均可以認爲是面向資源 Resource 的維護,即對資源的增刪改查。咱們的 UI 開發其實也能夠圍繞這個主題展開,好比本項目中的 adminRole、adminMember、adminPost 都是對一種資源的維護。
首先將每一個須要維護的資源定義爲一個獨立的 Module,而後在 src/entity/index.ts 中定義了一些 CommonResource 的抽象類型,一個通用的 CommonResourceState 彷佛應當是這樣的結構:
interface CommonResourceState {
routeParams: Resource['RouteParams']; //查詢條件放在路由參數中
list: Resource['ListItem'][]; //資源的搜索列表展現
listSummary: Resource['ListSummary']; //資源的搜索列表摘要信息
selectedRows: Resource['ListItem'][]; //當前選中了哪些列表項
currentItem: any; //當前要操做哪一條資源
}
複製代碼
資源的索引或叫列表查詢,一般這是一個資源展現的入口視圖,通常包括若干搜索條件、一個返回列表和一些摘要信息
//通用的查詢條件
interface BaseListSearch {
pageCurrent?: number;
pageSize?: number;
term?: string; //實時模糊搜索
sorterOrder?: 'ascend' | 'descend';
sorterField?: string;
}
//通用的列表數據結構
interface BaseListItem {
id: string; //不一樣Resource列表結構不同,但都會有一個id
}
//通用的列表查詢摘要
interface BaseListSummary {
pageCurrent: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
複製代碼
若是你閱讀過 @medux 路由篇 應當知道 medux 是將路由視爲 State 的,因此咱們把列表的查詢條件放在 RouteParams 中,這樣既能夠經過 redux 控制,也可用 url 控制。因而路由參數應當長這樣:
//通用的路由參數
interface BaseRouteParams {
listSearch: BaseListSearch; //查詢條件
listView: string; //用哪個列表view來展現數據
_listKey: string; //刷新hash
}
複製代碼
注意到以上結構中 listSearch 還好理解,那麼 listView 和_listKey 是什麼鬼?
不一樣的業務場景可能會有不一樣的 view 來渲染同一份數據,在上圖中咱們看到,對於角色列表
有 2 種展現方式:
搜索條件是角色名稱,列表項只有角色名稱一個字段
。因此咱們能夠把這個搜索下拉控件當成是角色列表的另外一個 ListView。推廣開來,任何 Resource 其實均可能存在至少 2 種列表視圖,一種是本身的維護列表,另外一種是如何被其它資源選擇與引用。咱們能夠將它們分別命名爲:List 和 Selector,對於複雜的 Selector 可能還須要多個查詢條件,例以下圖在「信息列表」中選擇「責任編輯」:
關於使用 Selector 選中後的回調,一般須要 2 個字段:id 和 name
,id 是給機器使用的,name 是給人看的:
interface SelectedItem {
id: string;
name: string;
}
複製代碼
展現詳細一般有 2 種入口方式:
superadmin
的資源能夠這樣訪問:/admin/member/list/detail/superadmin除了入口方式不一樣,詳情視圖自己也一般有 2 種展示方式:
新建與修改一般能夠重用一個 Form,新建的時候 ID 爲空,修改的時候 ID 有值。但有時候 2 個操做的所需字段並不同,因此視狀況而定,能重用仍是重用吧。
其實無論是"詳細/新建/修改"
,均可以看做是對某一條具體 Resource 進行展現,只是使用了 3 種不一樣的 ItemView 而已,這也能夠類比 ListView,一樣咱們將狀態 ItemView 放入路由中保存:
//通用的路由參數變成
interface BaseRouteParams {
listSearch: BaseListSearch; //查詢條件
listView: string; //用哪個listView來展現數據
_listKey: string; //刷新hash
itemId: string;
itemView: string; //用哪個itemView來展現數據
_itemKey: string; //刷新hash
}
複製代碼
detail
/superadminedit
/superadmin其它操做好比「啓用/禁用」、「審覈經過/審覈拒絕」等等,其實均可以抽象爲對資源進行 Status 改變。
要注意的一些通用的細節處理:
之因此總結和提取這麼多公共邏輯,是爲了在代碼上實現抽象與重用。
我在/src/common/resource.ts 中定義了 CommonResourceState、CommonResourceHandlers、CommonResourceAPI,基本上涵蓋了面向 Resource 的經常使用操做。以此做爲基類在 model 中繼承它,你會發現大量的代碼都被封裝在了基類中,例如 adminMember 的 model:
src/modules/admin/adminMember/model.ts
export interface State extends CommonResourceState<Resource> {}
export const initModelState: State = {routeParams: defaultRouteParams};
export class ModelHandlers extends CommonResourceHandlers<Resource, State, RootState> {
constructor(moduleName: string, store: any) {
super({defaultRouteParams, api, enableRoute: {list: true, detail: true, edit: true}}, moduleName, store);
}
}
複製代碼
能夠看到代碼已經很是少了....
由於 view 是外在的展示,它能重用的代碼比 model 要少一些,但仍是有很多交互代碼能夠提取,尤爲是配合 react hooks
,能夠更細力度的重用。我把它們放在了 src/hooks 目錄下,好比有:useSelector、useMTable、useDetail 等等,具體參見代碼。