三个阶段

王国维《人间词话》中,写过三种境界:

古今之成大事业、大学问者,必经过三种之境界: “昨夜西风凋碧树,独上高楼,望尽天涯路。” 此第一境也。 “ 衣带渐宽终不悔,为伊消得人憔悴。” 此第二境也。 “ 众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。”此第三境也。

多年之后,再次咀嚼这三种境界,似有心得。于是,我总结了自己技术学习过程中的三个阶段:

走马观花:遇到有趣的且我之前没有见过的知识点,记录下来,幻想着之后慢慢品味。结果,全在收藏或者笔记文件中尘封。

点到即止:遇到灵巧的技术点,可以输出笔记,可以研究它后面的原理。然而,遇到复杂的技术点,花费了一定的时间和精力还没有研究透,就会放弃。

刨根究底:无论是知识点还是技术栈,学习就会彻头彻尾,有始有终。即便可能现在用不到,也会将可能用到的场景记录一下。

而我目前在「点到即止」和「刨根究底」中间摇摆,也在努力向「刨根究底」靠拢。

所以当我遇到逻辑复用这个话题的时候,尝试从不同层面去思考问题。既有对具体问题的实现方案,也有实现方案背后包含了哪些技术点,以及延伸了不同方案可行与不可行的总结。

数据逻辑复用 && UI 逻辑复用_复用


表格数据项重置

先来介绍一下逻辑复用的开发场景。

使用表格展示多行多列数据是前端很常见的一个功能。通常情况下,通过一个page接口就可以获取表格全部数据。

但是,凡事有例外。有时候表格的某一列数据,需要通过额外的 HTTP 请求获得需要展示数据。

表格 UI

如下图,城市业务表格中的的查看资料列的有点击查看按钮,因为资料为图片类型的数据,所以不方便直接在表格中展示。

数据逻辑复用 && UI 逻辑复用_复用_02

查看资料功能

为什么资料的数据需要一次 HTTP 请求?

我说一下我知道的其中一个原因,是后端表设计,不同的数据会存入不同的数据库表中,取的时候同理。当然,每个公司的实际情况可能会有不同。我们这里的功能,在开发设计评审的时候,我的同事做了这样的解释。

所以文件类数据,比如图片、PDF文档等,需要额外的 HTTP 请求获取数据。

数据库表

大致的数据库表如下,城市表与资料表是一对一的关系

数据逻辑复用 && UI 逻辑复用_React_03

逻辑复用是必要的吗?

这个问题其实也是分情况的。综合对需求的提炼和对业务走向的关注。

先说我进行了逻辑复用但是用不上的情况。我预留了代码的复用空间,但是产品却只留下了这一个功能孤品。在没有相似的功能,又要继续迭代的情况下,复用的设计会让维护成本增加一些。

但是更多时候,逻辑复用,会将功能入口集中在一个位置,这样无论是进行现有功能的扩展还是新的相似功能的快速开发都是很有帮助的。

如何进行选择?

我的经验是,先对需求进行归纳总结,提炼功能点,当功能点存在相似性的时候,可以考虑进行复用。

当孤立的功能出现时,可以适当提问今后的业务走向。对业务的后续发展做一个简单粗略的了解,如果存在性质类似的业务,大概率也会出现相似的需求,也可以提前规划好复用功能。

数据逻辑复用 && UI 逻辑复用_React_04

所以对于查看资料的功能逻辑复用是必要的?

先来列一下,根据产品需求提炼出以下的功能点:

不同产品类型下的城市数据表格设计的很相似,但是不同产品类型的页面是独立的。产品类型的数量超过了10个。

概括就是,一系列相似的页面,拥有相同的功能。

这一系列页面,数量大于10,维护的量级足够了,此时的逻辑复用设计是必要的。

如何进行数据逻辑的重用?

组件封装

将表格数组重置功能进行封装,将得到的最终的表格数组通过 children 传递给引入该组件的页面。

点击事件

查看资料这一列,每个单元格都有点击事件,通过唯一 key 值确定需要增加处理逻辑的数据项。

组件代码

