逐步改写成React--开头

362 阅读9分钟

逐步改写成React--开头

特别是在我的案例中,从AngularJS。

照片:Milivoj KuharonUnsplash

最近,我被赋予了一项可怕的工作,那就是领导一个已经有7年历史的AngularJS应用程序逐步重写到React。我所说的渐进是指我们把应用程序分成几个部分。每个部分都被重写,测试,并在进入下一个部分之前进行部署,创建一个混合的React-AngularJs应用程序。
所有这些,同时仍有新的功能在并行开发。

我相信重写往往是正确的选择--这里有很多话要说,但重要的部分是,你要尝试把你的系统作为一个整体来重新想象。

当然,我们更愿意做一个完整的重写,但它是一次性部署的,很难正确测试,而且它使现有的应用程序处于功能冻结状态,直到全部完成--这往往比预测的时间要长很多。

完全重写可能发生的最糟糕的事情是我们从来没有部署过它,但是逐步重写可能发生的最糟糕的事情是我们从来没有完成它,因为它更复杂,时间更长,而且新功能不断涌现。

为什么渐进式重写会这么难?

  • 我们需要创建一个混合应用程序--2个应用程序--新的和旧的,并肩生活,同时看起来像一个连贯的应用程序。
  • 我们必须与以前的一些决定共存,即使它们使我们的生活变得复杂。想象一下一个锤子;你首先更换金属头,然后更换手柄。你的 "新 "锤子会有很多继承自旧锤子的功能,比如说尺寸。
  • 我们可能会被迫将一些新功能实现两次。一次用于旧代码,一次用于新代码。
  • 这将需要大量的回归测试

还有很多。

这是一场马拉松。你将有足够的力量在最后进行短暂的冲刺。

为了避免在最后被一堆事情困住,我建议在问题出现时就解决它们,不要把它们推迟到以后。
此外,我们应该以一种方式来计划我们的项目,使我们在进行时处理所有的应用层:与服务器的通信,状态管理,UI组件,CSS,测试,路由等。

本着这种精神,我们将从一开始就维护一个纯反应的应用程序。

维护纯React的应用程序

起初,这个应用程序看起来很奇怪:显示简单的组件,没有太多的背景。但它会成长并最终成熟为我们唯一的应用程序。

维护一个纯React的应用程序将对我们的开发过程产生积极的副作用。它将迫使我们。

  • 学习直接与后台互动(获取/更新数据)。
  • 编写所有需要的CSS。
  • 最终,处理路由和结构组件,如模版。

它将使我们一次完成一个垂直方向的工作:逐一替换(有时重复)功能。

想象一下,当你还住在房子里的时候,要进行房屋装修。你可能更愿意一个房间一个房间地进行,尽可能少地打断你的日常生活。并在一个房间里测试你的新想法,而不是在粉刷整个房子后发现颜色是错误的。

我们将更换每个房间的所有东西,从电力到墙上的图片。

混合应用。Angular in React或React in Angular

在我们开始写东西之前,我们必须决定是在React应用中使用Angular组件还是反过来。

我一开始认为在React应用中使用Angular组件听起来比较好:我们在一开始会有很多艰苦的工作:结构组件、路由器、页面等等,但到最后只剩下一些无关紧要的组件需要重写。
然后我意识到这在开始时可能太难了:
假设我们在React中有一些组件,而它的所有子女都在Angular中。我们的React组件将需要把所有必要的道具传递给它的Angular子代,这将迫使我们要么。

  • 遵守多年前Angular应用中的决定,我们可能不想这么做。否则,我们就不会在第一时间重写。
  • 重构Angular组件以更好地适应我们的计划,这是浪费时间,因为我们想删除所有的Angular代码。

现在,让我们仔细看看另一种选择,Angular应用程序中的React组件。
它更容易管理;如果我们想,我们可以替换任何UI元素。
有一个缺点;替换或加强路由器和结构组件(如侧边栏)将不得不等待。因为我们更希望避免在Angular组件内有一个React组件,而Angular组件又在React组件内,所以我们不能在一个组件的子组件被重写之前接近它。

我认为第二种方法的弊端不大。

在Angular应用中的React组件就是如此。

我们如何创建我们的混合应用程序

如何将React代码加载到我们的Angular页面。

从我们所做的最小的研究来看,最简单的方法是将我们的React应用捆绑到一个npm库中。

