KASHIMURA Blog

Webサービス開発のこと、個人的なことを書いているブログ

Cypressを使ってE2Eテストの導入をやったので振り返り

はじめに

最近、Cypressの導入をやったのでざっくりと振り返り。
導入環境は、React, Rails, GraphQL, Github Action を使ってるSaaS

なぜCypressを導入しようと思ったのか?

端折って簡単に書くと、お客さんにプロダクトの説明する時に正しく動いてないと大きな機会損失になる機能が何個かあって、これまでは、手作業でQAをしていたけど、確認するプラットフォームがWebとiOSAndroidの3つあるため、毎回とても手間で大変だということで、E2Eテストで動作の担保をしようと思い立ったのが導入の経緯。もちろん、保守工数の増加する点やCIが重くなるなど色々デメリットになりそうなことを天秤に掛けて考えてみたけども導入するメリットの方が上回ると判断した。

テスト環境の選定

手軽に始めたかったのでテストを動かす環境としては、すでに存在するステージング環境を使うようにした。いろいろネットの記事を読んでいると、CI上にテスト環境構築したり、E2E用のサーバー建てたりする案もあるみたいだったけど、あまり工数をかけずに始めたかったので、当分はステージングを使いながら様子を見て改善していく方針にした。

実行タイミング

ステージング環境にリリースされたあと、CIで自動実行するようにした。
なんでこのタイミングかというと、単純にテスト環境としてステージングを使っているので、ステージング環境へリリースされた後しかテストを実行できないから。

テストデータ作成の工夫

テストデータの作成は、それ用のAPIを用意した。テスト実行前にこのテストデータ作成APIを叩くようにしてる。

E2Eテストは、Webだけじゃなくモバイル側にも導入する予定なのでテストデータの作成を全プラットフォームで統一しておきたかったというのが理由。

最初は、Railsを使っているのでcypress-on-railsというCypressからRailsのコードを実行してテストデータを作成できるライブラリを採用するのもアリかと考えたけれども、これだとモバイル側では別途テストデータを作成するものが必要になるのでやめた。

ちなみに、このテストデータを作成するAPIは、テスト環境でしか動かないようにしてるので本番への影響はないようにしてる。

テストデータ削除の工夫

テストデータの削除は、バッチ処理を使って定期実行するようにした。だいたい、クリーンアップ処理はテスト前に実行するのが定石のようだけど、以下のような理由で難しかったのでバッチ処理に切り出すことにした。

  • テストデータをテナントごとまるっと作成しているため削除対象テナントを判断するユニークな情報を持ち回る必要があるけど難しい
  • CIでテストを実行するのでテストが並列で動いている可能性があり単純にどのテナントを削除していいのか判断できない

ちなみに、テスト終了後にクリーンアップ処理をしないのは、その処理の実行が保証されてないから。たとえば、テストが落ちた時とかは実行されないのでゴミデータが残ってしまう。詳しくは、このCypressのベストプラクティスにも書かれてる。

ディレクトリ構成

TypesScript Deep Driveを参考に、ルート直下にcypressディレクトリを作成してプロジェクトを入れ子にした。

.
├── cypress
│   ├── package.json
├── src
│   ├── 各コード
├── Gemfile
├── package.json

既存のプロジェクトに依存させてしまうと、Jestやテスト以外のコードと型が競合してしまうので分けるしかなかった。

一応、Cypressでグローバル変数を使わないようにする手段としてlocal-cypressというライブラリがあったけど、プロジェクトを入れ子にして解決するならばライブラリに頼らなくてもいいと思い使わなかった。

この構成にして思ったけど、もう一階層上にディレクトリ作って、そこにCypressプロジェクトと既存のプロジェクトを置いた方がよかったのでは?と今になって思ってる。プロジェクトを入れ子にしているので、Cypressプロジェクトで依存関係がインストールできてないと、親となる既存プロジェクトを動かしたときエラーになるし、Cypressを使わないときもCypressプロジェクトを気にしないといけないのがなんだかなと思ってる。

セレクタ命名規則

Cypressから操作する要素には、セレクタとしてdata-cyをつけるようにした。命名規則は「機能名-項目名-要素名」としてる。たとえば、hogeForm-fullName-inputのような感じ。一意になればなんでもよかったので個人的に一番分かりやすいこの形を採用した。

さいごに

Cypressは、ドキュメントが充実してるのでとても助かる!
いろいろなケースでサンプルコードが用意されているので、迷ったらサンプルを真似できるのもよかった。

実際に運用を何ヶ月かしてみて感じたことは後日で追記予定

参考

