项目地址:前端
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 可能还须要多个查询条件,例以下图在“信息列表”中选择“责任编辑”:
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 等等,具体参见代码。