这样,Angular只需要安装npm包,将js+CSS文件添加到它的index.html中,然后用节点调用一些init函数,将React的主要组件渲染进去。

作为一个甜蜜的奖励,你可以用`npm link.`在本地工作。

我们用Create React App启动我们的应用,并使用其默认的Webpack构建仅有React的应用;在此基础上,我们为npm库(安装在Angular应用中)增加了一个滚动构建。

前者的入口点是`index.tsx`,而后者则是不同的东西;例如,lib.ts

如何将React组件嵌入到Angular树中。

这里我们发现了两个可能的方向。

  1. 使用一个叫做 react2angular 的库。
    这个库为每个组件创建一个React根,它是Angular组件的直接子节点。它还可以让你把道具从父Angular组件传递给React子组件。
    缺点是,由于有多个React根,我们无法使用React上下文。

  2. 在portals中渲染React组件,将它们放在Angular树中的正确位置。
    缺点是我们需要自己实现桥梁来传递道具。这里有一篇关于这个问题的文章--Tonkean博客中的文章

我们不喜欢第二个选项。它需要写更多的代码,其中很大一部分是Angular的代码。所以我们找到了一个解决上下文问题的方法。

如何在多个React根上使用React上下文

我试着用一个例子来解释:我们想在应用程序的多个不相关的地方显示一个简单的计数器。所以在纯React的应用中,我们用一个有两个字段的`CounterContext`来实现它。`counter`和`setCounter`由`CounterProvider`提供。`同时,我们有组件A、B和C使用这个上下文。

现在,在混合应用程序中,组件A、B和C在不同的反应根中,所以它们不可能是同一个CounterProvider的子节点,如果我们使用三个不同的CounterProvider,每个组件中的计数器将是不同的。解决办法是在某个地方有一个真正的CoutnerProvider,以及三个与真正的CoutnerProvider 同步的特殊类型的CounterProvider

唉,我们需要一个地方--存储--来保存状态,并在变化时通知我们的种类--计数器提供者。React Query、Redux和类似的东西都可以,或者这里有一个简单的存储实现。

medium.com/media/eb61c…

好了,我们有一个同步机制。现在我们需要一个地方来保存真正的 "计数器提供者",并写入商店。为了实现这一点,我们将添加一个单一的App组件,没有任何UI,用一个特殊的ListenerComponent渲染CounterProvider,它将使用CounterContext并在变化时写入商店。我们可以直接从CounterProvider写入商店,但我们不希望在我们的纯反应式应用中出现只有混合式应用需要的任何代码。

总而言之,我们需要一个App组件和两个特殊的组件:一种我们称之为LinkProvider的CounterProvider 和一个ListenerComponent。
我们将称真正的提供者,如CounterProvider,为应用提供者,而真正的组件,如A、B、C,为应用组件。

这里有一张图和一些代码

在根组件之间同步上下文数据

medium.com/media/c6e10…

如何在两个应用程序之间传递信息

技术部分很简单,不要害羞,只要把API放在窗口上就可以了。

上述API的设计并不那么容易,主要取决于你的产品。

主要的问题是,我们的架构中有两个应用程序并存,每个应用程序都与后台独立通信。但是为了让它感觉像一个单一的连贯的产品,我们需要一种方法让一个应用程序知道另一个应用程序的数据已经改变。

我将尝试在这里给出一些提示。

  • React应用程序应该为angular应用程序提供一个`init`函数,它将接收一些初始的东西,如登录信息和App组件的dom节点。
  • 尝试将与这些API相关的代码写在一个单独的模块中。为了帮助解决这个问题,你需要一种方法来反应全局状态的变化。我使用Redux进行状态管理,所以我可以依靠`store.subscribe`来知道我应该通知Angular。
  • 如果可能的话,避免在应用程序之间传递信息。使用后端数据库作为真相的来源。
  • 如果你不能避免传递信息,就模仿与后台的通信。如果你的Angular应用不时地从后端轮询数据,那么React应用应该提供一个API,以与后端相同的方式返回数据。

最后一个建议

如果你没有自动测试(e2e测试)--现在是开始的时候了。
逐步重写需要大量的回归测试。

它并不像你开始之前看起来那么可怕 :)

希望这篇文章对你有所帮助,感谢你的阅读。


逐步重写到React--开头》最初发表于Dev Geniuson Medium,人们在这里通过强调和回应这个故事来继续对话。