分享

AngularJS迁移之战

 黄爸爸好 2020-05-18
在 React 和 Vue 还没有出现的时代,Angular 拥有着当时一众框架中最好的理念,但是在 Angular2 发布之后,开发者发现两者之前除了名字之外几乎毫无关联,这无疑会增加很多学习成本。与此同时,React 和 Vue 出现了,不仅难度低,而且还很好的继承了 Angular 中的很多理念。本文就是一名开发者将 AngularJS 迁移至 React 的案例。  

本文最初发布于 coding with J.S. 网站,经原作者授权由 InfoQ 中文站翻译并分享。

MEAN 的痛苦

最近我看到了一段来自 Mark Erikson(Redux 的主要维护者)的推文。

我刚被分配到一个项目,代码库是经典的 MEAN AngularJS 1.x。对于用惯了 React 的开发人员来说,这种代码库很吓人。“控制器”“服务”“提供者”“范围”,奇特的模板绑定语法和古怪的基于源正则表达式的依赖注入行为,都够让人望而却步的。我自己够专业,能理解其中一些架构内容背后的意图,也发现了与其他框架类似的 UI 逻辑工作机制。但这个代码库的所有概念都让我为之震惊。我习惯使用 Yarn/ESM/ 合适的 dev 和 prod 打包 /TS 来发现错误,可这个代码库用的是 Bower/IIFE/ 没有打包 / 纯 JS,非常难用。所以我做的第一件事就是用 CRA 重构了一遍:)

他提到的这个技术栈和我几年前迁移过的一种技术栈几乎一模一样。虽然对我来说这种东西已经是遥远的回忆了,但我看过一个数据,说在投入生产环境的软件工程项目的平均寿命为 7 年;这意味着接下来的很多年中,有许多代码库将被迁移或重写。我希望接下来讲的这个故事能给人带来一些启发。

初始技术栈

