ninjinkun's diary

ninjinkunの日記

iOSアプリ開発者がWebアプリ(PWA)をリリースするまでの流れ

先日リリースした個人アプリBlogFeedbackを開発した動機と、開発の時系列、開発してみての感想(ネイティブ開発者から見たPWAとか)を書いていきます。リリースエントリにも書きましたが、このアプリはiOSネイティブアプリからWebアプリへの移植です。

TL;DR

  • BlogFeedbackのケースではWebでもネイティブアプリとほぼ同等の体験を作ることができた
  • ネイティブ歴が長くHTML/CSSに明るくなかったので、まずReact Native for WebでUIを組んでいって、自力でHTML/CSSが書けるようになってから脱React Native for Webした
  • React / TypeScript / create-react-app / styled-components / storybook おすすめ
  • ブログを書いている人はBlogFeedbackを使ってみて欲しい!
続きを読む

create-react-appのService Workerサポートで手軽にオフラインキャッシュを使う

この記事はPWA Advent Calendar 21日目の記事です。

create-react-appService Workerサポートを使ってお手軽にオフラインキャッシュを組み込んでみたので、調べたことをメモします。とりあえずすぐにオフラインでPWAが動かせるようになって便利です。

使い方

create-react-app でアプリを生成していればindex.js|ts

import { register } from './serviceWorker';

register();

するだけ。

内部

  • create-react-app(2.0以上の場合、現在は2.1.4)のService Worker supportは内部でworkbox-webpack-pluginを使っている
    • 使われているのはgenerateSW モード
    • プロジェクト内のhtml/js/imageが一通りキャッシュされる設定になっている
    • カスタマイズはreact-app-rewireとか使わないとできない?かも。未検証

挙動

以下はcreate-react-appというよりはworkboxの挙動だと思われる

  • 新しくページを開くと前回pre-cacheされたindex.htmlやjsがロードされる
  • デプロイして更新があった場合はバックグラウンドでpre-cacheされる(tempに入る)
    • 該当ページを開いているタブを全部閉じてから開き直すと、tempに入っていた更新が有効になる
    • ページを開きっぱなしでスーパーリロードしても更新が反映されないので注意(はまる)
      • これサーバーのAPIと合わせて更新する必要がある時とかどうするんだ?
      • アップデートは検知できるから、「タブを全部閉じて開き直してください」みたいなプロンプトを出すのか
      • ドキュメントによれば、これは遅延ロードによるレースコンディションを防ぐためらしい
      • (推測)恐らくServiceWorkerのキャッシュは全てのタブプロセスで共通なので、同じアプリの古いアセットをロードしたのタブが残っている状態で新しいキャッシュを有効にしてしまうと、古いアセットで動いているページが新しいキャッシュを読み込んで壊れるから、ということだと思う

temp領域に入っているアセットはApplicationタブから確認できます。 f:id:ninjinkun:20181221175931p:plain

参考

使いだすと、たぶん以下を熟読することになると思います。

こちらも参考になりました。 qiita.com

Cloud FirestoreのSecurity RulesをCircleCIで自動テストする

この記事はFirebase Adventcalendar #2の13日目の記事です(もう12/17ですが、丁度書けそうなネタがあったので、空いてる日を見つけて埋めることにしました)。

Firabase Cloud Firestoreを使う場合、Security Rulesがアクセス制御全てのかなめと言えます。ここをミスるとデータが漏れて終わる、しかしその割に簡単に変更できてしまう。というわけで自動テストできると安心でしょう。

