Using React Native, React Native Web and React Navigation in a single project

TL;DR combination of react-native and react-native-web can be used on one project to build native and web apps with a common rendering codebase. It doesn’t quite work “out of the box” but it is possible to get there with a handful of tweaks.

I’m looking for a way of delivering native mobile apps for Android, iOS as well as a mobile web client with minimal code duplication.

One candidate was a hybrid application on each platform and most of the functionality delivered in a Webview. I rejected that as it is hard to make the webview feel native without infeasible amounts of wheel re-inventing or putting all of the navigation into the native app so that it becomes huge and defeats the object.

Flutter, a new cross platform mobile project from Google which is still in alpha looks very promising but had to be ruled out because it has no web target currently and in general it is so new that it doesn’t really have much of an ecosystem (read: way too much stuff is currently missing for my needs).

Next try was react-native. Reusable React components could target native mobile using react-native and then build a web based React app for the mobile web target. Researching further I found react-native-web and the picture is complete (well sort of, more on that later!).

So the objective is now to build an app in react-native, using vanilla react-navigation so that it can also be targeted at mobile web via react-native-web. There are of course lots of limitations as react-native-web doesn’t have direct support for many components that exist within react-native, but through the technique of aliasing react-native-web to react-native in a webpack configuration, it looked at least minimally possible.

I did get there, but there are some hurdles along the way. I’ve written this up as I didn’t find any really accessible solutions elsewhere to some of problems that I encountered so figured I may as well seed Google with some answers.

Hopefully the principles will help somebody but the detail of the whole post will quickly become out of date as the modules get revised and issues get fixed so I should declare the environment it applies to:

react-native@0.53.0
react-native-web@0.4.0
react-navigation@1.0.3
react@16.2.0

At this stage I’m simply trying to prove to myself that this is a viable approach before investing any more time in it. I could submit pull requests to the various projects involved (there are several), but I’m a React neophyte, and I doubt that I have come up with the right solutions so I probably wouldn’t be taken seriously.

Getting Started

Yarn seems to work better than npm for really big dependency trees so to install react-native:

yarn global add react-native-cli
react-native init native_plus_web
cd native_plus_web
yarn install

I’m also using react-navigation, and a modal pop-out selector so:

yarn add react-navigation
yarn add react-native-modal-selector

The react-native init command installs a demo App.js in the root of the project. If you have xcode, or Android studio installed and want to check everything is working OK, you could just do the following at this point.

react-native run-ios
react-native run-android  

Code
All of the code for the test project is on github at

https://github.com/rjp44/native_plus_web

image.png

I’m using a pattern for this application where all of my code sits in an app subdirectory, so I remove App.js from the top level and replace it with an index.jswhich just does an import of the top level App component from app/index.js

The index.js top level handles navigation using a StackNavigator instance from the react-navigation module. Screens and components sit in sub-directories and common config like the stylesheet sits in config (maybe that should be something different).

So far, so good, I have a working iOS and Android build of my application:

image.png

Adding React Native Web

This is where things start getting a little harder, we already have a react-native project, so the create-app setup script won’t work for us.

I went with the Web packaging for existing React Native apps section of getting started which suggests this (the babel-polyfill was an addition I spotted I needed later):

yarn add react react-dom react-native-web
yarn add --dev babel-loader babel-polyfill url-loader webpack webpack-dev-server

There is a suggested webpack.config.js which was a good starting point but did need a bit of work. This is noted in the instructions, so you have to be prepared to understand the webpack process and put some effort in to go down this route!

Basically any modules that make use of react-native are transpiled to instead alias this to the equivalent Component of the react-native-web module by way of a babel module alias. The babel react-native-web plugin loaded in webpack.config.js takes care of these for core react modules, but it doesn’t know about additional module dependencies that I created by importing react-navigation so these need to be spelled out.

Running the webpack dev server:

./node_modules/.bin/webpack-dev-server -d --config 
./web/webpack.config.js --inline --hot --colors

gets a bunch of errors like:

ERROR in ./node_modules/react-native/Libraries/react-native/react-native-implementation.js
Module not found: Error: Can't resolve 'AccessibilityInfo' in '.../node_modules/react-native/Libraries/react-native'
 @ ./node_modules/react-native/Libraries/react-native/react-native-implementation.js 19:35-63
 @ ./node_modules/react-native-drawer-layout/dist/DrawerLayout.js
 @ ./node_modules/react-native-drawer-layout-polyfill/dist/index.js
 @ ./node_modules/react-navigation/src/views/Drawer/DrawerView.js
 @ ./node_modules/react-navigation/src/react-navigation.js
 @ ./app/index.js
 @ ./app/index.web.js
 @ multi (webpack)-dev-server/client?http://0.0.0.0:9000 webpack/hot/dev-server babel-polyfill ./app/index.web.js

The key marker is that the modules are trying to pull in source files from node_modules/react-native… which should have been aliased to react-native-web if they had been included the loader pattern.

The fix is to add the module doing the import (in the above case ./node_modules/react-navigation) into the include array in the webpack config.

I ended up with:

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

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

