Dragon Arrow written by Tatsuya Nakaji, all rights reserved animated-dragon-image-0164

初めてのPWA実装

イメージ
Mar 19, 2019

PWA実装のメモ

環境 Ruby On Rails(5.2)


PWAに必要なもの

PWAを導入するには以下が必要になります。

  • レスポンシブ対応
  • HTTPS対応していること
  • Serviceworkerの導入
  • Manifestの設定

レスポンシブ対応

色々な方法がありますが、CSSで

hogehoge.scss

@media all and (min-width: 769px){
  // 769px 以上の画面用の CSS
}
@media all and (max-width: 768px){
  // 768px 以下の画面用の CSS
}
@media all and (max-width: 640px){
  // 640px 以下の画面用の CSS
}

のようにしてサイズを指定して切り分けることができます。

HTTPS対応


config/environments/production.rb

Rails.application.configure do
  # ...

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

  # ...
end


Serviceworkerの導入

そもそもServiceworkerってなんぞやというと「クライアント側でユーザーが見ている画面とは別にバックグラウンドで動かせるスクリプト」です。
ややこしい説明はここではしませんが、こいつのおかげでPWAの機能である、PUSH通知やオフラインでも画面を見せたりすることができます。

今回は gem serviceworker-rails という gem を使って実装しました。

まずはgemをインストールします。

gem 'serviceworker-rails'
$ bundle install

そして初期ファイルを作成します。
手で作ることもできますが、面倒くさいので今回はコマンドを叩いて作成します。

$ rails g serviceworker:install
      create  app/assets/javascripts/manifest.json.erb
      create  app/assets/javascripts/serviceworker.js.erb
      create  app/assets/javascripts/serviceworker-companion.js
      create  config/initializers/serviceworker.rb
      append  app/assets/javascripts/application.js
      append  config/initializers/assets.rb
      insert  app/views/layouts/application.html.haml
      create  public/offline.html

上記が初期ファイルになります。この初期ファイルの中でさまざまな設定が書かれています。

設定

config/initializers/assets.rb

Rails.configuration.assets.precompile += %w[serviceworker.js manifest.json]

config/initializers/serviceworker.rb

Rails.application.configure do
  config.serviceworker.routes.draw do
    # map to assets implicitly
    match "/serviceworker.js"
    match "/manifest.json"

    # Examples
    #
    # map to a named asset explicitly
    # match "/proxied-serviceworker.js" => "nested/asset/serviceworker.js"
    # match "/nested/serviceworker.js" => "another/serviceworker.js"
    #
    # capture named path segments and interpolate to asset name
    # match "/captures/*segments/serviceworker.js" => "%{segments}/serviceworker.js"
    #
    # capture named parameter and interpolate to asset name
    # match "/parameter/:id/serviceworker.js" => "project/%{id}/serviceworker.js"
    #
    # insert custom headers
    # match "/header-serviceworker.js" => "another/serviceworker.js",
    #   headers: { "X-Resource-Header" => "A resource" }
    #
    # anonymous glob exposes `paths` variable for interpolation
    # match "/*/serviceworker.js" => "%{paths}/serviceworker.js"
  end
end

ここで serviceworker.js と manifest.json の2ファイルを読み込み、パスを指定しています。

app/assets/javascripts/serviceworker-companion.js

if (navigator.serviceWorker) {
  navigator.serviceWorker.register('/serviceworker.js', { scope: './' })
    .then(function(reg) {
      console.log('[Companion]', 'Service worker registered!');
    });
}

app/assets/javascripts/serviceworker.js.erb

var CACHE_VERSION = 'v1';
var CACHE_NAME = CACHE_VERSION + ':sw-cache-';

function onInstall(event) {
  console.log('[Serviceworker]', "Installing!", event);
  event.waitUntil(
    caches.open(CACHE_NAME).then(function prefill(cache) {
      return cache.addAll([

        // make sure serviceworker.js is not required by application.js
        // if you want to reference application.js from here
        '<%#= asset_path "www_domain/application.js" %>',

        '<%= asset_path "www_domain/application.css" %>',

        '/offline.html',

      ]);
    })
  );
}

function onActivate(event) {
  console.log('[Serviceworker]', "Activating!", event);
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
          return cacheName.indexOf(CACHE_VERSION) !== 0;
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
}

