IonicにAMPページを埋め込むまでにやった事

この記事は[Ionic Advent Calendar 2017](https://qiita.com/advent-calendar/2017/ionic)の１8日目です。

---

社会人3年目にしてようやく技術が楽しくなってきました[@yosshitaku067](https://qiita.com/yosshitaku067)です。
最近はIonicで作ったiOSアプリをAppiumでテストとか試しては挫折したり、
FullCalendarをIonicに埋め込んで遊んだりしています。
宜しくお願い致します。

Ionicを1年間使ってみて感じたメリットの１つは、
経験の浅いエンジニアでも顧客の目に止まりやすいそれっぽいプロトタイプアプリを手軽に作れることで、
デメリットはそのプロトタイプで突き進むと、事故りやすいんじゃないかなーというところです。

# やったこと

[Bakaus氏の個人ブログ](https://paulbakaus.com/tutorials/html5/building-a-pwamp-0-introducing-the-shadowreader/)で紹介されている[デモアプリ](https://amp.cards/)をIonic版にしてみました。
要するに、よそのサイトにあるAMPページを引っ張ってIonicで作ったPWAに埋め込みました。

[成果物](https://amp-in-pwa-by-ionic.herokuapp.com/#/home)（モバイル、またはブラウザのモバイルエミュレーションモードで表示してみてください）

GitHubリポジトリはこちら -> [amp-in-pwa-by-ionic](https://github.com/yosshitaku067/amp-in-pwa-by-ionic)

![qiita-amp-in-ionic_01.gif](https://qiita-image-store.s3.amazonaws.com/0/164245/fdfb6bb4-4dbf-df29-c464-5d548eff2af3.gif)

![qiita-amp-in-ionic_02.gif](https://qiita-image-store.s3.amazonaws.com/0/164245/a240cd91-076e-e91f-0c85-4e0c79f7d623.gif)

![qiita-amp-in-ionic_03.gif](https://qiita-image-store.s3.amazonaws.com/0/164245/35bb0792-894e-1415-7238-412193223ed1.gif)



# やったことの詳細

**やりたいことを色々試していたら、片付かなくなったのでAMPに関係のある部分だけ抜粋して紹介します。**

[AMPの公式サイト](https://www.ampproject.org/ja/docs/guides/pwa-amp)では
AMPとPWAの組み合わせとして以下の3種類が紹介されています。

* PWA の機能を取り入れた AMP ページ(AMP as PWA)
* PWA へのエントリ ポイントとしての AMP(AMP to PWA)
* PWA のデータソースとしての AMP(AMP in PWA)

今回試してみたのは３つ目の「PWA のデータソースとしての AMP(AMP in PWA)」となります。

やったこととしては、[AMP を埋め込んでデータソースとして使用する](https://www.ampproject.org/ja/docs/guides/pwa-amp/amp-in-pwa)の内容をそのままなぞった形で、表示するコンテンツを[Bakaus氏のデモアプリ](https://amp.cards/)と同じものにしました。

## index.htmlで「Shadow AMP」をインクルード
index.htmlを以下（抜粋）のように編集して、Shadow AMP APIの読み込み、Service Workerの読み込み、
cordovaの読み込みを除外を行いました。

```html
    <!-- cordova.js required for cordova apps (remove if not needed) -->
    <!-- <script src="cordova.js"></script> -->

    <script async src="https://cdn.ampproject.org/shadow-v0.js"></script>

    <script>
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('service-worker.js')
                .then(() => console.log('service worker installed'))
                .catch(err => console.error('Error', err));
        }
    </script>
```

## 表示したいAMPページを取得して埋め込む

表示したいAMPコンテンツを引っ張り、表示します。
今回は取得するAMPページにヘッダーやメニューの定義があるので、それを取り除くサニタイズも行っています。

```typescript:article.ts

  ionViewDidLoad() {

　　　　　　　　// this.article.linkがURL
    // AMPが使用可能かチェックする
    (window.AMP = window.AMP || []).push((AMP: any) => {
      const container = document.getElementById('container');
      this.articleService.getArticlePage(this.article.link).then(doc => {
        // データ整形
        this.sanitize(doc);
        // AMPページをレンダリングする
        this.ampDoc = AMP.attachShadowDoc(container, doc, this.article.link);
      });
    });
  }

  sanitize(doc: Document) {

    // ヘッダーを取り除く
    let header = doc.getElementsByTagName('header');
    if (header.length) {
      header[0].remove();
    }

    // サイドメニューを取り除く
    let sidebar = doc.getElementsByTagName('amp-sidebar');
    if (sidebar.length) {
      sidebar[0].remove();
    }

    // コンテンツのヘッダー部を取り除く
    let contentHead = doc.querySelector('header.content__head');
    if (contentHead) {
      contentHead.remove();
    }

    // AMPページのTop画像を取り除く
    let featuredImage = doc.querySelector('.media-primary amp-img');
    if (featuredImage) {
      featuredImage.remove();
    }

  }

```

```html:article.html
<ion-header>
    <ion-navbar color="primary">
        <ion-title>
            AMP in Ionic
        </ion-title>
        <ion-buttons end>
            <button *ngIf="!navCtrl.canGoBack()" ion-button icon-only (tap)="onHome()">
                <ion-icon name="home"></ion-icon>
            </button>
        </ion-buttons>
    </ion-navbar>
</ion-header>

<ion-content no-padding class="card-background-page">
    <parts-card [article]="article"></parts-card>
    <div id="container">
    <!-- ここにAMP記事がはめこまれる -->
    </div>
</ion-content>
```

```typescript:article-service.ts
  getArticlePage(url: string): Promise<Document> {

    let ampUrl = this.backend.getAMPUrl(url);

    var xhr = new XMLHttpRequest();
    return new Promise((resolve, reject) => {
      xhr.open('GET', 'https://amp-in-pwa-by-ionic.herokuapp.com/' + encodeURIComponent(ampUrl), true);
      xhr.responseType = 'document';
      xhr.setRequestHeader('Accept', 'text/html');
      xhr.onload = () => {
        var isAMP = xhr.responseXML.documentElement.hasAttribute('amp') || xhr.responseXML.documentElement.hasAttribute('⚡');
        return isAMP ? resolve(xhr.responseXML) : reject('Article does not have an AMP version.');
      }; // .responseXML contains a ready-to-use Document object
      xhr.send();
    });
  }
```

## クリーンアップ
ここの動きの詳細を確認していないのですが、クリーンアップを実行することでこのドキュメントをもう使用しないことが AMPに伝わり、メモリやCPUのオーバーヘッドが解放されるようです。
ionViewDidLoadで実行することとしました。

```typescript
  ionViewDidLeave() {
    // クリーンアップ
    this.ampDoc.close();
  }
```

## AMP記事を表示するのに本当に必要なのはこれだけ

意外と必要な準備は少なかったです。
もちろん、表示したい対象やバックエンドの構成次第でやらなければならないことは色々あります。
今回だと、
・CORS
・記事のサニタイズ
などなど

## おわりに

作っていて楽しかったです。
私が、Webフロントエンドではなくネイティブよりのモバイル出身ということもあって、
PWAの大枠をIonicにすることで、画面遷移やライフサイクルの制御がやりやすくなったと感じました。

また、RSSフィードの制御にrxjsを採用してみました。
私には早かったかもしれません（笑）
ここの理解にかなり時間を取られてしまいました。

Readmeを用意したり、コードをリファクタリングしたりと、まだまだやるべきことはたくさんあるので、
粛々と進めていこうと思います。

ありがとうございました。

## 参考にしたサイト
[Embed & use AMP as a data source](https://www.ampproject.org/docs/guides/pwa-amp/amp-in-pwa)
[Building a PWAMP #0: Introducing the ShadowReader](https://paulbakaus.com/tutorials/html5/building-a-pwamp-0-introducing-the-shadowreader/)