React 历史项目维护与优化实践

本文介绍了作者接手维护一个中型 React 历史项目时的一系列改进实践,包括模块结构拆分、业务逻辑梳理、Webpack 打包优化等。

背景

这是一个 PC 的管理后台类项目,没有引入 react-router 和 redux。待维护的页面所有模板和逻辑全部在一个千行级的 JSX 中实现,包括调用组件库、发送 fetch 请求、切换子页面状态等。并且,该项目实际上并不是单页应用,而是通过 Webpack 区分多个 entry 的方式实现了多入口页面。

模块拆分

在开始实现新增需求前,首先要做的是了解代码,整理其结构并适当地以拆分模块的形式逐步重构之。在这一步中,并不涉及最令人畏惧的【重构业务逻辑】,而更多地是【更高级的代码美化】,在完整保留原有代码逻辑和调用方式的前提下,利用一些 JS 的技巧,按照单一职责原则拆分不同的业务逻辑代码到不同的模块中,以提高【面条代码】的模块化程度。这一步处理要解决的主要问题是:

  • 历史代码中混杂了 JSX 模板结构、数据处理、异步控制、状态管理的各种逻辑。

  • 代码中如菜单名称结构、表单字段名等的各种硬编码配置分散在各处。

  • 几乎全部的业务逻辑均在一个扁平的组件中实现。

解决上述问题,并不涉及到具体业务逻辑的重写,而是通过将同类功能提取为独立模块,通过一些简单的语法糖来保证仅更改尽量少的业务代码,就能实现初步的模块拆分。

针对上述的几个问题,初步的模块拆分包括:

  1. 包含大多数 React 组件方法的主页面组件。

  2. 包含异步请求的 action 模块。

  3. 包含各种硬编码配置的 consts 模块。

  4. 包含调用组件库中表单等组件的配置文件 model 模块。

然后就可以一步步将代码逻辑迁移到新模块中,在保证页面的功能不受影响的前提下逐步实现初步的模块拆分了。这个过程中多次用到的技巧包括:

将执行异步请求的组件方法拆分至模块中,再在构造器中 bind 回组件。如一个典型的查询逻辑:

// main.js
class Demo extends Component {
  fetchData () {
    fetch('...').then(data => {
      // 此处通常有冗长的业务逻辑
      this.setState({ data })
    })
  }
}

可将其先拆分至 action.js 模块中,形如:

// action.js
// 业务逻辑完全保留,只是添加了 export function 前缀
export function fetchData () {
  fetch('...').then(data => {
    this.setState({ data })
  })
}

然后在原组件中加载并 bind 该函数,从而实现模块拆分:

import { fetchData } from './actions'
 
class Demo extends Component {
  constructor() {
    // 在此 bind 即可
    this.fetchData = fetchData.bind(this)
  }
}

以及,将一些加载时引用了 this 的配置对象封装至新模块的工厂函数中:

render() {
  // 包含冗长表单配置的配置变量
  const demo = {
    // 直接将其提取至新模块在此会报错
    value: this.state.xxx
  }
}

新建一个返回 demo 的工厂函数:

// model.js
export const getDemo  = () => ({
  // 在此的业务代码同样可原封不动地移动
  value: this.state.xxx
})

修改原有位置的调用逻辑:

import { getDemo } from './model'

render() {
  // 在调用工厂函数时绑定上下文,即可使模块中 this 指向正确
  const demo = getDemo.call(this)
}

实践中在这一步完成后,其实已经实现【将千行级代码拆分至若干个百行级的模块,每个模块均仅包含类似的逻辑功能】了。

业务梳理

在初步整理模块后,对代码结构也有了初步的了解,此时可以开始添加一些新的业务需求了。这时,对于与新需求相关的原有代码,可以在理解基础上进行梳理与局部的重构,以实现新功能(注意这时重构是为了实现新功能,而非重写原有代码以实现相同功能)。

这一步主要需要解决的问题是:

  1. 原代码中有较多晦涩的 if-else 控制流逻辑,包含对某些状态的组合判断,这对新加入业务代码会有一定的障碍。

  2. 在 JSX 中大量【嵌套的三目表达式】长度很长且不易读(这实际上是 JSX 相对模板天生的问题),这也造成了一定的困扰。

由于业务逻辑的复用价值较低,这里较难通过代码的形式给出【最佳实践】的代码,但通用的处理模式可总结如下:

  1. 通过一些简单的 log 来判断一个事件触发流程中,基本的代码调用和执行顺序。

  2. 对执行过程中遇到的组件状态,在 React 开发工具中确认 state / props 执行前后的变化,确定【某段业务逻辑所依赖的组件状态,及其触发前后的组件状态】

  3. 以【编写输入新需求下输入状态,输出新需求下输出状态】为目标,维护并编写新业务逻辑代码。

  4. 新逻辑完成后,逐步注释并最终替换掉老代码,渐进地实现业务需求。

在这一步达到较高的完善程度后,可以重新审视新增的代码段做局部重构,或提取一些可复用的逻辑到上一步中的相应模块中。到这一步为止,即可基本上将老项目像个人起手的项目一样做到较为轻车熟路的开发维护了。

Webpack 优化

在业务需求按时完成的前提下,才有必要进行这一步的优化。对一个配置文件多达数百行的稳定期项目,切换当时的 Webpack 1 到 Webpack 2 难度较大,但相应的意义却并不大。因此,在构建方向上的优化策略最后以这几条为主:

  1. 分析多页面的公共依赖配置,优化公共依赖提取,去除冗余依赖。

  2. 修复已知问题。

  3. 优化构建速度。

