前端实战:React 多页签缓存处理

发表于 3年以前  | 总阅读数:1012 次

1 . 背景

ant design pro v2的项目需要多页签显示页面而且还需要页面能够缓存下来。

  • 多页面展示
  • 不同数据会对应不同的详情页面
  • 代码里面的路由跳转可以正常进行页面切换
  • 页签可以关闭

2 . 主要设计:

这里主要是考虑多页面+缓存问题。

这里是借用了ant tabs标签的缓存作用。tabs的多页面只是显示和隐藏,组件本身还存在。

路由这一块,其实路由目前的基本使用页面只是会渲染当前匹配成功的路由节点。

这里钻了一个空子。Route在没有标注path的情况下会作为没有匹配路径的url进行渲染,作为没有匹配任何路由的情况下渲染节点。

因为我们通过路由path去匹配是不行,只有使用不带path的情况下渲染节点,而且不能使用switch。

而且不使用path的情况下,路由对应渲染的组件则不能依赖于react-router的机制来自动匹配,页面的渲染就需要我们进行手动处理。因为打开多少个页面就会有多少个no path的Route的节点。

3 . 讲解

回到具体的实现:BasicLayout.js

3.1 原先的版本

const layout = (
      <Layout>
        {isTop && !isMobile ? null : (
          <SiderMenu
            logo={logo}
            theme={navTheme}
            onCollapse={this.handleMenuCollapse}
            menuData={menuData}
            isMobile={isMobile}
            {...this.props}
          />
        )}
        <Layout
          style={{
            ...this.getLayoutStyle(),
            minHeight: '100vh',
          }}
        >
          <Header
            menuData={menuData}
            handleMenuCollapse={this.handleMenuCollapse}
            logo={logo}
            isMobile={isMobile}
            {...this.props}
          />
          <Content className={styles.content} style={contentStyle}>
            <Authorized authority={routerConfig} noMatch={<Exception403 />}>
              {children}
            </Authorized>
          </Content>
          <Footer />
        </Layout>
      </Layout>
    );

以前的版本里直接渲染对应children。这里具体ant deisgn pro,或者umijs做了对应的处理,我没具体去看。现在的话我们不会用这个版本去做。

3.2 现在的版本

3.2.1 layout

const layout = (
      <Layout>
        {isTop && !isMobile ? null : (
          <SiderMenu
            logo={logo}
            theme={navTheme}
            onCollapse={this.handleMenuCollapse}
            menuData={menuData}
            isMobile={isMobile}
            {...this.props}
            // onHandlePage={this.onHandlePage}
          />
        )}
        <Layout
          style={{
            ...this.getLayoutStyle(),
            minHeight: '100vh',
          }}
        >
          <Header
            menuData={menuData}
            handleMenuCollapse={this.handleMenuCollapse}
            logo={logo}
            isMobile={isMobile}
            {...this.props}
          />
          <Content className={styles.content} style={contentStyle}>
            <div className={styles.contentBox}>
              <div className={styles.contentTabUrlBox}>
                <div className={styles.contentTabUrl}>
                  <Tabs
                    activeKey={activeKey}
                    onChange={this.onChange}
                    tabBarExtraContent={operations}
                    type="editable-card"
                    tabBarStyle={{ background: '#fff' }}
                    tabPosition="top"
                    tabBarGutter={-1}
                    onEdit={this.onEdit}
                    hideAdd
                  >
                    {listenRouterState.map(item => (
                      <TabPane tab={item.tab} key={item.key} closable={item.closable}>
                        <RouterContext.Provider value={customerMatchs}>
                          <Route key={item.key} component={item.content} exact />
                        </RouterContext.Provider>
                        {/* {item.component()} */}
                      </TabPane>
                    ))}
                  </Tabs>
                  <Footer />
                </div>
              </div>
            </div>
          </Content>
        </Layout>
      </Layout>
    );

