Reactでウェブアプリを作ってみたpost

React を利用してオフラインでも簡単な画像編集をできる1画面ウェブアプリを作ってみました。

作ったものは揺れる(ちょめ)(ちょめ)画像ジェネレータです。 ネーミングはまあ微妙かな…(汗

それを作るなかで調べたことなどをまとめました。

Light mode

基本の基

まずは、今回利用したツールについて。

利用したのは create-react-app です。

Set up a modern web app by running one command.

とあるようにコマンド一発で

  • React を利用するのに最適な環境を構築
  • PWA に簡単に対応できる Service Worker などの実装
  • 開発用サーバー&ビルド環境
  • ユニットテスト

がそろったプロジェクトが設定要らずで作成できます。

UI 周り

UI は React Bootstrap と…

React Bootstrap

ダークモードに対応するためにカスタマイズされたテーマの bootstrap-dark を…

bootstrap-dark

利用しました。 ダークモードについてはこの後に記載があります。

アイコンは、React から利用できる Font Awesome である react-fontawesome を利用しています。

あとは、

などを、このアプリに固有の UI を実装するため利用しています。

アプリ固有処理

今回のアプリは、

  1. 画像をアップロード
  2. 画像を加工
  3. 出来上がった画像をダウンロード

という感じに順次進んでいく操作が主となります。

それらの処理の実装についてさらっと記載しておきます。

画像をアップロード

Light mode

ここでは、単なる画像のアップロードと URL を利用したとえば Public Domain な画像などを利用した加工をできるようにしてあります。

このうち画像のアップロード(といいつつサーバーにはアップロードしない)は、 react-dropzone を使ってサクッと実装してあります。

また、URL を指定しての画像編集は、 CORS などによりブロックされるので cors-anywhere というプロキシを Heroku にデプロイし利用しています。

画像を加工

Select Page

画像の加工は react-image-crop を選択の UI に利用し、HTML5 Canvas をマスクや画像の加工に利用しています。

出来上がった画像をダウンロード

Download page

出来上がった画像のダウンロードには js-file-download を利用しています。

PWA 対応

react-create-app では、標準で Service Worker の実装が含まれていますが、プロジェクトの作成直後は無効にされています。

src/index.js の中身を


- serviceWorker.unregister(); + serviceWorker.register();

と変更すると、Service Worker でリソースのキャッシュが有効にされ、オフラインでも利用できるようになります。

ただ、ローカルでは実行されなかったり http では動作しなかったりと色々制限はあります。 もっとも、オフラインの場合に特別な処理を行うような機能はないので追加で独自に実装しています。

オフラインモードの検出

オフラインモードの検出は

    window.addEventListener('online',  () => console.log('change network: online mode'));
    window.addEventListener('offline', () => console.log('change network: offline mode'));

のような感じでできます。

また、今のモードの取得は

> console.log(navigator.onLine);
true

のような感じで取得できます。

まあ、それ以外にはどうしようもないのですが…

Lighthouse によるスコアの改善

Lighthouse によるスコアの改善などもしています。

大体は指摘に沿って直していけばいいのですが、不具合らしきものを見つけました。

[role]s are not contained by their required parent element

具体的には React Bootstrap の Card Navigation[role]s are not contained by their required parent element (訳:[role]は必須の親要素に含まれていません) と指摘がされます。 どうやら role 属性が Card Navigation に対して設定できない(設定しても React で生成された要素に付加されていない)状態になるようです。

ドキュメントによれば…

ARIA role for the Nav, in the context of a TabContainer, the default will be set to "tablist", but can be overridden by the Nav when set explicitly. When the role is "tablist", NavLink focus is managed according to the ARIA authoring practices for tabs:
訳: TabContainer のコンテキストでの Nav の ARIA ロールは、デフォルトが "tablist" に設定されますが、明示的に設定すると Nav によってオーバーライドできます。
ロールが「タブリスト」の場合、NavLinkフォーカスはタブの ARIA オーサリングプラクティスに従って管理されます。

role="tablist" がデフォルトで設定されるようですがどうやらそれすらも無視されているようです。

しばらく悩み、最終的に Nav の親に属性を着ける事でとりあえずの対応としています。

対応方法はこんな感じ。

  <Card>
-   <Card.Header>
+   <Card.Header role="tablist">
      <Nav variant="tabs" defaultActiveKey="#first">
        <Nav.Item>

ダークモード対応

macOS や Windows 10 や Android 10 にはダークモードなる通常とは色調が反転した色合いのテーマに変更する機能があります。

ライトモードダークモード
Light modeDark mode

ダークモード 対応 などと検索すると、画面上で切り替えスイッチを実装し、その設定を保存してテーマを切り替えるサンプルやライブラリが色々見つかりました。 とりあえず今回は CSS のメディア特性 prefers-color-scheme を利用し、システムの設定に沿って切り替わるようにしました。

現在の実装に落ち着くまで色々調べてみたのですが…

  • CSS 全部に prefix をつけて JavaScript で切り替えるのは面倒(たぶん CSS をビルドすればできると思うけど…)
  • import('darkmode.css') で読み込んで JavaScript で制御しようにもアンロードの方法が見つからない
  • CSS の @media (prefers-color-scheme: dark) { ... } のブロック内で @import してもビルド対象に含まれない(外側だと埋め込まれるがそれでは意味がない…)

と、いろいろ課題があり、最終的には… dark-theme.css という名前の CSS を用意し、@media (prefers-color-scheme: dark) { ... } のブロック内に bootstrap-dark を直接埋め込む、という対応をしています。

それもこれも react-create-app で webpack のビルド設定が隠匿されているのでカスタマイズできないことが1番の要因だと思っています。

また、 react-dropzone や react-stepper-horizontal はダークモードに対応していないので追加でいい感じのスタイルを用意し、同じく @media (prefers-color-scheme: dark) のブロック内に追加しました。

react-dropzone 用

@media (prefers-color-scheme: dark) {
  .dropzone {
    background-color: #444444;
  }
}

react-stepper-horizontal 用

@media (prefers-color-scheme: dark) {
  .stepper > div > div > div > a {
    color: #EEEEEE !important;
  }
  .stepper > div > div > div > div > a,
  .stepper > div > div > div > div > span {
    color: #333333 !important;
  } 
}

参考