/**
 * @description 表格数组重置
 */
import React, { useEffect } from 'react';

const GetNewColumnList = ({ ...props }) => {
  let { columns, children } = props;

  /**
   * 查看资料
   */
  const getMaterialShow = record => {
    // 通过接口获取资料数据
    // 弹窗展示资料数据
    console.log(record);
  };

  useEffect(() => {
    columns.map(column => {
      if (column.key === 'materialList') {
        column.render = function render(val, record) {
          return (
            <div onClick={() => getMaterialShow(record)}>
              点击查看
            </div>
          );
        };
      }
    });
  }, []);

  return children({ columns });
};
export default GetNewColumnList;

页面中的使用

页面中引入 GetNewColumnList 组件。

将展示的表格放到 GetNewColumnList 组件的标签之间。便可以获取组件中通过 children 属性返回的变量了。

/**
 * @description 表格页
 */
import React from 'react';
import { Table } from 'antd';
import GetNewColumnList from './components/GetNewColumnList';

const TablePage = () => {
  const columns = [
    {
      title: '城市ID',
      dataIndex: 'id',
      key: 'id',
      width: 80,
    },
    {
      title: '查看资料',
      dataIndex: 'materialList',
      key: 'materialList',
      width: 180,
    },
  ];
  // 获取列表
  const getList = (searchParams = {}) => {
    let params = { ...searchParams };
    return dtyClaimRecordPageList(params);
  };

  return (
    <div>
      <GetNewColumnList columns={columns}>
        {({ columns }) => {
          console.log(columns, 'columns');
          return <Table size="small" dataSource={list} columns={columns} pagination={false} bordered />;
        }}
      </GetNewColumnList>
    </div>
  );
};

export default TablePage;

如何进行 UI 逻辑的重用?

上面我们借用 children 属性渲染组件包裹的内容的渲染,被包裹的这部分内容能不能直接传入到组件中呢?

当然是可以的。props 允许传递一个函数。通过渲染 props 的函数可以达到同样的效果。这种方式有专业的术语,叫做 render props。

render props

一个组件用来了解要渲染什么内容的函数 props。

在 React 中使用 render props 的库包括 React Router 和 Downshift。

相同与不同

还是上面的功能,其实表格 UI 也可以放到子组件中,因为不同页面的表格具有很高的相似性,除了操作不同,操作功能,可以在 columns 数组追加之后,通过 props 传入 GetNewColumnList 组件中。

但是有一点不同,产品说表格上方要展示每个产品类型的基础信息,这个时候,不同产品展示的内容不同,其他都相同。

组件封装

新入参 render 负责渲染传入的额外的 UI 部分的展示内容。

/**
 * @description 查看资料
 */
import React, { useEffect } from 'react';
import { Table } from 'antd';

const GetNewColumnList = ({ ...props }) => {
  let { columns, render } = props;

  /**
   * 查看资料
   */
  const getMaterialShow = record => {
    // 通过接口获取资料数据
    // 弹窗展示资料数据
    console.log(record);
  };

  useEffect(() => {
    columns.map(column => {
      if (column.key === 'materialList') {
        column.render = function render(val, record) {
          return (
            <div className="action-button" onClick={() => getMaterialShow(record)}>
              点击查看
            </div>
          );
        };
      }
    });
  }, []);

  return (
    <div>
      {render()}
      <Table size="small" dataSource={list} columns={columns} pagination={false} bordered />
    </div>
  );
};
export default GetNewColumnList;

页面传参

这个时候组件不再包裹内容,而是通过 props 传递 render 函数。且我增加了函数的注释,方便后面的开发者了解这块代码的用途。

/**
 * @description 表格页
 */
import React from 'react';

import GetNewColumnList from './components/GetNewColumnList';

const TablePage = () => {
  const columns = [
    {
      title: '城市ID',
      dataIndex: 'id',
      key: 'id',
      width: 80,
    },
    {
      title: '查看资料',
      dataIndex: 'materialList',
      key: 'materialList',
      width: 180,
    },
  ];

  /**
   * 为页面添加公共头
   */
  const setColumnsAction = () => {
    return <div className="mb10">产品类型:A 上架状态:已上架</div>;
  };

  return (
    <div>
      <GetNewColumnList render={setColumnsAction} columns={columns} />
    </div>
  );
};