核心的代码是

  <Tabs
    activeKey={activeKey}
    onChange={this.onChange}
    tabBarExtraContent={operations}
    type="editable-card"
    tabBarStyle={{ background: '#fff' }}
    tabPosition="top"
    tabBarGutter={-1}
    onEdit={this.onEdit}
    hideAdd
  >
    {listenRouterState.map(item => (
      <TabPane tab={item.tab} key={item.key} closable={item.closable}>
        <RouterContext.Provider value={customerMatchs}>
          <Route key={item.key} component={item.content} exact />
        </RouterContext.Provider>
      </TabPane>
    ))}
  </Tabs>

这里使用的是tab + 路由 with no path的方式。现在我们需要将组件和path进行关联起来。因为没有使用路由匹配了。代码里的listenRouterState就是我们打开页面的key和对应组件的mapping关系。

这里path的处理我使用的是路由监控,因为是对于整个系统的页面多页签,所以我使用了路由监控。

3.2.2 componentDidMount路由监控

UN_LISTTEN = history.listen(route => {
      const { listenRouterState, listenRouterKey, customerMatchs } = this.state;
      let replaceRouter = routerArray.filter(itemRoute =>
        pathToRegexp(itemRoute.key || '').test(route.pathname),
      )[0];

      let currentKey = '';

      if (replaceRouter && replaceRouter.isOnlyOnePage) {
        currentKey = route.pathname;
      } else {
        currentKey = route.pathname + this.parseQueryString(route.search);
      }

      if (!listenRouterKey.includes(currentKey)) {
        if (!replaceRouter) {
          replaceRouter = routerArray.filter(itemroute => itemroute.key === '/404')?.[0];

          this.setState({
            listenRouterState: [
              ...listenRouterState,
              { ...replaceRouter, key: currentKey, tab: '404' },
            ],
            activeKey: currentKey,
            listenRouterKey: [...listenRouterKey, currentKey],
          });
        } else {
          const match = matchPath(route.pathname, { path: replaceRouter.key });

          this.setState({
            listenRouterState: [
              ...listenRouterState,
              {
                ...replaceRouter,
                key: currentKey,
                tab:
                  this.getPageTitle(route.pathname, breadcrumbNameMap) +
                  this.getDetailPagePrimaryId(route, match),
              },
            ],
            activeKey: currentKey,
            listenRouterKey: [...listenRouterKey, currentKey],
            customerMatchs: [...customerMatchs, { key: currentKey, match }],
          });
        }
      }

      this.setState({
        activeKey: currentKey,
      });
    });
  }

3.2.2.1 主要介绍

这里主要是在做什么,监控路由然后进行路由匹配,获取对应的组件。先介绍一下这里面用到的一些state变量

  • listenRouterState:打开页面数据对象,也是在layout渲染的数组,存储了pathname和component的mapping关系
  • activeKey:当前打开的页面key
  • listenRouterKey:listenRouterState对象key属性的数组集合,用于一些数据判断。
  • customerMatchs:适配match,这里可以先不管,因为这个是服务于下面实际问题的。

这里的主要逻辑就是,监控路由,判断路由是否已经打开,如果已经打开就不会重新打开。这里的key是全路径,是加上查询参数的。如下面的这个地址:

3.2.2.2 pathname 和 key的区别

但是匹配组件内容不能使用这个进行匹配的,还是需要使用pathname进行匹配的。还是先看一下具体路由监控的到的route数据是什么?

所以我们上面说的key:是pathname + query。这里要分清key和pathname的区别,因为pathname是用来匹配获取组件的,key是为了进行多个详情页面的区分,如果不是全路径是没有办法区分不同详情页面的。

当然我们这个pathname是比较好匹配的,假如是下面的这种,下面的路由对应的是:

          {
            path: '/List/:title/table-match-detail',
            hideInMenu: true,
            name: 'detail',
            code: 'list_tableDetail_page',
            component: './List/MatchDetail',
          },

3.2.2.3 如何将pathname和路由正确的匹配

