React NativeでAndroid、iOS、Webを1つのプロジェクトで共存させてみた
はじめに
React NativeでiOS、Android、Webを同一のプロジェクトで、ソースコードを共有する方法をやってみました。
以下を参考に自分なりにアレンジしました。
- brunolemos/react-native-web-monorepo: Code sharing between iOS, Android & Web using monorepo
- Tutorial: How to share code between iOS, Android & Web using React Native, react-native-web and monorepo - DEV Community ????
手順の概要
iOS,Android,Webで共有するcoreディレクトリを作り、それぞれから参照できるようにします。それぞれコマンドでプロジェクトをつくって、パス周りを修正していく作業です。
以下の作業したサンプルプロジェクトはこちらになります。
1.React Nativeプロジェクトを作成する。
2.Android、iOS、Webから参照されるcoreライブラリのディレクトリを作成する。
3.web用の通常のReactを作成する
4.プロジェクトルートのpackage.jsonでworkspaceを設定をして、参照できるようにする。
5.workspace設定をしたことでnode_moduleのパスが分かったので、修正していく
6.ReactプロジェクトをReact Native for Webに対応する
7.coreライブラリーの設定
1.React Nativeプロジェクトを作成する
React Nativeのプロジェクト作成に必要なものは公式を参考にしてください。
React NativeのプロジェクトはiOS、Android用にプロジェクトを作りますので、適当に名前はmobileにしました。
mkdir react-native-app cd react-native-app npx react-native init mobile --template react-native-template-typescript
念の為、iOSとAndroidがビルドできるか確認します。
iOS
以下プロジェクトルートからの手順です。
cd mobile/ios pod install cd .. yarn ios
Android
以下プロジェクトルートからの手順です。
cd mobile/android yarn android
2.Android、iOS、Webから参照されるcoreライブラリのディレクトリを作成する
ソースコード等は作らず、とりあえずディレクトリとpackage.jsonだけ作成します。
mkdir core
/core/package.json
を追加します。
{ "name": "core", "version": "0.0.1", "private": true }
3.web用の通常のReactを作成する
Reactのプロジェクトに必要なものについては公式を参考にしてください。
プロジェクトルートでReactプロジェクトを作ります。web用なので名前はwebとしました。
npx create-react-app web --typescript
念の為動作確認をしましょう。
cd web yarn start
4.プロジェクトルートの package.json
でworkspaceを設定をして、参照できるようにする。
プロジェクトルートに package.json
を追加します。
workspacesに今作成したプロジェクト達を追加します。dependenciesにreact-nativeを追加します。
{ "name": "sample-app", "private": true, "workspaces": { "packages": [ "core", "mobile", "web" ], "nohoist": [] }, "dependencies": { "react-native": "0.62.0" } }
各プロジェクトルートが変わったのでmobileのnode_modules等を削除してyarnを実行します。
cd mobile/ rm -rf node_modules/ rm yarn.lock cd .. yarn
必須ではないですが、皆さんGit使っていると思いますのでプロジェクトルートに .gitignore
を追加しておきます。
.DS_Store .vscode node_modules/ yarn-error.log
5.workspace設定をしたことでnode_moduleのパスが分かったので、修正していく
React Nativeプロジェクトから修正していきます。react-nativeのnode_modulesのパスが変更されたので、修正します。
パスが1階層深くなったので、設定されているnode_modulesのパスに../
を付与していきます。一括置換でも問題はないと思います。
mobile/metro.config.js
のプロジェクトルートを追加
const path = require('path'); module.exports = { projectRoot: path.resolve(__dirname, '../'), transformer: { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires: false, }, }), }, };
iOS
mobile/ios/Podfile
の ../node_modules
を../../node_modules
に修正する。その後 /mobile/ios
で pod install
してエラーに起きていないことを確認する。
AppDelegate.m
でjsBundleURLForBundleRootを修正
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"mobile/index" fallbackResource:nil]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif }
Build Phases の Bundle React Native code and Imagesを編集する
export NODE_BINARY=node ../../node_modules/react-native/scripts/react-native-xcode.sh
XcodeからRunで動作することを確認する。
プロジェクトルートからコマンドでiOSをRunする。
yarn workspace mobile start yarn workspace mobile ios
Android
build.gradleで設定されてるnode_modulesのパスとApplicationで起動するパスを修正する。
mobile/android/setting.gradle
の../node_modules
を ../../node_modules
に修正する
mobile/android/build.gradle
は以下の2つを修正する
$rootDir/../node_modules/
を $rootDir/../../node_modules/
に修正する
project.ext.react = [ enableHermes: false, entryFile: "mobile/index.js", root: "../../../" ]
mobile/android/app/build.gradle
の../../node_modules
を ../../../node_modules
に修正する
MainApplication.java
の getJSMainModuleName
を部分を修正する
protected String getJSMainModuleName() { return "mobile/index"; }
動作確認します。
yarn workspace mobile start yarn workspace mobile android
6.ReactプロジェクトをReact Native for Webに対応する
まずはsrcを全部消します。
rm -rf web/src/*
React Native for Web関連のライブラリを追加します。
cd web yarn add react-native-web react-art @types/react-native yarn add --dev babel-plugin-react-native-web react-app-rewired
web/.env
を追加します。
SKIP_PREFLIGHT_CHECK=true
web/package.json
のscriptsを修正します。
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" }
web/config-overrides.js
を追加します。
const fs = require("fs"); const path = require("path"); const webpack = require("webpack"); const appDirectory = fs.realpathSync(process.cwd()); const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); // our packages that will now be included in the CRA build step const appIncludes = [resolveApp("src"), resolveApp("../core/src")]; module.exports = function override(config, env) { // allow importing from outside of src folder config.resolve.plugins = config.resolve.plugins.filter( (plugin) => plugin.constructor.name !== "ModuleScopePlugin" ); config.module.rules[0].include = appIncludes; config.module.rules[1] = null; config.module.rules[2].oneOf[1].include = appIncludes; config.module.rules[2].oneOf[1].options.plugins = [ require.resolve("babel-plugin-react-native-web"), ].concat(config.module.rules[2].oneOf[1].options.plugins); config.module.rules = config.module.rules.filter(Boolean); config.plugins.push( new webpack.DefinePlugin({ __DEV__: env !== "production" }) ); return config; };
とりあえず確認用のweb/src/index.tsx
を作成します。
import React from "react"; import { AppRegistry, View, Text } from "react-native"; const App = () => { return ( <View> <Text>Hello</Text> </View> ); }; AppRegistry.registerComponent('sample-app', () => App); AppRegistry.runApplication('sample-app', { rootTag: document.getElementById("root"), });
動作確認します。
yarn workspace web start
7.coreライブラリーの設定
mobile,webのpackage.jsonにcoreライブラリーを追加します。
mobile/package.json
"dependencies": { ... "core": "0.0.1", },
web/package.json
"dependencies": { ... "core": "0.0.1", },
coreに共通のAppを作って呼び出します。
core/src/App.tsx
import React from "react"; import { SafeAreaView, Text } from "react-native"; const App = () => { return ( <SafeAreaView> <Text>Hello</Text> </SafeAreaView> ); }; export default App;
web/src/index.tsx
でAppからwebから呼び出す。
import React from "react"; import {AppRegistry} from "react-native"; import App from 'core/src/App'; AppRegistry.registerComponent('sample-app', () => App); AppRegistry.runApplication('sample-app', { rootTag: document.getElementById("root"), });
mobile/index.js
import {AppRegistry} from 'react-native'; import App from 'core/src/App'; import {name as appName} from './app.json'; AppRegistry.registerComponent(appName, () => App);
以上で、iOS、Android、webで1つのプロジェクトにすることができました。
あとがき
やってることはパス修正なんですが、疲れました。ワンソースでいろいろできてよいですよね。