2021年10-11月工作总结

前端异常监控系统

之前写微信小程序的时候使用的微信小程序自带的实时日志 API ,微信小程序端将错误信息进行上报,可以在微信小程序公众平台上进行查看。

当前公司使用的是 Sentry.io 管理前端异常。需要手动上传当前登录用户的信息和错误信息。然后在 Sentry 错误信息管理平台进行查看报错信息和系统性能。

Sentry’s SDK hooks into your runtime environment and automatically reports errors, uncaught exceptions, and unhandled rejections as well as other types of errors depending on the platform.

Sentry 后端系统可以使用 python 进行搭建。

其他异常和性能监控系统

webfunny:

推荐阅读

Sentry.io - docs

错误上报和应用性能监控平台 Sentry

前端数据埋点体系

前公司的数据埋点系统使用的是友盟、微盟、阿拉丁,因为是 To C 的应用所以埋点数据侧重于用户的按钮点击次数和页面加载次数等记录用户操作。

当前公司使用的是自己搭建的 apm 埋点系统 + grafana 数据展示平台。更加复杂的数据就使用的神策进行处理。因为是 To B 应用所以更加侧重于系统性能,如页面加载速度等。

前端自动化流程

使用的是 k8s + docker + gitlab + jenkins 搭建的自动化流程

每次发布构建需要十几分钟,整个流程是先创建 docker 然后在 docker 中运行 npm install --production --registry=http://npm.mokahr.com && npm run build 命令,就会重新安装一遍 npm 包,然后进行构建。巨型项目这个流程就会很耗时,构建好后触发 jenkins 进行镜像发布,这段时间耗时一般在一分半左右。

项目开发

项目使用的是 React + dva + umi + axios 技术栈。

全局提示和数据状态共享 - redux

https://redux.js.org/faq/general#when-should-i-use-redux

一般提示信息和数据状态多页面共享情况下,会使用 redux 。

但是一个业务页面中数据逻辑过于复杂时需要进行合理地按照组件职责进行划分和抽取组件。

组件设计

参考链接:dva-组件设计方法

在拆分 Component 的过程中要尽量让每个 Component 专注做自己的事情。

根据职责区分可以分为:容器型组件和展示型组件。

容器型组件主要负责业务逻辑部分,指的是具有监听数据行为的组件,一般来说它们的职责是绑定相关联的 model 数据,以数据容器的角色包含其它子组件,通常在项目中表现出来的类型为:Layouts、Router Components 以及普通 Containers 组件。需要组织子组件的交互逻辑和展示。

展示型组件,也称作为木偶组件,不会关联订阅 model 上的数据。每个组件跟业务数据并没有耦合关系,只是完成自己独立的任务,需要的数据通过 props 传递进来,需要操作的行为通过接口暴露出去。

Dva 的使用

dva 官网: https://dvajs.com/guide/

dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。

dva = React-Router + Redux + Redux-saga

经验帖:

使用 Dva 开发复杂 SPA

在 model 中使用 typescript:

玩转 TS - 实现 dva 的完整类型推导 - 掘金 :使用 dva-type 基于 typescript 4.1 进行类型推导。

react+typescript+umi+dva+antd - 掘金

扩展阅读:

一文彻底搞懂 DvaJS 原理 - 掘金

为什么 redux-saga 不能用 async await 实现 - 掘金

Redux-Saga妈妈级教程 - 掘金:https://juejin.cn/post/6975041237266989086 + https://juejin.cn/post/6979146131028574245

踏入进阶!redux-saga中的八个高阶知识点 - 掘金

计算页面剩余高度

表格容器需要占用页面中的剩余的空间,这个空间不固定,原来的处理逻辑是通过获取父容器的高度然后减去兄弟元素的高度得到表格容器的高度。

对于更加复杂的页面布局,可能会出现需要计算更多的兄弟元素的高度。使用 NewTable 组件和 BasicTable 组件高度计算方式不一样,BasicTable 设置的高度是表格数据的高度(即不包含表头的高度),NewTable 设置的高度是表格的整体高度(即包含了表头的高度),要在原来的基础上写会更加复杂和难懂。

