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 等等,具体参见代码。

安装&运行请看下篇

安装&运行

相关文章
相关标签/搜索