Polymer サンプルコード (4) PWA [1]

# PWA (Progressive Web Application) [1]

<img src="https://www.polymer-project.org/images/logos/p-logo.png" height="26"> [Polymer](https://www.polymer-project.org/)でMarkdown形式のテキストをページ毎に整形して切り替える簡単な[PWA](https://developers.google.com/web/fundamentals/getting-started/codelabs/your-first-pwapp/?hl=ja)を作ってみます。
ビルドツール([Polymer CLI](https://www.polymer-project.org/2.0/docs/tools/polymer-cli))を使い、<img src="https://firebase.google.com/_static/images/firebase/touchicon-180.png" height="26"> [Firebase](https://firebase.google.com/)の[ホスティング](https://firebase.google.com/docs/hosting/?hl=ja)で確認します。

※ [Polymer サンプルコード (3) SPA](http://qiita.com/howking/items/3fda07e706dfbd815b0e) の続きです。

<dl>
  <dt>動作確認</dt>
  <dd>Windows 10 : ○ Chrome60 Firefox54 Edge40 IE11</dd>
  <dd>Android 7.1 : ○ Chrome60</dd>
  <dd>macOS 10.12 : ○ Safari10.0</dd>
  <dd>iOS 10.3 : ○ Safari10.0</dd>
</dl>

## 画面イメージ

表示上は[前回](http://qiita.com/howking/items/3fda07e706dfbd815b0e)とほぼ同じです。

![qiita-polymer-pwa1](https://qiita-image-store.s3.amazonaws.com/0/90678/f7b4490d-9e05-c6b9-ea75-ad9ac22e8d5a.gif)

[:globe_with_meridians: デモサイト (Firebase)](https://sample-1-52253.firebaseapp.com/)

### Lighthouseの結果

PWAの要件チェックツール([Lighthouse](https://developers.google.com/web/tools/lighthouse/))で100点でした。chrome60からは[デベロッパーツールに統合](https://developers.google.com/web/updates/2017/05/devtools-release-notes#lighthouse)されたのでチェックしやすくなっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/90678/c98b9df0-5ec6-e534-cca0-748e8b642ff4.png)

## 実行ステップ

### 1. 環境設定(Firebase)の設定

ファイルのアップロード先となるFirebaseの[管理コンソール](https://console.firebase.google.com/)からプロジェクトを作成します。

- `プロジェクトを追加`をクリック
![image.png](https://qiita-image-store.s3.amazonaws.com/0/90678/9eeb9417-1c60-bf2b-725d-03db830109c3.png)

- `プロジェクト名`に適当な名前を入力、`国 / 地域`は日本を選択。**プロジェクト名からプロジェクトIDが生成される**ので、覚えておきます(サンプルでは`sample-1`と入力してプロジェクトIDは`sample-1-52253`となりました)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/90678/845a8257-db63-73eb-d1f9-285929888b9e.png) 

### 2-A. Google Cloud Shellでの実行

[Google Cloud Shell](https://cloud.google.com/shell/?hl=ja)にはgitやnpm、bower、firebaseが予めインストールされていて手軽に使えます。[こちら](https://console.cloud.google.com/cloudshell/editor?hl=ja)から起動して下記のコマンドで実行してください。
`polymer serve`での確認も[「ウェブでプレビュー」](https://cloud.google.com/shell/docs/features#web_preview)から表示することができます。

```bash:CloudShell
# polymer-cliのインストール(5分程度かかります)
# Cloud Shellはグローバルへのインストールが一定時間でクリアされてしまうので、ホームディレクトリ下で動かします
$ npm install polymer-cli

# polymer-cliをコマンドとして使えるようにaliasを設定
$ alias polymer="~/node_modules/polymer-cli/bin/polymer.js"

# githubからソースをダウンロード
$ git clone -b sample-1 https://github.com/howking/polymer-sample-pwa sample-1

# 作業ディレクトリに移動
$ cd sample-1

# 利用するWebライブラリを取得
$ bower install

# サーバを立ち上げて動作確認
$ polymer serve

# アップロードするファイルを構築
$ polymer build

# Firebaseにログイン(表示されたURLをブラウザで開いて認証コードを入力してください)
$ firebase login --no-localhost

# 使用するプロジェクトを指定( $ firebase list で一覧が表示されます)
$ firebase use [プロジェクトID]

# ファイルをアップロード
$ firebase deploy
```

### 2-B. Windows等のローカルマシンでの実行

#### パッケージ管理ソフトの準備

[Git](https://git-scm.com/)(ソース管理ツール)と[npm](https://www.npmjs.com/)([Node.js](https://nodejs.org/)のパッケージ管理ツール)が必要です。Windowsはダイアログに沿ってインストールしてください(途中の選択肢は特に変更しなくて大丈夫です)。

- 参考記事: [私家版 Git For Windowsのインストール手順](https://opcdiary.net/?page_id=27065)、 [windowsにNode.jsをインストールする](http://qiita.com/Masayuki-M/items/840a997a824e18f576d8)

#### コマンドの準備

コマンドプロンプトから下記を入力して[Bower](https://bower.io/)(Webコンポーネントのパッケージ管理ツール)、[Polymer CLI](https://www.polymer-project.org/2.0/docs/tools/polymer-cli)(Polymer操作ツール)、[Firebase CLI](https://github.com/firebase/firebase-tools)(Firebase操作ツール)をインストールしてください(途中エラーが表示される場合がありますが、最後まで終れば大丈夫です)。

```bash:localhost
$ npm install -g bower
$ npm install -g polymer-cli
$ npm install -g firebase-tools
```

- 参考記事: [コマンドプロンプトの使い方入門 & 基礎コマンド10選](https://tech-camp.in/note/1177/)

#### ビルドとファイルのアップロード

ファイル一式を[こちら](https://github.com/howking/polymer-sample-pwa)からダウンロード(`sample-1`ブランチに切り替えて)するか、後述の[サンプルコード](#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89)からファイルを作成してください。
下記を入力するとビルドされ、ファイルがアップロードされます。

```bash:localhost
# githubからダウンロードする場合
$ git clone -b sample-1 https://github.com/howking/polymer-sample-pwa sample-1

# 作業ディレクトリに移動
$ cd sample-1

# 利用するWebライブラリを取得
$ bower install

# ローカルでサーバを立ち上げて動作確認 → http://localhost:8000/
$ polymer serve

# アップロードするファイルを構築
$ polymer build

# Firebaseにログイン
$ firebase login

# 使用するプロジェクトを指定
$ firebase use [プロジェクトID]

# ファイルをアップロード
$ firebase deploy
```

- 参考記事: [firebaseでhostingするまでの備忘録](http://qiita.com/yasupeke/items/c27abd951e216bf48f4f)

### 3. サイト確認

:tada: :tada: :tada: https://[プロジェクトID].firebaseapp.com/ で確認できます。


### A. デバッグ方法

単純にソースを編集して更新、リロードしてもServiceWorkerによってオフラインキャッシュが使われてしまい、**ページが更新されない場合があります**。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/90678/c8660134-da97-9d03-9df8-237e7f76148e.png)

Chromeの場合は、

1. 「その他のツール」→「閲覧履歴を消去」及び、
2. ServiceWorkerの解除
  ![image.png](https://qiita-image-store.s3.amazonaws.com/0/90678/b84083d9-0f9c-c927-dec7-a67ff9c8c957.png)

か、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/90678/65469086-26d7-949e-fef5-f474ff45e990.png)

で再読み込みをしてください。

Firefoxはアドレスバーに`about:debugging#workers`を入力するとServiceWorkerがリストされるので解除してください。

## サンプルコード

```bash:files
├── README.md
├── bower.json
├── firebase.json
├── index.html
├── manifest.json
├── my-view.html
├── pages
│   ├── view1.md
│   ├── view2.md
│   └── view3.md
├── polymer.json
└── sw-precache-config.js
```

- `index.html` トップページ、 `<my-view>`タグの呼出し元

```html:index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Polymer Sample PWA</title>
    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#29549a">
    <script src="/bower_components/webcomponentsjs/webcomponents-loader.js"></script>
    <link rel="import" href="/my-view.html" async>
    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('/service-worker.js');
        });
      }
    </script>
  </head>
  <body>
    <my-view key="view1" pages='[["view1","Page 1"], ["view2","Page 2"], ["view3","Page 3"]]'>
      <svg viewBox="0 0 24 30" fill="#eee" width="128"
           style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; margin: auto;">
        <path d="M19 4h-4L7.11 16.63 4.5 12 9 4H5L.5 12 5 20h4l7.89-12.63L19.5 12 15 20h4l4.5-8z"/>
      </svg>
      <noscript>Polymer Sample PWA</noscript>
    </my-view>
  </body>
</html>
```

- `pages/view[1-3].md` マークダウンで書かれたテキスト

```md:view1.md
# こんにちわ

これは1ページ目です。
```

```md:view2.md
# 2ページ目

これはサンプルのテキストです。

## 見出し1

[リンク](https://www.google.com/)も書けます。

## 見出し2

**強調表示**
```

```md:view3.md
# こんにちわ

これは3ページ目です。
```

- `my-view.html` `<my-view>`タグを表示するHTML

```html:my-view.html
<link rel="import" href="bower_components/polymer/polymer-element.html">
<link rel="import" href="bower_components/polymer/lib/elements/dom-repeat.html">
<link rel="import" href="bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="bower_components/app-layout/app-drawer-layout/app-drawer-layout.html">
<link rel="import" href="bower_components/app-layout/app-header/app-header.html">
<link rel="import" href="bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="bower_components/app-layout/app-drawer/app-drawer.html">
<link rel="import" href="bower_components/iron-icons/iron-icons.html">
<link rel="import" href="bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="bower_components/paper-listbox/paper-listbox.html">
<link rel="import" href="bower_components/paper-item/paper-item.html">
<link rel="import" href="bower_components/marked-element/marked-element.html">

<dom-module id="my-view">
  <template strip-whitespace>
    <style>
      :host {
        display: block;
        --app-primary-color: #29549a;
      }
      app-header {
        background-color: var(--app-primary-color);
        color: white;
      }
      app-header paper-icon-button {
        --paper-icon-button-ink-color: white;
      }
      div[main-title] { margin-left: 10px; }
      marked-element { margin-left: 15px; }
      paper-item:not(.iron-selected) { cursor: pointer; }
    </style>

    <app-drawer-layout force-narrow fullbleed>
      
      <app-drawer slot="drawer" swipe-open>
        
        <app-header fixed>
          <app-toolbar>
            <paper-icon-button icon="close" drawer-toggle></paper-icon-button>
          </app-toolbar>
        </app-header>
        
        <paper-listbox selected="{{key}}" attr-for-selected="key">
          <template is="dom-repeat" items="[[pages]]">
            <paper-item key="[[item.0]]" drawer-toggle>[[item.1]]</paper-item>
          </template>
        </paper-listbox>
        
      </app-drawer>

      <app-header-layout has-scrolling-region>
        
        <app-header slot="header" reveals>
          <app-toolbar>
            <paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
            <div main-title>My app</div>
            <paper-icon-button icon="delete"></paper-icon-button>
            <paper-icon-button icon="search"></paper-icon-button>
            <paper-icon-button icon="close"></paper-icon-button>
          </app-toolbar>
        </app-header>

        <marked-element>
          <div slot="markdown-html"></div>
          <script type="text/markdown" src="pages/[[key]].md"></script>
        </marked-element>
        
      </app-header-layout>
      
    </app-drawer-layout>
  </template>
  
  <script>
    class MyView extends Polymer.Element {
      static get is() { return 'my-view' }
      static get properties() { return { key: String, pages: Object } }
    }
    customElements.define(MyView.is, MyView)
  </script>
</dom-module>
```

- `sw-precache-config.js` [Service Worker Precache](https://github.com/GoogleChrome/sw-precache)というオフラインキャッシュの設定ファイル

```js:sw-precache-config.js
module.exports = {
  staticFileGlobs: [
    '/index.html',
    '/manifest.json',
    '/bower_components/webcomponentsjs/*.js'
  ],
  navigateFallback: 'index.html'
};
```

- `manifest.json` Webアプリケーションの定義ファイル

```json:manifest.json
{
  "name": "Polymer Sample PWA",
  "short_name": "Polymer Smpl",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#29549a",
  "icons": [{
    "src": "https://www.polymer-project.org/images/logos/p-logo-192.png",
    "sizes": "192x192",
    "type": "image/png"
  },{
    "src": "https://www.polymer-project.org/images/logos/p-logo-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }]
}
```

- `bower.json` 使用するWebコンポーネントライブラリの設定ファイル

```json:bower.json
{
  "name": "polymer-sample-pwa-1",
  "dependencies": {
    "polymer": "Polymer/polymer",
    "webcomponentsjs": "webcomponents/webcomponentsjs",
    "app-layout": "PolymerElements/app-layout",
    "iron-icons": "PolymerElements/iron-icons",
    "paper-ui-elements": "PolymerElements/paper-ui-elements",
    "marked-element": "PolymerElements/marked-element"
  }
}
```

- `polymer.json` Polymer CLIの設定ファイル

```json:polymer.json
{
  "entrypoint": "index.html",
  "shell": "my-view.html",
  "extraDependencies": [
    "manifest.json",
    "bower_components/webcomponentsjs/*.js"
  ],
  "builds": [{
    "name": "default",
    "preset": "es5-bundled"
  }]
}
```

- `firebase.json` Firebaseの設定ファイル(Hostingのみ)

```json:firebase.json
{
  "hosting": {
    "public": "build/default"
  }
}
```

## 各行の説明

**[前回記載した部分](http://qiita.com/howking/items/3fda07e706dfbd815b0e)は省きます。**

### index.html

```html:index.html
<html lang="ja">
    ...
    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#29549a">
```

`<html>`の言語指定や`<meta>`のテーマカラーを設定しないと**Lighthouseで点数が下がります**。

```html:index.html
    <link rel="import" href="my-view.html" async>
    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('/service-worker.js');
        });
      }
    </script>
```

`<link async>`で非同期読み込みとし、レンダリングをブロックさせないようにしますが、ページ読み込み時には一瞬素のHTMLが表示されることになります。
`<script>`で対応しているブラウザ向けに`serviceWorker`を有効化します。

```html:index.html
    <my-view key="view1" pages='[["view1","Page 1"], ["view2","Page 2"], ["view3","Page 3"]]'>
      <svg viewBox="0 0 24 30" fill="#eee" width="128"
           style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; margin: auto;">
        <path d="M19 4h-4L7.11 16.63 4.5 12 9 4H5L.5 12 5 20h4l7.89-12.63L19.5 12 15 20h4l4.5-8z"/>
      </svg>
      <noscript>Polymer Sample PWA</noscript>
    </my-view>
```

作成する`<my-view>`に`key`と`pages`のパラメータを渡します。Arrayを渡すには`'`シングルクォーテーションでかこったJSON文字列を指定します。
素のHTMLが表示される部分に`<svg>`でPolymerのロゴ([ここ](https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html#L237)からコピー)を表示し、またPolymer(Webコンポーネント)に対応していないブラウザ向けには**Lighthouseで点数が下がる**ので`<noscript>`タグを入れておきます。

### my-view.html

```html:my-view.html
        <paper-listbox selected="{{key}}" attr-for-selected="key">
          <template is="dom-repeat" items="[[pages]]">
            <paper-item key="[[item.0]]" drawer-toggle>[[item.1]]</paper-item>
          </template>
        </paper-listbox>
```

`index.html`から渡された`[[pages]]`パラメータ(`[["view1","Page 1"], ["view2","Page 2"], ["view3","Page 3"]]`)をメニューに表示します。配列の各要素は`.0`のようにインデックスの数を指定します。

```html:my-view.html
        <marked-element>
          <div slot="markdown-html"></div>
          <script type="text/markdown" src="pages/[[key]].md"></script>
        </marked-element>
```

`index.html`から渡された`[[key]]`パラメータ(`view1`)を`marked-element`にパスとして指定してMarkdownのコンテンツを読み込ませます。

```html:my-view.html
  <script>
    class MyView extends Polymer.Element {
      static get is() { return 'my-view' }
      static get properties() { return { key: String, pages: Object } }
    }
    customElements.define(MyView.is, MyView)
  </script>
```

ページの一覧や内容、その切り替えは **全てHTML側に記述したので** 、単純にタグ(エレメント要素)の宣言だけとなっています。

### sw-precache-config.js

```js:sw-precache-config.js
module.exports = {
  staticFileGlobs: [
    '/index.html',
    '/manifest.json',
    '/bower_components/webcomponentsjs/*.js'
  ],
  navigateFallback: 'index.html'
};
```

ビルド時に自動でオフラインキャッシュに必要なServiceWorker用のファイル(`/serviceWorker.js`)が生成されますが、いくつか明示的に指定する必要があるファイルを[sw-precache](https://github.com/GoogleChrome/sw-precache)ライブラリを使って指定します。

### manifest.json

```json:manifest.json
{
  "name": "Polymer Sample PWA",
  "short_name": "Polymer Smpl",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#fff",
  "theme_color": "#29549a",
  "icons": [{
    "src": "https://www.polymer-project.org/images/logos/p-logo-192.png",
    "sizes": "192x192",
    "type": "image/png"
  },{
    "src": "https://www.polymer-project.org/images/logos/p-logo-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }]
}
```

[「ホーム画面へ追加」](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/)を実現する為に使用します。
ホーム画面で表示される名前の`short_name`は12文字以内、`background_color`や`theme_color`、`icons`も指定しないと**Lighthouseで点数が下がります**。

### bower.json

```js:bower.json
  "dependencies": {
    "polymer": "Polymer/polymer",
    "webcomponentsjs": "webcomponents/webcomponentsjs",
    "app-layout": "PolymerElements/app-layout",
    "iron-icons": "PolymerElements/iron-icons",
    "paper-ui-elements": "PolymerElements/paper-ui-elements",
    "marked-element": "PolymerElements/marked-element"
  }
```

[Bower](https://bower.io/)で使用するバッケージ設定ファイルです。`app-layout`パッケージで`<app-drawer-layout>`や`<app-header>`等が使えるようになり、`paper-ui-elements`の[エイリアスパッケージ](https://github.com/PolymerElements/paper-ui-elements/)で`<paper-icon-button>`や`<paper-listbox>`等が使えるようになります。

> ちゃんとパッケージとバージョンを指定するには下記のようにします(2017/05に[リリース](https://www.polymer-project.org/blog/2017-05-15-time-for-two.html)されたv2.0向け)
>
> ```js
>  "dependencies": {
>    "polymer": "Polymer/polymer#^2.0.0",
>    "webcomponentsjs": "webcomponents/webcomponentsjs#^1.0.0",
>    "app-layout": "PolymerElements/app-layout#^2.0.0",
>    "iron-icons": "PolymerElements/iron-icons#^2.0.0",
>    "paper-icon-button": "PolymerElements/paper-icon-button#^2.0.0",
>    "paper-listbox": "PolymerElements/paper-listbox#^2.0.0",
>    "paper-item": "PolymerElements/paper-item#^2.0.0",
>    "marked-element": "PolymerElements/marked-element#^2.0.0"
>  }
> ```

### polymer.json

```js:polymer.json
  "entrypoint": "index.html",
  "shell": "my-view.html",
```

PWAの[PRPLパターン](https://developers.google.com/web/fundamentals/performance/prpl-pattern/?hl=ja)として、`index.html`をトップページ、`my-view.html`をApp Shellとして設定します。

```js:polymer.json
  "extraDependencies": [
    "manifest.json",
    "bower_components/webcomponentsjs/*.js"
  ],
```

前述のオフラインキャッシュに必要なファイルを追加の依存関係として設定します。

```js:polymer.json
  "builds": [{
    "name": "default",
    "preset": "es5-bundled"
  }]
```

Polymer CLIでビルドする種類を指定します。現状では

- `es5-bundled`
- `es6-bundled`
- `es6-unbundled`

の[3種類](https://www.polymer-project.org/2.0/docs/tools/polymer-cli-commands#build)が定義されていて、`es5-bundled`はES5への変換、各ファイルをまとめつつminify(圧縮)します。
Firebaseのホスティングは[HTTP/2](https://ja.wikipedia.org/wiki/HTTP/2)に対応しているので、IE11等に対応する必要がなければ`es6-unbundled`が適しているのかもしれません。

### firebase.json

```js:firebase.json
    "public": "build/default",
```

アップロードするトップディレクトリを指定します(Polymer CLIのデフォルトは`build/default`にビルドされます)。

## 最後に

**:bow: コメント、編集リクエスト歓迎**
[Polymer サンプルコード (5) PWA \[2\]](http://qiita.com/howking/items/fce01f9768d4652290a2) へ続きます。

以上