Saltar al contenido

Tutorial: Cómo compartir código entre iOS, Android y Web usando React Native, react-native-web y monorepo

Hagamos nuestro react-native la aplicación funciona en el navegador, de la manera correcta.

El código de este tutorial está disponible en GitHub:
Puede bifurcarlo y usarlo para comenzar nuevos proyectos con código compartido 🎉

¿Por qué estoy escribiendo esto?

Hola, soy Bruno Lemos. Recientemente lancé un proyecto llamado DevHub – TweetDeck para GitHub y una de las cosas que llamó la atención de la gente fue el hecho de que es una aplicación hecha por un solo desarrollador y disponible en 6 plataformas: Web (react-native-web), iOS ( react native), Android (react native), macOS, Windows y Linux (electron, por ahora), con casi el 100% de código compartido entre ellos. ¡Incluso comparte código con el servidor! Esto es algo que requeriría un equipo de 3+ hasta hace un par de años.

Desde entonces, he recibido docenas de tweets y mensajes privados preguntando cómo lograr lo mismo y en este tutorial te lo guiaré.

¿Qué es react-native-web?

Si no está familiarizado, es una biblioteca de Necolas (ex ingeniero de Twitter) para hacer su React Native render de código en el navegador. En términos generales, escribirás <View /> y se rendirá <div />, asegurándose de que todos los estilos representen exactamente lo mismo. Hace más que eso, pero hagámoslo simple.

El nuevo Twitter se creó con esta tecnología y es increíble.

Si ya sabes react-native, no es necesario que aprenda ninguna sintaxis nueva. Es la misma API.

Resumen

  • Comenzando un nuevo proyecto React Native
  • Convirtiendo nuestra estructura de carpetas en un monorepo
  • Haciendo trabajo react-native en un monorepo
  • Compartiendo código entre nuestros paquetes monorepo
  • Creando un nuevo proyecto web usando CRA y react-native-web
  • Hacer que CRA funcione dentro de nuestro monorepo con código compartido
  • ???
  • Lucro

Tutorial paso a paso

Comenzando un nuevo proyecto React Native

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

Nota: Es mucho más fácil crear una aplicación multiplataforma desde cero que intentar portar un proyecto existente solo para dispositivos móviles (o incluso más difícil: solo para la web), ya que pueden estar usando muchas dependencias específicas de la plataforma. Si usa expo, es posible que pronto reciba algunas noticias. 👀

Convirtiendo nuestra estructura de carpetas en un monorepo

Monorepo significa tener varios paquetes en un solo repositorio para que pueda compartir código fácilmente entre ellos. Es un poco menos trivial de lo que parece porque tanto react-native y create-react-app requieren algo de trabajo para apoyar los proyectos de monorepo. Pero bueno, ¡al menos es posible!

Usaremos una función llamada Yarn Workspaces para eso.
Requisitos: Node.js, Yarn y React Native.

  • Asegúrate de estar en la carpeta raíz del proyecto.
  • $ rm yarn.lock && rm -rf node_modules
  • $ mkdir -p packages/components/src packages/mobile packages/web
  • Mueva todos los archivos (excepto .git) al packages/mobile carpeta
  • Edite el name campo en packages/mobile/package.json desde packagename a mobile
  • Crea esto package.json en el directorio raíz para habilitar Yarn Workspaces:
  • Crear un .gitignore en el directorio raíz:

Haciendo trabajo react-native en un monorepo

  • Abra su editor favorito, use el Search & Replace característica (generalmente Cmd+Shift+H) y reemplazar todas las apariciones de node_modules/react-native/ con ../../node_modules/react-native/. La mayoría de los archivos estarán dentro del ios y android carpetas.
  • Abierto packages/mobile/package.json. Tu start el guión actualmente termina en /cli.js start. Agregue esto al final: --projectRoot ../../.

cambios de iOS

  • $ open packages/mobile/ios/myprojectname.xcodeproj/
  • Abierto AppDelegate.m, encontrar jsBundleURLForBundleRoot:@"index" y reemplazar index con packages/mobile/index
  • Aún dentro de Xcode, haga clic en el nombre de su proyecto a la izquierda y luego vaya a Build Phases > Bundle React Native code and Images. Reemplaza su contenido con esto:
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

¡Ahora puede ejecutar la aplicación iOS! 💙

Cambios de Android

  • $ studio packages/mobile/android/
  • Abierto packages/mobile/android/app/build.gradle. Buscar el texto project.ext.react = [...]. Edítelo para que se vea así:
project.ext.react = [
    entryFile: "packages/mobile/index.js",
    root: "../../../../"
]
  • Abierto packages/mobile/android/app/src/main/java/com/myprojectname/MainApplication.java. Busque el getJSMainModuleName método. Reemplazar index con packages/mobile/index, por lo que se ve así:
@Override
protected String getJSMainModuleName() {
  return "packages/mobile/index";
}
  • Android Studio mostrará una ventana emergente Sincronizar ahora. Haz click en eso.

¡Ahora puede ejecutar la aplicación de Android! 💙

Compartiendo código entre nuestros paquetes monorepo