// Borrowed from https://github.com/TalAter/UpUp
function onFetch(event) {
  event.respondWith(
    // try to return untouched request from network first
    fetch(event.request).catch(function() {
      // if it fails, try to return request from the cache
      return caches.match(event.request).then(function(response) {
        if (response) {
          return response;
        }
        // if not found in cache, return default offline content for navigate requests
        if (event.request.mode === 'navigate' ||
          (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
          console.log('[Serviceworker]', "Fetching offline content", event);
          return caches.match('/offline.html');
        }
      })
    })
  );
}

self.addEventListener('install', onInstall);
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);

app/assets/javascripts/serviceworker-companion.jsapp/assets/javascripts/serviceworker.js.erb では

  • register
  • install
  • activate
  • fetch

のそれぞれの Serviceworker のイベントごとの挙動を設定しています。


続いて application.js に serviceworker-companion.js を読み込ませます。

app/assets/javascripts/application.js

//= require serviceworker-companion

最後に Serviceworker の目玉機能の1つでもある、オフラインページです。
ユーザーがオフライン時に表示される画面の設定です。Rails のデフォルトの 404ページや 500ページと同様、public 配下にデフォルトのファイルができているので、文言やデザインを変えたい方はこちらをいじると変えることができます。
また、404等と同様、public 配下ではなく、動的に作り直すこともできるようです
ちなみにオフライン用のファイルとして public/offline.html が使われるのは、app/assets/javascripts/serviceworker.js.erb 内の onInstall 関数で設定されているからなので、設定すれば、offline.html 以外のファイルもオフラインファイルとして設定できると思います。
デフォルトの offline.html は以下。

public/offline.html

<!DOCTYPE html>
<html>
<head>
  <title>You are not connected to the Internet</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <style>
  body {
    background-color: #EFEFEF;
    color: #2E2F30;
    text-align: center;
    font-family: arial, sans-serif;
    margin: 0;
  }

  div.dialog {
    width: 95%;
    max-width: 33em;
    margin: 4em auto 0;
  }

  div.dialog > div {
    border: 1px solid #CCC;
    border-right-color: #999;
    border-left-color: #999;
    border-bottom-color: #BBB;
    border-top: #B00100 solid 4px;
    border-top-left-radius: 9px;
    border-top-right-radius: 9px;
    background-color: white;
    padding: 7px 12% 0;
    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
  }

  h1 {
    font-size: 100%;
    color: #730E15;
    line-height: 1.5em;
  }

  div.dialog > p {
    margin: 0 0 1em;
    padding: 1em;
    background-color: #F7F7F7;
    border: 1px solid #CCC;
    border-right-color: #999;
    border-left-color: #999;
    border-bottom-color: #999;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
    border-top-color: #DADADA;
    color: #666;
    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
  }
  </style>
</head>

<body>
  <!-- This file lives in public/offline.html -->
  <div class="dialog">
    <div>
      <h1>It looks like you've lost your Internet connection</h1>
      <p>You may need to reconnect to Wi-Fi.</p>
    </div>
  </div>
</body>
</html>

Serviceworker の導入は以上です。


rails s で確認


Manifest の導入

最後に Manifest の導入をします。
Manifest は簡単にいうとホーム画面に追加したときの設定です。

設定

設定は上で作られた app/assets/javascripts/manifest.json.erb 内で行います。

以下がデフォルトでできたファイルです。

app/assets/javascripts/manifest.json.erb

<% icon_sizes = Rails.configuration.serviceworker.icon_sizes %>
{
  "name": "app name",
  "short_name": "app short name",
  "start_url": "/",
  "icons": [
  <% icon_sizes.map { |s| "#{s}x#{s}" }.each.with_index do |dim, i| %>
    {
      "src": "<%= image_path "serviceworker-rails/heart-#{dim}.png" %>",
      "sizes": "<%= dim %>",
      "type": "image/png"
    }<%= i == (icon_sizes.length - 1) ? '' : ',' %>
  <% end %>
  ],
  "theme_color": "#000000",
  "background_color": "#FFFFFF",
  "display": "fullscreen",
  "orientation": "portrait"
}

JSONでさまざまな設定がされているようですが、それぞれの意味と値はこんな感じです。

