React Native for WebでPWAやってみた
この記事はPWA Advent Calendar 2019の15日目の記事です。
はじめに
私は普段は主にネイティブアプリ(iOS/Android)開発をしておりまして、Webフロント周りは久しく触っていないのですが、ちょっと最近React Nativeを触る機会があり、その文脈で「React Native for Web」というワードを目にしたことで「これでPWAもいけるのでは?」と思い、軽い気持ちで書いてみることにしました。
PWAはWebの技術を使ってネイティブアプリに近い体験を作り出せるところが大きな特徴なので、今回のように元々React Nativeでネイティブアプリを作れる環境からPWAアプリを作るというのは少し違和感はありますが、ちょうど昨日のアドベントカレンダーでdennougorillaさんが書いてくれているように、ネイティブアプリの考え方をWebに持ち込み、シングルコードで多くのプラットフォームで動作するアプリを作れる点は大きなメリットあると考えています。
なお、今回紹介するReact Native for Webは必ずしもネイティブアプリは必要とせず、単独Web開発ツールとしても使えますので、HTMLをあまり書く必要なくWebアプリ、PWAアプリを開発する方法の一つとして「なるほど、こういう開発技術もあるんだな」という感じに捉えてもらっても良いかなと思います。
React Native for Webとは?
Webフロントの世界で生まれたのReact.jsの考えをネイティブアプリの世界に持ち込んだものがReact Nativeなわけですが、それを更にWebの世界に持ち込んだものがReact Native for Webです。
https://necolas.github.io/react-native-web/docs/
「Web→ネイティブアプリ→またWeb?」となる人が多いんじゃないかなぁと思いますが、この辺りの考え方というか解釈については以下の記事がとても参考になりました。
React Nativeはいわゆる「クロスプラットフォーム開発環境」なので、開発時においてはiOSやAndroidで一般的に使用されるようなGUIコンポーネントを抽象化してくれていて、そのコードを各環境向けにビルドすると、各環境に応じたGUIコンポーネントに展開されます。
この抽象化されたGUIコンポーネントをiOSやAndroidに適用するのと同じような形で、Webにも適用できるようにしたのがReact Native for Webです。
React Native for Web開発環境整備
では、実際にReact Native for Webを使える環境を作ってみます。
ちなみに、今回ご紹介したソースコードは全てこちらに公開してあります。一部説明を端折ってる部分がありますので詳細について知りたい場合はこちらを見て頂ければと思います。
前提
React Native自体についての説明も含めて書き始めるととても長くなるため、以下の通り前提をつけさせてください。
- 既にiOS, Androidは動く状態の既存React Nativeプロジェクトが存在する
- そこにWebプラットフォームを追加していく形
- 既存React Nativeプロジェクトのソースは宗教上の理由によりTypeScriptを使用
- ただ、ややこしいですがこの記事でいくつか追加するソースはJavaScript
- Expo未使用
- npmではなくyarnを利用
- Reduxとかの利用は無しで
ちなみに先に言っておくと、今回のエントリは環境整備がメインな感じなので、アプリ自体の中身はほとんどなく、以下の通り実質1ソースでカウントアップ/ダウン/リセットさせるだけです。
ReactNativeForWebSample/App.tsx at master · asmz/ReactNativeForWebSample · GitHub
(React Hooksはただ自分が使ってみたかっただけで、深い意味はありません)
ライブラリ導入
まず、追加で必要となるライブラリを導入していきます。
$ yarn add react-dom react-native-web # reactも必要だが、既存プロジェクトには導入済みのはず $ yarn add --dev @types/react-dom # 型定義
今回モジュールバンドラーとして利用するwebpackと、webpackでTypeScriptを扱えるようts-loaderを導入します。
$ yarn add --dev webpack webpack-cli ts-loader
webpackビルド設定
webpackでビルドするための設定ファイルを以下のように作成し、プロジェクトルートディレクトリに配置します。
[webpack.config.js]
const path = require('path'); const webpack = require('webpack'); const appDirectory = path.resolve(__dirname, './'); module.exports = { mode: 'production', entry: path.resolve(appDirectory, 'index.web.js'), // エントリとなるJS output: { // 最終的に docs/bundle.web.js に出力 filename: 'bundle.web.js', path: path.resolve(appDirectory, 'docs') }, module: { rules: [ { // ts-loaderを利用する設定 test: /\.(tsx?)$/, exclude: [ '/node_modules/' ], use: 'ts-loader', } ] }, resolve: { extensions: [ '.web.js', '.js', '.ts', '.web.ts', '.tsx', '.web.tsx' ], alias: { // POINT: // webpackビルドでは'react-native'は'react-native-web'として名前解決 'react-native': 'react-native-web' } } }
上記でPOINT:
として書いたとこが重要なところなんですが、React Native for WebではReact Nativeに似たようなWeb向けGUIコンポーネントを react-native-web
というパッケージで提供しています。
例えばReact NativeでTextコンポーネントを利用する場合、以下のような形でインポートしてから利用します。
import { Text } from 'react-native'
一方でReact Native for Webは類似のWeb向けTextコンポーネントを以下のような形で提供しています。
import { Text } from 'react-native-web'
上記のwebpackビルド設定では、このインポート文の react-native
を react-native-web
として名前解決させることで、Web向けのビルド時のみReact Native for Webのパッケージを利用するよう設定しているというわけです。
なお今回はwebpackを利用していますが、公式サイトにはwebpack以外を利用している場合の対応方法についても記載があるので、適宜参考にしてみてください。
エントリポイント用意
React Native標準のエントリポイントである index.js
をコピーリネームなどして、先の webpack.config.js
の中でエントリポイントとした index.web.js
を用意します。
内容は以下の通り index.js
と概ね同じで、1行だけ追記します。
[index.web.js]
import { AppRegistry } from 'react-native'; import App from './src/App'; import { name as appName } from './app.json'; AppRegistry.registerComponent(appName, () => App); AppRegistry.runApplication(appName, { rootTag: document.getElementById('root') }); // Web用独自定義
ここの rootTag
で設定されているエレメントに最終的にWebアプリが展開されることになります。
Web公開用ページ用意
実際にアプリを展開するWebページを以下のように簡単なHTMLで用意します。
[docs/index.html]
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React Native for Web Test</title> </head> <body> <!-- アプリ展開先 --> <div id="root"></div> <!-- webpackでビルドされたJS読み込み --> <script type="text/javascript" src="bundle.web.js"></script> </body> </html>
先に用意した index.web.js
の設定に従い、このHTMLの <div id="root">
のところに実際にWebアプリが表示されるイメージとなります。
Webアプリの実行・公開
webpackビルド実行
yarnで webpack-cli
を導入済みなので、以下のコマンドを実行することでビルドできます。
$ ./node_modules/webpack-cli/bin/cli.js
正常にコマンドが完了すると、 docs/bundle.web.js
が生成されているはずです。
ローカルで開いてみる
ローカルにある docs/index.html
をブラウザで開くと、以下のようにアプリがブラウザ上で動作することを確認できます。
この時点でネイティブアプリとほぼ同じUIがWebブラウザ上で再現できていることが確認でき、なかなか達成感あります…!
localhostサーバで開いてみる
webpack-dev-serverでローカル環境にWebサーバを立ち上げて動作確認してみます。
$ yarn add --dev webpack-dev-server
開発Webサーバのドキュメントルート設定(webpack.config.jsに追記)
@@ -27,5 +27,9 @@ module.exports = { // Webpackビルドでは'react-native'は'react-native-web'として名前解決 'react-native': 'react-native-web' } + }, + devServer: { + port: 8888, + contentBase: path.resolve(appDirectory, 'docs') } }
開発Webサーバ立ち上げ
./node_modules/webpack-dev-server/bin/webpack-dev-server.js
ブラウザで http://localhost:8888/
にアクセスすると開けます
Webに公開してみる
このプロジェクトの docs
ディレクトリをドキュメントルートとして任意の本番Webサーバへデプロイすることで、実際にWebアプリとして公開することが可能です。
PWA化にはHTTPSが必須要件になるため、実際はFirebase Hostingなど使うのが便利かなと思いますが、今回はちょっとズルをしてGitHub Pagesで公開してみました。(実はそのためにドキュメントルートディレクトリを docs
にした説ある)
PWA化する
ここまでで作成されたWebアプリをPWA化するため、以下の作業を行います。
manifest.json用意
[docs/manifest.json]
{ "short_name": "RNFW Test", "name": "React Native for Web Test", "display": "standalone", "start_url": "index.html", "icons": [ { "src": "images/icon.png", "sizes": "192x192", "type": "image/png" } ] }
アイコン画像は適当なものを上記パスに配置しておきます。
Service Workerの用意
すみません。今回はアプリ内容自体が大したことしていないこともあって、中身はありません...
[docs/service-worker.js] (ファイル名は任意)
self.addEventListener('install', event => { console.log('installed.'); }); self.addEventListener('activate', event => { console.log('activated'); }); self.addEventListener('fetch', event => { console.log('fetched.'); });
で、上記2つのファイルを読み込むよう docs/index.html
を修正します。
@@ -3,11 +3,17 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React Native for Web Test</title> + <!-- Web App Manifest --> + <link rel="manifest" href="manifest.json"> + <!-- PWA meta for apple --> + <meta name="apple-mobile-web-app-capable" content="yes"> + <meta name="apple-mobile-web-app-status-bar-style" content="black"> + <meta name="apple-mobile-web-app-title" content="RNFW Test"> + <link rel="apple-touch-icon" href="images/icon.png"> </head> <body> <!-- アプリ展開先 --> <div id="root"></div> <!-- WebpackでビルドされたJS読み込み --> <script type="text/javascript" src="bundle.web.js"></script> + <!-- Service worker登録 --> + <script> + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('service-worker.js') + .then((register) => { + console.log('Service worker registoration success. scope:', register.scope); + }).catch((error) => { + console.log('Service worker registoration failure. error:', error); + }); + } + </script> </body> </html>
iOS Safariは manifest.json
に対応していないようなので、今回はとりあえず手動でmetaタグ追加しました。
完成!
こんな形に修正して改めてGitHub Pagesにデプロイすると、先のWebアプリがPWA化されていることが確認できます。
- ホーム画面へ追加するときの画面
- インストールして起動してみる
おわりに
このような感じでネイティブアプリとソース共有しつつ、HTMLをほとんど書かずにWebアプリを作ってPWA化することが可能なUIフレームワークとしてReact Native for Webという選択肢があることをご紹介しました。
このPWAアドベントカレンダーの主な対象者はWebのフロントエンドエンジニアの方が多い気がするので、「だったらReact.js使えばいいじゃん」という感じになりそうではありますが、私のようにアプリ開発メインでやっていてWeb開発から遠ざかっていると、HTMLやCSSでの画面コーディング、スタイリングなどが結構苦手になってきていたりするので、今回のようにアプリ向けに作った画面がそのままWeb化できるというのは案外メリットがあったりします。
なので、まずはあまりUI凝ってない個人アプリとかで試しに導入してみようかなと目論んでいます。