pathToRegexp(itemRoute.key || '').test(route.pathname)

针对路由的匹配,因为有match参数的存在,所以这里我用的是pathToRegexp,可以很好的解决这个问题。

3.2.2.4 listenRouterState的逻辑判断

if (!listenRouterKey.includes(currentKey)) {
        if (!replaceRouter) {
          replaceRouter = routerArray.filter(itemroute => itemroute.key === '/404')?.[0];

          this.setState({
            listenRouterState: [
              ...listenRouterState,
              { ...replaceRouter, key: currentKey, tab: '404' },
            ],
            activeKey: currentKey,
            listenRouterKey: [...listenRouterKey, currentKey],
          });
        } else {
          const match = matchPath(route.pathname, { path: replaceRouter.key });

          this.setState({
            listenRouterState: [
              ...listenRouterState,
              {
                ...replaceRouter,
                key: currentKey,
                tab:
                  this.getPageTitle(route.pathname, breadcrumbNameMap) +
                  this.getDetailPagePrimaryId(route, match),
              },
            ],
            activeKey: currentKey,
            listenRouterKey: [...listenRouterKey, currentKey],
            customerMatchs: [...customerMatchs, { key: currentKey, match }],
          });
        }
      }

      this.setState({
        activeKey: currentKey,
      });

这里做的就是对当前的key进行判断,如果不存在,那就是页面没有打开,则添加新的数据进行,如果已经打开,则跳转到新的页面,如果匹配路径获取组件没有成功,则跳转到404。

3.2.2.5 不同详情页面的title如何处理

因为详情页有多个,但是tab的标签页title要不同

tab: this.getPageTitle(route.pathname, breadcrumbNameMap) 
     + this.getDetailPagePrimaryId(route, match),

getPageTitle主要用的还是之前的逻辑,主要说明一下getDetailPagePrimaryId

getDetailPagePrimaryId = (route, match) => {
    const detailPageIdEnum = ['id', 'title', 'activityNo'];
    let titleValue = '';

    // 处理query类型
    Object.keys(route.query).forEach(item => {
      if (detailPageIdEnum.includes(item) && !titleValue) {
        titleValue = route.query[item];
      }
    });

    // 处理match
    Object.keys(match.params).forEach(item => {
      if (detailPageIdEnum.includes(item) && !titleValue) {
        titleValue = match.params[item];
      }
    });

    return titleValue ? ` - ${titleValue}` : '';
  };

这里的逻辑主要是从query和match中间变量值,只要匹配成功,就会返回匹配的数据值。detailPageIdEnum主要是系统层级可能对应的变量名称

比如query中的title

或者match中title 【当然这里的match是有问题的,在下面实际问题的时候会说明一下】

3.2.3 其他代码

其他的就不是核心代码,基本分为两块,一块是初始化处理,另外一块是table的menu的处理

3.2.3.1 初始化处理

  constructor(props) {
    super(props);
    this.getPageTitle = memoizeOne(this.getPageTitle);
    this.matchParamsPath = memoizeOne(this.matchParamsPath, isEqual);
    routerArray = this.updateTree(props.route.routes);
    const homeRouter = routerArray.filter(itemroute => itemroute.key === '/')[0];

    this.state = {
      listenRouterState: [{ ...homeRouter, key: '/', tab: '首页', closable: false }],
      listenRouterKey: ['/'],
      activeKey: '/',

      customerMatchs: [],
    };
  }

主要就是会将routers的数据做一个基础处理,第二个就是添加一个首页在tab标签页面。

3.2.3.2 tab menu的处理

主要处理,关闭打开的页签,关闭当前页面,关闭全部页面,关闭其他页面

这一块比较简单,就不介绍了。