Hemos creado muchas carpetas en nuestro monorepo, pero solo usamos mobile hasta aquí. Preparemos nuestra base de código para compartir código y luego movamos algunos archivos al components paquete, para que pueda ser reutilizado por mobile, web y cualquier otra plataforma que decidamos apoyar en el futuro (por ejemplo: desktop, server, etc.).

  • Crea el archivo packages/components/package.json con el siguiente contenido:
  • [optional] Si decide admitir más plataformas en el futuro, hará lo mismo por ellas: cree un packages/core/package.json, packages/desktop/package.json, packages/server/package.json, etc. El campo de nombre debe ser único para cada uno.

  • Abierto packages/mobile/package.json. Agrega todos los paquetes de monorepo que estás usando como dependencias. En este tutorial, mobile solo está usando el components paquete:

  • Detenga el empaquetador react-native si se está ejecutando
  • $ yarn
  • $ mv packages/mobile/App.js packages/components/src/
  • Abierto packages/mobile/index.js. Reemplazar import App from './App' con import App from 'components/src/App'. Esta es la magia que funciona aquí. ¡Un paquete ahora tiene acceso a los demás!
  • Editar packages/components/src/App.js, reemplazar Welcome to React Native! con Welcome to React Native monorepo! para que sepamos que estamos procesando el archivo correcto.
  • $ yarn workspace mobile start

¡Hurra! Ahora puede actualizar las aplicaciones iOS / Android en ejecución y ver nuestra pantalla que proviene de nuestro paquete de componentes compartidos. 🎉

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

Proyecto web

Nota: Puede reutilizar hasta el 100% del código, pero eso no significa que deba hacerlo. Se recomienda tener algunas diferencias entre las plataformas para que el usuario las sienta más naturales. Para hacer eso, puede crear archivos específicos de la plataforma que terminen en .web.js, .ios.js, .android.js o .native.js. Ver ejemplo.

Creando un nuevo proyecto web usando CRA y react-native-web

  • $ cd packages/
  • $ npx create-react-app web
  • $ cd ./web (permanezca dentro de esta carpeta para los siguientes pasos)
  • $ rm src/* (o elimine manualmente todos los archivos dentro packages/web/src)
  • $ yarn add react-native-web react-art
  • $ yarn add --dev babel-plugin-react-native-web
  • Crea el archivo packages/web/src/index.js con el siguiente contenido:
import { AppRegistry } from 'react-native'

import App from 'components/src/App'

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

Nota: cuando importamos desde react-native dentro de una create-react-app proyecto, es webpack config automáticamente para nosotros.

  • Crea el archivo packages/web/public/index.css con el siguiente contenido:
html,
body,
#root,
#root > div {
  width: ;
  height: ;
}

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
  • Editar packages/web/public/index.html para incluir nuestro CSS antes de cerrar el head etiqueta:
...
<title>React App</title>
<link rel="stylesheet" href="%PUBLIC_URL%/index.css" />
</head>

Hacer que CRA funcione dentro de nuestro monorepo con código compartido

CRA no crea archivos fuera del src carpeta por defecto. Necesitamos hacer que lo haga, para que pueda entender el código de nuestros paquetes monorepo, que contiene JSX y otro código que no es JS puro.

  • Permanecer en el interior packages/web/ para los siguientes pasos
  • Crear un .env archivo (packages/web/.env) con el siguiente contenido:
  • $ yarn add --dev react-app-rewired
  • Reemplazar los scripts dentro packages/web/package.json con este:
"scripts":{"start":"react-app-rewired start","build":"react-app-rewired build","test":"react-app-rewired test","eject":"react-app-rewired eject"},
  • Crea el packages/web/config-overrides.js archivo con el siguiente contenido:
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[].include = appIncludes
  config.module.rules[] = null
  config.module.rules[].oneOf[].include = appIncludes
  config.module.rules[].oneOf[].options.plugins = [
    require.resolve('babel-plugin-react-native-web'),
  ].concat(config.module.rules[].oneOf[].options.plugins)
  config.module.rules = config.module.rules.filter(Boolean)
  config.plugins.push(
    new webpack.DefinePlugin({ __DEV__: env !== 'production' })
  )

  return config
}

El código anterior anula algunos create-react-appes webpack config para que incluya nuestros paquetes monorepo en el paso de compilación de CRA

¡Eso es! Ahora puedes correr yarn start dentro packages/web (o yarn workspace web start en el directorio raíz) para iniciar el proyecto web, compartiendo código con nuestro react-native mobile ¡proyecto! 🎉

Algunas trampas

  • react-native-web soporta la mayoría de los react-native API, pero faltan algunas piezas como Alert, Modal, RefreshControl y WebView;
  • react-native link puede no funcionar bien con proyectos monorepo; para solucionar esto, en lugar de solo instalarlos usando yarn workspace mobile add xxx, instálelos también en el directorio raíz: yarn add xxx -W. Ahora puede vincularlo y luego eliminarlo de la raíz package.json.

Algunos consejos

  • Si planea compartir código con el servidor, le recomiendo crear un core paquete que solo contiene funciones lógicas y auxiliares (sin código relacionado con la interfaz de usuario);
  • Para instalar nuevas dependencias, use el comando yarn workspace components add xxx desde el directorio raíz. Para ejecutar un script desde un paquete, ejecute yarn workspace web start, por ejemplo; Para ejecutar un script de todos los paquetes, ejecute yarn workspaces run scriptname;

¡Gracias por leer! 💙

Enlaces