Medux+React+Antd4+Hooks+Typescript開箱即用通用後臺(上)

項目地址:前端

本項目主要用來展現如何將 @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

首先咱們要發現並定義各類業務實體的類型與數據結構,能夠把它們稱之爲 entity,並將他們放在 src/entity 下瀏覽器

component

組件一般分 2 類:

  • 全局公共 component:各個 Module 公用的組件,放在 src/components 下
  • Module 內部公共 component:只被某個 Module 使用到的公共組件,放在 module/components 下,這樣能夠隨 Module 按需加載

assets

靜態資源與以上 component 同樣,分爲全局公用和 Module 內部公用 2 類:

  • 全局靜態資源放 src/assets 下
  • Module 內部靜態資源放 module/assets 下

unauthorized

從用戶可訪問性能夠把頁面分爲 2 類:

  • 須要提早登陸才能瀏覽的頁面,好比本例子中的  我的中心,我把他們都放在 src/modules/admin
  • 不須要提早登陸就能瀏覽的頁面,好比本例中的  幫助手冊,我把他們都放在 src/modules/article 下,固然這裏只是說不須要提早登陸,裏面部分功能仍是須要「按需登陸」,好比  幫助中心 - Banner 中的「立刻諮詢」按鈕

loginForm

若是細心的話,登陸界面也應當分 2 種:

  • 獨立 Page,路由跳轉到  登陸頁面。一般這樣的獨立登陸頁面比較有儀式感和個性化,但會中斷當前的操做流
  • Pop 彈窗,輕量級登陸界面。用彈窗方式會保留當前的操做流程,好比你可能費了不少時間填寫一個表單,點提交時發現沒有登陸(多是 session 過時了),此時若是應用自動將當前頁面路由到/login,顯然會丟失當前表單內容(固然你也能夠編碼保存),此時比較好的用戶體驗是保持當前頁面狀態,而後直接 Pop 登陸彈窗,讓用戶登陸後還能夠繼續以前的操做流,以下圖所示
    r-login.jpg

refreshPage

登陸/登出以後要不要刷新頁面?

  • 刷新頁面固然是 100%有效的,可是可能用戶體驗沒那麼好。
  • 不刷新頁面體驗最好,可是你可能必須手動清理和替換一大堆失效的狀態,有時這會讓問題和代碼變得很繁瑣,並且很容易引發 Bug。那麼能夠犧牲一下用戶體驗嗎?其實登陸登出對同用戶來講並非一個高頻的操做,刷新頁面除了時間上的等待,彷佛也沒有太大反作用,因此仍是刷新一下頁面吧。 但存在一種場景:**用戶在提交表單時發現 session 過時了,**此時應當彈出一個Pop登陸彈窗讓用戶從新登陸,從新登陸後判斷一下 session 過時若是隻是在短期內一般不會引發用戶數據失效,此時能夠不刷新頁面,從而讓用戶填寫的表單數據不至於丟失。

synchronized

如何保持 client 和 server 中用戶狀態的同步,一般須要一個 socket 長鏈接推送或是 ajax 輪詢,爲了減小併發的壓力一般使用一個 channel 就夠了,能夠本身定義這個 channel 的數據結構,一般只是用來推送增量差別化的數據

tabFrame

在 singlePage 單頁應用中,一般上一個頁面會直接覆蓋下一個頁面內容,沒有所謂在新窗口中打開這個用戶體驗,那麼當我想比較 2 個頁面時變得很難作到。

好比我想快速的比較一下不一樣搜索條件的列表結果,當你用第 2 個搜索條件從新搜索時,發現直接把原來的結果覆蓋了...

固然你能夠設計成相似於瀏覽器同樣的多 Tab 窗口,可是那樣會讓問題複雜化,好比 Dom 要銷燬嗎?考慮到此需求不必定是很是高頻,因此本項目利用相似瀏覽器收藏夾的功能來變相實現多窗口,如圖

tab-frame.png

面向資源 Resource 的維護

從 Restful 獲得啓發,現實中紛繁複雜的業務規則其實均可以認爲是面向資源 Resource 的維護,即對資源的增刪改查。咱們的 UI 開發其實也能夠圍繞這個主題展開,好比本項目中的 adminRole、adminMember、adminPost 都是對一種資源的維護。

Module

首先將每一個須要維護的資源定義爲一個獨立的 Module,而後在 src/entity/index.ts 中定義了一些 CommonResource 的抽象類型,一個通用的 CommonResourceState 彷佛應當是這樣的結構:

interface CommonResourceState {
  routeParams: Resource['RouteParams']; //查詢條件放在路由參數中
  list: Resource['ListItem'][]; //資源的搜索列表展現
  listSummary: Resource['ListSummary']; //資源的搜索列表摘要信息
  selectedRows: Resource['ListItem'][]; //當前選中了哪些列表項
  currentItem: any; //當前要操做哪一條資源
}
複製代碼

List

資源的索引或叫列表查詢,一般這是一個資源展現的入口視圖,通常包括若干搜索條件、一個返回列表和一些摘要信息

//通用的查詢條件
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;
}
複製代碼

ListRouteParams

