React 17最近发布了,这次更新没有引入太多新的API,更多的是底层改进和渐进式升级的支持。到底值不值得升级?升级过程中会遇到哪些问题?我在几个项目中做了升级实践,把经验和踩坑记录整理出来。
React 17是一个比较特殊的版本。和之前的大版本更新不同,React 17没有引入什么令人兴奋的新API,也没有像React 16那样带来Fiber架构这种颠覆性的改变。React团队把这个版本称为"阶梯版本",它的主要目的是为后续的大版本更新打下基础,让升级变得更加平滑。
我们团队在React 17发布之后,陆续把几个项目从React 16升级到了React 17。升级过程整体比较顺利,但也遇到了一些问题。今天把React 17的主要变化、升级经验和踩坑记录分享出来,帮大家判断要不要升级、怎么升级。
一、React 17的主要变化
虽然React 17没有引入太多新API,但还是有一些值得关注的变化。
第一个变化是事件系统的改进。React 17对事件委托机制做了重大调整。在React 17之前,React会把所有事件都委托到document上,也就是说,不管你在哪个元素上绑定了事件,最终都是在document上统一处理的。这种方式在单页应用中没问题,但如果页面上有多个React应用,或者有其他库也在处理事件,就可能出现冲突。
React 17把事件委托的容器从document改成了React应用的根容器(也就是你调用ReactDOM.render的那个DOM节点)。这样,每个React应用的事件都在自己的根容器内处理,不会和其他应用或库冲突。这个改进对于微前端架构(多个React应用共存于一个页面)来说非常重要,解决了很多事件冲突的问题。
但这个改变也可能导致一些问题。如果你的代码依赖了事件冒泡到document的行为(比如在document上监听事件来处理一些全局逻辑),升级到React 17之后可能会出问题。因为React事件不再冒泡到document了,你在document上监听不到React的事件了。这时候需要把事件监听从document改到React根容器上。
第二个变化是JSX转换的改进。在React 17之前,JSX代码会被编译成React.createElement的调用,所以你必须在文件顶部import React,否则就会报错。React 17配合新的JSX转换(@babel/plugin-transform-react-jsx的新版本),可以直接编译成jsx函数的调用,不需要再import React了。
这个改进看起来不大,但实际体验提升很明显。你不再需要在每个组件文件顶部写import React from 'react'了,代码更简洁。而且,这也为未来的优化打下了基础,因为不再依赖React全局对象,可以做更好的tree shaking和代码分割。
要使用新的JSX转换,需要升级Babel到7.9.0以上,并且配置@babel/preset-react的runtime为automatic。如果用的是TypeScript,需要升级到4.1以上,并且在tsconfig里设置jsx为react-jsx。
第三个变化是渐进式升级的支持。React 17支持在一个页面上同时运行多个版本的React。这个功能对于大型项目来说非常重要,因为大型项目的代码量很大,一次性升级所有代码风险很高。有了多版本共存的支持,你可以先把项目的一部分升级到React 17,其他部分继续用React 16,然后逐步迁移,最终全部升级到React 17。
多版本共存的实现方式是:用React 17的ReactDOM.render创建的应用,事件在自己的根容器内处理,不会和React 16的应用冲突。而且,React 17的组件可以嵌入到React 16的应用中,反过来也可以。这样就可以实现渐进式的迁移,不需要一次性全部升级。
第四个变化是一些小的改进和bug修复。比如:
- 效果清理(useEffect的cleanup函数)的执行时机调整为异步,和React 16的同步执行不同,更符合用户的直觉。
- 错误边界(Error Boundary)的处理更加一致,不会因为某些错误而导致整个应用崩溃。
- 原生组件的事件处理更加规范,比如onScroll不再冒泡,onFocus和onBlur使用原生的focusin/focusout事件。
- 移除了一些已经废弃的API,比如旧的上下文API(React.createContext之前的方式)、旧的ref字符串方式等。
二、升级实战经验
了解了React 17的变化之后,说说我们在实际升级过程中的经验。
第一步是评估升级风险。在升级之前,我们先评估了项目中可能受影响的地方:
- 检查是否有代码依赖了事件冒泡到document的行为
- 检查是否使用了已经废弃的API
- 检查第三方依赖是否支持React 17
- 检查测试用例是否能覆盖主要功能
评估之后,我们发现大部分项目的风险都比较低,主要的问题是一些第三方组件库还没有正式支持React 17,以及少量代码依赖了旧的事件行为。
第二步是升级依赖。把react和react-dom升级到17.x版本,同时升级相关的工具链:
- Babel升级到7.9以上,配置新的JSX转换
- TypeScript升级到4.1以上(如果用了TypeScript)
- ESLint的React插件升级到最新版本
- 测试工具(Jest、React Testing Library等)升级到支持React 17的版本
升级依赖的时候要注意,一些第三方组件库可能还没有正式支持React 17,这时候可以先在package.json里用overrides或者resolutions强制指定React版本,然后测试一下是否能正常工作。大部分兼容React 16的组件库在React 17下都能正常工作,因为React 17没有引入破坏性的API变化。
第三步是处理事件系统的变化。这是升级过程中最需要注意的地方。我们检查了所有代码,发现有几个地方在document上监听了React的事件,比如全局的点击事件处理(点击空白处关闭弹窗)。升级到React 17之后,这些监听器失效了,因为React事件不再冒泡到document了。
解决方案是把这些全局事件监听从document改到React根容器上。比如,在应用的根组件里,用useEffect给根容器添加事件监听,而不是给document添加。或者,用React的方式来处理这些全局逻辑,比如用一个全局的Context或者状态管理来控制弹窗的显示隐藏,而不是依赖DOM事件。
第四步是处理废弃API。React 17移除了一些已经废弃很久的API,大部分项目应该早就不用了,但如果你的项目比较老,可能还会用到。我们检查了一下,发现有一个老项目还在用字符串ref(ref="myRef"),这种方式在React 17中已经被移除了,需要改成React.createRef或者useRef的方式。
另外,旧的上下文API(getChildContext和contextTypes)也被移除了,如果还在用的话,需要改成新的Context API(React.createContext)。这些改动都比较机械,花点时间逐个替换就行。
第五步是测试。升级完代码之后,要做充分的测试。我们的做法是:
- 先跑所有的单元测试和集成测试,确保基本功能正常
- 然后做手动的回归测试,重点测试事件相关的功能(点击、表单、弹窗等)
- 最后在测试环境运行一段时间,观察有没有异常
测试过程中,我们发现了几个问题:
- 一个弹窗组件的点击外部关闭功能失效了,原因就是上面说的事件委托变化,修复之后正常了
- 一个第三方的日期选择器组件在React 17下有样式问题,升级了组件库版本之后解决了
- 一些测试用例因为事件行为的变化失败了,调整了测试用例的写法
第六步是灰度发布。测试通过之后,我们没有立刻全量发布,而是先做了灰度发布,让一小部分用户使用React 17的版本,观察有没有异常。灰度运行了一周,没有发现严重问题,才全量发布。
三、踩坑记录
升级过程中踩了一些坑,这里记录一下,帮大家避免。
第一个坑是第三方组件库的兼容性。虽然React 17没有引入破坏性的API变化,但还是有一些第三方组件库因为依赖了React的内部实现或者旧的事件行为,在React 17下出了问题。我们遇到的有:
- 一个老版本的antd,在React 17下有一些样式和交互问题,升级到最新版本之后解决了
- 一个自定义的拖拽组件,依赖了旧的事件冒泡行为,在React 17下拖拽失效了,修改了事件处理方式之后解决了
- 一个测试工具库(enzyme)对React 17的支持不够完善,有些测试用例跑不通,后来改成了React Testing Library,问题就解决了
建议在升级之前,先检查项目中所有的第三方依赖,看看它们是否支持React 17。如果有不支持的,要么升级依赖,要么找替代方案。
第二个坑是新JSX转换的配置问题。我们在配置新的JSX转换的时候,遇到了一些问题:
- Babel的版本不够,需要升级到7.9以上
- @babel/preset-react的配置不对,需要设置runtime: 'automatic'
- TypeScript的jsx配置不对,需要设置为'react-jsx'而不是'react'
- ESLint报React未使用的错误,需要关闭react/react-in-jsx-scope规则
这些配置问题都不大,但如果不注意的话,会导致编译失败或者运行时报错。建议按照官方文档一步步配置,不要漏了任何一步。
第三个坑是useEffect清理函数的执行时机变化。React 17调整了useEffect清理函数的执行时机,从同步执行改成了异步执行(在浏览器绘制之后执行)。这个变化对于大部分代码来说没有影响,但如果你的清理函数依赖了同步执行的时机(比如在清理函数里同步修改DOM,期望立刻生效),就可能出问题。
我们遇到的一个例子是:一个组件在useEffect的清理函数里移除了一个全局的事件监听器,在React 16下是同步执行的,组件卸载后立刻就移除了。但在React 17下是异步执行的,组件卸载后还要等一会儿才移除,这期间如果触发了事件,就会调用已经卸载的组件的方法,导致警告。解决方案是把事件监听的添加和移除都放在useEffect里,并且正确处理依赖项,确保React能正确管理生命周期。
四、值不值得升级
最后,回答一个大家最关心的问题:React 17值不值得升级?
我的答案是:值得升级,但不需要着急。
值得升级的原因:
- React 17是未来版本的基础,后续的React 18等大版本都会基于React 17的架构,早升级早受益
- 事件系统的改进解决了很多微前端和多应用共存的问题,对于复杂项目来说很有价值
- 新的JSX转换让代码更简洁,也为未来的优化打下基础
- 渐进式升级的支持让大型项目的升级风险大大降低
- 升级过程整体比较平滑,大部分项目不需要做太多改动
不需要着急的原因:
- React 17没有引入什么必须用的新API,React 16完全能满足大部分项目的需求
- 升级需要花时间测试和修复问题,如果项目比较稳定,没有迫切的需求,可以等一等
- 一些第三方库可能还没有完全支持React 17,升级可能会遇到兼容性问题
- React 16还有长期支持,不会很快被淘汰
我的建议是:如果你的项目比较新,第三方依赖都比较新,而且有时间做测试,那就尽早升级。如果你的项目比较老,有很多历史包袱,或者第三方依赖不支持React 17,那就可以等一等,先做好升级的准备工作(比如替换废弃API、升级依赖版本),等时机成熟了再升级。
不管升不升级,了解React 17的变化都是有好处的,因为这些变化代表了React未来的发展方向。
五、写在最后
React 17是一个"不起眼"但很重要的版本。它没有带来什么炫酷的新特性,但它在底层做了很多改进,为React的未来发展打下了坚实的基础。作为开发者,我们需要理解这些变化,并且根据自己的项目情况做出合理的升级决策。
升级框架是一件需要谨慎对待的事情,不能盲目跟风,也不能固步自封。要做好充分的评估和测试,确保升级不会影响业务的稳定性。同时,也要关注框架的发展趋势,提前做好准备,这样在需要升级的时候才能从容应对。
希望我的这些升级经验和踩坑记录能对大家有帮助。如果有什么问题或者不同的看法,欢迎在评论区交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录