キー
内容
name
アイコンのラベルとして使われる名前
short_name
name が入り切らないときなどに表示される名前
start_url
ユーザーがアプリケーションを起動したときに最初にロードされるURL
icons
Serviceworkerでさまざまな場所で使われるアイコン。
それぞれの用途の推奨サイズに一番近い画像が使われます。
ただし、iOSの場合、app_touch_icon がアイコンに使われてしまうのでそちらをちゃんと設定しないといけない模様。
・src(画像のパス)
・sizes(画像のサイズ。例:"128x128")
・type(例:"image/png")
の3つを指定します。
デフォルトでは Rails.configuration.serviceworker.icon_sizes のサイズでループ処理で設定していますが、サイズの中身は 36 48 60 72 76 96 120 152 180 192 512 です。
theme_color
テーマカラー。アンドロイドのタスクスイッチャーではこの色で囲まれるらしい。
background_color
アプリの背景色。そもそもCSSで各々のサイトは背景色をつけていることも多いと思いますが、アプリの起動からコンテンツをロードするまでの間などにこの色が使われます。
display
アプリの表示の仕方を設定。
・fullscreen(デバイスのメニューバー含めて非表示)
・standalone(ブラウザのUIを非表示。ネイティブアプリと同じ感じ。)
・browser(通常のブラウザ表示)
orientation
ページの最初の向きを設定。
・landscape にするとデフォルトで横向きになるので、横表示のみで表示するゲームなどには便利。他にも以下の値を指定できます。
any・natural・landscape-primary・landscape-secondary・portrait・portrait-primary・portrait-secondary

確認

先程と同様、検証ツール内の Application の中の Manifest というところを開くと、
Manifest の設定が反映されているか確認することができます。

rails sをしたまま Xcode の Simulator を使ってすることもできます。


アイコン設定


さて、アイコンはデフォルトだとハートアイコンになってると思います。後、IOSアイコンは個別に作成しなければならないので、手順メモを載せます


iconフォルダ作成

favicon.icoはブラウザで開くと上部でページタイトルの左に小さく表示されているアイコンのことです。

はじめに以下のiconフォルダ作成

/app/assets/images/favicons(このフォルダの中にiosアイコンを入れる)

*faviconsでなくても好きな名前でいい


/app/assets/images/serviceworker-rails(このフォルダの中にアンドロイドアイコンを入れる)

*serviceworker-railsでなくても好きな名前でいい


iconリサイズ

<Android用>

/app/assets/images/serviceworker-rails配下に

  • icon-36x36.png
  • icon-48x48.png
  • icon-60x60.png
  • icon-72x72.png
  • icon-76x76.png
  • icon-96x96.png
  • icon-120x120.png
  • icon-152x152.png
  • icon-180x180.png
  • icon-192x192.png
  • icon-512x512.png を作成


<ios用>

/app/assets/images/favicons配下に

  • apple-touch-icon57.png
  • apple-touch-icon60.png
  • apple-touch-icon72.png
  • apple-touch-icon76.png
  • apple-touch-icon114.png
  • apple-touch-icon144.png
  • apple-touch-icon152.png
  • apple-touch-icon180.png を作成

<favicon.ico>

/app/assets/images配下に

favicon.ico を作成


読み込み

app/views/layouts/application.html.erb の<head>タグ内に以下を追記

<%= favicon_link_tag('favicon.ico') %>
<link rel="apple-touch-icon" sizes="57x57" href="/assets/favicons/apple-touch-icon57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/assets/favicons/apple-touch-icon60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/assets/favicons/apple-touch-icon72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/assets/favicons/apple-touch-icon76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/assets/favicons/apple-touch-icon114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/assets/favicons/apple-touch-icon120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/assets/favicons/apple-touch-icon144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/assets/favicons/apple-touch-icon152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon180.png">

これでfaviconおよびiosのアイコン設定完了


app/assets/javascripts/manifest.json.erbのパスをデフォルトから変更
<% icon_sizes = Rails.configuration.serviceworker.icon_sizes %>
{
"name": "My App Name",
"short_name": "Short Name",
"start_url": "/",
"icons": [
<% icon_sizes.map { |s| "#{s}x#{s}" }.each.with_index do |dim, i| %>
{
"src": "<%= asset_path "serviceworker-rails/icon-#{dim}.png" %>", # image_pathからasset_pathに変更
"sizes": "<%= dim %>",
"type": "image/png"
}<%= i == (icon_sizes.length - 1) ? '' : ',' %>
<% end %>
],
"theme_color": "#F83E26",
"background_color": "#000000",
"display": "standalone",
"orientation": "portrait"
}
これでAndroidアイコン設定も完了


次回起動アイコンやプッシュ通知も書きたい


完! 


起動アイコン