onClickHover = e => {
    // message.info(`Click on item ${key}`);
    const { key } = e;
    const { activeKey, listenRouterState, listenRouterKey, routeKey } = this.state;

    if (key === '1') {
      this.setState({
        activeKey: routeKey,
        listenRouterState: listenRouterState.filter(
          v => v.key !== activeKey || v.key === routeKey || !v.closable,
        ),
        listenRouterKey: listenRouterKey.filter(
          v => v !== activeKey || v === routeKey || !v.closable,
        ),
      });
    } else if (key === '2') {
      this.setState({
        activeKey,
        listenRouterState: listenRouterState.filter(
          v => v.key === activeKey || v.key === routeKey || !v.closable,
        ),
        listenRouterKey: listenRouterKey.filter(
          v => v === activeKey || v === routeKey || v === '/',
        ),
        customerMatchs: listenRouterState.filter(
          v => v.key === activeKey || v.key === routeKey || !v.closable,
        ),
      });
    } else if (key === '3') {
      this.setState({
        activeKey: '/',
        listenRouterState: listenRouterState.filter(v => v.key === routeKey || !v.closable),
        listenRouterKey: listenRouterKey.filter(v => v === routeKey || v === '/'),
        customerMatchs: listenRouterState.filter(v => v.key === routeKey || !v.closable),
      });
    }
  };

  onEdit = (targetKey, action) => {
    this[action](targetKey);
  };

  remove = targetKey => {
    const { activeKey, listenRouterState } = this.state;

    let newActiviKey = activeKey;
    let lastIndex;

    listenRouterState.forEach((pane, i) => {
      if (pane.key === targetKey) {
        lastIndex = i - 1;
      }
    });
    const tabList = [];
    const tabListKey = [];
    listenRouterState.forEach(pane => {
      if (pane.key !== targetKey) {
        tabList.push(pane);
        tabListKey.push(pane.key);
      }
    });
    if (lastIndex >= 0 && activeKey === targetKey) {
      newActiviKey = tabList[lastIndex].key;
    }
    router.push(newActiviKey);
    this.setState({
      listenRouterState: tabList,
      activeKey: newActiviKey,
      listenRouterKey: tabListKey,
    });
  };

4 . redux应用

4.1 问题

redux在多页签的页面里会存在问题,比如以下两种情况:

  • 详情页面:因为详情页面可以打开多个,但是都是公用同一个redux。
  • 多列表页面共用同一个model

4.2 解决思路

  1. 动态路由,手动注册model实现,但是在ant design pro内部不是很好实现。提了一个官方issue问题:ant design pro多页签缓存问题
  2. 不使用redux,大部分页面是不需要使用redux。只通过页面调用接口请求,数据存储都放在组件state中去。
  3. 使用redux,同一个model通过关键key进行数据区分

4.3 公用同一个model的具体操作

4.3.1 hoc

为什么使用hoc,这里是为了不影响业务页面做修改,将修改的地方放在hoc统一处理掉。这里主要是两个问题:

  • dispatch需要将当前primaryKey传到redux,因为对应的数据更新需要放到对应的primarykey对应的state里面去。
  • 业务组件使用state数据的时候,需要将当前primaryKey对应的数据传入到props里面
/**
 * 高阶函数
 * @param {*} namespace
 * @param {*} primaryKey
 */
function wrapperWithSubscription(namespace, primaryKey) {
  // eslint-disable-next-line no-use-before-define
  const modelNameSpace = namespace;
  const modelPrimaryKey = primaryKey;

  return function withSubscription(WrappedComponent) {
    // ...并返回另一个组件...
    return class extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          currentProps: Object.assign({}, props.location),
          initData: {},
        };
      }

      componentWillMount() {
        const { dispatch, location } = this.props;

        dispatch({
          type: `${modelNameSpace}/initData`,
          payload: {
            primaryKey: location.query[modelPrimaryKey],
          },
        });

        dispatch({
          type: `${modelNameSpace}/getExample`,
          payload: {},
          callback: result => {
            this.setState({
              initData: result,
            });
          },
        });
      }

      componentWillUnmount() {
        // 可以自定扩展如何消除当前primarykey对应的数据
        // 一般情况下,前端业务组件会自己清除state的数据
      }

      wrapperDispatch = (dispatchPrams) => {
        const {
          dispatch,
        } = this.props;

        const { currentProps: { query } } = this.state;

        dispatch({
          ...dispatchPrams,
          primaryKey: query[modelPrimaryKey],
        });
      };

      render() {
        const {
          initData,
          currentProps: { query },
        } = this.state;

        const modelNameSpaceProps = {
          // eslint-disable-next-line react/destructuring-assignment
          [modelNameSpace]: this.props[modelNameSpace][query[modelPrimaryKey]] || initData,
        };

        return (
          <WrappedComponent
            {...this.props}
            dispatch={this.wrapperDispatch}
            {...modelNameSpaceProps}
          />
        );
      }
    };
  };
}

