使用React——开发系统页面流程

401 阅读5分钟

前言

在工作过程中经常会遇到开发一个具有新增、编辑、详情的页面,如果一个一个页面的编写会出现工作量大、难以维护等等问题。这里提供一种开发常见业务页面的方法。

流程

代码结构如下:

├──src
│   ├──pages                   // 页面
│       ├──ExamplePage               
│           ├──components      // 该页面专用组件
│           ├──Add             // 新增页  
│           ├──Edit            // 编辑页  
│           ├──Detail          // 详情页  
│           ├──hooks           // 该页面用到的一些自定义hooks
│           ├──hoc             // 该页面用到的一些自定义hoc
│           ├──constants.js    // 该页面用到的一些常量
│           ├──context.js      // createContext
│           ├──index.scss      // 该页面用到的scss  

1. 准备工作

定义插槽组件和供插槽消费的hoc。之后开发页面可以进行复用。

  • 插槽组件本体:
import React, { forwardRef, useContext } from 'react';
import withSlot, { SlotElementContext } from './withSlot';

function Slot(props, ref) {
  const { name, ...rest } = props;

  const elementDict = useContext(SlotElementContext);
  const targetElement = elementDict[name];
  const { props: targetProps } = targetElement || {};
  const targetChildren = targetProps?.children;
  if (typeof targetChildren === 'function') {
    return React.cloneElement(targetElement, {
      ...targetProps,
      ref,
      children: targetChildren({ ...rest }), // 占位组件 Slot 将Container 内部的状态 通过 render props 传出
    });
  }

  return targetElement
    ? React.cloneElement(targetElement, { ...rest, ...targetProps, ref: targetElement.ref ?? ref })
    : null;
}

export default forwardRef(Slot);

  • withSlot
import React, { createContext, useMemo } from 'react';

export const SlotElementContext = createContext(null);

// NOTE: 收集插槽子元素,并注入到「SlotElementContext」当中,供「Slot」组件消费
const withSlot = (CustomComponent) => {
  const Wrapper = (props) => {
    const { children: originChildren } = props;
    // 转换为数组,每个元素上均会带一个key属性
    const [slotElementDic, restChildren, slotList] = useMemo(() => {
      const [restChildren, slotList] = [[], []];
      const slotElementDic = React.Children.toArray(originChildren)
        .filter((childElement) => {
          const { props: elementProps } = childElement;
          if (!elementProps?.slot) {
            restChildren.push(childElement);
          } else {
            slotList.push(elementProps?.slot);
          }

          return elementProps?.slot;
        })
        .reduce(
          (acc, slotElement) => ({
            ...acc,
            [slotElement.props.slot]: slotElement,
          }),
          {},
        );
      return [slotElementDic, restChildren, slotList];
    }, [originChildren]);

    return (
      <SlotElementContext.Provider value={slotElementDic}>
        <CustomComponent {...props} slotList={slotList}>
          {restChildren}
        </CustomComponent>
      </SlotElementContext.Provider>
    );
  };

  return Wrapper;
};

export default withSlot;

2. 定义页面的基本结构

假设现在有一个页面包含新增、编辑和详情,页面主体包含一个表单、一个表格和附件。 首先定义页面通用组件Container组件,用于包裹页面结构。


const Container = () => (
  <div>
    <Slot name="baseInfo" />
    <Slot name="tableInfo" />
    <Slot name="fileList" />
  </div>
);
export default withSlot(Container);

然后是定义具体新增、编辑、详情页。这里给出新增页的页面结构,编辑和详情页类似。 新增页:

const Add = () => {
  // somothing
  return (
    <Container>
      <Form slot="baseInfo" />
      <Table slot="tableInfo" />
      <Upload slot="fileList" />
    </Container>
  );
};

export default Add;

3. 页面具体处理

现在页面的大致布局已经出来了,但是还缺少一些东西,比如Form表单的属性、Form表单的数据等等。这些我们还没进行处理。 这里通过自定义hooks来处理页面。

1. 全局数据存储和传递

React父子组件进行数据传递可以通过props和回调,还可以通过React提供的useContext来进行数据的传递,这里对useContext的使用进行一些封装,方便使用。

首先定义一个useStore用于定义一些数据。useStore的大致代码如下:

const useStore = () => {
    // baseInfoRef将会通过provider传递给Form表单的ref属性
    const baseInfoRef = useRef();
    const tableRef = useRef();
    const fileListRef = useRef();
    const readonlyRef = useRef();
    const [fileList, setFileList] = useState();
    
    // getMethodsByRef用于获取表单的一些方法,包含set和get,用于之后取值和设置值,同样传出去
    const baseInfoMethods = useMemo(() => getMethodsByRef(baseInfoRef), []);
    
   return useMemo(
    () => ({
      baseInfoRef,
      tableRef,
      fileListRef,
      readonlyRef,
      fileList,
      setFileList,
      baseInfoMethods
    }),
    [
    ],
  );
}

useStore作为生产者,包含了页面的一些数据和方法,需要给消费者(具体页面)使用,

通过react提供的creatContext和useContext来暴露给消费者使用。 这里定义一个hoc(withProvider)和hook(usePageContext),方便之后Add、Edit、Detail页面复用。

首先创建一个 Context

import { createContext } from 'react';

const TextContext = createContext({});
export default TextContext;

withProvider用于提供数据,withProvider代码如下:

import React from 'react';
import TextContext from '../context';
import { useStore } from '../hooks';

const Provider = ({ children }) => {
  const store = useStore();
  return <TextContext.Provider value={store}>{children}</TextContext.Provider>;
};

const withProvider = (Component) => (props) => (
  <Provider>
    <Component {...props} />
  </Provider>
);

export default withProvider;

定义自定义hooks usePageContext,之后都在usePageContext中取数据。 usePageContext的代码如下:

import { useContext } from 'react';
import TextContext from '../context';

const usePageContext = () => useContext(TextContext);
export default usePageContext;

可以看到其实就是用到了createContext和useContext来进行数据的传递,不过进行了一些封装,方便之后的使用。

使用:将新增、编辑、详情页通过withProvider包一层,之后在其子组件、子页面中可以通过usePageContext直接取到相关数据。 使用示例:

const Add = () => {
  // somothing
  return (
    <Container>
      <Form slot="baseInfo" />
      <Table slot="tableInfo" />
      <Upload slot="fileList" />
    </Container>
  );
};
// 通过withProvider包一层
export default withProvider(Add);

Add页面下的子组件、hooks中均可以通过usePageContext来取到useStore中保存的值。

2. 页面的属性设置

现在已经处理好了如何存储一些临时数据和传递这些数据,但是页面中的组件还缺少关键的属性配置,比如表单的配置、表格的配置等等。

对表格、表单进行属性配置,这里采用自定义hooks的方式来注入属性。

定义一个usePageProps钩子,主要作用是设置页面的属性。代码如下:

const usePageProps = () => {
  // 使用前面定义好的usePageContext来取值
  const {
    baseInfoRef,
    tableRef,
    fileListRef,
    readonlyRef,
    fileList,
    setFileList,
    baseInfoMethods
  } = usePageContext();

  const baseInfoProps = {
    ref: baseInfoRef,
    schema: SCHEMA(),
    layout: 'horizontal',
  };
  
  const tableProps = {
    ref: tableRef,
  }

  return {
    baseInfoProps,
    tableProps,
  };
};

页面中通过usePageProps来获取页面的属性配置,再将具体的属性配置设置到对应的组件当中。

const Add = () => {
  const { baseInfoProps, tableProps } = usePageProps();
  return (
    <Container>
      <Form slot="baseInfo" {...baseInfoProps} />
      <Table slot="tableInfo" {...tableProps} />
      <Upload slot="fileList" />
    </Container>
  );
};
// 通过withProvider包一层
export default withProvider(Add);

3. 页面的数据设置

现在页面的数据传递、属性设置都有了,但是还缺少获取后台数据,设置后台数据到页面中的操作。

同样这里通过自定义hooks来设置后台获取的值到页面当中。

定义usePageRecord钩子,获取后台数据,代码如下:

const usePageRecord = (pageMode) => {
  const { id, type } = history.location.query;
  const location = useLocation();
  const {
    baseInfoRef,
    tableRef,
    fileListRef,
    readonlyRef,
    fileList,
    setFileList,
    baseInfoMethods
  } = usePageContext();
  // setData就是表单提供的set方法的封装
  const { setData } = baseInfoMethods;
  const setDefaultData = async (record) => {
    if (record) {
      // 在设值前可以进行一些转换 
      setData(record);
    }
  };

  const updateRecord = async () => {
    const { id, type } = location.query;
    const record = await getRecord(id);
    await setDefaultData(record);
  };

  useEffect(() => {
    updateRecord();
  }, [location]);
  if (pageMode === 'add') {
    // 新增页可以给一些初始值
    setData();
  }
};