export default TablePage;

最终的 UI

这种实现方式有替代吗?

HOC(高阶组件)也可以实现上述的功能。但是我个人觉得 render props 设计模式,使用上更方便。

思考延伸

--------------------------- 问题1 --------------------------

问:那为什么不用同一个 JSX 文件?

答:最初我先考虑了用同一个 JSX 文件实现的可行性。但是很快发现用同一个 JSX 文件有很多缺陷:

  • 可读性很差,增删改功能的时候需要重读一遍代码,
  • 容易出现代码冗余,特殊的需求需要增加额外的判断条件。
  • 某一个类型下的内容改动时,开发除了要注意进行特殊区分,改动公共代码还有额外注意不能导致现有功能出现问题。

弊大于利的情况下,我采用了别的方式来集中管理公共部分。

我也遇见过别的项目中使用同一个 JSX 文件。我接手时,梳理代码就花费了不少时间。

--------------------------- 问题2 --------------------------

问:可以提炼成公共方法吗?

答:公共方法这个方案是不错的选择。

不过我习惯把 render 放到 JSX 文件里。还有一点,除了查看资料,其实列表还有其他的需要额外加的功能,比如是否有在售的标识,这个标识需要额外的样式设置。

实际功能比上面的举例更复杂,使用 JSX 比 JS 文件的代码更直观一些。

当有多种可选的不错的方案时,可根据约定俗称的代码规则或者个人习惯进行选择。

今日总结

  1. 对逻辑复用的必要性进行了探讨,给出了如何判断是否可以进行逻辑复用的建议。
  2. 对表格数据项重置的功能,进行了数据逻辑和 UI 逻辑两个层面复用的实现。
  3. 除了对需求的归纳提炼,也可以提前跟产品讨论业务今后的走向,从而在代码设计之初,就预留好可复用和可扩展性。
  4. 我在阅读一些优秀的文章时所做的笔记,除了知识点,还会将可能使用到的场景一并记录下来,尤其是文章里面没有提及但是根据自己的经验推测的使用。

彩蛋

最近翻看,发现思维定式无处不在。在做代码调整的时候,总结了一些技巧点,放在文末作为彩蛋。

小技巧,比没有很厉害,主要是帮助提醒我摆脱惯性思维的干扰,可以今后老代码的迭代中,付出更小的改动成本。

表格 UI 的二次封装

我们的项目引入了 antd 的 UI 框架,其中对表格的使用较为频繁,半数以上的页面都用到了表格。使用表格是需求也很相似,列表数据的 HTTP 请求、分页、可搜索、搜索项可以重置等。

于是,我们基于这样对表格进行了二次封装。使用页面直接引入,然后可自定义搜索项、表格项、以及 HTTP 请求。

数据变量名的配置

其中对 HTTP 请求返回数据中的处理是固定了响应体里的实体变量名,也就是「list」。因为一般返回的变量名都是 list。但是也是有特例的。

以为的代码,变量名配置化设置,当表格数据返回的变量名特殊时,可以通过 props 传入的方式修改变量名:

static propTypes = {
  listKey: PropTypes.string,
};
static defaultProps = {
  listKey: 'list',
};
......
list: res[listKey]

但是实际我看到的代码是这样的:

list: res.list || res.logList || res.operateList

有个别的接口返回了不同的变量名,于是在原来的基础上增加了或判断,好像也没什么毛病。

为什么一连加了两个变量也没有反应过来?

看代码,一连加了两个有别于「list」的变量名,为什么还是没有改成可配置的呢?我分析是思维定式和改动成本。

  • 思维定式,直接在代码后面累加或判断最快最方便。
  • 成本低,不用担心其他用到该组件的地方有问题。

小结

我之前遇到问题都是直接改了完事,最近一直在考虑还能不能做些其他方面的改进。比如会反向思考某些问题出现的原因,帮助锻炼开发思维。