4.3.1.1 wrapperDispatch

其实页面组件的dispatch会走当前的页面,这里会统一将primaryKey传入进去

wrapperDispatch = (dispatchPrams) => {
  const {
    dispatch,
  } = this.props;

  const { currentProps: { query } } = this.state;

  dispatch({
    ...dispatchPrams,
    primaryKey: query[modelPrimaryKey],
  });
};

4.3.1.2 render

render函数会处理redux的state,将对应当前页面的数据传回,页面组件还按照之前直接访问,下面的detail就是当前页面对应的model的namespace名称

    const { history, location, detail } = this.props;
    render() {
        const {
          initData,
          currentProps: { query },
        } = this.state;

        const modelNameSpaceProps = {
          // eslint-disable-next-line react/destructuring-assignment
          [modelNameSpace]: this.props[modelNameSpace][query[modelPrimaryKey]] || initData,
        };

        return (
          <WrappedComponent
            {...this.props}
            dispatch={this.wrapperDispatch}
            {...modelNameSpaceProps}
          />
        );
      }

4.3.2 model文件会做哪些改变呢?

4.3.2.1 初始state的不同

const initDataExample = {
  data: {
    name: '',
  },
};

export default {
  namespace: 'detail',

  state: {},

  *****
}

现在我们的state里面初始是没有值,因为state的一级属性值使我们页面对应的primaryKey。

我们会定义一个基础结构,initDataExample。用于在组件初始化的时候使用这个初始值添加到state对应的primaryKey。

4.3.2.2 新增两个服务于hoc方法effects

   *getExample({ callback }) {
      if (callback) callback({ ...initDataExample });
    },
    *initData({ payload }, { put }) {
      yield put({
        type: 'init',
        payload: {
          [payload.primaryKey]: {
            ...initDataExample,
          },
        },
      });
    },

getExample:获取初始数据结构 initData:初始化数据结构

getExample呢,是因为hoc内部初始化函数的时候,state是异步的,不会直接在页面render的时候直接初始成功,所以这里的getExample是为了在state还没有更新的情况下,使用初始函数去拿到值,传递给组件。

hoc

    componentWillMount() {
        const { dispatch, location } = this.props;

        dispatch({
          type: `${modelNameSpace}/initData`,
          payload: {
            primaryKey: location.query.title,
          },
        });

        dispatch({
          type: `${modelNameSpace}/getExample`,
          payload: {},
          callback: result => {
            this.setState({
              initData: result,
            });
          },
        });
      }



   *****


     render() {
        const {
          initData,
          currentProps: { query },
        } = this.state;

        const modelNameSpaceProps = {
          // eslint-disable-next-line react/destructuring-assignment
          [modelNameSpace]: this.props[modelNameSpace][query[modelPrimaryKey]] || initData,
        };

        return (
          <WrappedComponent
            {...this.props}
            dispatch={this.wrapperDispatch}
            {...modelNameSpaceProps}
          />
        );
      }

4.3.2.3 更新state的不同

   *fetch({ payload, primaryKey }, { put, select }) {
      const currentPrimaryKeyState = yield select(state => state.detail[primaryKey]);

      yield put({
        type: 'save',
        payload: updateWrapperModel('data', payload, primaryKey, currentPrimaryKeyState),
      });
    }