若是你閱讀過  @medux 路由篇  應當知道 medux 是將路由視爲 State 的,因此咱們把列表的查詢條件放在 RouteParams 中,這樣既能夠經過 redux 控制,也可用 url 控制。因而路由參數應當長這樣:

//通用的路由參數
interface BaseRouteParams {
  listSearch: BaseListSearch; //查詢條件
  listView: string; //用哪個列表view來展現數據
  _listKey: string; //刷新hash
}
複製代碼

注意到以上結構中 listSearch 還好理解,那麼 listView 和_listKey 是什麼鬼?

  • listKey 你能夠把它理解爲對當前搜索條件的一個 version 控制,若是_listKey 發生了變化,即便搜索條件沒有變化依然會強制從新搜索,相似於咱們常爲靜態資源 URL 後加一個隨機數強制更新。另外加前綴能夠將這筆數據放入 hash 中保存,參見  @medux 路由篇
  • listView 指示用哪一個 view 來展現列表數據,下面詳細討論一下,以下圖:

list-view-800.png

ListView 與 Selector

不一樣的業務場景可能會有不一樣的 view 來渲染同一份數據,在上圖中咱們看到,對於角色列表有 2 種展現方式:

  • 圖 1 在它本身的頁面可能就是一個普通的列表搜索,能夠按照角色名稱和用戶權限來搜索。
  • 圖 2 它被用戶列表頁面做爲下拉選框彈出。此時雖然它被用在了別的模塊,但其實仍是屬於角色列表:搜索條件是角色名稱,列表項只有角色名稱一個字段。因此咱們能夠把這個搜索下拉控件當成是角色列表的另外一個 ListView。

推廣開來,任何 Resource 其實均可能存在至少 2 種列表視圖,一種是本身的維護列表,另外一種是如何被其它資源選擇與引用。咱們能夠將它們分別命名爲:List 和 Selector,對於複雜的 Selector 可能還須要多個查詢條件,例以下圖在「信息列表」中選擇「責任編輯」:

selector-view-800.png
關於使用 Selector 選中後的回調,一般須要 2 個字段: id 和 name,id 是給機器使用的,name 是給人看的:

interface SelectedItem {
  id: string;
  name: string;
}
複製代碼

ItemDetail

展現詳細一般有 2 種入口方式:

  • 從 listView 資源列表中點擊「詳細」按鈕進入,這是最多見的方式
  • 直接從路由中經過 ID 進入,這樣的好處是能夠經過 url 分享給其它人,方便交流。好比對於 ID 爲superadmin的資源能夠這樣訪問:/admin/member/list/detail/superadmin

除了入口方式不一樣,詳情視圖自己也一般有 2 種展示方式:

  • 獨立頁面展現:相對重量級交互,優勢是能夠展現更多內容,缺點是破壞了原頁面,返回時不得再也不次刷新原頁面。
  • pop 彈窗展現:輕量級交互,優勢是能夠維持當前頁面其它元素,好比搜索列表;缺點是展現區域比較小。至於 pop 彈窗可否經過路由到達?固然也是能夠的,好比:/admin/member/list/detail/superadmin

Create/Update

新建與修改一般能夠重用一個 Form,新建的時候 ID 爲空,修改的時候 ID 有值。但有時候 2 個操做的所需字段並不同,因此視狀況而定,能重用仍是重用吧。

ItemView

其實無論是"詳細/新建/修改",均可以看做是對某一條具體 Resource 進行展現,只是使用了 3 種不一樣的 ItemView 而已,這也能夠類比 ListView,一樣咱們將狀態 ItemView 放入路由中保存:

//通用的路由參數變成
interface BaseRouteParams {
  listSearch: BaseListSearch; //查詢條件
  listView: string; //用哪個listView來展現數據
  _listKey: string; //刷新hash
  itemId: string;
  itemView: string; //用哪個itemView來展現數據
  _itemKey: string; //刷新hash
}
複製代碼

ChangeStatus

其它操做好比「啓用/禁用」、「審覈經過/審覈拒絕」等等,其實均可以抽象爲對資源進行 Status 改變。

通用交互邏輯

要注意的一些通用的細節處理:

列表查詢

  • 搜索條件過多時能夠折起展開
  • 操做可分爲單條操做和批量操做
  • 點擊搜索、排序或者改變 pageSize 時都自動回到第 1 頁

新增/修改

  • 新增成功後應當以建立時間排序來刷新列表,以保證列表中看到變化
  • 修改爲功後應當以當前搜索條件刷新列表,以保證列表中看到變化

列表選擇

  • 在列表中選擇多條後,翻頁、從新搜索應當保持當前選中條數
  • 在列表中選擇多條後,修改了某一條數據應當保持當前選中條數
  • 在列表中選擇多條後,刪除了某一條數據應當將當前選中條數清空

提取公共代碼

之因此總結和提取這麼多公共邏輯,是爲了在代碼上實現抽象與重用。

model 的重用

我在/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 的重用

由於 view 是外在的展示,它能重用的代碼比 model 要少一些,但仍是有很多交互代碼能夠提取,尤爲是配合 react hooks,能夠更細力度的重用。我把它們放在了 src/hooks 目錄下,好比有:useSelector、useMTable、useDetail 等等,具體參見代碼。

安裝&運行請看下篇

安裝&運行

相關文章
相關標籤/搜索