🔬

React Nativeの `require` を挙動から深掘りした

2020/10/03に公開

はじめに

みなさん require('./relative-path') してますか?
ありがちなのはロゴ画像みたいな静的PNGを読んでアプリ内に出すとかでしょうか?

<Image source={require('./logo.png')} />

ところで、デバッグ環境(Metroを接続して行う開発)において、
require(./logo.png) の返値をconsole.logなどで見ると、単に整数値が返ってきます。
2とか4とか、おおむねファイル数に比例してカウントが増えていくような気がします。

単にReact Nativeの標準コンポーネントを使うだけなら、この謎を解明する必要はないのですが、
requireのシステムを利用したカスタムコンポーネントやNative Moduleを開発したくなったので、
少し調べてみることにしました。

Image.resolveAssetSource()

まず、なんとなくですが、Androidなどの R.res.logo のようなRクラスに近い気もします。

となると、Androidでも用意されているような、ResIDからBitmapインスタンスやResourceインスタンスを得るような仕組みがあるはずです。

結論から述べると、 react-native が提供する Image コンポーネントが提供するメソッド Image.resolveAssetSource(resID) をコールすることで、オブジェクトを得られます。

iOSにおいてMetroを接続した環境、つまりdebugビルドでは以下のデータを返しました(見やすくするために改行を加えています)。

{
	"__packager_asset": true,
	"width": 1000
	"height": 1000,
	"scale": 1,
	"uri": "http://localhost:8081/assets/example/booklist-test.png?platform=ios&hash=6ffab54f1ce2fd718f05f696f0ed9fb7",
}

iOSにおいてMetroを介さないreleaseビルドの場合はこちら(XcodeのOutputで確認したJSONではない)

{ 
  __packager_asset: true,
  width: 1000,
  height: 1000,
  scale: 1
  uri: 'file:///Users/keima/Library/Developer/CoreSimulator/Devices/EE8E1339-DC30-4948-BE39-020E60874379/data/Containers/Bundle/Application/B915DC08-FD21-4528-A4E9-F76C9A071CEC/example.app/assets/example/booklist-test.png',
}

React NativeのImageコンポーネントでネットワーク越しのファイルを取得する場合に以下のように実装するようなサンプルがありますが、ここでsourceに渡している型と、resolveAssetSource() が返す型に共通点があります。

<Image source={{
    uri: 'https://reactnative.dev/img/tiny_logo.png'
}} />

なお、このオブジェクトフォーマットが今後も同じである保証はないようです。
今後画像がスプライト形式(ひとつの大きな画像ファイルの矩形エリアを指す形)で返すようになって、Imageコンポーネントなどがそれをサポートしたら、単にrequireを使うだけなら影響はないけれど、requireの返すデータに依存した仕組みになってる場合は知らんぞ・・・って感じかと思います。

というわけで謎解明!

なお、3rd partyライブラリでの実装例としては、react-native-videoがよく出来ていると思いました。

https://github.com/react-native-community/react-native-video/blob/5683167a3a8e6a1d7d49526c995cf253def48e67/Video.js#L265

応用例

  • file:スキーマなURLでPDFファイルを開く(こちらで用意したNative Moduleを使う)サンプルアプリを実装したい
  • React Nativeのファイル管理手法で解決したいと思った
    • 例えばAndroidならassetsフォルダに置くとか、iOSならBundleリソースとして管理するといった方法があります
    • しかし、今回の要件では、「ダウンロードしたPDFを開く」為、アプリからアクセス出来るフォルダにPDFファイルを置く必要があります。
    • 検証環境では「ダウンロードする」という手順は要らないので、どこかに置かれたファイルをアプリからアクセス出来るフォルダにコピーすることで擬似的に再現することにしました
    • そのためにrequireでPDFを取得し、アプリからアクセス出来るフォルダにコピーする、、、ということをしたかったのですが、さてどうやって実現しよう?

結果として、debug環境ではネットワーク経由でのダウンロード処理が行われ、release環境ではfileスキーマによるコピー処理が行われるようにすると良いだろうという結論に至りました。
実装例を以下に示します(いろいろ雑かも知れませんがサンプルアプリでのみ使っていると言うことでご容赦ください)。

import { Image } from 'react-native';
import RNSF from 'react-native-fs'

...

const FILEPATH_PDF = `${RNSF.DocumentDirectoryPath}/file.pdf`

...

const fileExists = await RNSF.exists(FILEPATH_PDF)
console.log('file status: exists:', fileExists)
if (!fileExists) {
    // store file
    const pdf = Image.resolveAssetSource(require('./file.pdf'))
    console.log('pdf:', pdf) // <- resolveAssetSource のデータを見る
    const uri = pdf.uri
    if (uri.match(/^https?:/)) {
        try {
            await RNSF.downloadFile({
                fromUrl: uri,
                toFile: FILEPATH_PDF,
            }).promise
        } catch (e) {
            console.warn('OpenPDF:', e)
            return
        }
    } else if (uri.match(/^file:/)) {
        try {
            await RNSF.copyFile(uri, FILEPATH_PDF)
        } catch (e) {
            console.warn('OpenPDF:', e)
            return
        }
    }
}

実際、あまり使わないかも知れませんが、知っておくといつか役に立つ・・・かも?

以上です。

Discussion