React NativeをExpo無しで始めて、React Native for Webも動かすまで

Android / iOS共通で、サッとアプリケーションを作りたかったので、React Nativeを始めてみました。
また、PC上でも使いたかったので、Webでも使えるように[React Native for Web](https://github.com/necolas/react-native-web)もやってみました。
（そのまま、Electronでデスクトップアプリ化したい）

Expoというものがあり、それを利用するといろいろ楽にできるっぽいんですが、いくつか制約がついたりするようなので、避けました。
また、React Native for Webの方は、公式のREADMEやGetting Startedだけを見ても実装できず、困ったのでまとめました。

https://github.com/noboru-i/ReactNativeSample にあるのが、完成形とだいたい同じものです。

## 環境

macOS 10.13.2
Node.js 8.9.4

react 16.2.0
react-native 0.52.2
react-native-web 0.3.2

## React Nativeをはじめる

https://facebook.github.io/react-native/docs/getting-started.html

"Quick Start"の方ではじめると、Expoが付いてくるようです。
今回は外したかったので、"Building Projects with Native Code"の方からはじめました。

Node.jsはnodebrewで最新をインストールしました。

続いて、getting startedに記載の通り、いろいろインストール。

```
brew install watchman
npm install -g react-native-cli
```

インストールしたCLIツールを使って、実際にプロジェクトを作成します。
（"-"区切りのプロジェクト名にしようとしたら、エラーとなりました。。）

```
react-native init ReactNativeSample
```

### 動かしてみる

`cd ReactNativeSample` で、 `react-native run-ios` とすると、シミュレータとターミナルが起動して、サンプルアプリが起動します。

Androidについては、 `ANDROID_HOME` を環境変数で設定しておき、デバッグ可能な端末を繋いでおけば、iOSと同様にサンプルアプリが起動します。

### さわってみる

ディレクトリ直下にある `App.js` を見ると、表示されているテキストがあります。
そこを修正して、reloadすると、変更が反映されます。iOSシミュレータの場合、Cmd+Rでreloadされます。
Android実機の場合は、端末を振って出てくるメニューから"Reload"を選択するとreloadされました。ちょっとめんどいですね。
共通部分に関しては、iOSシミュレータで開発を進めるのが良さそうです。

## React Native for Webをはじめる

### その前に、`App.js` を移動しておく

babelの設定的に、`App.js` を `src` の下に移動しておきます。

```
mkdir src
mv App.js src
```

それに伴って、 `index.js` も `import App from './src/App';` に変えておきます。
`react-native run-ios` で、以前と同じ表示になることを確認しておきます。

### React Native for Webの導入

https://github.com/necolas/react-native-web

READMEのInstallationにある通り、依存するパッケージをインストールします。

```
yarn add react react-dom react-native-web
yarn add --dev babel-plugin-react-native-web
```

続いて、 [Getting started](https://github.com/necolas/react-native-web/blob/master/website/guides/getting-started.md)に従って、いくつかのファイルを作成していきます。

まずは "Client-side rendering" にあるように、 `index.web.js` を作成します。

```js
import App from './src/App';
import React from 'react';
import { AppRegistry } from 'react-native';

// register the app
AppRegistry.registerComponent('App', () => App);

AppRegistry.runApplication('App', {
  rootTag: document.getElementById('root')
});
```

続いて、entry pointが無いので、[サンプル](https://github.com/kauffecup/react-native-web-hello-world)を探してきて、 `web/public/index.html` を下記のようにしました。
ここで作成したdivのidと、上のindex.web.jsのidを一致させる必要があります。

```html
<!DOCTYPE html>
<html>
  <head>
    <title>ReactNativeSample</title>
    <meta name="viewport" content="width=device-width">
  </head>
  <body>
    <div id="root"></div>
    <script src="bundle.web.js"></script>
  </body>
</html>
```

また、"Getting started"の"Web packaging for existing React Native apps"にあるように、必要なパッケージのインストールを行います。

```
yarn add --dev babel-loader url-loader webpack webpack-dev-server
```

そして、 `web/webpack.config.js` を作成します。

```js
const path = require('path');
const webpack = require('webpack');

const appDirectory = path.resolve(__dirname, '../');

// This is needed for webpack to compile JavaScript.
// Many OSS React Native packages are not compiled to ES5 before being
// published. If you depend on uncompiled packages they may cause webpack build
// errors. To fix this webpack can be configured to compile to the necessary
// `node_module`.
const babelLoaderConfiguration = {
  test: /\.js$/,
  // Add every directory that needs to be compiled by Babel during the build.
  include: [
    path.resolve(appDirectory, 'index.web.js'),
    path.resolve(appDirectory, 'src'),
    path.resolve(appDirectory, 'node_modules/react-native-uncompiled')
  ],
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true,
      // Babel configuration (or use .babelrc)
      // This aliases 'react-native' to 'react-native-web' and includes only
      // the modules needed by the app.
      plugins: ['react-native-web'],
      // The 'react-native' preset is recommended to match React Native's packager
      presets: ['react-native']
    }
  }
};

// This is needed for webpack to import static images in JavaScript files.
const imageLoaderConfiguration = {
  test: /\.(gif|jpe?g|png|svg)$/,
  use: {
    loader: 'url-loader',
    options: {
      name: '[name].[ext]'
    }
  }
};

module.exports = {
  // your web-specific entry file
  entry: path.resolve(appDirectory, 'index.web.js'),

  // configures where the build ends up
  output: {
    filename: 'bundle.web.js',
    path: path.resolve(appDirectory, 'dist')
  },

  // ...the rest of your config

  module: {
    rules: [babelLoaderConfiguration, imageLoaderConfiguration]
  },

  plugins: [
    // `process.env.NODE_ENV === 'production'` must be `true` for production
    // builds to eliminate development checks and reduce build size. You may
    // wish to include additional optimizations.
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'development'
      ),
      __DEV__: process.env.NODE_ENV === 'production' || true
    })
  ],

  resolve: {
    // If you're working on a multi-platform React Native app, web-specific
    // module implementations should be written in files using the extension
    // `.web.js`.
    extensions: ['.web.js', '.js']
  }
};
```

その後、 `./node_modules/.bin/webpack-dev-server --content-base web/public/ -d --config ./web/webpack.config.js --inline --hot --colors` を実行すると、webpackによるコンパイル、dev serverの起動が完了します。

http://localhost:8080/ にアクセスすることで、アプリと同じような画面の表示ができました。

## 付録

毎回実行コマンドを打つのがめんどうなので、 `package.json` にscriptを追加しました。

```js
// ...
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "start:android": "react-native run-android",
    "start:ios": "react-native run-ios",
    "start:web":
      "webpack-dev-server --content-base web/public/ -d --config ./web/webpack.config.js --inline --hot --colors",
    "test": "jest"
  },
// ...
```

これにより、下記のように実行できるようになります。

```
yarn start:android
yarn start:ios
yarn start:web
```