Nuxt.js+TypeScriptなアプリをProgressive Web App(PWA)化する

[Nuxt.js #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nuxt-js-2)の4日目の投稿となります。

Nuxt.jsにTypeScriptを適用したプロジェクトとして下記をよく利用させてもらっています。そちらがまだPWA化していなかったので、対応してみました。

jeehyukwon/nuxt-serverless: Nuxt.js Serverless SSR Starter on AWS (Lambda + API Gateway + S3) with Serverless Framework
https://github.com/jeehyukwon/nuxt-serverless


## Progressive Web App(PWA)とは

Googleが力を入れるプログレッシブウェブアプリ（PWA）とはなにか？ | SEO研究所サクラサクラボ
https://www.sakurasaku-labo.jp/blogs/pwa

>PWAとはなにか？という問いに、非常に簡単に答えるとすると「アプリのようなウェブサイト」になります。


## 前提

- AWSアカウントがある
- serverlessがインストール・設定済み
- node.js、npm(、yarn)がインストール済み

## 手順

### 環境構築

今回はPWAに対応して動作確認することを目的としますので、上記リポジトリをベースにS3を利用しないようにしたリポジトリを用います。

kai-kou/nuxt-serverless at feature/no-use-s3
https://github.com/kai-kou/nuxt-serverless/tree/feature/no-use-s3

ベースにするリポジトリに関しては下記に利用方法をまとめています。

Nuxt.js(v2.2.0)+TypeScriptなアプリをAWS Lambda+αにデプロイしてみた(S3なし版) - Qiita
https://qiita.com/kai_kou/items/ffc4b7780e61f740e5d2


今回のソースはGitHubにもアップしていますので、ご参考ください。
https://github.com/kai-kou/nuxt-serverless/tree/feature/add-pwa

```sh
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> git clone https://github.com/kai-kou/nuxt-serverless.git
> cd nuxt-serverless
> git checkout feature/no-use-s3
> npm install
```

### PWAライブラリの導入

Nuxt.jsの場合、```@nuxtjs/pwa``` というモジュールが提供されていますので、それを利用します。

nuxt-community/pwa-module: ⚡ Supercharge Nuxt with a heavily tested, updated and stable PWA solution
https://github.com/nuxt-community/pwa-module

```sh
> npm install --save @nuxtjs/pwa
```


利用方法については下記を参考にさせていただきました。

Nuxt.js で作った静的サイトを PWA 化する - Qiita
https://qiita.com/QUANON/items/880eaa43c1d2f55f5b4d

PWAをNuxt.jsで簡単に体験する - SCOUTER開発者ブログ
https://techblog.scouter.co.jp/entry/2017/12/07/080416


両方の記事ともにTypeScriptを利用していないため、プロジェクトに合わせて設定する必要がありました。

```handler.js``` に```application/manifest+json``` を追加します。

```js:handler.js
const awsServerlessExpress = require('aws-serverless-express')
const app = require('./server')

const binaryMimeTypes = [
  'application/javascript',
  'application/json',
  'application/manifest+json',
  'application/octet-stream',
  'application/xml',
  'font/eot',
  'font/opentype',
  'font/otf',
  'image/jpeg',
  'image/png',
  'image/svg+xml',
  'text/comma-separated-values',
  'text/css',
  'text/html',
  'text/javascript',
  'text/plain',
  'text/text',
  'text/xml'
]
const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes)

module.exports.render = (event, context) => awsServerlessExpress.proxy(server, event, context)
```

```nuxt.config.js``` の```modules```に```@nuxtjs/pwa``` を追加、pwaのオプション```manifest``` と```workbox``` を追加します。

```js:nuxt.config.js
  modules: [
    '@nuxtjs/pwa',
  ],
  manifest: {
    name: 'Nuxt.js+TypeScriptアプリをPWA化する',
    lang: 'ja',
    start_url: '/dev/',
    icons: [
      {
        src: "static/icon.png",
        sizes: "512x512",
        type: "image/png",
      }
    ]
  },
  workbox: {
    swDest: 'static/sw.js',
  },
}
```

```server.js``` に```/sw.js``` のルーティング設定を追加します。
PWAで利用するのに```static/icon.png``` を指定していますので、適当に用意してください。

```js:server.js
const express = require('express')
const {Nuxt} = require('nuxt')
const path = require('path')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')

const app = express()

app.use(awsServerlessExpressMiddleware.eventContext())

app.use('/_nuxt', express.static(path.join(__dirname, '.nuxt', 'dist', 'client')))

app.use('/static', express.static(path.join(__dirname, 'static')))

app.use('/sw.js', express.static(path.join(__dirname, 'static', 'sw.js')))

let config = require('./nuxt.config.js')
const nuxt = new Nuxt(config)

app.use((req, res) => {
  req.url = `${config.router.base}${req.url}`.replace('//', '/')
  nuxt.render(req, res)
})

module.exports = app
```

### デプロイして動作確認

Expressでパス指定しており、ローカルでの検証ができませんので、デプロイして確認してみます。

デプロイ前に```serverless.yml``` の設定```service``` や```region``` など、適宜変更してください。

```sh
> npm run build
> serverless deploy
```

ブラウザで確認するとsw.jsが読み込まれていることが確認できました。

<img width="1596" alt="Hello__Nuxt_Serverless_Template-4.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/07951076-3f55-7f48-8ff0-f0fada6c66c2.png">

スマホ(Android)では、Google Chromeの「ホーム画面に追加」機能で、アプリっぽく利用できることが確認できました。

<img width="50%" src="https://qiita-image-store.s3.amazonaws.com/0/48549/fc7b13ac-6635-daee-d50e-d52879ffc301.png" />
<img width="50%" src="https://qiita-image-store.s3.amazonaws.com/0/48549/31224812-05d7-c23f-ee7c-1ce6992356db.png" />
<img width="50%" src="https://qiita-image-store.s3.amazonaws.com/0/48549/949f57f9-c21f-ebb4-aeda-2d24162723d9.png" />
<img width="50%" src="https://qiita-image-store.s3.amazonaws.com/0/48549/4b4274ea-e909-05d0-83d3-61bb4c5c860c.png" />

やったぜ。


## ポイント

### PWAのmanifest設定を変更する

TypeScriptを利用しているので、```nuxt.config.js``` で諸々パスを変更する必要がありました。

```manifest.start_url``` は初期設定だと```.``` となり、かつ```publicPath```が```_nuxt``` となるため、```/dev``` とします。カスタムドメインを利用する場合には、```/``` となります。

```js:nuxt.config.js_一部抜粋
  manifest: {
    start_url: '/dev',
  },
```

アイコンについても```static/icon.png``` が参照されるように指定します。

```js:nuxt.config.js_一部抜粋
  manifest: {
    icons: [
      {
        src: "static/icon.png",
        sizes: "512x512",
        type: "image/png",
      }
    ]
  },
```

```sw.js``` の出力先が初期設定だと、```src/static/sw.js``` となるため、```static/sw.js``` としています。

```js:nuxt.config.js_一部抜粋
  workbox: {
    swDest: 'static/sw.js',
  },
```

また、sw.jsのパスが```static``` 以下のままですと、PWAのスコープ(対象範囲)が```static``` 以下となってしまい、使い勝手が悪くなるため、```/sw.js``` でアクセス可能になるようにします。

```js:server.js
app.use('/sw.js', express.static(path.join(__dirname, 'static', 'sw.js')))
```

スコープや```manifest``` や```workbox``` の設定については下記が参考になりました。

Service Worker、はじめの一歩 - Service Workerとは | CodeGrid
https://app.codegrid.net/entry/2016-service-worker-1

Workbox Build  |  Workbox  |  Google Developers
https://developers.google.com/web/tools/workbox/modules/workbox-build

Workbox Module - PWA Module
https://pwa.nuxtjs.org/modules/workbox

Webアプリケーションマニフェスト| MDN
https://developer.mozilla.org/en-US/docs/Web/Manifest


### タイムアウトエラーが発生したら```yarn``` を利用する

```npm``` でライブラリをインストールしていると、デプロイできても、URLへアクセスするとタイムアウトエラーになることがありました。その場合には、```yarn``` でライブラリをインストールし直すとうまく動きました。
その際に```package-lock.json``` が含まれたままだと、Serverlessでデプロイ時に```node_modules``` 以下すべてが含まれてパッケージングされてしまうので、注意しましょう。

package-lock.jsonがある状態でyarn installするとServerlessのデプロイで痛い目にあう
TODO: あとでURLを貼る


```sh
> rm -rf node_modules/
> rm package-lock.json
> npm install -g yarn # yarnがインストールされていなかったら
> yarn
> yarn build
> serverless deploy
```


## 参考

Googleが力を入れるプログレッシブウェブアプリ（PWA）とはなにか？ | SEO研究所サクラサクラボ
https://www.sakurasaku-labo.jp/blogs/pwa

Service Worker、はじめの一歩 - Service Workerとは | CodeGrid
https://app.codegrid.net/entry/2016-service-worker-1

Workbox Build  |  Workbox  |  Google Developers
https://developers.google.com/web/tools/workbox/modules/workbox-build

Workbox Module - PWA Module
https://pwa.nuxtjs.org/modules/workbox

Nuxt.js で作った静的サイトを PWA 化する - Qiita
https://qiita.com/QUANON/items/880eaa43c1d2f55f5b4d

PWAをNuxt.jsで簡単に体験する - SCOUTER開発者ブログ
https://techblog.scouter.co.jp/entry/2017/12/07/080416

Webアプリケーションマニフェスト| MDN
https://developer.mozilla.org/en-US/docs/Web/Manifest

package-lock.jsonがある状態でyarn installするとServerlessのデプロイで痛い目にあう
TODO: あとでURLを貼る