现在更新数据,需要定位到更新到哪一个primaryKey。所以这里提供了一个函数:更新的时候传入对应的值,然后更新对应primaryKey下的具体的key / value

/**
 * updateWrapperModel
 * @param {*} updateKey
 * @param {*} updateValue
 * @param {*} primaryKey
 * @param {*} currentPrimaryKeyState
 */
export function updateWrapperModel(updateKey, updateValue, primaryKey, currentPrimaryKeyState) {
  return {
    [primaryKey]: {
      ...currentPrimaryKeyState,
      [updateKey]: updateValue,
    },
  };
}

4.4 业务组件的修改

我们使用hoc就是为了尽量少的减少业务组件的改动。

hoc内部是一个统一的函数处理,所以hoc是不知道具体当前业务组件对应的modelspace是什么,以及当前路由下对应的primaryKey的key是什么,因为有的页面可能是id,有的页面取得是title。所以modelNamespace和primaryKey需要传入到hoc。

业务组件只需要添加下面的一行代码进行hoc的传递就可以了。

@withSubscription('detail', 'title')

高阶函数会接受这两个值使用

5 . 实际问题

5.1 控制是否使用多页签

这里其实是想可以控制部分页面不需要根据key进行判断,而是根据pathname进行判断就好了。

解决的代码提交是https://github.com/rodchen-king/ant-design-pro-v2/commit/86430c03d3c13f2aed1090c71fb96cf95f195853

路由需要进行只会存在一个页面标示:

路由监控的地方,判断当前路由如果是isOnlyOnePage,则采用pathname进行key标示

5.2 支持路由match数据

这里其实就是为了处理参数在pathname里面的参数

这里主要的问题是采用目前的这种方式,match的数据是不会正常返回的

所以这里对于项目中用到的props.match都需要单独处理一下。

所以在BasicLayout里面做了单独的处理,就是上面说的customerMatch。其实是为了处理这个问题的。

5.2.1 具体实现

整体的思想和redux应用里面有类似的思路。不同的是

监控路由匹配的时候会处理得到当前路由的match值。

const match = matchPath(route.pathname, { path: replaceRouter.key });

这里其实是处理当前url和匹配的路由pathname处理得到一个match的结果。

至于matchPath这个方法,其实是我从react-router源码里面复制出来的:

import pathToRegexp from 'path-to-regexp';

const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };

  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    // eslint-disable-next-line no-plusplus
    cacheCount++;
  }

  return result;
}

/**
 * Public API for matching a URL pathname to a path.
 */
function matchPath(pathname, options = {}) {
  if (typeof options === 'string' || Array.isArray(options)) {
    // eslint-disable-next-line no-param-reassign
    options = { path: options };
  }

  const { path, exact = false, strict = false, sensitive = false } = options;

  const paths = [].concat(path);

  // eslint-disable-next-line no-shadow
  return paths.reduce((matched, path) => {
    if (!path && path !== '') return null;
    if (matched) return matched;

    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive,
    });
    const match = regexp.exec(pathname);

    if (!match) return null;

    const [url, ...values] = match;
    const isExact = pathname === url;

    if (exact && !isExact) return null;

    return {
      path, // the path used to match
      url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) => {
        // eslint-disable-next-line no-param-reassign
        memo[key.name] = values[index];
        return memo;
      }, {}),
    };
  }, null);
}

export default matchPath;

然后不同页面的match值会存储在customerMatchs。然后通过context进行数据传递。

<RouterContext.Provider value={customerMatchs}>
  <Route key={item.key} component={item.content} exact />
</RouterContext.Provider>

HOC函数进行消费withRouterMath

import React from 'react';
import { RouterContext } from '@/layouts/BasicLayout';

/**
 * 高阶函数: 适配match
 */