在项目中使用 flex: 1 1 0; / flex: 1 1 0px; / flex: 1 1 0%; 构建后变成了 flex: 1 1; 。但是实际情况下 flex: 1 1 0;flex: 1 1 0%; 的效果是不一样的,所以在项目中使用只能将三个属性拆开写 flex-grow: 1; flex-shrink: 1; flex-basis: 0; 才能达到目标效果。

项目中的具体原因需要进行 css loader 之类的库进行实验。

解决方案:

父容器使用 flex: 1 1 0px; (即flex-grow: 1; flex-shrink: 1; flex-basis: 0;)设置样式,然后使用 ResizeObserver 进行监听父容器的高度变化,因为页面中部分区域是需要根据数据动态显示的,所以组件挂载后的高度不是固定的,需要等待数据加载并更新渲染后才能确定父容器的真正高度。

关于 ResizeObserver 扩展阅读:检测DOM尺寸变化JS API ResizeObserver简介 - 张鑫旭

相关代码片段(注意 ResizeObserver 的兼容性 ):

componentDidMount() {
  const tableWrapElement = document.getElementsByClassName(styles.reportTable)[0];
  if (window.ResizeObserver) {
    this.resizeObserver = new ResizeObserver((entries) => {
      const tableWrapContentRect = entries[0].contentRect;
      this.throttleSetHeight(tableWrapContentRect.height);
    });

    if (tableWrapElement) {
      this.resizeObserver.observe(tableWrapElement);
    }
  } else {
    this.setState({
      tableWrapHeight: tableWrapElement.clientHeight,
      isBrowserSupportResizeObserver: false,
    });
  }
}

componentWillUnmount() {
  this.resizeObserver?.disconnect();
  this.resizeObserver = null;
}

React 生命周期的使用

UNSAFE_componentWillReceiveProps()

我需要监听到 model 中传递的 props 发生了改变且进行新旧 props 对比后计算并触发一些事件,如封存状态从封存中变为封存成功时需要重新刷新数据表格,数据表格数据维护在容器型组件中而不是在 model 中。在这种情况下就需要用到 UNSAFE_componentWillReceiveProps(nextProps) 生命周期。

todo:需要进行实验 componentDidUpdate 和 getSnapshotBeforeUpdate 生命周期,并总结生命周期使用的场景

错误记录

这块主要是记录在这个月组内或者自己解决或遇到的问题,做个记录。

弱网环境,数据请求过慢,下一步操作数据 undefined 问题。

redux 和 vuex 具有一样的问题。页面强制刷新(F5)后,redux 和 vuex 中 store 数据就会被清空,回到初始状态。如果当前页依赖上一页储存到 redux 中的数据,用户进行手动刷新页面或者移动端系统机制强制刷新,上一页的数据就会被清空,当前页的操作就会报数据 undefined 错误。

解决方案

数据请求是在 redux model 中进行的,所以数据最后传输是通过 props,如果要知道数据情况有两种方案 —— 受控组件和非受控组件

受控组件:可以监听 props 中的数据是否更改,即从初始空值变为有值的状态,如果需要使用这个状态,则就需要在 state 中存入 loading 状态。

非受控组件:在 model 中添加一个 loading 变量来记录数据加载状态传递给组件。

最后使用的是非受控组件的方案解决的。(推荐)

交互效果:弱网情况下数据还没有返回就点击下一步按钮,显示 loading 提示,然后开始轮询检查数据是否请求成功,请求成功则继续执行按钮接下来的事件。

