AngularJSとAngularのハイブリッドアプリ基本(AngularJS+ui-router+Angular+Angular Router+ngUpgrade+Lazy Load)

2018年10月にAngular7がリリースされましたが、ngUpgradeの使い方がよくわからなくてまだAngularJSな案件もあると思います。
今回は動くコードが欲しいのために、手っ取り早くngUpgradeを利用するための手引を紹介したいと思います。

# §0 対象読者
- AngularJSもAngularも知ってるけど、ngUpgradeの使い方がよくわからない人
- AngularJS＋Angularのハイブリッドアプリになかなか踏み込めない人

# §1 準備

AngularJSやAngular、Angular CLI等の必要なフレームワーク/ライブラリは事前にnpmでインストール済みとします。
Angular、Angluar CLIともに最新バージョンで問題ありません。
※筆者はAngular v7.0.2で確認

## §1.1 URLをHTML5モードにする

`$locationProvider.html5Mode(true);`を設定してURLをHTML5パターンにしておきましょう。

## §1.2 HTMLソース内から`ng-app`を削除
HTMLソース内の`ng-app`を削除します。
※もし`ng-strict-di`を利用しているようならそれも削除

## §1.3 AngularJSのルートコンポーネントを設定する
AngularJSでルートコンポーネントがない場合、作成してください。

```angularjs
angular.module('app')
  .component('appRoot', {
    templateUrl: 'path/to/html' // or template:  ``
    controller: function () {}
  });
```

## §1.3 Angular用のディレクトリを作成する

`src/ng2`などでよいと思います。
`angular.json`,`tsconfig.json`,`polyfills.ts`,`main.ts`などは普段お使いのプロジェクトから拝借してきましょう。

`angular.json`内のエントリーポイントを作成した`main.ts`に設定します。

```angular.jsonの例
{
...省略
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "public/ng2",
            "index": "src/index.html",
            "main": "src/ng2/main.ts",
            "tsConfig": "src/tsconfig.app.json",
            "polyfills": "src/polyfills.ts",
            "assets": [
              "src/assets",
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/bootstrap/js/tooltip.js",
              "node_modules/bootstrap/js/popover.js",
              "node_modules/bootstrap/js/dropdown.js",
              "node_modules/bootstrap/js/tab.js",
              "node_modules/bootstrap/js/modal.js"
            ]
          },
...省略
```

## §1.4 タスクランナー

### §1.4.1 タスクランナーにwebpack以外(gulp/grunt)を利用しているバターン
アプリケーションファイルは最終的に一つのファイルに生成していると思います。
その生成先を`src`内に変更します。

※私は `src/ng2/ng1/application.min.js`として配置していました。 

`main.ts`から上記ファイル（例では`src/ng2/ng1/application.min.js`)をimportします。

### §1.4.2 タスクランナーにwebpackを利用しているパターン
`main.ts`からng1のエントリーファイルを`import`する

# §2 Angular部分の設定
以降はAnguluar CLIの作法に則ったファイルだと考えてください。

## §2.1 `main.ts`ファイルの修正
`main.ts`ファイルを修正します。

```ng2/main.ts
import 'ng2/ng1/application.min'; // webpackの場合はng1のエントリーファイルを指定
import 'ng2/ng1/downgrade'; // AngularのルートコンポーネントとなるMainComponentをAngularJSに登録しておく
import 'reflect-metadata';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { setAngularLib } from '@angular/upgrade/static';
import * as angular from 'angular';
import { enableProdMode } from '@angular/core';
import { environment } from 'environments/environment';
import { AppModule } from 'ng2/app.module';

if (environment.production) {
  enableProdMode();
}

setAngularLib(angular);
platformBrowserDynamic().bootstrapModule(AppModule);
```
## §2.2 `AppModule`の修正
AppModuleを修正します。

```ng2/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, UrlHandlingStrategy, Router } from '@angular/router';
import { setUpLocationSync } from '@angular/router/upgrade';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppRoutes } from './app.routing';

import { Ng1Ng2UrlHandlingStrategy } from './routing-strategy';
import { MainComponent } from './main/main.component';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    RouterModule.forRoot(AppRoutes)
  ],
  declarations: [
    MainComponent
  ],
  providers: [
    { provide: UrlHandlingStrategy, useClass: Ng1Ng2UrlHandlingStrategy },
  ],
  entryComponents: [ MainComponent ]
})
export class AppModule {
  constructor(
    private upgrade: UpgradeModule,
    private router: Router
  ) {}

  ngDoBootstrap() {
    // 'app'はng1でbootstrapに利用していたトップモジュール名に適宜変更しましょう
    this.upgrade.bootstrap(document.body, ['app'], { strictDi: true });
    setUpLocationSync(this.upgrade);
    this.router.initialNavigation(); // これがないと初期表示がされないので注意
  }
}
```

## §2.3 Angular Routerのルーティング設定
Angular Routerのルーティング設定をします。
それぞれのModule1Moduleなどの定義はAngularの世界なため、本稿では割愛します。
普通にAngularの感覚で作成して大丈夫です。
※ redirectToはまだ実装してはいけません。