Xcode14に更新後、発生したエラー「Signing for ** requires a development team ~」の対処

Xcode14.2に更新した後、以下のエラーが発生して実機でのビルドができなくなった。
(※ちなみにReactNativeプロジェクト。)

Signing for "React-Core-AccessibilityResources" requires a development team. Select a development team in the Signing & Capabilities editor.
Signing for "GoogleSignIn-GoogleSignIn" requires a development team. Select a development team in the Signing & Capabilities editor.
Signing for "TOCropViewController-TOCropViewControllerBundle" requires a development team. Select a development team in the Signing & Capabilities editor.
Signing for "RNImageCropPicker-QBImagePicker" requires a development team. Select a development team in the Signing & Capabilities editor.

ReactNativeのissueを確認すると、同様なエラーについて報告してるissueがあった。

そこには、ReactNative 0.70.3 で対応済みってコメントが。

github.com

そのままバージョンアップして終了でもいいけど、自分は開発都合ですぐにバージョンアップ作業に取り掛かれなかったので別の方法を調査。

再度ググると、CocoaPodsのissueに対処方法と思われるコメントを発見。

以下のリンクのコメントに書かれているコードをPodfileに追記してpod install をしてからビルドするとよさそうなので実行!

結果、ビルド通るようになった 👍

github.com

ReactNative 0.70.3 と同様の対応であることはリリースノートの差分で確認した。

github.com

最近 React × Apolloで余計なrefetchを呼び出していたことに気がついた

例えば、名前が並んでいて「並び替え」ボタンを押すと順番が変わる機能を作るとき、これまでは次のように実装してた。
ローカルstateに条件を保持して、useEffect()がstateの変更をキャッチしてrefetch()を実行させる。こんな実装。
ただ、3年もこの実装していて最近気がついたけど、このuseEffectは不要だった...

以下に具体的な説明。

👇 ダメなコード(実装は適当)

import React, { FC, useState, useEffect } from "react";
import { useUsersQuery } from "~/graphql/generated/schema";

export const Users: FC = () => {
  const [order, setOrder] = useState<"asc" | "desc">("asc");
  const { data, refetch } = useUsersQuery({ variables: { order } });

  # ここがいらない...
  useEffect(() => {
    refetch();
  }, [order, refetch]);

  return (
    <div>
      <button type="button" onClick={() => setOrder("desc")}>並び替え</button>
      {data.users.edges.map((v) => (
        <div>{v.node.name}</div>
      ))}
    </div>
  );
};

※ useUsersQuery()はgraphql-codegenが自動生成したApollo.useQueryの結果を返す関数

上記のコードは、並び順をローカルstateのorderが持っていてQueryの条件に指定している。

orderが変わったことをuseEffect()がキャッチして、Queryを別の条件で再度実行するようになってる。

ただ、このuseEffect()は無くても問題なかった...

理由は、Queryのvaliablesにorderを指定しているので、orderが変更されたらよしなにQueryが実行されるようになってるから。

しかも、このrefetch呼ぶ実装の良くない点は、本来キャッシュが効く条件の場合でもQueryが実行されてしまっていたこと。

以上

PopoverTriggerの子要素に自作コンポーネントを使う場合はforwardRefを使う(ChakraUI)

ChakraUI (v2.4.1)のポップオーバーを使ってたとき、トリガーになるボタンを独自のコンポーネントにしたら動かなくなったので、原因と対応内容を備忘録として残す。

🙆‍♂️ まず、ChakraUIのポップオーバーは以下のように実装する。
PopoverTriggerで囲んだButtonやBoxがクリックされた時、PopoverContentが表示されるような仕様。

<Popover>
  <PopoverTrigger>
    <Button>ボタン</Button>
  </PopoverTrigger>
  <PopoverContent>
    ...
  </PopoverContent>
</Popover>

🙅‍♂️ ただ、以下の実装だと動かない。
PopoverTriggerで囲っている要素が独自で作成したコンポーネントの場合はクリックしても何も起きなくなった...

export const HogeButton: FC = ({children}) => (
  <Button>
    {children}
  <Button>
)
<Popover>
  <PopoverTrigger>
    <HogeButton>ボタン</HogeButton>  // これだと発火しない
  </PopoverTrigger>
  <PopoverContent>
    ...
  </PopoverContent>
</Popover>

これはなぜかというと、Popoverがrefを使っているためっぽい。

じゃ、どうするかというとforwardRefを使ってrefを扱えるようにしてあげる。
HogeButtonを↓こう書き換える。

import { ButtonProps, forwardRef } from "@chakra-ui/react";