我最近加入的公司使用 AngularJS 已经有很长的历史了。我们使用一套内部启动套件开始了这个项目,其中包括:

  • 基础框架是 AngularJS 1.5。

  • UI 库是 Angular Material。

  • FE 包管理是 Bower。

  • 原始模块系统是 IIFE(https://developer.mozilla.org/en-US/docs/Glossary/IIFE),由 Gulp 串联。

  • 构建环境是 NodeJS 0.12。

  • EcmaScript 5 语言规范。

  • 还有很多特性本文就不谈了。

虽然当时 AngularJS 并不是最前沿的框架,这个套件也足以让我们在第二个周末就交付出接近生产水平的产品了。以往我参与的版本发布周期基本都是半年时长的,所以这次能这么快让我感到很神奇。

这套技术栈最让人头疼的是 JavaScript 语言的限制。在其他项目上使用过 ES6 及更高版本后,突然要倒退好几年是很难适应的。虽然某些特性比我们想得要好用些,但 ES5 严重影响了代码库的复杂度和可读性。这甚至会成为我们招募新人时的一大阻碍。

当时我们没有迁移计划。我们只是想在交付功能时改善这套技术栈。我们所有的改进都必须是原子性的,并且要在不破坏产品的前提下逐渐扩散开来。这不是那种大型重构项目,那种项目中重构时一切都会中止运行的。我们花时间构建了过渡性的临时解决方案,从而有效地控制了这一增量流程的风险,并保证了路线图的连续性。

采用 Redux 架构

随着应用愈加复杂,AngularJS 数据绑定的局限性也愈加明显(性能和长树遍历)。团队开始寻求替代方案,并决定采用 Redux 架构。由于 Bower 上没有这个包,并且 Redux 库背后的基本逻辑很小巧,因此我们使用几行 RxJS 临时重新实现了它。一名团队成员要求使用 mapStateToThis 函数换掉了 mapStateToProps。毕竟,AngularJS 可以使用突变。摆脱 AngularJS 数据绑定的过程中我们必须更加小心,以免错过哪个 digest cycle。

迁移到 Redux 架构的一大好处是提升代码质量。Action Creator、Reducer 和 UI 组件之间的关注点分离让我们的单元测试更加专注,更有价值。Redux 提倡的纯函数让我们更容易编写测试。用 Redux,HTML 模板和一些实用程序就能很容易做出一个组件控制器。

我记得我非常支持这种变化。我喜欢 Redux(现在还是喜欢它),那时它正在向更函数式的方向发展,更重要的是它打破了 AngularJS 的单体模式。

构建系统升级

经过一大堆分析和会议后,我们终于设法更新了 NodeJS 版本。0.12 已经完全过时了,每天都有很多库放弃对它的支持。这次迁移的一个关键点是在我们的上下文中。当时不同的工程团队使用一个共享的持续集成服务器,于是大家使用的各种构建工具都被锁定为相同版本。因此,基本上 NodeJS 版本需要照顾我们整个公司中可能用到的最古老组件。我们 Docker 化了构建流程,与 CI 服务器构建工具彻底脱钩,从而解决了这个问题。

转到 NodeJS 8 后,我们的技术栈中有了许多新工具。现在,构建系统可以使用大多数 ES6 特性。要在我们的应用程序代码中使用 ES6,我们仍然需要 Babel 将我们的代码转译到目标浏览器上。linter 之类的细节也影响了我们的迁移过程。团队没有在 Gulp 上投入更多精力,而是决定采用构建系统标准:Webpack。你可能会注意到 Mark 的推文提到了他使用 Create React App 来做这一步。那时我们没有选择这条路线。当时 CRA 刚刚结束 beta 测试,我们的一些特性也和 CRA 的默认设置冲突。如果是现在的话我很愿意尝试 CRA 这条路。

为了专注于构建系统而不是改善应用程序代码,我们没有在第一次迭代中添加 Babel。我们的 linter——JSHint 在应对 ES6 语法时遇到了麻烦,尚待解决。同时,我们放弃了 JSCSRC 格式化程序,转而使用 Prettier。这是我见过的对开发体验影响最大的代码库改进。在一次影响所有代码行的隔离提交中,我们删除了不必要的 IIFE 模块系统。

伴随 Webpack 而来的是 CommonJS 和我们的第一个不用 AngularJS 维护的依赖树。我们的模块现在导出 AngularJS 模块和组件名称,这意味着这些 token 可以导入其他模块,并在 AngularJS 依赖注入中使用。这样我们就不至于因为字符串 token 中的一个拼写错误而花好几个小时查看长长的错误日志了。

有了 Webpack,我们也用不着 Bower 了。NPM 取代了旧版的 FE 包管理器,一夜之间我们项目可用库的数量大幅增长。这意味着我们可以使用真正的 Redux 换掉我们的 RxJS 临时实现了。不久之后就出现了一个关于这个的拉取请求。团队现在也能用上 Redux Dev Tools 了。

在十月的最后一步中,我们添加了 Babel,并用 ESLint 取代了 JSHint。这样代码库就开始支持 ES6。痛苦地使用 ES5 一年后,我终于又能用自己喜欢的方式编写代码了。

移除 AngularJS 模块系统

AngularJS 模块系统是基于依赖注入原理构建的。它的声明式方法解决了经典的依赖顺序问题(请记住,我们的 Gulp 构建系统只是串联了所有文件)。现在它已经被 Webpack 取代了。

依赖注入的另一个好处是允许在不同的模块实现之间轻松切换,这对于面向对象的单元测试来说非常重要。我们一半的状态管理(Reducer 和 Selector)只使用了易于测试的纯函数,而无需 mocking 或模块交换。在这个阶段,显然 AngularJS 模块系统对这些文件来说更多是麻烦。我们已经在 AngularJS 范围之外的 CommonJS 模块中编写了纯实用程序函数。我们开始从 AngularJS 模块系统中移除所有不依赖 AngularJS 模块的文件。到 2018 年 1 月,我们所有的 Reducer 和 Selector 文件都已经是简单的 CommonJS 模块了。

在这一阶段,我们的方向是在 UI 组件中减少 AngularJS 的使用,保留灵活的选择权。我们继续将所有纯逻辑提取到实用程序文件和 Redux 状态管理中,但是一个关键因素阻止了 Redux Action Creators 和 AngularJS 服务的迁移。AngularJS 依赖 digest cycle 的概念来跟踪重渲染 UI 组件所需的条件。为了识别应导致 digest cycle 的异步事件终止信号,所有这些事件都必须由框架包装。这就是为什么在 AngularJS 中,我们必须使用 $http 而不是 fetch,使用 $q 而不是 ES Promises,使用 $timeout 而不是 setTimeout,并要在回调后手动触发 digest cycle 的原因所在。否则,框架就无法得知在这些异步事件之后何时触发 digest cycle。

由于我们将所有状态管理和数据流转到了 Redux,因此触发 digest cycle 的同步点已基本转移到了 Action Creators。在 2018 年 2 月,我们删除了所有 $,并将 Redux 存储耦合到了主应用程序控制器中的 digest cycle,如下所示:
plain
function initStore($rootScope, store) {
  // Triggers a throttled digest cycle after each store update.
  store.subscribe(
    throttle(() => $rootScope.$apply(() => {}), {
      leading: false,
      trailing: true
    })
  );
}

从现在开始,没人再需要关心 digest cycle 了。AngularJS 迅速退出了日常的开发工作。最重要的是,Redux 流程现在开始运转了!我终于开始喜欢上编写 AngularJS 代码了。现在它只剩下一个任务,并且表现良好:构建和更新展示 UI 组件。

向 AngularJS 告别

2018 年 1 月,谷歌宣布 AngularJS 进入生命周期终止阶段。从那时起,要更新我们的 UI 库就会越来越困难。我们需要找到一个替代品,最后团队决定使用 React。我们于 2018 年 5 月开始迁移。

在这个阶段,我们的 AngularJS 只用在了 UI 组件上。我们采用了与之前相同的方法开始迁移:从依赖树的叶子开始,并在不再有 AngularJS 依赖项时迁移组件。这种方法非常适合 React,因为它可以处理多个独立的组件树。由于 React 的 Material 主实现(MaterialUI)相当先进,我们可以轻松使用 Material-UI 组件替换 Angular-Material 组件来渐进迁移。

迁移没法一次完成。像之前一样,我们的旅途是一步步走完的。新组件都是用 React 编写的,每个人在交付功能时都迁移了一小部分组件。要迁移组件,开发人员必须:

  • 移除其 Angular 模块系统。

  • 将组件模板转换为 JSX;在 CommonJS 依赖项中添加必要的 Material UI 组件。

  • 将 mapStateToThis 迁移到 mapStateToProp(与 mapDispatchToProps 相同)。

  • 在父组件中使用一个小型 Angular 到 React connector。

这也意味着我们有一段时间需要同时使用两个框架及其 UI 库。由于我们的业务场景可以承受比较大的包,因此这不是问题。

这个项目的各方利益相关者都在自己的仪表板上看到了 React 迁移的进展,但产品并没有出现任何重大的外观变化。并发的 React 树节点增加到了大约 5 或 6,然后在迁移其父节点时又减少了。新加入的开发人员连一行 AngularJS 代码都不会遇到。2018 年 10 月的一天,最后一个 PR 用了 15 行代码中从项目中删除了 AngularJS。

    总结    

Mark Erikson 的推文是对 Tom Dale 以下问题的回应:

Angular 1 有哪些缺陷让人们更喜欢 React 呢?

根据我们的经验,随着我们一点点剥离 AngularJS,我们的代码库也变得越来越简单,并获得了越来越多的自由。

这段旅程花了一些时间,但每一步都是值得的。我们每周都会不断发布候选版本,用不着每次都让产品负责人紧张兮兮的。每次合并 PR 后,开发团队的体验就会有所改善。从新手到专家,所有成员都能发挥自己的作用。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多