教程:如何使用 React Native、react-native-web 和 monorepo 在 iOS、Android 和 Web 之间共享代码

2025-05-24

教程:如何使用 React Native、react-native-web 和 monorepo 在 iOS、Android 和 Web 之间共享代码

让我们的react-native应用程序以正确的方式在浏览器中运行。

本教程是为 制作的react-native <= 0.61。如果您使用的是较新版本,我建议您 fork 此仓库:brunolemos/react-native-web-monorepo,我会持续更新它 🙌


我为什么要写这个?

大家好👋我是Bruno Lemos 。我最近在 GitHub 上发布了一个名为 DevHub - TweetDeck的项目,它之所以受到大家的关注,是因为它是一个由一位开发者开发的应用,目前支持 6 个平台:Web(react-native-web)、iOS(react native、Android(react native)、macOS、Windows 和 Linux(electron,这些平台之间几乎 100% 的代码共享。它甚至与服务器共享部分代码!而几年前,这还需要 3 人以上的团队才能完成。

从那时起,我收到了数十条推文和私人消息,询问如何实现同样的目标,在本教程中我将引导您完成它。

react-native-web

如果您不熟悉react-native-web ,它是Necolas(前 Twitter 工程师)开发的一个库,用于让您的React Native代码在浏览器中渲染。简单来说,您编写代码<View />,它就会渲染<div />,确保所有样式渲染的内容完全相同。它的功能远不止于此,但我们还是简单介绍一下。

新的 Twitter就是使用这项技术创建的,非常棒。

如果您已经了解react-native,则无需学习任何新语法。它们是相同的 API


概括

  • 开始新React Native项目
  • 将我们的文件夹结构转变为 monorepo
  • react-native在 monorepo 中工作
  • 在我们的 monorepo 包之间共享代码
  • create-react-app使用和创建新的 Web 项目react-native-web
  • 通过代码共享CRA在我们的内部开展工作monorepo
  • ???
  • 利润

分步教程

开始新React Native 项目

  • $ react-native init myprojectname
  • $ cd myprojectname
  • $ git init && git add . -A && git commit -m "Initial commit"

注意:从头开始创建跨平台应用程序比尝试移植现有的仅限移动设备(或更难:仅限网络)项目要容易得多,因为它们可能使用大量特定于平台的依赖项。

编辑:如果您使用 expo,看来他们很快就会内置对 web 的支持

将我们的文件夹结构转变为 monorepo

Monorepo 意味着在单个仓库中拥有多个包,以便您可以轻松地在它们之间共享代码。这听起来可能有点复杂,因为两者都react-native需要create-react-app一些工作来支持 monorepo 项目。不过,至少这是可能的!

Yarn Workspaces我们将使用为此调用的功能。
要求:Node.jsYarnReact Native。 

  • 确保您位于项目根文件夹
  • $ rm yarn.lock && rm -rf node_modules
  • $ mkdir -p packages/components/src packages/mobile packages/web
  • 将所有文件(除.git)移动到packages/mobile文件夹
  • 编辑到的name字段packages/mobile/package.jsonpackagenamemobile
  • package.json在根目录中创建此项以启用Yarn Workspaces
{
  "name": "myprojectname",
  "private": true,
  "workspaces": {
    "packages": [
      "packages/*"
    ],
    "nohoist": []
  }
  "dependencies": {
    "react-native": "0.61.3"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • .gitignore在根目录下创建 :
.DS_Store
.vscode
node_modules/
yarn-error.log
Enter fullscreen mode Exit fullscreen mode
  • $ yarn

使 React Native 在 Monorepo 中工作

  • 检查react-native安装位置。如果是在/node_modules/react-native,那就没问题。如果是在/packages/mobile/node_modules/react-native,那就有问题了。确保您拥有node和 的最新版本yarn。同时,请确保在 monorepo 包之间使用完全相同版本的依赖项,例如"react": "16.11.0"在 和 上mobilecomponents而不是在它们之间使用不同的版本。

  • 打开您最喜欢的编辑器并使用该Search & Replace功能将所有出现的 替换node_modules/react-native/../../node_modules/react-native/

  • 对于 react-native <= 0.59 版本,请打开packages/mobile/package.json。您的start脚本当前以 结尾/cli.js start。请将此内容附加到末尾:--projectRoot ../../

  • 打开packages./mobile/metro.config.js并在其上设置projectRoot字段,使其看起来像这样:

const path = require('path')

module.exports = {
  projectRoot: path.resolve(__dirname, '../../'),
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
}
Enter fullscreen mode Exit fullscreen mode
  • [解决方法] 您当前需要将react-native依赖项添加到根目录package.json才能捆绑 JS:
  "dependencies": {
    "react-native": "0.61.3"
  },
Enter fullscreen mode Exit fullscreen mode

iOS 更改

  • $ open packages/mobile/ios/myprojectname.xcodeproj/
  • 打开AppDelegate.m,查找jsBundleURLForBundleRoot:@"index"并替换indexpackages/mobile/index
  • 仍在 Xcode 中,点击左侧的项目名称,然后转到Build Phases> Bundle React Native code and Images。将其内容替换为以下内容:
export NODE_BINARY=node
export EXTRA_PACKAGER_ARGS="--entry-file packages/mobile/index.js"
../../../node_modules/react-native/scripts/react-native-xcode.sh
Enter fullscreen mode Exit fullscreen mode
  • $ yarn workspace mobile start

现在您可以运行 iOS 应用了!💙 选择一个 iPhone 模拟器,然后按下 Xcode 中的“运行”三角形按钮。

图像

Android 变更

  • $ studio packages/mobile/android/
  • 打开packages/mobile/android/app/build.gradle。搜索文本project.ext.react = [...]。编辑它,使其看起来像这样:
project.ext.react = [
    entryFile: "packages/mobile/index.js",
    root: "../../../../"
]
Enter fullscreen mode Exit fullscreen mode
  • Android Studio 将显示“立即同步”弹出窗口。点击它。
  • 打开packages/mobile/android/app/src/main/java/com/myprojectname/MainApplication.java。搜索getJSMainModuleName方法。将其替换indexpackages/mobile/index,如下所示:
@Override
protected String getJSMainModuleName() {
  return "packages/mobile/index";
}
Enter fullscreen mode Exit fullscreen mode

如果出现Cannot get property 'packageName' on null object错误,请尝试禁用自动链接

现在您可以运行 Android 应用了!💙 按下 Android Studio 中的“运行”绿色三角形按钮,然后选择模拟器或设备。

图像

在我们的 monorepo 包之间共享代码

我们在 monorepo 中创建了很多文件夹,但mobile目前只用过一次。让我们准备一下代码库,以便代码共享,然后将一些文件移到components包中,这样就可以在mobileweb以及我们决定将来支持的任何其他平台(例如desktopserver等)上重复使用。

  • packages/components/package.json创建包含以下内容的文件:
{
  "name": "components",
  "version": "0.0.1",
  "private": true
}
Enter fullscreen mode Exit fullscreen mode
  • [可选] 如果您决定在将来支持更多平台,您将为它们执行相同的操作:创建packages/core/package.json、、等packages/desktop/package.jsonpackages/server/package.json每个平台的名称字段必须是唯一的。

  • 打开packages/mobile/package.json。添加所有要使用的 monorepo 包作为依赖项。在本教程中,mobile仅使用以下components包:

"dependencies": {
  "components": "0.0.1",
  ...
}
Enter fullscreen mode Exit fullscreen mode
  • 如果 react-native 打包程序正在运行,请停止它
  • $ yarn
  • $ mv packages/mobile/App.js packages/components/src/
  • 打开packages/mobile/index.js。替换import App from './App'import App from 'components/src/App'这就是魔法的原理。现在一个包可以访问其他包了!
  • 编辑packages/components/src/App.js,替换Welcome to React Native!为,Welcome to React Native monorepo!这样我们就知道我们正在渲染正确的文件。
  • $ yarn workspace mobile start

太棒了!现在您可以刷新正在运行的 iOS/Android 应用,并查看来自共享组件包的屏幕了。🎉

  • $ git add . -A && git commit -m "Monorepo"

Web 项目

注意:您可以复用多达 100% 的代码,但这并不意味着您应该这样做。建议在不同平台之间保留一些差异,以便用户感觉更自然。为此,您可以创建以 、 或 结尾的平台特定.web.js文件.ios.js.android.js.native.js参阅示例

使用 CRA 和 react-native-web 创建新的 Web 项目

  • $ cd packages/
  • $ npx create-react-app web
  • $ cd ./web (请留在此文件夹中以进行后续步骤)
  • $ rm src/* (或者手动删除里面的所有文件packages/web/src
  • 确保package.json所有 monorepo 包中的依赖项完全相同。例如,将web和两个mobile包中的“react”版本更新为“16.9.0”(或任何其他版本)。
  • $ yarn add react-native-web react-art
  • $ yarn add --dev babel-plugin-react-native-web
  • packages/web/src/index.js创建包含以下内容的文件:
import { AppRegistry } from 'react-native'

import App from 'components/src/App'

AppRegistry.registerComponent('myprojectname', () => App)
AppRegistry.runApplication('myprojectname', {
  rootTag: document.getElementById('root'),
})
Enter fullscreen mode Exit fullscreen mode

注意:当我们从项目react-native内部导入时create-react-app,它的webpack配置会自动为我们将其别名化。react-native-web

  • packages/web/public/index.css创建包含以下内容的文件:
html,
body,
#root,
#root > div {
  width: 100%;
  height: 100%;
}

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
Enter fullscreen mode Exit fullscreen mode
  • 在关闭标签之前进行编辑packages/web/public/index.html以包含我们的 CSS head
...
<title>React App</title>
<link rel="stylesheet" href="%PUBLIC_URL%/index.css" />
</head>
Enter fullscreen mode Exit fullscreen mode

通过代码共享使 CRA 在我们的 monorepo 中工作

默认情况下, CRA 不会构建src文件夹外的文件。我们需要让它执行此操作,以便它能够理解来自 monorepo 包的代码,这些代码包含 JSX 和其他非纯 JS 代码。

  • 留在室内packages/web/进行下一步
  • 创建一个 包含以下内容的.env文件( ):packages/web/.env
SKIP_PREFLIGHT_CHECK=true
Enter fullscreen mode Exit fullscreen mode
  • $ yarn add --dev react-app-rewired
  • 将里面的脚本替换packages/web/package.json为:
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-app-rewired eject"
},
Enter fullscreen mode Exit fullscreen mode
  • 创建packages/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('../components/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
}
Enter fullscreen mode Exit fullscreen mode

上面的代码覆盖了一些create-react-app配置,webpack因此它在 CRA 的构建步骤中包含了我们的 monorepo 包

  • $ git add . -A && git commit -m "Web project"

就这样!现在你可以yarn start在里面packages/web(或yarn workspace web start根目录)运行来启动 Web 项目,与我们的react-native mobile项目共享代码!🎉

图像

一些陷阱

  • react-native-web支持大多数react-nativeAPI,但缺少一些部分,AlertModalRefreshControlWebView
  • 如果您遇到与 monorepo 结构不能很好地兼容的依赖项,您可以将其添加到nohoist列表中;但尽可能避免这样做,因为它可能会导致其他问题,特别是对于 metro bundler。

一些建议

  • 导航可能有点挑战;您可以使用最近添加了 Web 支持的react-navigation之类的东西,或者您可以尝试在和移动设备之间使用两个不同的导航器,以防您想通过牺牲一些代码共享来获得两全其美的效果;
  • 如果您计划与服务器共享代码,我建议创建一个core仅包含逻辑和辅助函数(没有与 UI 相关的代码)的包;
  • 对于 Next.js,你可以使用 react-native-web 查看其官方示例
  • 对于原生windows,可以尝试react-native-windows
  • 对于原生 macOS,您可以使用新的 Apple Project Catalyst,但对它的支持尚未 100% 实现(请参阅我的推文);
  • 要安装新的依赖项,请使用根目录中的命令yarn workspace components add xxx。例如,要从某个包运行脚本,请运行yarn workspace web start;要从所有包运行脚本,请运行yarn workspaces run scriptname

谢谢阅读!💙

如果您喜欢 React,请考虑在 Dev.to 和Twitter上关注我。


链接

文章来源:https://dev.to/brunolemos/tutorial-100-code-sharing- Between-ios-android--web-using-react-native-web-andmonorepo-4pej
PREV
重构:我最喜欢的 6 种模式
NEXT
我的 2018 年 Linux 开发环境