export const HogeButton = forwardRef<ButtonProps, "button">((props, ref)) => (
  <Button ref={ref} {...props} />
)

これでPopoverTriggerの子要素にしても動くようになった 👍

参考:Popover - Chakra UI

UGCポリシーに引っかかってAndroidアプリがリジェクトされた

この間、めずらしくAndroidの審査に引っかかった。 原因は、Google PlayUGCポリシーを厳守してないかららしい。

UGCポリシーってなんだ?と思い確認してみると、アプリ内でユーザーが作成したコンテンツに関するポリシーで、例えばユーザーが投稿したコメントとかが該当するらしい。

👇 これが、ポリシーの説明ページ

support.google.com

審査結果のメールには、対応内容が書かれていてザックリまとめると「不適切な投稿に対して報告やブロックができるようにして」とのことだった。
👇これが審査結果メールの一部

ただ、自分達のアプリは、SaaS契約企業向けに提供しているもの。
会社管理下でのみ利用可能なものなので、今回の指摘は対応しなくていいかどうか問い合わせをしてみた。

結論「どんな理由があろうともポリシー守って」 と返信が来たので対応することに...

👇 異議申し立てに対する返信メールの一部がこれ

とはいえ、実際にやったことは単純で、ユーザーが投稿したコメントのメニューに報告ボタンを追加して、ボタンを押したらサービスの問い合わせページに飛ばすようにしただけ。(UIとしてはTwitterFacebookの投稿にある三点リーダーのようなもの)

アプリを利用開始するとき、利用規約に同意をしてもらう必要があるかも考えてはいたけど、上記の対応1つで許してもらえた。

おしまい

BitriseのビルドでRubyのバージョンが存在しないと言われたときの対処

BitriseでiOSのビルドをしようとすると以下のようなエラーが発生。

