前言
一直以来,前端被认为是一个系统中复杂度最低的一环,只是扮演接收数据和展示数据的角色。随着前端技术的不断发展,前端开发的复杂度有所增加,但比起后端的业务逻辑,确实是大巫见小巫,以至于有这样的观点
前端的模式已经比较固定,无非是 MVC、MVP 或者 MVVM,不需要架构设计
但前端真的不需要架构设计吗?让我们跟随小白一起来接受洗礼
小白的实现之旅
小白作为校招新人,初入职场,学习了 React 开发,跃跃欲试。于是,主管给小白分配了一个小任务:用 React + mobx 实现一个用户列表页,包括数据的获取
注:为了节省篇幅,后续代码均忽略错误处理
V1
小白拿到需求后很快就有了思路,觉得十分简单,便开始撸起了代码
entity/user.tsx
用 mobx 实现 user 的 entity,并用 单例模式 暴露出一个实例,用于数据存储
import { observable, action } from 'mobx';
import axios from 'axios';
interface IUser {
account: string;
name: string;
}
class Entity {
@observable loading: boolean;
@observable list: IUser[];
@action async fetchList() {
this.loading = true;
const fetchListResult = await axios.get('/apis/user');
this.loading = false;
if (fetchListResult.data.status === '0') {
this.list = fetchListResult.data.data;
}
}
}
export default new Entity();
page/UserList.tsx
用 mobx-react + React 实现用户列表页
import * as React from 'react';
import { observer } from 'mobx-react';
import User from '../entity/user';
@observer
export default class extends React.Component<{}, {}> {
async componentDidMount() {
await User.fetchList();
}
render() {
return (
<div className="UserListPage">
{User.loading ? (
<div className="loading" />
) : (
User.list.map(user => <div className="item">{user.name}</div>)
)}
</div>
);
}
}
自测了一下,完美实现需求,小白迫不及待地去交差。主管看了下代码,眉头微微一皱,提出了一个建议
「画一下你的代码分层结构以及依赖关系的图,看看有什么问题」
两个文件能有什么分层啊,不就是两层吗?小白虽然感到奇怪,但还是照做了
分层结构图
「恩,画的是对的,有没有发现什么问题?你的 View 是不是直接依赖了 Model?」
小白很苦恼,这有什么问题吗,难道一定要强行变成 MVC 或者 MVP 吗?
「这样吧,我给你加个需求,做一个管理员列表页,跟用户列表页长得一模一样,只是操作数据的接口不同而已」
小白瞬间明白了,自己这么实现虽然省事,但是无法复用啊!
V2
先把列表当成一个组件抽取出来,再通过属性的方式传入,并支持自定义列表项的渲染
component/List.tsx
import * as React from 'react';
export interface IProps {
loading: boolean;
listData: object[];
itemRender: (item: any) => React.ReactNode | string;
}
export default (props: IProps) => (
<React.Fragment>
{props.loading ? (
<div className="loading" />
) : (
props.listData.map(item => (
<div className="item">{props.itemRender(item)}</div>
))
)}
</React.Fragment>
);
page/UserList.tsx
修改 UserList 页面的实现方式,引入 List 组件
import * as React from 'react';
import { observer } from 'mobx-react';
import List from '../components/List';
import User from '../entity/user';
@observer
export default class extends React.Component<{}, {}> {
async componentDidMount() {
await User.fetchList();
}
render() {
return (
<div className="UserListPage">
<List
loading={User.loading}
listData={User.list}
itemRender={item => item.name}
/>
</div>
);
}
}
接下来实现 Admin 的 entity 和 AdminListPage 即可
entity/admin.tsx
import { observable, action } from 'mobx';
import axios from 'axios';
interface IAdmin {
account: string;
name: string;
}
class Entity {
@observable loading: boolean;
@observable list: IAdmin[];
@action async fetchList() {
this.loading = true;
const fetchListResult = await axios.get('/apis/admin');
this.loading = false;
if (fetchListResult.data.status === '0') {
this.list = fetchListResult.data.data;
}
}
}
export default new Entity();
page/AdminList.tsx
import * as React from 'react';
import { observer } from 'mobx-react';
import List from '../components/List';
import Admin from '../entity/admin';
@observer
export default class extends React.Component<{}, {}> {
async componentDidMount() {
await Admin.fetchList();
}
render() {
return (
<div className="AdminListPage">
<List
loading={Admin.loading}
listData={Admin.list}
itemRender={item => item.name}
/>
</div>
);
}
}
主管肯定要我画图,那我就先画了
分层结构图
看着改造后的代码和分层结构,小白十分满意,觉得上升了一个台阶,底气十足地找到主管进行验收
主管点了点头,「不错,实现了复用,代码结构比之前的要合理多了。你知道这是什么架构模式吗?」
小白琢磨了一下,List 是一个纯展示的组件,不直接依赖于 Entity,而是通过 Page 将二者连接起来,List 和 Entity 都实现了解耦,可被不同的 Page 复用,这可以类比为 MVP 模式
- Entity => Model [提供数据和操作数据的方法]
- Component => View [根据数据展示视图]
- Page => Presenter [处理业务逻辑,从 Model 获取数据传给 View,响应 View 的用户交互并操作 Modal]
「没错,恭喜你又进步了」
V3
小白高兴地拿着代码去找自己非常崇拜的小明,希望可以得到认可和更多的指导。小明微笑说道:「站在 Component 角度进行了解耦,给你点赞。不过,站在 React 本身的角度,其实 UserList 只能算是一个容器,虽然作为 Presenter,但还是直接依赖了 Entity,导致这个 容器组件 无法得到有效的复用。在你这个场景下,UserList 和 AdminList 其实是有重复的,对吗?」
小白看了看 UserList 和 AdminList 的代码,确实发现了一些重复的地方,可是应该怎么去消除这种重复呢?
「你可以想想设计原则中的 DIP 原则」
DIP,依赖倒置原则,上层模块不应该依赖底层模块,它们都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象
「Entity 属于底层模块,Page 属于上层模块,这种依赖关系导致 Page 无法获得有效的复用。React 的 props 天然可以作为这个场景下的一个抽象,让 Entity 这个底层模块通过属性的方式传递过来,而作为上层模块的 Page 只需要依据 interface 的定义来使用它实现自己的业务逻辑,我给你画个图感受一下」
分层结构图
在这里我们引入了一个 Provider,收集所有 entity,再引入一个 injector 作为依赖注入的 HOC,将 Entity 以 props 的方式传递到 Page 中,并且由它来响应 mobx 的数据变化,最终我们的 Page 就比较纯粹,只依赖于传递进去的 Props,可以用 SFC(Stateless Function Component) 的形式来实现
而为了复用列表中的一些逻辑,我们再抽象出一层 Container,作为 Component + 业务逻辑的组合,是带有一定业务功能的组件
而对于 User 和 Admin 两个 Entity,只需要使用 implements 的方式实现 IPageEntity 定义的接口,则可以提供 ListPage 所需要的数据和方法,最终作为 props 注入到 ListPage 中
Talk is cheap. Show me the code.
container/ListPage.tsx
将列表页再次封装,利用传入的 Entity 执行逻辑,并且暴露 entity 需要的 interface
import * as React from 'react';
import List from '../components/List';
export interface IListPageEntity {
fetchList: () => Promise<void>;
loading: boolean;
list: any[];
}
export default class extends React.Component<
{
entity: IListPageEntity;
itemRender?: (item: any) => React.ReactNode | string;
},
{}
> {
async componentDidMount() {
await this.props.entity.fetchList();
}
render() {
const { entity, itemRender = item => item.name } = this.props;
return (
<div className="ListPage">
<List
loading={entity.loading}
listData={entity.list}
itemRender={itemRender}
/>
</div>
);
}
}
entity
user.tsx
import { observable, action } from 'mobx';
import axios from 'axios';
import { IListPageEntity } from '../container/ListPage';
interface IUser {
account: string;
name: string;
}
class Entity implements IListPageEntity {
@observable loading: boolean;
@observable list: IUser[];
@action async fetchList() {
this.loading = true;
const fetchListResult = await axios.get('/apis/user');
this.loading = false;
if (fetchListResult.data.status === '0') {
this.list = fetchListResult.data.data;
}
}
}
export default new Entity();
admin.tsx
import { observable, action } from 'mobx';
import axios from 'axios';
import { IListPageEntity } from '../container/ListPage';
interface IAdmin {
account: string;
name: string;
}
class Entity implements IListPageEntity {
@observable loading: boolean;
@observable list: IAdmin[];
@action async fetchList() {
this.loading = true;
const fetchListResult = await axios.get('/apis/admin');
this.loading = false;
if (fetchListResult.data.status === '0') {
this.list = fetchListResult.data.data;
}
}
}
export default new Entity();
entity/provider.tsx
provider 需要收集所有可用的 entity,并提供 inject 函数,供页面选择注入需要的 entity,同时响应 mobx 的变化
import * as React from 'react';
import { observer } from 'mobx-react';
import admin from './admin';
import user from './user';
export interface IEntities {
admin: typeof admin;
user: typeof user;
}
class Provider {
private entities: IEntities = {
admin,
user
};
getEntity(name: string) {
return this.entities[name];
}
}
const provider = new Provider();
export interface IProps {
entities: IEntities;
[propName: string]: any;
}
export function inject(params: string[]) {
return (Component: (props: IProps) => JSX.Element) => {
return observer(
class WithEntity extends React.Component<{ [propName: string]: any }> {
render() {
const entities: any = {};
params.forEach(
entity => (entities[entity] = provider.getEntity(entity))
);
return (
<React.Fragment>
<Component entities={entities} {...this.props} />
</React.Fragment>
);
}
}
);
};
}
page
最后,UserListPage 和 AdminListPage 只需要选择对应的 entity 注入到 ListPage 中
page/UserList.tsx
import * as React from 'react';
import { inject } from '../entity/provider';
import ListPage from '../container/ListPage';
export default inject(['user'])(({ entities }) => {
return <ListPage entity={entities.user} />;
});
page/AdminList.tsx
import * as React from 'react';
import { inject } from '../entity/provider';
import ListPage from '../container/ListPage';
export default inject(['admin'])(({ entities }) => {
return <ListPage entity={entities.admin} />;
});
「这种实现是为了 ListPage 中逻辑的复用。有时候我们并没有那么多逻辑复用的场景,则可以直接减少 Container 这一层,并保留依赖注入的思想,如下图」
看着小白有点懵的状态,小明笑了笑,「你先消化一下,过几天我再给你讲解一下继续优化的思路,我们的目标是实现具有扩展性的架构,加油」
TO BE CONTINUE...