```ng2/app.routing.ts
import { Routes } from '@angular/router';

export const AppRoutes: Routes = [
{
  path: '',
  children: [
    {
      path: '',
      loadChildren: 'ng2/modules/dashboard/dashboard.module#DashboardModule'
    },
    {
      path: 'module1',
      loadChildren: 'ng2/modules/module1/module1.module#Module1Module'
    },
    {
      path: 'module2',
      loadChildren: 'ng2/modules/module2/module2.module#Module2Module'
    }
  ]
}
];
```

## §2.4 Aunglar Routerのルーティング戦略
Angular Routerのルーティング戦略を設定します。

```ng2/routing-strategy.ts
import { UrlHandlingStrategy, UrlTree } from "@angular/router";

export class Ng1Ng2UrlHandlingStrategy implements UrlHandlingStrategy {
  shouldProcessUrl(url: UrlTree) {
    const str = url.toString();
　　// 以下にAngular化が終わったモジュールのURLを追記していきます
    return str === '/' || // ダッシュボードのURL ※startsWithでやると全てがハンドリングされてしまうので注意
      str.startsWith('/module1') || // URLが/module1で始まるモジュールはAngular化完了
      str.startsWith('/module2'); // // URLが/module2で始まるモジュールはAngular化完了
  }

  extract(url) { return url; }
  merge(url, whole) { return url; }
}
```

## §2.5 Angularのルートコンポーネント設定
Angularのルートコンポーネントを定義します。
※中身は特に必要ありません

```ng2/main/main.component.ts
import { Component, OnInit } from "@angular/core";

@Component({
  selector: 'app-main',
  template: `<router-outlet></router-outlet>`
})
export class MainComponent implements OnInit {
  constructor(
  ) { }

  ngOnInit() {
  }
}
```

## §2.6 AngularルートコンポーネントをAngularJSへ登録
MainComponentをAngularJSに登録します。
下記では`ng2/ng1/downgrade.ts`として新規にファイルを作成していますが、AngularJSをTypeScript化していた場合はAngularJSのほうで設定してしまっても構いません。

```ng2/ng1/downgrade.ts
import * as angular from 'angular';
import { downgradeComponent } from '@angular/upgrade/static';
import { MainComponent } from 'ng2/main/main.component';

// AngularJSからもMainComponentを利用できるようにする
angular.module('app')
  .directive('appMain', downgradeComponent({ component: MainComponent }));
```

以上でAngular側の設定完了です。

# §3 AngularJSの修正

## §3.1 Angular化したルーティングの設定
Angular化したルーティングでもAngularJSが識別できるように設定します。

```ng1/upgraded.routing.js
angular.module('upgraded.route', ['ui-router'])
  .config([
    '$stateProvider',
    function ($stateProvider) {
      $stateProvider
        .state({ name: 'dashboard', url: '/', template: '' }) // Angular化したルーティングはAngularJSでは何も表示しないようにする
        .state({ name: 'module1', abstract: true })
        .state({ name: 'module1.list', url: '/module1/list', template: '' }) 
        .state({ name: 'module1.edit', url: '/module1/edit/:id', template: '' })
... 以後、Angular化完了したルーティングの分だけ追記していきます。
    }
  ]);
```

## §3.2 Angularのルートコンポーネントを設定
HTML内のメインビューとなっている`<ui-view></ui-view>`(ngRouteの場合は`<ng-view></ng-view>`)の直後に`<app-main></app-main>`を追加
※app-rootはAngularJSのルートコンポーネント。適宜読み替えてください。

```index.html
...省略
  <app-root>
    <ui-view></ui-view>
    <app-main></app-main>
  </app-root>
...省略
```

以上でAngularJSの設定は完了です。

あとは`ng build --watch`などでビルドしていけばうまくいくと思います。
※うまくいかない場合はご連絡ください。。。

# §4 Angularへの移行
粛々と進めていきましょう。
Angular化が完了したコードはAngularJSから削除してルーティングの設定するのを忘れずに。
Angular化が進むにつれてLazy Loadの恩恵で初回読み込みファイルサイズが小さくなっていくのが気持ちいいです。
簡単そうな枝葉から移行してみて感覚を掴むとスムーズに進みます。

# §5 まとめ
`app-root`はAngularJSの世界、`app-main`はAngularの世界になっています。
ui-viewではAngularJSのURLを処理、Angular化したURLなら何も表示しない。
router-outletでは、Angular化したURLを処理、それ以外は何も表示しない。
このことによって、完全にAngularJSの世界とAngularの世界を切り分けることができます。

AngularからAngularJSのDIを使いたい場合は[Inject angularjs service into Angular](https://stackoverflow.com/questions/38743423/inject-angularjs-service-into-angular)のAnswerを参考にするといいかもしれません。
AngularJSからAngularのサービスを使いたい場合は [downgradeInjectable](https://angular.io/api/upgrade/static/downgradeInjectable) あたりが良さそうです。
（どちらも私は使ったことがありません）

ただしupgradeやdowngradeは、あくまでも自作のServiceとかProviderのみに限定して、ライブラリが用意しているものは素直に移行しましょう。

# §6 付録
ngUpgradeを利用するにあたり、非常に参考になったスライドです。

- [ngUpgradeと移植戦略](https://speakerdeck.com/okunokentaro/ngupgradetoyi-zhi-zhan-lue)