本文是对原文个人翻译
这是一个demo演示如何在一个app里面使用两个不同版本的React. 这不是最佳实践, 仅仅适用于不让你的app一直卡在旧React版本上的情况
了解更多渐进式升级.
贡献
这个demo是用 Create React App 创建的,同时它也适用于其他构建方案
其他构建方案例子
如果你用其他方案重新实现了这个demo,请发PR到下面链接
由社区维护
为什么不这么做
注意 这种方案仅仅是一个逃生手段,而不是常规方案.
通常,我们鼓励你在整个app里面只使用单一React版本. 当你需要升级React,最好是全部一起升级. 我们努力保证不同版本间差异最小, 通常会有自动脚本(“codemods”)帮助你迁移. 你总能在我们的博客发现任何版本的迁移信息.
使用单一React版本减少了很多复杂性. 这对用户体验十分重要因为用户不需要下载2次代码(2个React版本). 如果可以,永远只使用单一React版本
为什么要这么做
然后, 有些app已经发布数年, 全部一次性升级太过艰难. 例如, 2014年写的React组件可能仍然依赖 非官方遗留上下文API (不要与当现在的那个混淆),已经不再维护。
通常来说, 那就意味着如果一个遗留API被废弃, 你将永远困在旧React版本. 这阻止了你整个app接受新功能和bug修复. 这个仓库使用了混合方案. 它演示如何在app里面部分功能使用新版本React, 同时对于没有迁移的部分使用懒加载旧版本React
这种方法原本就更加复杂,只有当你无法全部升级时,作为一种最后手段
版本要求
这个demo使用2个不同版本React: “现代” 组件使用React 17 ( src/modern), “遗留”组件使用React 16.8 ( src/legacy).
我们仍然推荐一次性将你的整个app升级到React 17. React 17发布版本特意保证了微小差异,所以它很容易升级. 事实上, React 17 解决了一些之前React版本没有处理好的事件传播问题. 我们期望当一些长期废弃API被删除的时候,这个demo在将来升级React 17到更高版本的时候不会像现在这么有用
然而,如果你还困在旧React版本 ,你可能发现这个方案现在可用. 如果你在 src/shared/Clock.js 移除Hook调用,你可以将遗留React版本降级到 React 16.3. 如果你接着从src/legacy/createLegacyRoot.js 移除 上下文API , 你可以降级到更低的遗留React版本, 但是需要注意的是这个demo里面的第三方库 (React Router and React Redux) 可能需要修改或移除
安装
为了运行项目, 克隆当前工程, 打开当前工程并执行:
npm install
npm start
如果你想测试发布构建, 执行下面命令:
npm install
npm run build
npx serve -s build
这个演示app使用客户端路由导航,一共有2个路由:
/渲染一个页面使用新的React版本. (在发布构建里,你可以验证,当该路由被渲染时,只有一个版本的React被加载)/about渲染一个页面部分由React旧版本组成. (在发布构建里, 你可以验证2个版本的React chunks都加载了.)
这个demo主要目的如下:
- 如何用npm安装2个不同React版本.
- 如何避免 “无效Hook调用” 错误
- 如何在不同React版本之前传递上下文.
- 如何只在使用时才懒加载第二个React bundle.
- 如何做这些而不用一个特殊的bundler 管理.
它是如何工作
文件结构在demo里极其重要. 它直接影响到哪个React版本代码被使用. 这个demo使用Create React App,没有将配置弹出修改, 所以 它没有依赖任何bundler插件或配置. 这个demo的原则是方便被移植安装
依赖
我们使用3个不同的 package.json: 一个在根节点与React代码无关, 两个在 src/legacy and src/modern 文件夹指定了对应的React依赖
package.json: 根节点package.json负责构建依赖 (比如react-scripts) 和React无关库 (例如,lodash,immer, orredux). 这里不包含任何React相关代码.src/legacy/package.json: 这里,我们声明reactandreact-dom依赖使用”遗留”版本. 这个demo里, 我们使用 React 16.8 (尽管, 正如上面所提, 我们可以降级到更低版本). 这里 同样 指定了任何依赖React的第三方库. 例如 , 我们包含react-router和react-redux.src/modern/package.json: 这里,我们声明reactandreact-dom依赖使用”现代”版本.这个demo里, 我们使用 React 17. 这里,我们同样指定了使用React依赖的第三方库, 这些第三方库被我们app里面的现代组件所使用. 这就是为什么我们 同样 包含react-router和react-redux. (它们的版本不必和遗留代码里面的版本一致, 但是如果功能依赖上下文,版本可能会有不同.)
当你运行根节点的 package.json 里面的 npm install 命令, 它会调用 对应 src/legacy 和 src/modern 目录里面的npm intall.
注意 这个demo使用了一些第三方库 (React Router and Redux). 这些库不是必须的, 你可以移除它们. 我们用它们只是为了演示在这种方案中,它们是如何运行的。
目录
例子里面有一些关键目录
src: 源码文件根目录. 在这个目录里 (或子目录,除了下面提到的特定目录), 你可以将任何React无关的源码放这里. 例如src/index.js是这个app的入口,src/store.js暴露了一个Redux store,我们将它们都放在这里. 这些常规模块仅执行一次, 不会 在多个bundles之间进行复制.src/legacy: 遗留React源码存放位置 . 包括了 React 组件 和 Hooks, 通用生产源码 只 依赖旧版本.src/modern: 现代React源码存放位置. 包括了 React 组件 和 Hooks, 通用生产源码 只 依赖新版本.src/shared: 你可能有些组件或者 Hooks 希望被新旧两个版本都使用. 项目创建了一个构建脚本,使得 在**src/shared一切都会被一个文件监听器拷贝** 到src/legacy/shared和src/modern/shared目录,同时监听任何变动,然后进行拷贝. 这就让你可以只写一次组件或者Hook,在2个地方复用。
懒加载
在一个页面加载2个版本React,用户体验非常差, 所以在你app的关键路径上要努力避免这种情况. 例如, 如果有个弹框很少用, 或者一个路由很少访问, 最好让它待在旧React版本而不是成为你首页一部分。
为了鼓励按需加载旧React, 这个demo创建了一个helper类似 React.lazy. 例如, src/modern/AboutPage.js,大致如下:
import lazyLegacyRoot from './lazyLegacyRoot';
// 懒加载一个遗留React组件.
const Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));
function AboutPage() {
return (
<>
<h3>This component is rendered by React ({React.version}).</h3>
<Greeting />
</>
);
}
因此, 只有当 AboutPage (同样的, <Greeting />) 被渲染, 我们将载入 遗留React 和遗留 Greeting 组件.和 React.lazy()一样, 我们 <Suspense> 包裹这个组件,并设置了一个加载状态
<Suspense fallback={<Spinner />}>
<AboutPage />
</Suspense>
如果这个遗留组件只在特定情况下渲染, 我们只会在它显示的时候加载第二个React版本。
<>
<button onClick={() => setShowGreeting(true)}>
Say hi
</button>
{showGreeting && (
<Suspense fallback={<Spinner />}>
<Greeting />
</Suspense>
)}
</>
你可以根据自己的需要修改 src/modern/lazyLegacyRoot.js . 如果你修改了这个文件,记得在生产环境下测试下懒加载功能,这个bundler在开发环境下可能没被优化过。
上下文
如果你的源码节点树有多个React版本组成, 里面的节点树看不见外面节点树上下文
这导致第三方库如React Redux 或 React Router失效, 自己创建的上下文也无效 (例如,主题).
为了解决这个问题,我们读取我们所需的外部节点树上下文 , 传给内部节点树, 将内部节点树用Providers包裹起来.你可以在下面两个文件看到对应行为:
src/modern/lazyLegacyRoot.js: 查找useContext调用, 所有的结果被合并成一个对象然后传递。如果你的app需要, 你可以获取更多的上下文src/legacy/createLegacyRoot.js: 查找Bridge组件,它接受了结果对象 然后用上下文 Providers将子组件包裹了起来. 如果你的app需要,你可以用更多Providers进行包裹.
注意, 通常来说, 这种方案有点脆弱, 因为 有些库不会正式暴露他们的上下文,或者认为其上下文应该是私有. 你可以暴露私有上下文借助patch-package, 但请记住保持版本固定,因为即使第三方库只发布了一个补丁,也有可能改变这种情况。
内置方向
在demo里, 我们用新React版本做主体,旧React版本被托管. 然而, 我们也可以重命名目录,调换两者的位置。
事件传播
React 17之前版本, 内部React节点树的event.stopPropagation()事件会传播到外部React节点树.这会导致异常行为 比如当一个弹框使用了单独的React版本. 这是因为React17之前, 所有的React都将事件绑定到了 document 对象. React 17 解决了这个bug,将事件处理函数绑定到根节点(外部容器节点而不是document对象). 所以我们强烈推荐升级到React17.
注意点
这些设置不是常规方案, 所以有一些注意点.
- 不要在
src/shared目录里面创建package.json. 例如, 如果你想在src/shared添加一个React组件, 你应该把它同时加入src/modern/package.jsonandsrc/legacy/package.json目录. 你可以使用不同版本的组件,但是确保你的源代码兼容这个组件的所有版本,这个组件也兼容不同的React版本。 - 不要在
src/modern,src/legacy, 或src/shared之外使用React. 不要在src/modern/package.json或src/legacy/package.json目录之外添加React相关的第三方组件. - 记住,
src/shared目录是你写共享组件的地方, 但是你写的文件会自动拷贝到src/modern/shared和src/legacy/shared目录, 这才是你引入组件的地方. 这两个shared目录在.gitignore加了配置. 直接从src/shared引入无法工作。 因为针对上面情况 ,无法确定使用哪个react版本。 - 注意 在
src/shared的任何代码都会复制到“遗留”和“”现代“”两个React bundles. 不想被拷贝的代码应该写在src其他地方 (但你不能使用React,因为版本是模糊的). - 你想将
src/*/node_modules移除你的lint配置, 可以参考demo的.eslintignorerc文件.
这种设置非常复杂,对于大部分app来说,不推荐. 然而,对于app来说,有这样一个选项是十分重要的,否则会一直留在老版本. 如果有工具链,这种方法可能简单一点, 但是这个例子主要是展示底层机制,其他工具可能会利用这种机制。
许可
这个例子遵循MIT licensed.