React 编码实战 ---- 一步步实现可扩展的架构(1)

前言

一直以来,前端被认为是一个系统中复杂度最低的一环,只是扮演接收数据和展现数据的角色。随着前端技术的不断发展,前端开发的复杂度有所增长,但比起后端的业务逻辑,确实是大巫见小巫,以致于有这样的观点前端

前端的模式已经比较固定,无非是 MVC、MVP 或者 MVVM,不须要架构设计react

但前端真的不须要架构设计吗?让咱们跟随小白一块儿来接受洗礼ios

小白的实现之旅

小白做为校招新人,初入职场,学习了 React 开发,跃跃欲试。因而,主管给小白分配了一个小任务:用 React + mobx 实现一个用户列表页,包括数据的获取axios

注:为了节省篇幅,后续代码均忽略错误处理后端

V1

小白拿到需求后很快就有了思路,以为十分简单,便开始撸起了代码api

entity/user.tsx

用 mobx 实现 user 的 entity,并用 单例模式 暴露出一个实例,用于数据存储markdown

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>
    );
  }
}
复制代码

自测了一下,完美实现需求,小白火烧眉毛地去交差。主管看了下代码,眉头微微一皱,提出了一个建议less

「画一下你的代码分层结构以及依赖关系的图,看看有什么问题」async

两个文件能有什么分层啊,不就是两层吗?小白虽然感到奇怪,但仍是照作了

分层结构图

「恩,画的是对的,有没有发现什么问题?你的 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...

相关文章
相关标签/搜索