search
LoginSignup
26

More than 3 years have passed since last update.

posted at

updated at

Nuxt × PWA × CDNでハマった件について

この記事はパーソルキャリア Advent Calendar 2018の2日目です。
Nuxt.jsを使ってPWAを作る際にCDNを使おうとしてハマったのでそのことについて書いていきます。

この記事の内容

  • Nuxt.jsにおけるCDNの利用
  • Service Workerとクロスオリジン
  • Opaque Responseとその取扱い
  • manifest.jsonとクロスオリジン

この記事で触れないこと

  • Nuxt.jsとは何か
  • PWAとは何か

Nuxtについては公式ドキュメントがわかりやすいのでこちらをご参照ください。
PWAについては、Googleのコードラボ私自身が簡単にまとめたスライドをご参照いただければと思います。

tl;dr

  • Service Workerからクロスオリジンのリクエストができないので、静的アセットをCDNに置いてService Workerでキャッシュする場合CDN側でAccess-Control-Allow-Originを設定してあげる
  • manifest.json(マニフェストファイル)は原則として同一オリジンに置く必要があるのでCDNから読まないようにする

Nuxt.jsにおけるCDNの利用

NuxtでCDNを利用する場合は、nuxt.config.js(設定ファイル)のbuild.publicPathに対応するCDNのURLを指定します。こうすることによって、ビルド時に.nuxt/dist/clientの内容がCDNにアップロードされます。

nuxt.config.js
export default {
  build: {
    publicPath: 'https://cdn.nuxtjs.org'
  }
}

詳しくはこちら

https://cdn.nuxtjs.org/はサンプルなので、適宜ご自身の利用するものを想定して読み替えてください。

これだけだでは何ということもなく、ページを表示した際にjavascriptのファイルが普通にCDNから読まれるようになります。問題となるのはPWA化してService Workerを稼動させた場合です。

Service Workerとクロスオリジン

Nuxt.jsをPWA化したい場合、pwa-moduleを使うととても簡単です。nuxt.config.jsmodulesプロパティに追加して、設定を少し書くだけでホーム画面への追加やService Workerによるキャッシュができるようになります。

NuxtでCDNを利用する場合は、nuxt.config.js(設定ファイル)のbuild.publicPathに対応するCDNのURLを指定します。こうすることによって、ビルド時に.nuxt/dist/clientの内容がCDNにアップロードされます。

と述べましたが、pwa-moduleを追加してからビルドすると、Service Workerのスクリプトが自動生成されます。npm run buildしたあとにstatic/sw.jsに出力されます。

sw.js
importScripts('https://cdn.nuxtjs.org/workbox.3rm34239g.js')

workbox.precaching.precacheAndRoute([
  {
    "url": "https://cdn.nuxtjs.org/0392146f2d6284.js",
    "revision": "028b52e95a28bb08c34ad092c99422ab8f"
  },
  // 以下、ビルドされたファイルが続きます
], {
  "cacheId": "sample",
  "directoryIndex": "/",
  "cleanUrls": false
})

この記事ではpreCacheについて述べます。publicPathを設定すると、対象のCDNに.nuxt/dist/clientの中身がアップロードされますが、Service WorkerのpreCache対象のファイルとしても登録されます。

preCacheでは、アプリケーションの静的アセットが最初のページ読み込み時にキャッシュされます。PWAにおけるプログレッシブ・エンハンスメントの概念で言うと、アプリケーションの骨格であるAppShellがキャッシュされると考えても良いでしょう。

この状態でいざページを読み込んでブラウザのコンソールを見ると下記のようなエラーが出ます(CDN側の設定を特に何もしていない場合)。

Access to fetch at 'https://cdn.nuxtjs.org/f9fbee91a383e90d0.js' from origin 'https://your.domain' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

見慣れてる人も多いかもしれませんが、「クロスオリジンのアクセスするならサーバ側でAccess-Control-Allow-Origin指定してあげてね。opaque responseでいいならフェッチするときにmodeをno-corsで指定してね」という旨のエラーです。

なんでPWA化した途端にCORSのエラーが出るの?

「何もしてないのにエラーが出た」まではいきませんが、「なんでダメなの?」となってしまいます。
状況を整理すると次の通りです。

  • PWA化する前はエラーが出なかった
  • Service WorkerによるpreCacheを追加するとエラーになった

ページを見てみるとわかると思いますが、正しく表示されているはずです。これはHTMLに埋め込んでいる<script>タグからはうまく参照できているということです。ということは、Service Workerでキャッシュしようとしたときにリクエストがエラーになっているということになります。

HTML5におけるcross-orginの仕様

原点に立ち返ると、このエラーは特殊でもなんでもなく、ごく普通の内容です。

  • 通常、サーバ側の対応がないとクロスオリジンのリクエストは通らない
  • Service Workerのリクエストがコケるのは当たり前

これらを意識すると「なんで<script src="https://cdn.nuxtjs.org/f9fbee91a383e90d0.js">はうまくいくんだ?」という話になります。

MDNを見ると次のように記述されています。

HTML5 では、 CORS への対応が <img>, <video>, <script> など一部の HTML 要素で行われ、 crossorigin 属性 (crossOrigin プロパティ) で、要素が取得するデータに関する CORS リクエストを構成することができます。

また、こちらのサイトには次にように記述されていました。