首先,在优化公共依赖方面,难点并不是【如何更改公共依赖】,而是如何获知【有哪些依赖需要被提取为公共依赖】。在这方面,需要的是一个查看各 Bundle 内容及尺寸的可视化工具,可以使用 webpack-bundle-analyzer 这一 Webpack 插件来实现。使用该插件的方式也很简单,直接将其添加在 Webpack 的 plugins 配置中,重新执行打包命令即可。打包成功后,会弹出浏览器窗口展示各 Bundle 的公共依赖,如下图是优化前的公共依赖配置:

可以发现原始的依赖配置中,位于图中角落的 common 包仅包括了原始的 React,而组件库、lodash、moment 等依赖在每个页面包中都重复出现了。因此,在 Webpack 的 entry 配置字段中,为 common 包添加 ['babel-polyfill','lodash','moment'] 等依赖名后,即可实现公共依赖的提取。

实际上,提取公共依赖并不能减少每个页面最终的打包输出体积。只有去除冗余依赖,才能直接影响页面最终的包大小。那么这样的冗余依赖是否存在呢?答案是肯定的。在排查过程中发现,导入 moment 这一非常常用的时间库时,会默认导入其对应的多语言依赖 locale 包,而这对当前项目是完全无用的。对于这种【依赖本身依赖了冗余依赖】的情形,Webpack 同样提供了优化方案。在 Plugins 中添加如下的一行即可:

new webpack.IgnorePlugin(/^\.\/locale$/,/moment$/)

这一行代码能够直接减少开发环境 300K 的包大小!在进行了依赖优化后,得到的包体积可视化为下图:

可以发现,common 的大小得到了大幅增加,而各个页面的业务包体积则减少了 2/3 以上。不过,在这个优化方向上并没有做到极致。由于 Webpack 1 不支持原生的 Tree Shaking 功能,导致了 UI 组件库即便通过 import { xxx } 语法引入,最终还是会将整个组件库导入公共依赖包中,没有做到按需加载。而相应的 import 插件又存在配置上的不便,其结果是最终没有在这个项目中实现 UI 组件库的按需加载。当然,随着 Webpack 2 的普及,新项目中这应当不会成为问题。

接下来,在修复已知问题方面,优化过程中修复了两个较为常见的问题:common 包随业务包变更而变更的问题;hash 值每次全量变更的问题。

在直接通过 CommonsChunkPlugin 拆分 common 包的配置方式下,每个页面最终使用的包都是 common 包和业务包两个。这时,在页面 A 中修改业务逻辑,会造成 common 包的细微变动,导致新的打包文件中,common 包虽然没有源码变更,却随着业务包的变更而变更了。这会导致每次版本更新时包括 common 在内的所有包都会被全量更新,没有实现按需的更新。

解决方案是,在 CommonsChunkPlugin 的配置中,将 name 字段改为 names 字段,提供 ['common','manifest'] 两个公共依赖入口。这样,在业务包变动时,只有 manifest 会随之变动,而 common 的内容不会受到影响,这也就实现了真正意义上的按需更新,更大限度地利用浏览器缓存。虽然这一实践实际上是 Webpack 2 文档中官方的推荐做法,但 Webpack 1 也完全支持。

另一个问题是,每次打包的产物文件中虽然都附带了一个 hash 值,但对所有打包文件,该值都是一样的。这同样会导致仅有某个 bundle 变更时,全量的生产包名称变更,造成缓存的失效。相应的解决方案也很简单:将 output 配置字段中的 [hash] 改为 [chunkhash],即可为每个包添加不同的 hash 值。

最后,在提升面向开发者的打包体验方面,本次优化中主要实现的是 lint 与 Webpack 的解耦。在使用 IDE 开发时,lint 的引入较为繁琐,因此当时采用的是将 lint 作为 Webpack 的 loader 形式引入,在每次增量打包后执行 lint,对存在不符合风格指南的代码在终端报错并不予编译通过的策略。这个模式兼容性绕过了编辑器和 IDE 的配置,因而更加通用,但问题在于:

  1. 每次打包都需要重复的 lint 过程,降低了打包速度。

  2. lint 规则较严格时,调试过程受到了较大的限制。如 class 方法必须存在对 this 的引用、函数参数必须全部被使用、不允许 return 后存在业务逻辑等 lint 策略,它们虽然确实能提高代码质量,但在调试过程中局部存在这样的代码非常常见,禁止编译这些不存在语法问题的代码,对开发效率存在较大的影响。

因而,在优化中果断去除了 Webpack 的 lint 配置,转而通过 VSCode 等编辑器的 lint 插件实现开发过程中的动态 lint 提示和自动美化。另外,对 Webpack 每次打包的输出格式也进行了优化,去除了较多冗余的包信息 log 内容,仅保留每次打包的 hash 信息即可。最后的开发体验与新 Webpack 2 项目相近,实现了一定的开发效率提升。

总结

在维护过程中,首先还是理解已有业务代码,然后循序渐进地走改良路线,而不应以【老代码好乱】为理由贸然重写,这会存在很大的风险。虽然 React 本身设计较为松散,使得开发者更容易产出较无序的代码,但 JS 目前的模块和 OO 机制为无需重写的填坑提供了很大的帮助,实践中最后本质上重写的也只有新需求相关的部分,已有的逻辑得到了尽可能的保留和复用。而性能优化则属于锦上添花的【折腾向】内容,优先级较低,可以在时间相对宽松的时候处理,优化方式上也有较多的工具和插件支持,相对需要实际编码的业务而言,难度较低。

希望以上实践经验对于更多开发者的踩坑 / 填坑路能够有所帮助。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom