Yuri’s Tech Note

技術系アウトプット

react-native-webを自社アプリに組み込んでみた

自社プロダクトのweb版を作ることを検討するため、以前react-native-domを自社アプリに組み込んでみたのに引き続き、今度はreact-native-webを既存のプロダクトに組み込んでみて得た知見をまとめます。

react-native-webを使ってやりたいこと

react-native-webを使う理由として同じプロダクトのwebサービスを作るにあたり、「既存のソースコードの共有ができること」が大きいです。
下図のようなディレクトリ構成にすることで今後の開発でも最小限でモバイルにあわせてwebも開発していくことが可能になると考えています。
ピンクの枠で囲まれた部分がソースコードを共有する部分です。
f:id:yuri_iOS:20181209195114p:plain

reduxのコードを全部共有

APIを叩く処理、受け取ったデータの更新などデータフローに関する処理は基本モバイルもwebも同じで問題ないと考えています。
裏側の処理の実装もロジックテストも先にモバイルで開発した時に担保できていれば、後から作るwebでは画面のみ作るだけで済みます。
更に言えばモバイルで実装した人が同じ部分をweb版も実装すれば大幅な時間の節約が期待できます。

Atomic Designの思想でスタイルの統一

コンポーネントはatoms と一部のmoleculesを共有し、それより大きい粒度ではモバイルとwebわけて画面を組みあげたいです。
これができるとモバイルとweb両方作る身としてはにスタイルの書き方が全く同じで良いので気持ちが良いし開発スピードもあがります。
なによりパーツのトンマナが揃い、メンテナンスの余地が減るのはとても素晴らしいことだと思っています。

開発体験

上記2つによって、開発者体験はとても良いものになります。
ブランチを切り替えるだけで、いつもと同じスタイル記法、reduxの思考でそのまま気軽にweb版も開発できるのはとても素敵です。
モバイルはどうなってたっけ?webはどうなってたっけ?もエディターを開き直す必要はありません。

組み込み作業

ここから実際に組み込む際に行なった作業手順を紹介します。
コード量が多いのと会社のプロダクトに実際に組み込んだためソースコードを見せることができませんが、必要だと思うポイントを絞って記載します。

1. Entry Point (index.js)

import { AppRegistry } from 'react-native';
import Root from './root';
import registerServiceWorker from './registerServiceWorker';

AppRegistry.registerComponent('TeamHub', () => Root);
// 下記2行をweb用に追加
AppRegistry.runApplication('TeamHub', { rootTag: document.getElementById('root') });
registerServiceWorker();

2. Root
ProviderのstoreにconfigureStoreで生成したstoreを渡してreduxを組み込みます。
I18nはブラウザで設定されている言語の取得方法がわからなかったので一旦強制的に日本語にしています。

import React from 'react';
import { Provider } from 'react-redux';
import I18n from 'react-native-i18n';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import App from './app';
import configureStore from '../redux/configureStore';
import loadTranslations from '../i18n/translations';

const store = configureStore();
loadTranslations();

// 強制的に日本語に設定
I18n.locale = 'ja-JP';
if (I18n.locale === 'ja-JP') {
  require('moment/locale/ja');
}

const Root = () => (
  <Provider store={store}>
    <Router>
      <Route exact path="/:filter?" component={App} />
    </Router>
  </Provider>
);

export default Root;

3. redux/modules
ここの作業ボリュームが多いです。
webではネイティブモジュールを読み込んでいるファイルをコンパイルすることができないため、ネイティブモジュールを使用するreduxのモジュールは全て別を用意しました。
例) auth.js / auth.web.js
こうすることでreducer.jsでモジュールをcombineReducersでまとめ上げる際にweb.jsの方を読んでくれるのでネイティブモジュールの読み込みによるコンパイルの失敗を回避します。

具体的には、ログインやサインインなどの認証周りのモジュールをまとめたauth.jsというファイルの中でreact-native-fabricなどのネィティブモジュールを使用していたところ、そのモジュールを実際に使う処理を実行しなくてもimportの時点でモジュールを読み込みに行くため、そこでネィティブにアクセスする前提のモジュールにとって色々なものがなくてコンパイルが失敗しました。
f:id:yuri_iOS:20181209014937p:plain

ネイティブモジュール読み込めないよ問題はreact-native-domでも同じでしたがreact-native-webではこの回避策としてimportした時点でモジュールの生成をしないようにできるwebpackのIgnorePluginが使えるのではないかと考えました。
IgnorePlugin

IgnorePluginを使ってネィティブモジュールを全て登録しておき、ネィティブモジュールを使う処理だけモバイル環境でのみ実行するようにする最低限の変更で済むことを期待しました。

f:id:yuri_iOS:20181208222626p:plain
上記が実際に出力されたエラーです。
./node_modules/react-native-fabric/Crashlytics.jsを読み込まないように下記記述でコンパイルを試みましたがエラーは変わりませんでした。

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/, /^\.\/Crashlytics$/, /react-native-fabric$/),

結果としてネィティブモジュールを読み込むファイルは重複するコードが多くなってしましますが全て別途用意することになりました。
react-native-domの時と違ってファイルの拡張子をweb.jsとすれば勝手に読み分けてくれるのは嬉しいです。

Webpack設定

webpackを使うために下記ファイルを用意しました。

  • webpack.config.dev.js
  • webpack.config.prod.js
  • env.js
  • paths.js
  • polyfills.js

公式ドキュメントにもありますが、webpack.configにてreact-nativeにエイリアスを貼ってreact-native-webを読み込むようにします。

module.exports = {
  resolve: {
    alias: {
      'react-native$': 'react-native-web'
    }
  }
}

reactn-native-webというのは基本react-nativeと同じ名前のパーツをweb用に用意しているので例えば

import { View } from 'react-native'

とあった場合エイリアスによってreact-native-webのViewを見に行きます。
React Native examples

webpackの設定は自分も初めてやったので確証はないのですが、多分このエイリアス以外特別なことはしてないと思うので他は割愛します。

devserver設定

同じくdevserverを使うために下記ファイルを用意しました。

  • webpackDevServer.config.js
  • registerServerWorker.js

Tips: PropTypes
PropTypesを使っているモジュールはコンパイルできないのでbabel-plugin-transform-react-remove-prop-typesを使うことで削除する必要があります。
またこの時babel-loaderの対象としてincludeにPropTypesを使うモジュールを指定する場合はpaths.jsでアプリのディレクトリからの相対パスに変換してくれるresolveAppを使った形でないとパスをうまく解釈できません。

//paths.js
module.exports = {
  ....
  appNodeModules: resolveApp('node_modules'),
  appRNNodeModules: resolveApp('node_modules/react-native-material-design'),
  appRNNodeModules2: resolveApp('node_modules/react-native-vector-icons'),
};
//webpack.config
{
  test: /\.(js|jsx|mjs)$/,
  loader: require.resolve('babel-loader'),
  include: [paths.appSrc, paths.appRNNodeModules, paths.appRNNodeModules2],
  options: {
    presets: ['react-native'],
    plugins: ['react-native-web', ['babel-plugin-transform-react-remove-prop-types', { mode: 'remove' }]],
    cacheDirectory: true,
    babelrc: true,
  },
},

react-native-webのプロダクション採用について

採用するにあたり懸念している点を列挙します。

web版でのスタイルの情報が曖昧

f:id:yuri_iOS:20181209053607p:plain
当たり前ですがToggle InspectorのようにViewにあてているスタイルの文言ががそのまま表示されません。
Elementsで該当箇所を指定すると遠からずなスタイルの表示も確かに出てはいますが、原因不明のレイアウトが崩れが起きた場合に原因を探すのは苦労しそうです。
特にブラウザによって異なる系問題はかなり苦労しそうです。

バージョン問題

モバイルでは最新バージョンがでたらすぐにアップデートしていたのですが、react-native-webはreact-native最新バージョンにすぐに追従してくれるわけではないのでバージョンが引っ張られる、もしくはwebの時だけ毎回バージョンを下げるという絶対めんどくさい作業が発生することになります。
バージョンで書き方が変わる箇所があるとなった場合にはこれはけっこうなネックになりえます。

エラーがわかりにくい

実際に他の人が触って見てもらった感想です。
基本問題があればコンパイルできないので開発最中は困ることはないと思いますが、バージョンアップ時や最初の0から構築する場合に苦労することになります。

共有できるコードが案外少ない

redux側はほとんどまんま使い回せるかと思いきや、アクションごとに数値を取るためにそこかしこにネィティブモジュールを使ってしまっていたのでけっこうredux側でも手を入れなきゃいけない箇所が多くなってしまいました。
表側に関してはそれこそほとんど別に組まなくてはならないのですが、感覚値完全に共有できるのは全コードのうち30%行かないくらいかな。。という感じです。

参考になる記事が少ない

日本だとなかざんさん(@Nkzn)のウォーターセルしか採用事例がなく、海外でも軽く作ってみた系はあってもがっつり使ってんでー!知見あるでー!というのは見つけられませんでした。

チームやプロダクトにあっていない

自分たちのチームにはwebに詳しい人がいないのでコンパイルできない時にwebpackの設定が悪いのかreact-native-webに原因があるのか自分たちのコードに原因があるのか判断するのが大変そうです。
またこのプロダクトは今後もたくさんの機能を開発予定で、既に多機能なアプリなので追加したい機能によってはいつかスタックする事案が出てきそうなのが怖いです。

最後に

react-native-webはTwitterUberThe Timesなども使っているので挑戦してもいうほど悪い賭けではないと思うのですが、やはり会社でやるには強くやろうぜ!と言えないです。
ここは強いパッションがないと採用するのは難しいのかな。
もっとシンプルなアプリケーションであれば、ReduxとAtomsを共有してモバイルとwebを作るという発想は最速プロダクトリリース手法として悪くないと思っています。

webpackのコンパイルでnode_modules配下のPropTypesを解決する

状況

既にたくさんのライブラリを使っているReact NativeのプロジェクトにReact Native for Webを組み込んでWebpackの設定をしていた際にprop-typesを使っているモジュールの解決ができずwebpackのコンパイルがこけた。

自分たちの書いているJsのコードで使用されているPropTypesは解決できているのでnode_modules配下だけ解決できていなかった。

PropTypesの排除

PropTypesについてはwebpackでコンパイルする前にbabelの方でprop-typesを取り除くという作業が必要になり、具体的にはbabel-plugin-transform-react-remove-prop-typesというプラグインを使う必要がある。

@.babelrc
{
  "plugins": [
    ["babel-plugin-transform-react-remove-prop-types", { "mode": "remove" }],  // <- Add
  ],

modeについては remove / wrap / unsafe-wrap の3つがある。
詳しくは下記で紹介されているがデフォルトはremove。
GitHub: babel-plugin-transform-react-remove-prop-types

原因の推測

これがnode_modules配下のみ効いていないということなのだから怪しいのはwebpackのloaderの設定の箇所だと絞り込める。

@webpack.config.dev.js 修正前
modules: {
    {
        test: /\.(js|jsx|mjs)$/,
        include: [paths.appSrc],
        exclude: /node_modules/,
        loader: require.resolve('babel-loader'),
        options: {
            presets: ['react-native'],
            plugins: ['react-native-web'],
            cacheDirectory: true,
            babelrc: true,
        },
    },
}

上記だとnode_modulesをbabel-loaderの対象外としている。
そこでPropTypesを使用しているnode_modules配下のライブラリを直接指定してbabel-loaderの対象としたい。

解決方法

これを解決するためには2つの手順が必要だった。

  • paths.jsに対象のライブラリを追加
@paths.js
module.exports = {
    appRNNodeModules: resolveApp('node_modules/react-native-material-design'),
    appRNNodeModules2: resolveApp('node_modules/react-native-vector-icons'),
}
  • includeにpathsに追加した名前を追加
include: [paths.appSrc, paths.appRNNodeModules, paths.appRNNodeModules2],

つまづいたのはここの記述方法

調査の最中そこかしこで下記のような記述をみかけた。
exclude: /node_modules/,

「node_modulesだけはwebpackがコンパイル後もきっとわかるということなのだろう」と思いここで
include: [paths.appSrc, /node_modules/react-native-material-design],

のように記述していたがこれが動かなかった。

公式ドキュメントを見ても詳しく記述方法に注釈がなかったので(もしかしたら最初から読めばお作法として書かれていたのかもしれない)ずっとはまっていた。

もしここについて詳しい方がいたら、また記事の内容に間違いがあれば気軽にコメントお願いします。
まさかり歓迎という言葉が好きではないので、バッテリー募集(投げてくれたらしっかり受けめるよ)と言っておきます⚾️

最後に

これについて、react nativeではPropTyepsはなくす方向なのに未だにPropTypesを使っているような活発でないreact-native-material-designは使うべきではないのではという話になった。
その際下記のnpmモジュールの利用率を調べることができる便利なサービスを見つけた。

www.npmtrends.com

推移的依存関係が原因のバージョン競合によるAndroidのビルドエラーと解決策

エラー詳細

RN制のアプリのAndroidのビルドができなくなった。
ビルド時に出力されるエラーは下記

```
org.gradle.api.resources.ResourceException: Could not get resource 'https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-core/16.0.5/firebase-core-16.0.5-javadoc.jar'.
    at org.gradle.internal.resource.ResourceExceptions.failure(ResourceExceptions.java:74)
    at org.gradle.internal.resource.ResourceExceptions.getFailed(ResourceExceptions.java:57)
    at org.gradle.internal.resource.transfer.AccessorBackedExternalResource.withContentIfPresent(AccessorBackedExternalResource.java:146)
    at org.gradle.internal.resource.BuildOperationFiringExternalResourceDecorator$11.call(BuildOperationFiringExternalResourceDecorator.java:237)
    at 
    ...
```

build.gradle(Module.app)では下記のようにfirebase-coreは16.0.4を指定している

dependencies {
    implementation "com.google.firebase:firebase-core:16.0.4"
}

ところがMVN レポジトリを見る限り、firebase-coreの最新版16.0.5は存在するが依存関係の指定通りのバーションを取得しない不具合が起きている。

対応と解説

バージョンのミスマッチを防ぐ為に強制的にfirebase-coreのバージョンを指定する

configurations.all {
    resolutionStrategy {
        force "com.google.firebase:firebase-core:16.0.4"
    }
}


configurations
Gradleにおいて依存関係をグループ化したもの。

resolutionStrategy
依存関係のバージョンが競合を起こした場合は、resolutionStrategyを使い、ビルドスクリプトで全てのバージョン競合をユーザーが明示的に解決することができる。

公式ドキュメントではある依存関係がさらに依存する関係のことを「推移的依存関係」と呼び、これによる生じるバージョンの競合はそんなに稀な問題でもないらしいのでこの現象と解決方法は覚えておきたい。

参考サイト
Gradle 公式ドキュメント
- 8.3 依存関係のコンフィグレーション
- 51.2.3 バージョン競合の解決策
Gradleプロジェクトで依存関係の競合を解決する | GuildWorks Blog
Gradleで陥りやすい問題点の解決策TIPS集 - 猫好きモバイルアプリケーション開発者記録

react-native-domを自社アプリに組み込んでみた

ログイン画面を表示して実際にログインができることをゴールに見据えて挑戦しました。

INDEX

  • できたこと
  • できなかったこと
  • わかったこと
  • 組み込んだ方法
  • react-native-domのプロダクション採用について

できたこと

アプリで使用されている画面をそのまま表示

f:id:yuri_iOS:20180929160229p:plain
コンポーネントを表示すること自体は問題ありませんでした。
ただ、react-native-vector-iconsはテキストと認識され正しくアイコンが表示されません。
(アプリのロゴとパスワードリセットテキストの手前)

これが該当部のエレメントです。
rct-raw-textというタグ名で"□"というテキストになっています。

<rct-raw-text style="position: static; background-color: rgba(0, 0, 0, 0); contain: style; box-sizing: border-box; user-select: inherit; overflow: visible; display: inline; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased;"></rct-raw-text>


また、スタイルでデバイスのサイズを取得して画像にサイズを当てている部分については、webではソースを読み込んだ時点でのwindowのサイズが使われます。*1
下の画像をよく見ると、背景画像の領域が左右で異なっています。
読み込み後にwindowを広げると黒い背景が現れています。
f:id:yuri_iOS:20180929163731p:plain

justifyContentやalignItemsのcenter指定はwindowのサイズを変えても変化に応じて柔軟にセンターに配置されます。

Reduxのデータを扱う

Reduxの組み込み、具体的には下記を確認しました。
・store
・bindしたアクションの実行(actions.function())
・dispatch
・redux-logger
f:id:yuri_iOS:20180929162109p:plain
ただし、アプリで使用しているreducerをそのまま使用することはできず、web用のreducerが別途必要になりました。
これについては後述します。


できなかったこと

TextInputが使用できません。
具体的にはフォーカス状態にならないため文字入力ができません。
これは既にissueにも上がっています。
TextInput support · Issue #37 · vincentriemer/react-native-dom · GitHub

これは「できないこと」の氷山の一角だとは思いますが、これに気が付いた時点でreact-native-domでアプリのweb版を作ることは断念しました。
(もともと本命だったreact-native-webでやります)


わかったこと

package.jsonのdependencies見ると全部で55のモジュールを使用しているのですが、そのうち下記モジュールに関してモジュールをimportするだけでwebがビルドできなくなります。

・react-native-adjust
・react-native-fabric
・react-native-permissions
・react-native-scrollable-tab-view
・react-native-simple-toast
・react-native-zendesk-support

エラーにはいくつかタイプがありますが下記2つが多いです。

Module AppRegistry is not a registered callable module (calling runApplication)
blob:http://localhost:8081/8b78e274-51be-41e7-8e5e-f0246cb9df42:15 Uncaught DOMException: Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at 'http://localhost:8081/dom/entry.bundle?platform=dom&dev=true&hot=true' failed to load.

おそらくimportした時点でネィティブにアクセスして何かしらの処理をしているモジュールが共存できなさそうです。
なのでこれらのモジュールを読み込んでいるファイルは軒並みwebは分ける必要があります。


組み込んだ方法

上記のネィティブにアクセスするモジュールがwebで読み込まれるファイルに記載されていてはいけないということから、いくつかのファイルをweb用に新しく作成しました。

1. Entry
大元のコンポーネントを登録する処理を書くファイル

import { AppRegistry } from 'react-native';
import Root from './src/containers/rootForWeb';

AppRegistry.registerComponent('TeamHub', () => Root);

また、dom/entry.jsにweb起動時に最初に読み込むファイルを指定できるので上記ファイルを読み込むよう修正

2. Provider
先ほどのAppRegistryに登録されるコンポーネント

const store = configureStore();
loadTranslations();

export default class Root extends Component {
  constructor(props) {
    super(props);
    if (I18n.locale === 'ja-JP') {
      require('moment/locale/ja');
    }
  }

  render() {
    return (
      <Provider store={store}>
        <WebApp />
      </Provider>
    );
  }
}

3. App
仮にログイン画面をそのまま返すようにしてあります。

export class WebApp extends Component {
  constructor(props) {
    super(props);
    props.actions.requestApp();
  }

  render() {
    return (
      <Login {...this.props} />
    );
  }
}

export default connect(
  state => ({ ...state }),
  dispatch => ({
    actions: bindActionCreators({
      ...AppActions,
      ...AuthActions,
    }, dispatch),
  }),
)(WebApp);

4. reducer
storeに入れるモジュールを結合しているのでどこか1つでもimport時にネィティブにアクセスするモジュールがあってはビルドできません。
基本モバイルと同じですが、ログインなどのアクションに応じて記録を取るためにAdjustなどを使用しているauthなどは別を用意して一部web用のファイルを読み込むように修正しました。

import { combineReducers } from 'redux';
import { modelReducer, modeled } from 'react-redux-form';

import app from './app';
import route from './route';
import auth from './authForWeb';

export default combineReducers({
  app,
  route,
  auth: modeled(auth, 'auth')
});

5. auth
上記でも触れた通り、ログイン時の処理は裏側でモバイルアプリ特有の処理もたくさん走っているのでauthはまるごとweb用に新しく用意しました。


react-native-domのプロダクション利用について

TextInputの改善なくしてプロダクション採用はありえません。
しかし、バージョン0.57が出た時は対応も早かったですし、コミット状況からも開発はまだまだしっかり行われていると思うので期待寄りの保留としようと思っています。

*1:const { width, height } = Dimensions.get('window');を使用した画像のサイズ指定は読み込み時のwindowサイズで固定

XcodeのReact NativeにおけるBuild Phases

f:id:yuri_iOS:20180922175648p:plain

XcodeのArchiveビルドが私だけできないという現象に遭遇し、原因がBuild Phasesにあったのだがその際にBuild Phasesの処理を概要だけでも知っておこうとまとめた。

React NativeアプリのBuild Phasesは下図のようになっていて全部で10 項目ある。
f:id:yuri_iOS:20180922161157p:plain

各項目でどんな処理が行われているか順を追って見て行く。

Target Dependencies

アプリで使用されるフレームワークはそれを使用するアプリよりも先にビルドされるよ必要があり、これを指してdependency(依存関係)と呼ぶ。
下図のように自作したライブラリのプロジェクトを使用するような場合に、どちらを先にビルドするかXcodeに教えてあげる際に使用される。
f:id:yuri_iOS:20180922143740p:plain
xcode target dependencies between two projects - Stack Overflow

[CP]Check Pods Manifest.lock

Pods Manifest.lockはPodfile.lockのコピーであり、pod installをする度に生成される。
Pods Manifest.lockとPodfile.lockに差分があるとエラーが出力される。

diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
if [ $? != 0 ] ; then
    # print error to STDERR
    echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
    exit 1
fi
# This output is used by Xcode 'outputs' to avoid re-running this script phase.
echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"

Podsディレクトリは常にバージョン管理下にあるわけではないので、開発者が実行前にポッドを更新するように教えるための仕組み。

Bundle React Native Code And Images(1つめ)

React NativeのバグなのかBundle React Native Code And Imagesは2つ存在し、しかも1つ目では手前のCheck Pods Manifest.lockで行っているのと同様にPods Manifest.lockとPodfile.lockの差分を確認している。
f:id:yuri_iOS:20180922145627p:plain

Compile Sources

ライブラリはリンクの仕方によって,Static LibraryとDynamic Libraryに分類できる。
Static Libraryはアプリケーションのコンパイル時に組み込まれる形で(静的に)リンクし、Dynamic Libraryはアプリケーションの実行時にローダがライブラリを検索し,(動的に)リンクするという違いがある。

ここではコンパイルする静的ライブラリの中でも実行ファイルであるAppDelegate.mとmain.mが指定されている。

Link Binary With Libraries

ここも静的ライブラリのコンパイルするものが並んでいるがライブラリの静的ファイルを扱っている。

Copy Bundle Resources

フォントや画像など、bundleしたアプリで使用する素材たちが列挙されている。

Bundle React Native Code And Images(2つめ)

ここではnode_modules/react-native/scripts/react-native-xcode.shを実行し、その中でjsのコードと手前でコピーした画像をbundleする処理を行っている。

react-native-xcode.sh

コード
react-native/react-native-xcode.sh at master · facebook/react-native · GitHub

今回私の環境でArchiveビルドができなかった原因としてnvm lsコマンドでインストールされているバージョンを見ることができるがここのdefaultが設定されていなかった。
(default設定後)
f:id:yuri_iOS:20180922144056p:plain

react-native-xcode.shの中でnodeを設定しているがここら辺で問題が起きた様子。
ちなみにシミュレーターだとビルドできていたのは下記のコードによるもの。

if [[ "$PLATFORM_NAME" == *simulator ]]; then
      if [[ "$FORCE_BUNDLING" ]]; then
        echo "FORCE_BUNDLING enabled; continuing to bundle."
      else
        echo "Skipping bundling in Debug for the Simulator (since the packager bundles for you). Use the FORCE_BUNDLING flag to change this behavior."
        exit 0;
      fi
    else

run script

ここでもreact-native-xcode.shを実行している。
同じ処理をしてしまっている項目については削除しても問題ないかもしれないしビルド時間が短縮されそうなので今後上司に聞いてみたい。

[CP]Copy Pods Resources

Pods-AppName-resources.shを実行
bundleしたライブラリたちをインストールした後にシンボリックリンクをコピー

if [[ "$CONFIGURATION" == "Debug" ]]; then
  install_resource "${PODS_ROOT}/FBSDKCoreKit/FacebookSDKStrings.bundle"
  ・・・
fi

mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then
  mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
  rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
fi
rm -f "$RESOURCES_TO_COPY"

[CP]Embed Pods Frameworks

Pods-AppName-frameworks.shを実行
おおよそ上と同じことをframeworkを対象にしている。

まとめ

1.ライブラリを使用する前にその管理ファイルであるPods Manifest.lockを確認
2.アプリの大本となるAppDelegate.mやmain.mをコンパイルしてバイナリファイルを生成
3.使用するライブラリのバイナリファイルを紐づける
4.アプリで使用する素材系のbundleされたデータをコピー
5.JavaScriptのコードをbundle
JSはスクリプト言語なのでコンパイル不要bundleのみ
6.bundleされたライブラリをコピー
7.フレームワークを埋め込む

コンパイルする必要があるものをコンパイル
その後で既に提供されているであろうライブラリのバイナリファイルを紐づけ、
その後に素材やJSなどのコードをbundle

ライブラリのコンパイルやbundleをしているところが見当たらないのでライブラリの方で既に作成して提供してくれているのかもしれない。

React Native 0.57 アップデート

react-native-git-upgradeコマンドが使えない

起こったこと

コマンドを打ったところでエラーが出力される。

git-upgrade ERR! An error occurred during upgrade:
git-upgrade ERR! Error: Plugin 0 specified in "/Users/yuri/Documents/src/LinkSports/teamhub/node_modules/babel-preset-react-native/index.js" provided an invalid property of "default" (While processing preset: "/Users/yuri/Documents/src/LinkSports/teamhub/node_modules/babel-preset-react-native/index.js")
それに対し行なったこと(またそれを行なった理由)

react-native-git-upgradeコマンドが起こすエラーについてはいくつかissueが立ってはいるがreact-native-git-upgradeコマンドが失敗する理由とその解消法について言及するものは見当たらなかった。

しかしchangelogの中にUpdating to this versionという項目があり、こちらではreact-native-git-upgradeコマンドを使わない方法でのアップデート方法が記載されていた。

公式ドキュメントではreact-native-git-upgradeを使ったアップデート方法が記載されており、どちらが正しいか判断できなかったがうまくいく方でやればいいかと考えchangelogのUpdating to this version方法に切り替えた。


具体的に実施したのは下記の通り
・package.jsonのreact-nativeとreactのバージョンをそれぞれ^0.57.0と16.5.0に書き換える
(この時react-domは基本reactとバージョンをあわせた方が良いというアドバイスをもらったのでreact-domも16.5.0に書き換えた。)
・babel-preset-react-nativeを削除し、代わりに"metro-react-native-babel-preset": "^0.45.0"を追記
・.babelrcのpresetsを下記の通り修正

{
    "presets": ["module:metro-react-native-babel-preset"]
}

・@babel/coreを7.0.0に変更
・yarnをinstallし、yarnを実行

react / react-dom 16.5.2

起こったこと

react と react-dom のバージョンを16.5.2でビルドするとJSのビルドで下記エラーを出力

error: bundling failed: Error: Unable to resolve module `schedule/tracking` from `/Users/yuri/Documents/src/LinkSports/teamhub/node_modules/react-native/Libraries/Renderer/oss/ReactNativeRenderer-dev.js`: Module `schedule/tracking` does not exist in the Haste module map
それに対し行なったこと(またそれを行なった理由)

react と react-dom のバージョンを16.5.0に下げたら解消
エラーをぐぐったら16.5.1で解消したという投稿がったので16.5.1に落としてみた。
bundling failed: Error: Unable to resolve module `schedule/tracking` · Issue #21140 · facebook/react-native · GitHub
しかしこれでも同じエラーが出た。
さらに16.5.0に下げたら起こらなかったので深掘りせず16.5.0のバージョンを固定で使うことで落ち着いた。





今回はRN本体のアップデート自体とは関係ないがアップデート作業で得た知見もまとめる。



raect-native-twitter-signin

いいかげんこのライブラリの開発者には少しオコだ。

起こったこと

パッケージャーを起動し、BUNDLE中に下記エラーが出力

error: bundling failed: Error: jest-haste-map: @providesModule naming collision:
  Duplicate module name: react-native
それに対し行なったこと(またそれを行なった理由)

raect-native-twitter-signinのpackage.jsonにてDependenciesに今回アップデートした0.57.0とは異なるバージョンのreact-nativeを使用していた。
これによりreact-nativeが複数存在してしまいエラーを起こしていた。

package.jsonのDependenciesの理解についてはこちらの記事が参考になった。
ちゃんと使い分けてる? dependenciesいろいろ。
package.jsonのpeerDependenciesについての理解 - Flicker's Style++

プラグインモジュールではdevDependenciesに使用するモジュールを記載してくれれば親アプリでnpm installした時にdevDependenciesに記載されているモジュールはinstallされないので親アプリと重複してしまうことはない。

結果チームのメンバーにraect-native-twitter-signinをforkしてもらい、Dependenciesを空に修正した上でそちらのモジュールを使用するようにすることで問題を解消できた

react-native-pdf

起こったこと

react-native-pdfのバージョンを5.0.1から5.0.5にあげるとXcodeのビルドで存在するはずのファイルを参照してなぜかそんなファイルないよと参照エラー

それに対し行なったこと(またそれを行なった理由)

pod installをした際にこの差分が気になっていた。
react-native-pdf直下に記載されていたReactの記述がなくなっている。
f:id:yuri_iOS:20180921123918p:plain

Xcodeを見るとPods配下のDevelopment Podsフォルダ配下にあるreact-native-pdfが同じDevelopment Podsフォルダ直下のReactのファイルを参照しようとしてエラーが起きていた。
f:id:yuri_iOS:20180921132940p:plain

devDependenciesでreact-native-pdfがreactのバージョンを指定して使用していたのかと思っていたがpackege.jsonの履歴を見ても特に変更なく、結果原因はわからなかった。

最終的に元使っていたバージョンに戻すことで解消した。

React Native0.56アップデート(失敗した選択たちとその選択理由・正解の解説)

React Native本体のbreaking changeなアップデートを始めて自分で担当した。
その道中たくさんの選択ミスをした。
とても大変だったので2度と苦しまないよう

  • 起こったこと
  • それに対し行なったこと(またそれを行なった理由)
  • 正解だった選択肢とその理由

の3点を時系列に記して行く。

第一の壁 アップデート方法

いつもはreact-native-git-upgradeを使っている。

起こったこと

ものすごく差分が出た。

それに対し行なったこと(またそれを行なった理由)

差分が多すぎる上に何が変更されたのか差分を見て理解できなかった。
アップデート作業を担当する以上、後々の不具合に備えて何が変更されたのかを自覚しておくべきなのでまずはchange logを読んでどんな変更が行われたのかを把握し、記事を検索して差分を見ながら手動で差分を当てるという手法を選択した。

ビルドして失敗する理由がライブラリのせいである可能性も考えると、原因の追求先を減らすためにまずは本体(react-nativeとreact)だけアップデートし、ビルドした上で動作確認してライブラリを1つずつ確認していこうという方針が安全だと自分では思った。

正解だった選択肢とその理由

react-native-git-upgradeを使うこと。
上記のやり方が間違っているというわけではないが、「アプリは動くが他のライブラリが互換性的に最適とはいえないバージョンのまま」という状態ができあがった。

当時かなり多くのライブラリが最新バージョンではなかった。
これを解消しようと他のタスクに「ライブラリのアップデート」というタスクを切ってあったので、今回は本当に安全にReact Native本体のバージョンだけをあげ、そこの安全確保ができた上で1つずつライブラリをあげる作業をすればいいやという考えで、その目的は果たしていたが、そこまで臆病にならずともreact-native-git-upgradeをした方が効率良いということだった。

また今回はreact-native-git-upgradeによるコンフリクト解消は一緒にみてもらった。

第二の壁 Babel

起こったこと

・Babelに必要なモジュールがなくてビルドできない。
・Babel7になったことでライブラリ内の構文がBabel7のルールに即していないと怒られてビルドできない。

// エラー内容
SyntaxError: A trailing comma is not permitted after the rest element

// 該当箇所
// 1行で記述する際に末尾に","があると怒られる。
const {shouldUpdated, ...props, } = Props;
それに対し行なったこと(またそれを行なった理由)

・下記モジュールを追加した。

babel-preset-react-native v5.0.2
@babel/core v7.0.0-beta.47
babel/core v^7.0.0-bridge.0

・Babel7に対応してないライブラリも対応できるようになるとchange logにも紹介のあるbabel-bridgeの導入を試みた。

正解だった選択肢とその理由

babel-preset-react-nativeと@babel/coreに関してはchange logに記載があったがbabel-coreも必要だった。
しかもバーションは微妙に異なる。
なんとなくbabel/coreと@babel/coreはバージョンを揃えるもんだと思い込みもあって記事の通り両方v7.0.0-beta.47にしようとして「babel/coreはそんなバージョンないぞ」と怒られた。
npm outdate コマンドを打てばbabel/coreにv7.0.0-beta.47が本当にないことがわかったのでwantedのバージョン(v7.0.0-bridge.0)を指定したらうまくいった。
(しかしこれに関してはどこかのissueの回答にも両方v7.0.0-beta.47にしろと書いてあったのも頑なにそうだと思い込む一因となってしまった)

途中で気が付いたが上記のエラーが起きたライブラリのgithubをのぞいたらどれも構文(RN0.56)に対応してくれていた。
結論、該当ライブラリを最新にアップデートをするだけでよかった。
この時点でbabel-bridgeの必要は無くなったので導入しなかった。
そもそも先に各ライブラリがRN0.56に対応しているかどうかを調べるということをしていれば時間を浪費しなくてすんだ

第二の壁 Jest

起こったこと

・jestが実行できない
・setTimeout関数を使うライブラリで`TypeError: _setter is not a function`エラー
・mockの方法が変わった

それに対し行なったこと(またそれを行なった理由)

ここは分担して主に他のメンバーにやってもらった。

正解だった選択肢とその理由

・jestが実行できない問題の解消
jestもBabel7の影響を受けているようでbabel/coreと@babel/coreを7のバージョンを使うこと、またReact Native内部のjestプロセッサーを使うようにすることで解消ができた。
具体的には下記のコードをpackage.jsonに追記した。

"jest": {
    "transform": {
      "^.+\\.js$": "<rootDir>/node_modules/react-native/jest/preprocessor.js"
    },
}

・setTimeout関数を使うライブラリで`TypeError: _setter is not a function`が多発
JavaScriptのコードはオブジェクトに格納されている必要があり、その格納するオブジェクトの最上位に位置する単一のオブジェクトはグローバルオブジェクトと呼ぶが、これはWebブラウザでの実装の場合「window」、Node.jsの場合は「global」がグローバルオブジェクトとなる
setTimeoutを提供してくれているreact-timer-mixinの中でこのグローバルオブジェクトを設定する処理があった。

var GLOBAL = typeof window === 'undefined' ? global : window;

package.jsonに下記の記述をしていたためグローバルオブジェクトがwindowに設定されてしまっていた。
この記述をまるっと削除することで解決した。

"globals": {
      "window": {
        "navigator": {
          "userAgent": false
        }
      }
    },

・mockの方法が変わった
今まで1つのテストファイルの頭でmockを用意している箇所があった。
これが効かなくなったライブラリがいくつかある。
対応としてはテストファイルの頭でしていたmockをマニュアルモックとして__mocks__配下につくりなおした。

// 削除
jest.mock('shortid', () => ({
    generate: () => 'shortID_mock',
}));

@__mocks__/shortid.js

// 新規作成
export default {
    generate: () => 'shortID_mock',
};

第三の壁 react-native-scrollable-tab-view

起こったこと

0.56アップデートとは直接関係なかったが番外編として。

自社のプロダクトでは様々なケースでreact-native-scrollable-tab-viewを利用している。
左側はタブビューが入れ子になる場合。
右側はタブビューの中にtextIputが大量にあり、抱えている情報が大量になる場合。
f:id:yuri_iOS:20180907002307p:plainf:id:yuri_iOS:20180907002317p:plain
これらに対してreact-native-scrollable-tab-viewとreact-native-scrollable-tab-view-latestという元が同じライブラリから派生したライブラリを画面ごとに使い分けていた。

この2つのライブラリが両方ともbabel7の構文にひっかかって修正する必要があった。

それに対し行なったこと(またそれを行なった理由)

react-native-scrollable-tab-viewの本体が0.56対応して更新されていたのでアップデートが必要なことに加え、この期に統一を図ろうとした。

正解だった選択肢とその理由

実はこのreact-native-scrollable-tab-viewは入れ子にして使うことは非推奨であり、本体の方はそれを考慮したつくりになっていないためにタブビューを入れ子にするとandroidで不具合が起きる。
これに対応するためreact-native-scrollable-tab-viewはandroid入れ子にしても動くような修正をした今まで使っていたライブラリを構文だけ修正して使う必要があり、textInputが大量にあって画面で抱えるデータが多くなる画面ではパフォーマンスが改善されている本家最新版を使った方が良いのでこれもまた今まで使っていたライブラリを構文だけ修正して使うことになった。

問題だったのは、担当した私が「なんでこれわかれてるんだっけ?」状態であったこと。
わからないのでひとまず本家最新にして動作確認するところからはじめたので不具合が生じたときにreact-nativeのバグなのか、react-native-scrollable-tab-viewのバグなのか、はたまた分けているライブラリの意図を自分が汲めていないのかでさんざん時間を使って調査・検証して頭を悩ませるはめになった。

ここでは正解というより、適切にコメントを書くことは大事という教訓を得た。

第四の壁 Twitter

起こったこと

・端末にTwitterアプリがない状態でTwitterサインインやログインができない
 (以前はWebViewが開いてサインインもログインもできた。)

iOSアーカイブビルドのみfail

clang: error: no such file or directory: '/Users/MyUser/react-native-twitter-signin/Example/ios/build/Build/Products/Release-iphonesimulator/libRNTwitterSignIn.a'
それに対し行なったこと(またそれを行なった理由)

Twitterログインの問題は他のメンバーが担当。
アーカイブビルドに関しては古いキャッシュが残っているのだろうと下記キャッシュの削除を一通りおこなった。

・rm -rf node_modules/
・rm -rf ios/Pods
・npm cache clean --force
・npm install
・pod install
・npm start -- --reset-cache
・XcodeでBuild Clean
正解だった選択肢とその理由

・端末にTwitterアプリがない状態でTwitterサインインやログインができない
これはアプリ側ではなく、開発者アカウントの設定の問題だった。

iOSアーカイブビルドのみfail
Xcode > Library配下のRNTwitterSignin.xcodeprojとPods.xcodeproj > Pods配下のTwitterCoreとTwitterKitがバッティングを起こしていた。
そもそもreact-native linkの挙動が変わったことが原因となる。
以前のreact-native linkではXcode > Library配下にRNTwitterSignin.xcodeprojが生成されていたのだが、いつからかPodsfileに下記記述が加わるようになった。

pod 'react-native-twitter-signin',: path => '../node_modules/react-native-twitter-signin'

以前他のネィティブモジュールでも似たようなことがあったのだが、xcodeprojとPodsのモジュールはどちらかがあればよいらしく、むしろ両方あるとどこかでバッティングを起こしビルドできないというケースを引き起こす。
README.mdにしたがって書かれている手順をそのまま実行してライブラリを入れているが、そこに書かれたことを行なってどうなればよいのか。
こうあるべきを知らないが故に解決できなかった。

クロスプラットフォーム開発をするということは両方のネィティブの知識がある程度ないとできないということを痛感した。

大きなアップデートがあった時は古いものは基本消したほうが良いというのと、キャッシュクリアだけではなく今後はxcodeprojとPods配下にも目を配るようにしたい。

第五の壁 Podfile.lock

起こったこと

忘れてしまったがなんかpod installがうまくいかなかった?

それに対し行なったこと(またそれを行なった理由)

ここらへんではもう心がおれかけていたのでまた特に考えずキャッシュクリアをしていた。
ただライブラリやモジュールのバージョンを変えない方が良いだろうとxx.lockファイル系は削除せずにライブラリやモジュールを取得し直した。
(これの前にpackage.jsonを削除して必要ないものまでアップデートしてしまってjestが失敗するということがっあったこともあって、絶対に.lockは消さない方が良いと思い込んでいた。)

正解だった選択肢とその理由

xxx.lockは他の開発メンバーがまったく同じ環境でライブラリをインストールできるために使うのであって、だれかのブランチを持ってきたときはもちろん消さずにlockファイルを頼りにinstallするべきだが、RNのアップデートでは古いバージョンをひきづっていることによる不具合の方が考えられるので削除して各種新しいバージョンで入れ直した方が良い。



RNアップデートは初めてということもあるがネィティブに詳しくなかったり過去の修正を記憶してないと大変。
他にも
・何度もやっているとnpm installが遅くなる
・./gradlew cleanBuildCacheがやたら時間かかる
・./gradlew cleanBuildCache後のAndroid Studioがビルドできるまでにやたらめった時間がかかる
など時間もかかるし1回の検証に時間かかる割に経験が少ない故に原因の追求先が多くて、あえて一人でやりきろうと抱え込むのは学習のコスパ悪かった気がする。


ただ次は3割増くらいにはうまくできるのではないか。