The browser does allow us to access cross-origin resources for a subset of elements including <script>, <img>, <video>, <audio>, <link rel="stylesheet">, <object>, <embed> and <iframe>.

というわけで、(特定の)HTMLタグで埋め込んであればOKで、Javascriptでリクエストを投げるとNGということで状況が整理できます。
試しに、ブラウザのコンソールにfetch('https://cdn.nuxtjs.org/f9fbee91a383e90d0.js')という感じでクロスオリジンへのリクエストを打ち込んで実行してみると、うまくいかないことが確認できます。

Service Workerでクロスオリジンのリソースをキャッシュするには

実は、どのように対処すれば良いかはエラーメッセージが親切に教えてくれています。
すでに述べたとおりですが、対処方法は次の2通りあります。

  1. CDNからのレスポンスヘッダにAccess-Control-Allow-Originをつける
  2. Javascriptでフェッチするときにmode: 'no-cors'を指定する

結論としては何のへんてつもないのですが、1の対応をするのが一番良いと考えます。
CDNもAmazon CloudFrontやGoogle Cloud CDN等色々あると思いますが、管理画面から設定できるかと思います(当方はAkamaiで試しました)。

結論は出たのですが、じゃあ2番目の対処方法はなんなんだよってことなので、ちょっと脇道にそれますが補足します。

Opaque Responseとその取扱い

試しにブラウザのコンソールに下記のようなクロスオリジンのリソースをフェッチするスクリプトを打ち込んでみます。

const res = await fetch('https://cdn.nuxtjs.org/f9fbee91a383e90d0.js', {mode: 'no-cors'});

こんな感じで、fetch関数の第2引数でno-corsを指定してあげると、クロスオリジンのアクセスでもエラーが出なくなります。
ではレスポンスはどうなっているでしょうか。

スクリーンショット 2018-11-30 15.36.11.png

console.logでレスポンスを見てみるとこんな感じになってます。
これがOpaque Response!

  • bodyがnull
  • typeがopaque
  • statusが0

特徴的なのはといたっところでしょうか。

Opaque Responseとはなにか

直訳すると「不透明なレスポンス」となりますが、定義を見てみましょう。

MDNには次のようにあります。

opaque: Response for “no-cors” request to cross-origin resource. Severely restricted.

もうちょっと掘り下げて仕様書を見ると、

An opaque filtered response is a filtered response whose type is "opaque", URL list is the empty list, status is 0, status message is the empty byte sequence, header list is empty, body is null, and trailer is empty.

とありました。
文中に出てくるfiltered responseは次の通り。

A filtered response is a limited view on a response that is not a network error. This response is referred to as the filtered response’s associated internal response.

要するに、no-corsを指定してクロスオリジンにリクエストした結果得られるレスポンスであり、その中身はフィルタリングされていて見えないものとざっくり解釈できます。

Opaque ResponseはService Workerでキャッシュできるか?

結論としてはできます
Workboxのドキュメントを見ると、Opaque Responseを強制的にキャッシュする方法が書かれています。
ただ、注意が必要です。

Warning: This will cache a response that could be an error, which would then never get updated!

とあるように、Opaque Responseはステータスコードが0なので成功してるか失敗してるか(正しいデータがとれてるのかどうか)わかりません。つまり、実は失敗してたレスポンスをキャッシュしてしまう可能性があります。

なので、よほどの理由がない限りOpaque Responseはキャッシュしないほうが良いと思います。

Service Workerとクロスオリジン、Opaque Responseについては下記を参考にさせていただきました。

manifest.jsonとクロスオリジン

無事にAppShellがキャッシュされるようになって一安心、となったのですが、また別のエラーが発生していました。
manifest.jsonが読み込まれないというものです。

nuxt.config.jsbuild.publicPathでCDNを指定すると、manifest.jsonもCDNを参照するようになります。
manifest.jsonはPWAにおいてとても大事なファイルで、ホーム画面に追加する際のアイコンを指定したり、アプリ名やテーマカラー、表示モードなどの各種設定を記述します。

少し古いですがgithubのissueを見たところ、manifestファイルは同一オリジンであることが求められるようです。
(クロスオリジンから読める方法知ってるよ〜という方がいらっしゃいましたら是非ご指摘いただければと思いますmm)

pwa-moduleにはmanifestモジュールが含まれていて、各種設定が行えます。少なくとも日本語ドキュメントにはほとんど情報がありませんが、実はCDNを使っていてもmanifestファイルを同一オリジンで参照できます。manifestモジュールにもpublicPath属性が存在するので指定すればOKです。

nuxt.config.js
      modules: [
        [
          '@nuxtjs/pwa',
          {
            manifest: {
              publicPath:  '/_nuxt/',
            },
          },
        ],
      ],

/_nuxt/はビルドファイルが出力されるデフォルトのパスです。
何も指定しない場合は最初に指定したbuild.publicPathと同じものが入ります。
気になる方はソースコードをご確認ください。

まとめ

Nuxtで作ったWebアプリををPWA化してかつCDNを使うにあたって、多くの学びを得られました。
本記事が同じような構成でPWAに挑戦する方のお役に立てば幸いです。
AppShellは最初のアクセスでキャッシュされるのにわざわざCDN使うメリットはそんなにないような気もしますが、ご参考まで。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
26