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...

相關文章
相關標籤/搜索