goAskForHolidayFlow = async () => {
  const { flowMenuList } = this.props;
  // 如果 flowMenuList 数据没有加载完成 则跳转操作进行不下去(弱网)
  if (flowMenuList.length === 0) {
    const HolidayFlowLoadingId = 'HolidayFlowLoadingId';
    // 检查状态
    const checkStatus = (time) => {
      return new Promise((resolve) => {
        const timer = setTimeout(() => {
          const { flowMenuList: curFlowMenuList, flowMenuLoading } = this.props;
          // 当前flowMenuList数组有数据 或 flowMenuLoading 加载状态已经重置(处理接口返回失败关闭loading弹窗)
          if (curFlowMenuList.length > 0 || flowMenuLoading === false) {
            clearTimeout(timer);
            resolve();
          } else {
            resolve(checkStatus(time));
          }
        }, time);
      });
    };
    this.setState({
      btnLoadingId: HolidayFlowLoadingId,
    });
    sendToast({
      id: HolidayFlowLoadingId,
      type: 'success',
      isLoading: true,
      content: '加载中,请稍候',
    });
    // 开始轮询检测
    await checkStatus(500);
    withdrawToast(HolidayFlowLoadingId);
    this.setState({
      btnLoadingId: '',
    });
  }
  // 此处跟我的出勤跳转逻辑形同 DayUnusualList中 onProcess 方法 只拿请假类菜单中的第一个;
  let flow = this.askForHoliday;
  // 接口数据返回失败时没有数据
  if (!flow) {
    return;
  }
  // ...
};
页面出现闪动的问题

最开始查找问题的时候,只进行了最直接的值的查询,而忽略了影响值的查询,结果发现了一个离奇 bug 。

Bug 示例:

useEffect(() => {
  setConfigList(formConfigList);
  setIsShowClearBtn(false);
  return () => {
    if (departmentToggleReload) {
      departmentToggleChecked.current = false;
    }
  };
}, [formConfigList]);

useEffect(() => {
  const haveValue = getIshaveValue(configList);
  if (haveValue) {
    setIsShowClearBtn(true);
  } else {
    setIsShowClearBtn(false);
  }
}, [configList]);

Bug 分析:第一个 useEffect 中设置了第二个 useEffect 的依赖值,所以并不是两个 useEffect 顺行执行 setState 打包进行值的更新,而是异步执行。所以会出现第一轮 isShowClearBtn 先设置为 false 即按钮隐藏,又会在第二轮中将 isShowClearBtn 设置为 true,出现闪动问题。(找这个 bug 都对 react 的认知产生了怀疑🤨,所以还是要细心和耐心!!!)

解决方案:删除第一个 useEffect 中的 setIsShowClearBtn,使用第二个 useEffect 中计算出来的值。

总结反思

CR 问题

TS 使用问题
  • 避免使用 [props: string]: unknown

    • 只声明需要使用的对象属性,不需要做其他多余的类型处理。
  • 尽量减少使用 as xxx,有的时候不得已也没办法,如 qs.parse 的处理结果。需要写一个转换类型工具函数进行类型转换。

    • as xxx 是欺骗编译器的行为,并不是真正把 String 转为 Number 的处理。
代码问题
  • 避免使用 Object.assign 使用对象扩展符进行替换。
    • 如果需要使用 Object.assign 则使用 lodash 中的 assign api。
  • 短路逻辑运算,最好在脑子清醒的时候进行优化。
  • 命名问题,减少通用命名,命名最好有业务关联。新值最好以 newXxxx 进行命名。
    • 需要贯彻执行《代码整洁之道》。
  • 后端接口返回值不幂等时要及时沟通,前端减少数据兜底操作。
  • 减少魔术字符串的使用,通用部分尽量使用常量处理。

常犯问题:

  1. 只记得更新 table 了,总是忘记重置 pagination,即使写了 testplan 但是还是忘记了。
  2. 边界值处理和特殊情况处理,有的时候会多想,有的时候会少想,总想着给后端兜底。有的时候考虑到了,但是打断就忘记了,还是要及时 todo 记录下来。自测时可以询问测试同学如何造数据操作。

这些也是不应该犯的基础问题,记录一下下次 testplan 和技术方案覆盖上。

细节问题:

  1. 关于根据数据的值不同来展示样式细节问题。最开始模拟数据的时候没有考虑到这个问题,所以接通数据能跑通主流程后就忘记了。模拟数据 mock 要做全套。
  2. 对于后端的错误返回特殊处理 message 问题,需要和后端沟通进行统一,然后及时关掉全局的统一处理设置,避免出现两次错误提示的情况。

疑惑或待优化的问题:

  1. 关于数据请求接口,是否需要判断请求参数的可用性,需要进一步确认。如果是一连串的接口请求操作,后一个接口依赖前一个接口的数据时,接口数据都在请求函数中做了错误捕获或者返回数据处理,就无法在 async 中断 await 调用,后果会在接口动荡的时候出现一连串的服务器内部错误或者参数为 null 的错误提示。
  2. 表格滑动容易出现浏览器页面切换的问题,需要和 sugar 同学进行沟通,可以使用 overscroll-behavior 样式属性来控制浏览器滚动到边界的情况。

