React NativeでAndroid、iOS、Webを1つのプロジェクトで共存させてみた

2020.03.30

はじめに

React NativeでiOS、Android、Webを同一のプロジェクトで、ソースコードを共有する方法をやってみました。

以下を参考に自分なりにアレンジしました。

手順の概要

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/iospod 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.javagetJSMainModuleName を部分を修正する

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つのプロジェクトにすることができました。

あとがき

やってることはパス修正なんですが、疲れました。ワンソースでいろいろできてよいですよね。