翻译出现错误: Angular.js/Angular 混合应用分离实践总结

12 months切换至中文

翻译出现错误

Angular 官方对将 Angular.js 应用升级至 Angular 有相关的官方指导,但指导中的情况相对比较理想,Angular.js App 代码都会被重写为 Angular 代码,最终可以移除所有的 Angular.js 相关的依赖。但实际的商业项目中,往往 Angular.js 代码可能会非常多,设计模式也可能千奇百怪,迁移重写至 Angular 的成本非常高,所以很多用老的 Angular.js 应用升级到 Angular 过程中会长时间保持混合(Hybrid)模式。

目前灵雀云 Alauda EE 产品的前端项目就是一个典型的混合应用,而今年以来我们是产品重心不断往 kubernetes 迁移,很多老的模块都不再做产品升级,所以遗留的 Angular.js 代码也不会做重大调整,迁移到 Angular 意义不大,但一直保留 Angular.js 相关代码在主产品中也是一种累赘,所以我们一直想把 Angular.js 和 Angular 的代码分离开,今天就来分享一下这次的实践工作。

最初我们的想法很简单,直接将两部分拆分成两个应用,但这会导致两个应用互相跳转时会整个页面重新刷新,体验会非常差。之后又考虑了将 Angular.js 应用以 iframe 的模式嵌入到 Angular 应用中来避免应用跳转时整个页面重新刷新的情况,但这种情况从 Angular 应用每次跳转到 Angular.js 应用时 iframe 都是重新加载的(现在想来其实也可以从 Angular.js 跳出时只是将 iframe 隐藏,跳回将 iframe 回显),但是在之前迁移过程中 Angular.js 中的部分组件和服务已经由 Angular 版本降级提供,再重写这些组件和服务回 Angular.js 版本得不偿失,因此又要将 Angular.js 应用包裹在一个 Angular 应用中,似乎依然不是一个好的选项。最终我们采取了在 Angular 应用中懒加载 Angular.js 应用的方式完成了混合应用的分离工作,只有进入遗留模块的时才会加载 Angular.js 应用代码。

在真实的项目应用中,应用肯定是由路由来分离成一个个子模块的,我们首先利用 Angular 提供的惰性加载功能将应用多个子模块分离开,增加一个降级相关的模块:

{
  path: '**',
  loadChildren: './downgrade/downgrade.module#DowngradeModule',
},

即将 Angular 无法处理的所有路由都交由降级的 Angular.js 应用处理,而在 DowngradeModule 中用

RouterModule.forChild([
  {
    path: '**',
    component: DowngradeComponent,
  },
]),

将所有路由都使用 DowngradeComponent 进行渲染处理,同时在 DowngradeModule 中引入 Angular.js 应用入口代码,但不引导启动,同时将外部已经加载的一些 Angular 组件和服务通过 downgradeComponentdowngradeInjectable 降级到 Angular.js 应用中使用。而在 DowngradeComponent 中只需要设置模版为 <div ui-view></div>,然后在 ngOnInit 声明周期中进行 Angular.js 应用的引导启动:

@Component({
  template: '<div ui-view></div>',
})
export class DowngradeComponent implements OnInit {
  constructor(private upgrade: UpgradeModule, private elRef: ElementRef) {}

  ngOnInit() {
    this.upgrade.bootstrap(this.elRef.nativeElement, ['app'], {
      strictDi: true,
    })
    setUpLocationSync(this.upgrade)
  }
}

至此基本就大功告成啦,真的!

既然说是『基本』,自然还有一些优化收尾工作要做,因为还有一些边界情况需要处理:

  1. 多次从 Angular 应用跳转到 Angular.js 应用,DowngradeComponentngOnInit 声明周期每次都会执行一次,导致 Angular.js 应用会产生多个实例,继而进入同一个 Angular.js 路由时 API 可能被重复调用多次,所以我们需要在 DowngradeComponentngOnDestroy 声明周期将 Angular.js 应用销毁以待下次重新引导启动:this.upgrade.$injector.get('$rootScope').$destroy()
  2. 从 Angular.js 应用跳转到 Angular 应用时由于 Angular 的路由未在 Angular.js 应用中注册,因此无法处理外部路由,因此需要 $urlRouterProvider.otherwise 处理 Angular.js 应用本身无法处理的路由。
angular.module('app').config([
  '$locationProvider',
  '$urlRouterProvider',
  (
    $locationProvider: angular.ILocationProvider,
    $urlRouterProvider: UrlRouterProvider,
  ) => {
    let ngRoutes: string[]

    $urlRouterProvider.otherwise(($injector, $location) => {
      // 注意须先将 Angular Router 通过 `downgradeInjectable` 降级到 Angular.js 中使用
      const ngRouter = $injector.get<Router>('ngRouter')

      if (!ngRoutes) {
        ngRoutes = ngRouter.config.map(({ path }) => path)
      }

      // 我们只在乎第一级路由是否能被 Angular 处理
      const canBeHandled = ngRoutes.includes($location.path().split('/')[1])

      ngRouter.navigateByUrl(canBeHandled && $location.url(), {
        replaceUrl: !canBeHandled,
      })
    })

    $locationProvider.html5Mode(true)
  },
])

到此就真的结束啦。(要是贵司项目有特别的用法无法覆盖欢迎打脸)

对比之前想的两种方案,页面打开渲染速度提升了不少,可以说是又快又好用的方案了!当然此方案比 Angular 应用一加载时就整个 Angular.js 应用并引导启动的情况会稍微慢一点,毕竟每次进入都要重新引导启动,但相对来说成本已经很低了,而且大部分页面用不到 Angular.js 的情况下由于 Angular.js 应用被惰性加载了,页面渲染速度也会有提升。

这次分离实践演示源码:https://github.com/JounQin/ng-study/tree/master/src/app/console/downgrade