教程:如何使用 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.js、Yarn和React Native。
- 确保您位于项目根文件夹
$ rm yarn.lock && rm -rf node_modules
$ mkdir -p packages/components/src packages/mobile packages/web
- 将所有文件(除
.git
)移动到packages/mobile
文件夹 - 编辑从到的
name
字段packages/mobile/package.json
packagename
mobile
package.json
在根目录中创建此项以启用Yarn Workspaces
:
{
"name": "myprojectname",
"private": true,
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": []
}
"dependencies": {
"react-native": "0.61.3"
}
}
.gitignore
在根目录下创建 :
.DS_Store
.vscode
node_modules/
yarn-error.log
$ yarn
使 React Native 在 Monorepo 中工作
-
检查
react-native
安装位置。如果是在/node_modules/react-native
,那就没问题。如果是在/packages/mobile/node_modules/react-native
,那就有问题了。确保您拥有node
和 的最新版本yarn
。同时,请确保在 monorepo 包之间使用完全相同版本的依赖项,例如"react": "16.11.0"
在 和 上mobile
,components
而不是在它们之间使用不同的版本。 -
打开您最喜欢的编辑器并使用该
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,
},
}),
},
}
- [解决方法] 您当前需要将
react-native
依赖项添加到根目录package.json
才能捆绑 JS:
"dependencies": {
"react-native": "0.61.3"
},
iOS 更改
$ open packages/mobile/ios/myprojectname.xcodeproj/
- 打开
AppDelegate.m
,查找jsBundleURLForBundleRoot:@"index"
并替换index
为packages/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
$ 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: "../../../../"
]
- Android Studio 将显示“立即同步”弹出窗口。点击它。
- 打开
packages/mobile/android/app/src/main/java/com/myprojectname/MainApplication.java
。搜索getJSMainModuleName
方法。将其替换index
为packages/mobile/index
,如下所示:
@Override
protected String getJSMainModuleName() {
return "packages/mobile/index";
}
如果出现
Cannot get property 'packageName' on null object
错误,请尝试禁用自动链接
现在您可以运行 Android 应用了!💙 按下 Android Studio 中的“运行”绿色三角形按钮,然后选择模拟器或设备。
在我们的 monorepo 包之间共享代码
我们在 monorepo 中创建了很多文件夹,但mobile
目前只用过一次。让我们准备一下代码库,以便代码共享,然后将一些文件移到components
包中,这样就可以在mobile
、web
以及我们决定将来支持的任何其他平台(例如desktop
、server
等)上重复使用。
packages/components/package.json
创建包含以下内容的文件:
{
"name": "components",
"version": "0.0.1",
"private": true
}
-
[可选] 如果您决定在将来支持更多平台,您将为它们执行相同的操作:创建
packages/core/package.json
、、等packages/desktop/package.json
。packages/server/package.json
每个平台的名称字段必须是唯一的。 -
打开
packages/mobile/package.json
。添加所有要使用的 monorepo 包作为依赖项。在本教程中,mobile
仅使用以下components
包:
"dependencies": {
"components": "0.0.1",
...
}
- 如果 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'),
})
注意:当我们从项目
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;
}
- 在关闭标签之前进行编辑
packages/web/public/index.html
以包含我们的 CSShead
:
...
<title>React App</title>
<link rel="stylesheet" href="%PUBLIC_URL%/index.css" />
</head>
通过代码共享使 CRA 在我们的 monorepo 中工作
默认情况下, CRA 不会构建src
文件夹外的文件。我们需要让它执行此操作,以便它能够理解来自 monorepo 包的代码,这些代码包含 JSX 和其他非纯 JS 代码。
- 留在室内
packages/web/
进行下一步 - 创建一个 包含以下内容的
.env
文件( ):packages/web/.env
SKIP_PREFLIGHT_CHECK=true
$ 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"
},
- 创建
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
}
上面的代码覆盖了一些
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-native
API,但缺少一些部分,如Alert
、和;Modal
RefreshControl
WebView
- 如果您遇到与 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上关注我。
链接
- 源代码:react-native-web-monorepo
- DevHub:devhubapp/devhub(使用此结构的生产应用程序+桌面+TypeScript)
- 推特:@brunolemos