ninjinkun's diary

ninjinkunの日記

ブログのSNSシェア数を毎朝メールでお知らせする機能をリリースしました

BlogFeedbackにシェア数の増加をメールでお知らせしてくれる機能を追加しました。

シェア数が昨日より増加していると、以下のようなメールが配信されます。

f:id:ninjinkun:20190120124338j:plain:w320

設定

設定はブログの追加時

f:id:ninjinkun:20190120125642p:plain:w320

または以下の設定画面から行うことができます。

f:id:ninjinkun:20190120125944p:plain:w320

配信する時間はとりあえず朝の9時にしてありますが、予告なく変更する場合があります。自分で時間や配信メールアドレスを選ぶ機能は今のところありません。

実装

  • クロールからメール送信まで全てFirebase Cloud Functionsで実装
  • Firebaseにはcronがなかったので、GCPにappengineをデプロイする方法で実装(これが一般的な方法らしい)
  • 動きは以下の通り
    • GCP cron→AppEngine→Cloud Pub/Sub→Cloud Functionsの対象者抽出Function→Cloud Pub/Subで対象ブログごとにFunctionsを起動
  • Firebase Cloud Functionsの環境にはGCP用の環境変数が設定されているらしく、Cloud Pub/SubのSDKが設定なしで動いてびっくり
    • Firebase Cloud Functionsの中身はGoogle Cloud Functionsなのでよく考えたら当たり前ではあるが
  • Cloud Pub/Subは1回以上実行保証で、2回実行されることが普通にあるのでロック処理を入れた
    • Firestoreにロック用テーブルを作ってjobごとにUUIDを入れる実装
  • フロント側のクローラとコードが共有できそうな気もしたが、細かい部分で差異が出てくるのでとりあえずはコピペで対応
  • HTMLメールのマークアップ辛すぎる
    • Gmailがflexboxタグを抜いてくるので、古き良きtableとfloatでマークアップした(やったことなかったけど)
  • 今はメールのSMTPサーバが自分のGmailなので、500通/dayしか送れない
    • ユーザーが増えたらSESなりSendGridなり検討する
  • 対象ブログ抽出の実装が1000ユーザーで上限決め打ち
    • 拡張する対応は簡単なので、とりあえずこれも今は放置

スマホ2台生活

右ポケットにiPhone X、左ポケットにPixel 3 XL。どう考えても無駄な気がするが、この生活を始めて2ヶ月くらい経ってしまった。

元々iPhone Xを持っていたところにPixel 3 XLを買ったのだが、iOSからAndroidへの環境移行がとても面倒で、特に支払い周りのApple Pay(iD)とMobile Suicaが直接移行できなかった(新規でセットアップ & 登録が必要。Mobile SuicaはなぜかAndroidだけ年会費もかかる)のが原因で、二台持ちが始まってしまった。

日常的なブラウジングやSlackのチェックはどちらか出てきた端末でやる。Netflixなどの動画は画面が大きいPixel、支払いはiPhoneとなんとなく使い分けがされてきている。

開発者としてはMobile SafariAndroid Chromeがすぐに試せるのが最大のメリット。両OSの進んでいる部分がちょっとずつ試せるのも悪くない。

ランニング、水泳、睡眠トラッキングApple Watchが手放せないのもあってiOSも捨てられない。

なんとなくこのまま二台持ちで行きそうな気がする。

create-react-appで作ったアプリのFetch as Google対応

結論から言うと以下のエントリを参考に

import 'babel-polyfill';

したらできた。

scrapbox.io

追記

yarn add babel-polyfill して埋め込んでいたが、その後 yarn start が遅くなったような気がしたので、CDNからの読み込みに修正した。

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.min.js"></script>

経緯

  • 先日作ったPWAである BlogFeedbackGoogleにインデックスして欲しかった
    • 一応トップページのtitleとdescriptionは登録されている

f:id:ninjinkun:20190112210513p:plain

  • しかしbodyはインデックスされていない
    • create-react-app でSPAを作っただけは普通こうなると思われる
    • 大半のページはユーザー毎のprivateページなので、そんなにSEOをがんばってもうまみはない
    • でも規約とかキャンポリとか、もうちょっとは登録するページもあるし…
  • 職場ではServer Side Renderingが導入されているので、それなりに茨の道であることは知っていた
  • この辺りの話は以下のスライドが参考になります
  • Dynamic Rendering…するほどページ数もない(正直に言うとよくわからんのでやりたくない)のでとりあえずクローラが読めるJSにするぞ

挫折の歴史

とりあえずChrome 41対応したらええんやろ

  • "browserslist": ["chrome >= 41"] したらできるのでは?
  • pollyfill.io使ったらできるのでは?
    • なぜかfetch as googleで読めず…
  • react-app-polyfillIE対応すればChrome 41でも動くのでは?
    • なぜかfetch as googleで読めず…
  • 俺が悪かった import 'babel-polyfill' するわ
    • 読めた!
    • JSの容量は増えるが、まあ個人サービスなので目をつぶろう(11KB+だった)

f:id:ninjinkun:20190113101206p:plain

赤坂見附の飲み屋で聞こえた会話

「俺はあのジョブズのプレゼンを見て、俺もジョブズになるんだと決心したんだわ」

「…でも最近俺はジョブズじゃないって気づいたんだわ…」

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時にレイトチェックアウトできるプランで、お風呂に入ってから帰ることができたのでだいぶ良い体験だった。

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

中目黒で聞こえた会話

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

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

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