今回は先日公開した拙作のBlogFeedback(repo https://github.com/ninjinkun/blog-feedback-app/ )で用いているCircleCIによる自動テスト事例を紹介します。

なお、Security Rulesをテストするというアイデアは以下のエントリから頂きました。

エミュレーターの準備

Cloud Firestoreのエミュレーターとテスト用のライブラリが提供されているので、これを利用することでテストを記述・実行できます。エミュレーターの利用はこちらの記事を参考にしました。

firebase-toolsが入っていれば以下でインストールして

firebase setup:emulators:firestore

以下で立ち上げられます。

firebase serve --only firestore

テストの実装

公式のサンプルを参考にfirestore.rulesをテストしていきます。

サンプルではテストはmochaで記述されていますが、今回はcreate-react-appに同梱のJestで記述しました。

アプリ内で使っているRepositoryの関数を呼んで書き込みのテストをしているのと、他人のデータや本来書き込めない領域への書き込みが失敗するテストも記述しました。

src/tests/firebase-rulest.test.tsより一部抜粋

describe('/users/:user_id/blogs', () => { 
    it('save blog', async () => {
    const db = authedApp({ uid: 'ninjinkun' });
    // repositoryが内部で使っているdbを差し替える
    firebaseDB.mockReturnValue(db);

    await firebase.assertSucceeds(
    // repositoryに定義した関数で書き込めるか
        saveBlog(
        'ninjinkun',
        'https://ninjinkun.hatenablog.com/',
        "ninjinkun's diary",
        'https://ninjinkun.hatenablog.com/feed',
        'atom'
        )
    );
    });

    it('save undefined blog field', async () => {
    const db = authedApp({ uid: 'ninjinkun' });
    firebaseDB.mockReturnValue(db);
    const user = userRef('ninjinkun');
    // 存在しないフィールドに値をセット
    // rulesでちゃんとフィールドを制御しないと書き込めてしまうので注意
    await firebase.assertFails(user.set({ hoge: 'fuga' }));
    });
});

ローカルでテストが成功するようになったら、CircleCIの設定へ進みます。

CircleCIでの実行

CircleCIで実行する際のポイントとしては、エミュレータの実行にJavaが必要なため、OpenJDKとNode.js両方が入ったDocker Imageを選ぶことです。今回はとりあえず適当にcircleci/openjdk:11-jdk-sid-nodeを選択しました。

余談ですがCloud FunctionsのビルドにはNode.js v6 or v8が必要なため、デプロイ用のjobの方では circleci/node:8.14 を指定してあります。Cloud Functionsのテストも同時に行う場合は、OpenJDK + Node.js v8の組み合わせが入っているDocker Imageを用意する必要があるでしょう。

テストの実行前にエミュレーターのインストールと実行を追記する必要があります。

.circleci/config.ymlから一部抜粋(インストールしたエミュレーターをキャッシュできるとは思うのですが、そんなに時間がかかっているわけでもないのでさぼっています)

- run: yarn firebase setup:emulators:firestore
- run:
    command: yarn firebase serve --only firestore
    background: true

CIを実行すると以下の様にローカルと同じようにテストが実行されました。これで一安心。

CI実行結果から抜粋

#!/bin/bash -eo pipefail
yarn test:ci
yarn run v1.12.3
$ CI=true react-scripts test --env=jsdom
    PASS  src/__tests__/save-count-response.test.ts
    PASS  src/__tests__/firebase-rules.test.ts
    PASS  src/__tests__/App.test.tsx

Test Suites: 3 passed, 3 total
Tests:       10 passed, 10 total
Snapshots:   0 total
Time:        3.208s
Ran all test suites.
Done in 4.68s.

テストの速度

テストケース10個で以下の通りでした。エミュレーターの起動に18秒かかっているので、ここが一番遅いですね。とりあえず許容範囲内としていますが、他のテストでも時間がかかっているとストレスになるかもしれません。

f:id:ninjinkun:20181217172804p:plain

2019/2/18 追記

firebase-tools > 6.1.1 ではCircleCIでのエミュレータ起動が失敗するので、.jar の直接起動が必要になりました。そのうち解消されるとは思いますが 具体的な作業は以下に。

Upgrade firebase tools by ninjinkun · Pull Request #54 · ninjinkun/blog-feedback-app · GitHub

2019/6/14 追記

BlogFeedback(Web版)をリリースしました

これは何?

ブログについたFacebookいいねやはてなブックマーク数などのシェア数を集計して表示してくれるWebアプリです。ブログを書いた後にシェア数が気になってしまう人におすすめです。

詳しくは以下のデモ動画をご覧ください。

デモ動画

前に同じようなのなかった?

数年前にiOSアプリとして同じ機能を持ったネイティブアプリをリリースしていました。今回はこれをWebブラウザ上で動作するPWA (Progressive Web Application) として再実装したサービスになります。

かつてのiOS

無料?

以前のiOSアプリは100円の有料アプリでしたが、今回のWeb版は無料です。

OSS?

GitHubでコードを公開しています。MITライセンスなので、コードを何かに利用していただいても構いません。

Issue、PullRequest歓迎です。不具合や要望などあればお寄せください。一応github作法として英語っぽく書いてはいますが、日本語で大丈夫です。

なんで作ったの?

PWAってやつを試してみたくなってな。

iOSでAdd to Home Screenするとまともに動かないので注意(iOSの仕様のため)。

湘南国際マラソンを走った

先々週の12/2に湘南国際マラソンを走った。ランニングはそれなりに長い期間やってきたが、フルマラソンを初めて。 結果は4:58で何とか5時間は切ったが、ペース配分を間違えたようでとにかくきつかった。 25kmくらいから足が辛くなり出して、35kmくらいからは歩いたり走ったりしながら気力だけでゴールを目指していた。

一休のランニング部のメンバーと大磯プリンスホテルに前泊していたのだが、マラソン当日も16時にレイトチェックアウトできるプランで、お風呂に入ってから帰ることができたのでだいぶ良い体験だった。

参加された皆さん、おつかれさまでした。

中目黒で聞こえた会話

男「○○○って本 (よく聞き取れず)、あれはマジいい本だからおすすめ」

女「えーすごい私もあの本買ったんだけどまだ積んでる」

男「松下幸之助の本とかも読んでる?あの人はマジ本当の経営者で‥」

ランニングと水泳を毎週一回ずつはやるようにしている。

以前は仕事が忙しくなると運動を止めてしまいがちだったのだが、こうなると 忙しくなる→運動習慣なくなる→ストレス増大、体調不良→余計忙しくなる という負のループに陥るので、最近は疲れていても運動がゼロにならないように気をつかっている。

京都のコワーキングスペースで聞こえた会話

VRはもう確定でしょ、まだやったことない人居るの?まじか〜」「京都は5年遅れてるから。あdisっちゃった(笑)」「シンギュラリティが…」

コワーキングスペースで聞こえた会話 - ninjinkun's diary

Netflixループの夢

異なる高校の生徒を集めた合宿中に、謎の怪生命体が発生して、生徒が襲われる。生命体は怪獣のようなロボットのような質感で、最初は人間ほどの大きさだが最終的にはビルくらいになる。様々な形になることができ、手足や触手をナイフやハンマーの形にして人を襲う。

我々はビルや橋を乗り越えて、本州からの脱出を目指す。

しかし合宿のチームメイト(さっき会ったばかりの男1女2)が死んでしまい、自分だけが残される。そこでループが発生して、時間が怪獣発生前に戻る。2ループ目からは怪獣を避けるように行動するが、もちろんまたみんな死んでしまう。

ループが発生する度にNetflixロゴが出る。

毎回チームメイトの天才少女と元気少女の掛け合いがあったような気がするが、内容はうろ覚え、もう一人の男子は何か居たような…というくらいの印象。

何回かループした後に、うまいことループの外側のメタ世界に移動することができ、脱出できた。

なぜか最後は実家に戻っていて、大学受験のためにZ会か予備校に行かせてくれと頼んで終わる。春から高三だしこのままじゃ浪人だ!と必死な気持ちで目が覚めた。