整个开发过程中,低效的问题:

  1. 这次开发是重构页面,由于个人习惯总是喜欢 follow 之前项目中别人写的代码,这种基操对于当前情况并不适用,没有足够的知识储备,所以开始写的的时候就出现了低效的问题。要避免 follow 不好的代码习惯。
  2. 对于 react 和 typescript 使用经验不足导致的低效问题,在使用过程中对于某些特殊的用法需要询问其他同学才敢下手,其实有点干扰别人工作。
  3. typescript 对接 sugar 组件库,为了减少 typescript 报错行为,所以补全了很多实际用不到但 sugar 组件需要的类型,这个归根结底还是对 typescript 不熟和不主动与其他部门同学交流导致的。
  4. 文件项目过于巨大的时候就应该想实际办法,比如是否可以使用 redux 去改善,没有灵活变通,后期使用了大量的时间去拆分组件和避免拆分后的组件出现问题,进行巨轮组件拆分后,之前遇到的一些问题反而更加容易解决了,减少了使用 unsafe 的生命周期和复杂的判断计算,更容易维护。
  5. 对于前期已经考虑到的交互优化效果,对高阶组件使用不太明确,也没有想其他想法去实现相同的功能,被之前的模式给框住了,浪费了很多时间在样式处理实验和进行复杂的高度计算,反而写出更多难懂和难以维护的逻辑。如果出现了自己都觉得难懂和难以维护的解决方案就要适时放弃,对自己好一点,也对后续维护的人好一点。且要坚持交互优化效果,保持良好的交互体验。
  6. 不合理的开发安排。技术方案的问题,主要想要先实现通用组件再实现业务组件,但是通用组件的效果是在业务组件的基础上进行测试的,所以初期考虑的开发顺序有问题。先业务后通用组件,更加容易实现通用组件的数据交互和测试。
  7. 过度思考。总是会多考虑一些问题,如果拿不定的时候应该多和产品、设计或其他同学进行讨论。关于交互问题的疑问不要只问设计同学,要与产品、设计、涉及到的技术同学一起询问,避免出现设计同学的方案被产品否定的情况,确定方案再下手修改,避免重复修改
  8. 对于需求的理解不够透彻。最开始写的时候单纯的信了产品说的没有数据交互关系,反讲的时候也是这样讲的,结果实际开发的时候其实有数据逻辑关系的,和后端的接口文档沟通上也不是很全,浪费了很多时间。
  9. 后端接口的问题不要去多写兜底方案,最后还是要修改的,要积极主动去推动后端同学修复 bug 避免前端白屏或报错问题,影响主流程测试。
  10. 深刻的意识到技术方案的重要性。需要深入思考和前期多花时间去进行规划,使用思维导图将临时想到的问题及时记录下来,不要相信金鱼的记忆力。

反思:

  1. 沟通能力待提升。沟通时需要提供上下文以及相关链接,以及重点文字标注,更加容易让人清楚知道接下来需要沟通的内容。以及沟通的时候需要仔细查看关键字,需要耐心和细心。
  2. 效率提升。需要按照计划来进行,前期需要合理的开发安排、组件设计、通用设计。
  3. 基础知识积累和提升。需要花更多时间在 react + typescript + css 方面进行基础知识的提升,利用等待电梯和地铁的时间进行知识积累,目前最重要的是快速提升对 react + typescript 的使用经验。
  4. 更加专心、耐心、认真仔细,减少过度考虑和多余的想法,拿不定的交互效果需要和其他人沟通并讲清楚。

措施 🚩:

  1. 技术方案:覆盖内容常犯问题和细节问题、边界问题。

    整个逻辑交互需要用思维导图整理下来,包括边界问题、弱网问题、特殊情况问题。

  2. 每日总结,早睡早起,饮食规律。

  3. 使用番茄钟,保持良好的工作状态和合理用脑。

  4. 每日完成 typescript-challenge 或阅读一篇 typescript 相关文章并整理总结。