rbenv: version `3.1.2' is not installed (set by /Users/vagrant/git/.ruby-version)

ローカルではビルドできているので軽くググると、
Bitriseで使えるRubyバージョンは指定されているみたい。

Bitriseで使えるRubyバージョンは、Bitriseを開いて対象アプリのWorkflowsEditorのStackタブに表示されているDefault Stackの中の「Apple Silicon Stack」というテキストリンクをクリック!

表示されたページでキーワード検索を使って「ruby version」と書かれて所を探す。

検索結果がこれ👇

=== Ruby and rubygems ==================
* Ruby (default): ruby 2.6.9p207 (2021-11-24 revision 67954) [arm64-darwin21]
--- Available ruby versions ---
  system
* 2.6.9 (set by /Users/vagrant/.rbenv/version)
  2.7.2
  2.7.5
  3.0.0
  3.0.3

これを見ると、指定していたruby 3.1.2は対象外のようなので、3.0.3に変更してビルドしてみると問題なく完了した 🚀

おしまい!

いいデザインドックを作成するため、個人的にやっていること・やったこと

セルフチェックシートを使う

デザインドックを作成するときに注意するべき点をチェックシートとしてまとめています。

チェックシートは、推敲するときに使っていて、分かりづらくないか?漏れがないか?などをチェックしてます。

チェックシートの一部を抜粋すると以下のような感じですね。

[ ] 目的が書かれているか?
[ ] 専門用語の説明はしてあるか?
[ ] 章立てを行い読み手が必要な情報に辿り着けるようになっているか?
[ ] 全ての権限が考慮されているか?
[ ] 全てのデバイスを考慮しているか?

チェックシートを使って推敲すると、クオリティを一定に保つことができるので、個人的には効果がある取り組みだと思ってます。

デザインドックは、今後の開発方針をまとめて書くので、考えることが多く記述漏れなどが起こりやすいんですよね。

このチェックシート、最初は、自分用に会社の社内Wikiに書いていたのですが、他のメンバーにも好評で今では有効活用してくれているらしいです。結構嬉しい。

レビューは口頭でやるようにしている

作成したデザインドックは、メンバーのレビューを通すようにしてます。

自分の考えている方針が間違ってないか?内容が分かりづらくないか?をチェックしてもらうためです。

以前は、チャットを使ってレビューのやりとりをしていたのですが、

  • 議論に参加できない人が出てくる(チャット見れてないとか)
  • 細かいニュアンスが伝わらず議論が長引く
  • レビュワーが何度も見ないといけない

などの問題が出てきたので、今は、MTGを設定して口頭でやるようにしてます。

デザインドックのレビューは、内容的に必ず議論が発生するので、明らかに口頭の方が早いと思ってます。

また、小さな疑問を拾いやすいので全員が納得した状態で開発が進むようになりますし、読み手が分かりづらいと感じた所も明確になりデザインドック自体のクオリティも改善できます。

ちなみに、今のチームでは、毎日30分、レビュー専用の時間を確保していて、必要に応じてMTGを開催するというやり方をとっています。時間が確保されていると気軽にレビューをお願いできますし、全員が必ず集まることができるので便利。

TextLintの導入は失敗だった

誤字脱字や表記のブレがあるだけレビュワーに負荷がかかるので、TextLintを入れて自動校正できるようにしたかったのですが、これはうまく機能しなかったですね...

Notionを使ってデザインドックを書いているのですが、

  • ブラウザ上だと直接TextLintを適用できない
  • 一度Atomで文章を書いて、TextLintを適用してからコピペする

というめんどくさい手順を踏む必要があり、結局使わなくなりました。

よしなにブラウザ上で自動校正するものがあればいいのですが、いまだに見つけられてないですね...

さいごに

他にやったこととしては、

  • ドキュメントの質を振り返るMTGをやった
  • チームでデザインドックのあるべき姿を定義してドキュメントにした

などがあったのですが、特にストーリーがないので省略しました。

ドキュメント作成能力は、エンジニアとして結構大事なスキルだと思うので日々高めていきたいですね。ちなみに自分は読んでないですが、👇この本は結構いいらしいです。いつか読む。

https://www.amazon.co.jp/Docs-Developers-Engineer%E2%80%99s-Technical-Writing/dp/1484272161www.amazon.co.jp

メンバーを知るきっかけとしてライトニングトークを試した話

リモートワークで希薄になったコミュニケーション

某ウイルスが流行り始めた2年ぐらい前からフルリモートになりました。

満員電車に乗らなくてよくなった。朝ギリギリまで寝てられる。など嬉しいこともありますが、仕事の方では、ちらほら課題が出てきます。

その1つとして、部署を超えたコミュニケーションが減っていること。出社していたときは、雑談する機会が頻繁にありましたが、リモートになってからはほぼゼロに近くなってました。

自分が携わっている事業は、まだまだ立ち上げ途中で関わっている人数も20人ほどなので、全員が足並み揃えて課題に向き合っていく必要がありました。

まずは、相手の人となりを知る

コミュニケーションを取りやすくするために「相手の人となりを知る」ことは、心理的な面で重要だと思います。とくに、この2年で人の入れ替わりが激しく起きたことで、一度も話したことがない人も増ました。そのため、より一層このように感じたのだと思います。

では「どのように相手の人となりを知るのか?」そう考えて思いついたのがライトニングトークを開催してきっかけを作ることでした。

なぜ、ライトニングトーク

  • 会話のキャッチボールが無いので気楽
  • オンラインで開催できる
  • 聞き専なら片手間でも参加しやすい

理由としては、こんなところですかね?

登壇者は、話すことを考えるのに少し手間はかかりますが、聞き専ならば気軽に参加できてメンバーの人となりを知ることができます。また、リアルに集まることができないのでオンラインで開催できることは必須要件でした。

どんな感じで開催しているのか?

毎月末に全員が集まる全体会議の後1時間を使って業務中にやらせてもらってます。

登壇者は4人。話すことはなんでもOKとしています。

発表資料は、参加できなかった人のために誰でも閲覧できるところに置いてます。

やってみて

目的である「人となりを知る」というのは達成できそうです。登壇者の話を聞いていると、意外な一面を知れて面白いですね。

あとは、発表を取っ掛かりに雑談が生まれていて、ちょっとした交流の場になりつつあります。雑談はとても重要で、思いも寄らないソリューションが生まれたりするので大事にしていきたいです。

もちろん、ライトニングトークが根本的な課題の解決にはならないと思います。とはいえ、心理的な壁を取り払う施策の1つとては機能してくれそうです。

さいごに

正直、思いつきで始めたのでうまく機能するのかドキドキでした。でも、一緒に盛り上げてくれる人がいたおかげで何とか形になっています。ありがたい。

ちなみに、組織体制も少しずつ変更になっていて、部署間連携の問題は、仕組みでの改善も行われています。

心理的な面と仕組みでの改善。この2つが同時に進んでいるので、今後はよりスピーディーに仮説検証を回していけそうです。

情報収集の仕方を改善した話 InoreaderとNotionの活用

これまでの情報収集

最近は、Twitterに流れてくる面白そうな記事とTechFeedを定期的に見るぐらいでした。
自分は、Webエンジニアなので、Web界隈の人をたくさんフォローしていますが、だいたい注目の話題は流れてきます。
そのため、自分から情報を取りに行くことをサボるようになっていました。
この状況は、ホントによろしくないのでInoreaderとNotionを使って情報収集の方法を改善することにしました。

Inoreader を活用

Twitterばかり見ていると情報が偏ります。
最近は、技術より開発工程や組織改善周りに興味があったので、技術のキャッチアップができていませんでした。
Inoreaderは、この情報の偏りを改善するために取り入れることにしました。

Inoreaderとは、

RSSリーダーです。
ブログページやポッドキャストを登録しておけば、更新情報をまとめて表示してくれます。
また指定した条件に合致するツイートのみを取得することも可能で、不要な情報を省いた状態でTwitterのつぶやきを見ることができます。

Inoreaderのいいところ

  • 無料プランでも150フィードまで登録できること
  • ポッドキャストのバックグラウンド再生ができるのでInoreader内で完結すること
  • どの記事がよく読まれているか分かるところ
  • シンプルでわかりやすい

使ってみて

Inoreaderを見ていれば、必要な情報が流れてくるという体験はとてもいいです。
特に、ライブラリの更新情報が自然と目に入るようになったので、当初、課題と感じていた「技術のキャッチアップができていない」という問題は、解決していけそうです。
ちなみに、RSSリーダーといえばFeedlyもあると思いますが試してないので比較できません。
試しにInoreaderを使ってみて気に入ったのでそのまま使っています👀.

後で読む記事をどうするか?問題

インプットの改善できましたが、まだまだ課題があります。
それは、後で読む記事をまとめておく場所がないことです。
読みたい記事を見つけても、すぐに読むことができないことが多いので、どこかに記事をまとめておく必要がありました。

Notion で記事DBを作成

記事は全てNotionにまとめるようにしました。
既読/未読の管理をチェックボックスで行い、記事の分類はタグを使っています。

どうやって登録しているか?

Save to Notion というChrome拡張を使って登録しています。
Notion側のテーブルに、タグやフラグを設定しておけば、登録する時に設定することができます。

残念なところもある

スマホ版がありません。しょうがなくタグやフラグを設定できないNotionが公式で出しているものを使って登録しています。
まぁ、一応登録できるので困ってはいないのですが、できればタグやフラグを設定したいですね。

使ってみて

Save to Notionを使ってワンクリックで記事を登録していける体験はとてもいいです。
また、記事をタグごとに分けて表示できるので、後から見返す時に記事を探しやすいですね。
他には、全文検索でうろ覚えの記事をざっくりと探し出せたり、コメントを気軽に書けたりといい点を上げるとキリがないです。

さいごに

これが、自分が行っている情報収集になります。
インプットは、質の高いアウトプットをするために大事なので今後も改善して行きたいと思ってます💪

Rechartsのグラフにグラデーションをかけてイイ感じにする

f:id:kasssssy:20210206142402p:plain

はじめに

Rechartsというチャートライブラリを使って↓こんな感じのグラフを作る。

f:id:kasssssy:20210206134706p:plain

実装

まずチャートに表示するデータはこう。

const data = [
  { name: "2016", a: 44, b: 28 },
  { name: "2017", a: 48, b: 35 },
  { name: "2019", a: 36, b: 27 },
  { name: "2020", a: 14, b: 3 },
  { name: "2021", a: 5, b: 3 },
];

次にチャートの設定を書いていく。

<div>
  <ResponsiveContainer width="100%" height={350}>
    <BarChart data={data}>
      <CartesianGrid vertical={false} />
      <XAxis dataKey="name" axisLine={false} tickLine={false} />
      <YAxis axisLine={false} tickLine={false} />
      <Bar dataKey="a" fill="url(#gradationColorA)" radius={[6, 6, 0, 0]} label={{ position: "top" }} />
      <Bar dataKey="b" fill="url(#gradationColorB)" radius={[6, 6, 0, 0]} label={{ position: "top" }} />
      <defs>
        <linearGradient id="gradationColorA" gradientTransform="rotate(90)">
          <stop offset="0%" stopColor="#ff4e50" />
          <stop offset="98%" stopColor="#f9d423" />
        </linearGradient>
        <linearGradient id="gradationColorB" gradientTransform="rotate(90)">
          <stop offset="0%" stopColor="#495aff" />
          <stop offset="98%" stopColor="#0acffe" />
        </linearGradient>
      </defs>
    </BarChart>
  </ResponsiveContainer>
</div>

<ResponsiveContainer />で囲むとチャートをレスポンシブ対応にすることができる。

<BarChart />dataに表示する情報を渡す

<CartesianGrid />はチャートに表示される縦線と横線設定をする。
今回は縦線がいらなかったのでverticalをfalseにしてる。

<XAxis />dataKeyでチャートの下に表示するラベルを指定する。

<XAxis /><YAxis />axisLinetickLineをfalseにしているのは目盛り線を消すため。

<Bar />dataKeydataのプロパティ名を指定する。
fillurl(#gradationColorA)を設定するとこの後記述する<linearGradient />の設定が反映される。

Rechatsが生成するのは<svg>なのでdefsでいろいろ設定できる。

<linearGradient />でグラデーションを作成する。
gradientTransform="rotate(90)"とすると上下でグラデーションを設定できる。

最後に<stop />で色と範囲を指定して完成。

さいごに

さっと調べただけだから、もっとイイ書き方あるかもです。

参考

https://recharts.org/en-US/

graphql-rubyで認証の有無を判断して処理を分ける

f:id:kasssssy:20201128134441p:plain

はじめに

GraphQLをRails APIで採用する時、認証の有無で処理を分ける方法です。
実装方法は何パターンかあるみたいですが、自分はオペレーション名を見て認証が必要なオペレーションが1つ以上存在するなら認証処理を行うようにしました。
GraphQLを利用するためにgem graphql-rubyを利用しています。

こうした

GraphqlControllerのexecuteのcontextを次のように修正

def execute
  variables = prepare_variables(params[:variables])
  query = params[:query]
  operation_name = params[:operationName]
  context = { current_user: auth_free?(query) ? nil : current_user }
 ... 

auth_free?()は認証の有無を判断するメソッド。
オペレーションの名前を確認してbooleanを返す。
current_userは認証処理を実行して現在のユーザー情報を取得するメソッド。
firebase authのjwt認証などはこのメソッド内で行う。

auth_free?(query)メソッドの中身

def auth_free?(query)
  if query.blank?
    # エラー返す処理など
    ...
  end

  parsed_query = GraphQL.parse(query)
  operations = parsed_query.definitions.select { |q| q.is_a?(GraphQL::Language::Nodes::OperationDefinition) }

  auth_free_operation_names = %w[
    createUser
  ] 

  operations.each do |operation|
    return false if operation.selections.find { |o| auth_free_operation_names.exclude?(o.name) }
  end

  true
end

・最初にクエリーの存在を確認して、存在しなかったらエラーを返すなどを行う。

GraphQL.parse(query)で文字列のQueryをパースしてOperationの情報を取得できるようにする。
・パースした結果から対象のGraphQL::Language::Nodes::OperationDefinition型のオブジェクトを取得する。

auth_free_operation_namesは、認証を不要とするオペレーション名を配列で保持。

・最後に1つでも認証が必要なオペレーションが存在していたらfalse(認証必要)を返す。
・そうでなければtrue(認証不要)を返す。

カメラを使わずにQRコードを読み取る方法 #ReactNative

f:id:kasssssy:20200706020344p:plain

はじめに

カメラを使わずにQRコードの画像から情報を取得する方法です。
↓こんな感じで、画像を選択してQRコードからURLを取得してアラートに表示することをします。 f:id:kasssssy:20200706001411g:plain

環境

  • react-native 0.62.2
  • typescript 3.8.3
  • @react-native-firebase/app 7.3.1
  • @react-native-firebase/ml-vision 7.1.7
  • react-native-image-picker 2.3.1

Setup

コード

38行目:ImagePickerのオプションを定義しています。指定できるオプションはこちらを参考にしてください。

49行目:ボタンを押したらimagePickerを開くことをやってます。第一引数にしていしたオプションで表示する項目や文言を変更することができます。

50行目:エラーになったときの処理を書いてます。レスポンスの情報はこちらを参考にしてください。

55行目:選択した画像のPathは、プラットフォームによって取得の仕方が違うので、このような書き方になっています。

60行目:画像が選択されていない場合は何もせずに終了するようにしています。57行目のresponse.pathの戻り値の型がnullを含むのでチェックする必要があります。

64行目:選択した画像からQRコードの情報を抜き取っています。ここでML Kitが提供しているライブラリを使っています。

65行目:画像からなんの情報も取得できなかった場合はreturnしています。

69行目:情報は配列で取得されるのでfor文を使います。

70行目:QRコードから取得した情報がURLだった場合に処理をする分岐を定義しています。中では取得したURLをアラートのメッセージに表示しています。

追記 2020/07/22

64行目〜74行目あたりの処理はbarcodeDetectorProcessImageの第二引数に読み込むバーコードのフォーマットを指定する、以下のような書き方の方がいいかもしれません。

import vision, { VisionBarcodeFormat, VisionBarcodeValueType } from '@react-native-firebase/ml-vision'
...
const [barcode, ...otherBarcodes] = await vision().barcodeDetectorProcessImage(path, {barcodeFormats: [VisionBarcodeFormat.QR_CODE]})

if (barcode && barcode.valueType === VisionBarcodeValueType.URL) {
    Alert.alert('Barcode contains URL info: ', barcode.url.url)
}

最後に

react-native-image-pickerとML Kitを使えば簡単に実現できました。(少し設定がめんどくさいですが) imagePickerは、画像選択をキャンセルした時やカスタムボタンを押下したときなど柔軟に設定可能なので、 実際のサービスで採用する場合は考慮してもいいかもしれません。

記事のコードはGitHubにあげました。↓
GitHub - kashimura0001/qr-cord-scanner

参考

GitHub - react-native-community/react-native-image-picker: A React Native module that allows you to use native UI to select media from the device library or directly from the camera.

react-native-image-picker/Install.md at master · react-native-community/react-native-image-picker · GitHub

react-native-image-picker/Reference.md at master · react-native-community/react-native-image-picker · GitHub

React Native Firebase | React Native Firebase

ML Kit Vision | React Native Firebase

Barcode Scanning | React Native Firebase

WebViewの画面遷移をスライドインで表現する #ReactNative

f:id:kasssssy:20200531135218p:plain

はじめに

React NativeのWebViewで表示しているブラウザの画面遷移をiPhoneのようにスライドインで表現する方法です。
↓こんな感じです f:id:kasssssy:20200612172256g:plain

環境

  • react-native: 0.62.2
  • react-native-webview: 9.4.0
  • react-navigation/native: 5.4.3
  • react-navigation/stack: 5.4.0

コード

サンプルなのでApp.tsxに全て書いています。
少し見辛いかもしれませんが基本WebViewScreenだけ見ていただければ大丈夫です。

やりたいことは、ブラウザ内でリンクが押されたら(ロードされたら)新しいスクリーンにブラウザを表示するだけです。

※8行目でanyを使っていますが、サンプルコードなので型を定義してないだけです。

15行目:表示するURLを指定しています。

17行目:onShouldStartLoadWithRequest()は、trueを返すと画面のロードが行われ、falseを返すと画面のロードを止めることができます。

18行目:最初、画面が表示された時のロードは正常に行われるようにしています。画面のロードは最初に表示するときと画面遷移をするときにも発生するため判断できるようしています。ここの分岐がないと永遠に新しいスクリーンが生成されるという怖い状況になります。

21行目:新しいスクリーンを作成して遷移先のページを表示します。

24行目:onLoad()は、画面のロードが完了すると実行されます。

さいごに

サンプルコードはGitHubにあげました。
GitHub - kashimura0001/web-view-sample

追記 2020/06/09

この書き方だと、HTTPメソッドがGET以外だと正常に動かないです😭
どのような処理が行われるのか分かっているサイトで使うにはいいかもしれませんが、それ以外の場合は他に工夫する必要がありそうです。

ブリッジの実装は気をつけないと永遠に処理が止まる #ReactNative

f:id:kasssssy:20200425164207p:plain

はじめに

ReactNativeでAndroid用のブリッジを実装する時、@ReactMethodを付与したメソッドにPromiseを渡している場合は、何か返さないと呼び出し元でawaitがついているときにずっと待ってしまいます。
new Promise() ではなく、普段async/awaitで非同期処理を書いていると結構やらかしてしまうかもしれません。

ということで、ブリッジでも非同期実装する時は気をつけましょうという記事です。
これだけだと分かりづらいので以下に簡単なサンプルを書きました。

環境

  • react native: 0.62.2
  • typescript: 3.8.3

サンプル

ブリッジの実装
(コードは公式のものをそのまま使ってます。)

呼び出し元の実装

解説

ToastModuleに定義したshow()メソッドは、トーストを表示するメソッドです。
引数にPromiseを渡しているので非同期なメソッドです。
ただ、何も返してません。

@ReactMethod
public void show(String message, int duration, Promise promise) { 
    Toast.makeText(getReactApplicationContext(), message, duration).show();
}

App.tsxでは、ボタンを押した時にトーストとアラートを表示するようにしています。
ボタン押下時に呼ばれるshow()メソッドの中の1行目でブリッジのメソッドを呼んでいます。

const show = async () => {
  await ToastExample.show('Hello!', ToastExample.SHORT);
  Alert.alert('Hello^^');
};

トーストもアラートも表示されそうですが、アラートは表示されません。
理由は、ToastExample.show()にはawaitを付けているので何か返ってくるのを待つことになります。
ただ、ToastExample.show()は中で何も返していないので、呼び出し元で永遠に待つことになり処理が止まってしまうからです。

実際に動かしてみるとこんな感じです。
アラートが表示されていないのが分かると思います。

f:id:kasssssy:20200425155842g:plain

解決策

  • 非同期なメソッドである必要性を考え直す
  • promise.resolve()/promise.reject() で何か返す

さいごに

これは、エラーも何も吐かずただ処理が止まってしまうので厄介です。
ま、理解していれば、なんてことないことですね。

最後まで読んで頂きありがとうございます。

GitHubに上げたサンプルコード

参考

Native Modules · React Native

Restyleを利用してスタイルを定数化する #ReactNative

f:id:kasssssy:20200419020827p:plain

はじめに

React Summitで紹介されていた@shopify/restyleを触ってみました。
@shopify/restyleは、基礎となるスタイルを定数化してコンポーネントの役割ごとに色や間隔のサイズを柔軟に指定できるようにするライブラリのようです。

ReactNativeでスタイルを実装する場合、Stylesheetを定義されているかと思いますが、@shopify/restyleを利用することでStyleSheet.create()からおさらばできて、かつ、スタイルの実装がシンプルになります。

また、Typescriptで実装されていて型が強制されるので、定義していないスタイルがあたることを防いでくれます。

セットアップ

$ npx react-native init RestyleSample --template react-native-template-typescript
$ yarn add @shopify/restyle
$ cd ios && pod install && cd ../

テーマを定義

テーマオブジェクトを実装します。

app/theme.tsx

  • const palette = {}で利用する色を定義します。
  • const theme = {}でグローバルテーマオブジェクトを定義し、colors、spacing、breakpointsを設定します。この3つは必須です。
  • colorsは、コンポーネントの中で利用する為に名前付きで色を定義しています。
  • spacingは、marginなどに指定するサイズを定義します。ちなみに、キーはTシャツのサイズ(s,m,l)を使うのがいいと公式が言ってます。
  • breakpoints は、画面サイズの最小幅を定義します。(本記事では使いません)
  • textVariantsは、この後でてくるTextコンポーネントで利用するデザインを定義しています。colorにはcolorsのキーを指定することで値を取得することができます。

Box と Text を定義

BoxとTextという既存のコンポーネントが用意されています。
この2つに利用するテーマを指定して定義します。

app/Box.tsx

app/Text.tsx

App.tsx を編集

定義したテーマを利用するには、ThemeProviderに利用するテーマを渡してルートコンポーネントを囲む必要があります。

  • BoxのbackgroundColorに指定しているmainBackgroundColorは、テーマオブジェクトのcolorsに定義した色になります。
  • Textのvariantには、テーマオブジェクトのtextVariantsに定義したスタイルを指定します。
  • BoxとTextに指定しているプロパティはライブラリが用意したものになります。それぞれ指定できるものが異なるので、公式サイトを確認するのをおすすめします。
  • Boxで利用できるRestyle Function->backgroundColor, opacity, visible, layout, spacing, border, shadow, position
  • Textで利用できるRestyle Function-> color, opacity, visible, typography, textShadow, spacing

この時点で動作確認をすると、
ちゃんとそれぞれのスタイルが反映されているのが確認できます。 f:id:kasssssy:20200419010650p:plain

ダークモードに対応させる

theme.tsxにダークモード用のテーマを定義します。以下を追記します。

ダークモードを切り替えるられるようにする為、App.tsxを以下のように修正します。

  • useState()をつかってダークモードか判断できるようにします。
  • <ThemeProvider theme={darkMode ? darkTheme : theme}> で利用するテーマが切り替わるようにしています。
  • <Box margin={'l'}>lは、テーマオブジェクトのspacingで定義したものを指定しています。

実行してみると、ちゃんと切り替わることが確認できました。

f:id:kasssssy:20200419015146g:plain

さいごに

React Summitで紹介されていたので、軽い気持ちで触ってみましたが、よさげだったので記事にしました。
スタイルの重複を最小限にすることができるので実装が楽になると思います。
Boxのようなコンポーネントを独自で定義できたり、コンポーネントの外でテーマを利用するためにフックが用意されていたりするので、公式サイトを覗いてみるのをおすすめします!

本記事で実装したコードはGitHubに上げました。

最後まで読んでいただきありがとうございました!

参考

GitHub - Shopify/restyle: A type-enforced system for building UI components in React Native with TypeScript.

React Summit Remote Edition まとめ(前編) | naturalclar.dev