// 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(rootDirectory, 'app'),
    path.resolve(rootDirectory, 'node_modules/react-native-uncompiled'),
    path.resolve(rootDirectory, 'node_modules/react-native-vector-icons'),
    path.resolve(rootDirectory, 'node_modules/react-navigation'),
    path.resolve(rootDirectory, 'node_modules/react-native-drawer-layout'),
    path.resolve(rootDirectory, 'node_modules/react-native-dismiss-keyboard'),
    path.resolve(rootDirectory, 'node_modules/react-native-safe-area-view'),
    path.resolve(rootDirectory, 'node_modules/react-native-tab-view')
  ],
  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: [
          // This is needed to polyfill ES6 async code in some of the above modules
          'babel-polyfill',
          // This aliases 'react-native' to 'react-native-web' to fool modules that only know
          // about the former into some kind of compatibility. 
          '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: [
      // Need babel polyfills before we include the bundle
      "babel-polyfill",
      // Bundle has to come second
      path.resolve(appDirectory, 'index.web.js')
  ],

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

  // ...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']
  },

  // Want my devserver to bind to an interface IP so I can browse 
  //  it on my phone from the LAN
  devServer: {
    contentBase: path.join(__dirname, "../public"),
    compress: false,
    port: 9000,
    host: "0.0.0.0"
  }
}

Even after doing this, I still ended up with a couple of persistent packaging errors:

ERROR in ./node_modules/react-native-safe-area-view/index.js
Module not found: Error: Can't resolve 'react-native-web/dist/exports/DeviceInfo' in '/Users/rob/code/native_plus_web/node_modules/react-native-safe-area-view'

and

ERROR in ./node_modules/react-native-tab-view/src/TabViewPagerAndroid.js
Module not found: Error: Can't resolve 'react-native-web/dist/exports/ViewPagerAndroid' in '/Users/rob/code/native_plus_web/node_modules/react-native-tab-view/src'

Google found me https://github.com/necolas/react-native-web/issues/801which describes the reason for these. Unfortunately the fix should be in the underlying modules rather than react-native-web so these remain until those modules change their code.

Monkey Patching

For now, I’m going to rely on working around and monkey patching the modules needed to make this work. I chose to use patch-package:

  yarn add --dev patch-package postinstall-prepare

I’ve patched my local copy of react-native-safe-area-view and react-native-tab-view to fix these two issues, details are on github here. If you are building your own project then just copy my patches directory to your own project and:

  yarn patch-package

These patches fix the outstanding packaging issues so now we have a clean webpack build, so lets go to http://localhost:9000/

But does it run

Nope.

Uncaught TypeError: Cannot read property 'animationType' of undefined
    at eval (index.js?cf30:1)
    at Object.<anonymous> (bundle.web.js:8593)
    at __webpack_require__ (bundle.web.js:679)
    at fn (bundle.web.js:89)
    at eval (NightMode.js?cced:1)
    at Object.<anonymous> (bundle.web.js:8581)
    at __webpack_require__ (bundle.web.js:679)
    at fn (bundle.web.js:89)
    at eval (NightModeScreen.js?52fe:1)
    at Object.<anonymous> (bundle.web.js:8570)

This comes from the dependency of a module (react-native-modal-selector), on the Modal component of react-native, which is not present in react-native-web. I’m not using it in the web rendering but it is imported so I split the file out to remove it. NightMode.js within my app got a NightMode.web.js cousin which uses a vanilla <Picker> and crucially doesn’t import the Modal dependency.

Next:

Uncaught ReferenceError: regeneratorRuntime is not defined
    at Transitioner._callee (Transitioner.js?3342:1)
    at commitCallbacks (react-dom.development.js?cada:6163)
    at commitLifeCycles (react-dom.development.js?cada:8784)
    at commitAllLifeCycles (react-dom.development.js?cada:9946)
    at HTMLUnknownElement.callCallback (react-dom.development.js?cada:542)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?cada:581)
    at invokeGuardedCallback (react-dom.development.js?cada:438)
    at commitRoot (react-dom.development.js?cada:10050)
    at performWorkOnRoot (react-dom.development.js?cada:11017)
    at performWork (react-dom.development.js?cada:10967)

Webpack.config.js needs the babel-polyfill runtime loading before the rest of the bundle.

entry: [
      // Need babel polyfills before we include the bundleType
      "babel-polyfill",
      // Bundle has to come second
      path.resolve(appDirectory, 'index.web.js')
  ],

Bingo:

image.png

Now to do something useful with it!

origin

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,279评论 0 10
  • **2014真题Directions:Read the following text. Choose the be...
    又是夜半惊坐起阅读 9,355评论 0 23
  • The Inner Game of Tennis W Timothy Gallwey Jonathan Cape ...
    网事_79a3阅读 11,670评论 2 19
  • 为儿女祝福100天: 第十九天:慷慨分享 你们中间谁有儿子求饼,反给他石头呢?求鱼,反给他蛇呢?你们虽然不好,尚且...
    关锁的园阅读 173评论 0 0
  • 昨天我做了一个梦 梦到日日夜夜思念的人 他来到我面前 我没有很开心 反倒惶恐不安 如果你真的和我在一起 我却逃了 ...
    甜宋儿阅读 133评论 0 0