function withRouterMath() {
  // eslint-disable-next-line no-use-before-define

  return function withSubscription(WrappedComponent) {
    // ...并返回另一个组件...
    return class extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          currentLocation: Object.assign({}, props.location),
        };
      }

      getMatch = value => {
        const {
          currentLocation: { pathname },
        } = this.state;

        const returnValue = value.filter(item => item.key === pathname);

        if (returnValue.length) {
          return returnValue[0].match;
        }

        return {};
      };

      render() {
        return (
          <RouterContext.Consumer>
            {_value => <WrappedComponent {...this.props} match={this.getMatch(_value)} />}
          </RouterContext.Consumer>
        );
      }
    };
  };
}

export default withRouterMath;

使用的时候

@withRouterMatth()
@withSubscription('detail', 'title')
class ListDetail extends React.PureComponent {
  componentDidMount() {
    const { match, dispatch } = this.props;
    dispatch({
      type: 'detail/fetch',
      payload: {
        name: match.params.title,
      },
    });
  }
}

这样页面就可以正常使用match,和原先的location一样获取值,然后系统层级也会匹配query和match的数据进行primarykey处理。

5.3 业务页面不是由单个的参数进行唯一表示,而是多个业务参数进行唯一表示

前面说的都是单个primarykey作为唯一页面标示的,但是可能部分代码存在很复杂的情况。

举个例子,存在一个业务组件既是详情页面,也是新增页面,而且业务上还存在多个。

类似营销活动:为了更好的说明,我在单独加一个字段,templateName

满减活动【templateName:满减】

  • activityType:1;
  • 详情的时候会有一个activityNo
  • 新增的时候则没有activityNo

满折活动【templateName:满折】

  • activityType:2:
  • 详情的时候会有一个activityNo,
  • 新增的时候则没有activityNo

5.3.1 具体实现

https://github.com/rodchen-king/ant-design-pro-v2/commit/d0ecfd2e795cb90837b0ed94de5f4ad13012af31

这里主要是支持多个参数:

修改BasicLayout.js

因为可能参数当中涉及到中文,所以判断key的逻辑用了decodeURIComponent方法解析。

第二个在getDetailPagePrimaryId,这里加上这个是为了适配新的查询参数。

修改wrapperWithSubscription

主要是兼容多个参数的情况,传进来的是一个数组。

调用的方式参考新增的业务Maket组件的调用方式

@withSubscription('detail', ['activityNo', 'templateName', 'activityType'])

6 . 优化

6.1 dispatch.then的使用

代码:https://github.com/rodchen-king/ant-design-pro-v2/commit/5f160db67aaad31cb1ac04d4d01a66a1fc6d0582

开发过程中存在dispatch().then的方式: 所以这里支持也要优化一下:

    wrapperDispatch = dispatchPrams => {
        const { dispatch } = this.props;
        const { primaryKeyValue } = this.state;

        dispatch({
          ...dispatchPrams,
          primaryKey: primaryKeyValue,
        });
      };

6.2 redux-state更新多个参数

代码:https://github.com/rodchen-king/ant-design-pro-v2/commit/5f160db67aaad31cb1ac04d4d01a66a1fc6d0582

/**
 * model相关的处理函数
 */

/**
 * updateWrapperModel
 * @param {*} updateStateObject         要更新state的健值对
 * @param {*} primaryKey                当前页面对应的primaryKey
 * @param {*} currentPrimaryKeyState    primaryKey对应的数据源
 */
export function updateWrapperModel(updateStateObject, primaryKey, currentPrimaryKeyState) {
  return {
    [primaryKey]: {
      ...currentPrimaryKeyState,
      ...updateStateObject,
    },
  };
}

7 . Ant design pro v5 如何做?

https://procomponents.ant.design/components/layout

因为最新的v5 菜单已经采用ProLayout作为布局。所以这里一种方式是利用配置项目的childRender

另一种方式则是注释这里的代码,然后重新写以前版本的BasicLayout.js

好了,今天的分享就到这里了 。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/UBYy1BmdVinU-lLDF9DHVw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录