如何用React Native开发Web应用

6,311 阅读6分钟

明确使用RN开发Web的意义

就像React Native的标语说的那样:“学习一次,可到处写”。RN作为一个跨平台的大前端解决方案,必然绕不过去Web这道坎。Web应用上线的成本和周期,远远低于App上线,所以在我看来,在android和ios平台之外,再配上web的,至少有三个积极的意义(自己瞎想的,欢迎指证):

  1. 在App上架或者更新前,利用Web平台的便利性,在线上做各种验证
  2. 一些受限于移动端的操作,可以更方便利用PC在Web上操作
  3. Web作为一次性的体验平台,可以引流转化那些潜在的App客户

利用RN开发Web有一点一定要明确:Web只不过是App的一种补充,是为这个App锦上添花的。 如果只是要开发一个Web应用,那么我认为不应该使用RN来开发,直接上React等纯前端框架就好了。

在RN项目中开发Web的两种方式

我在github上下载了一些RN的项目,搜了其中的一些项目中的Web的部分,发现了两种实现Web的方式:

  1. 直接在RN项目中,再写一套纯React的代码。具体来说,将原来项目中的非UI部分提取出来公用,而UI部分,每一个组件,都写了两套代码,一套Native的,一套Web的。请看下面的例子:
// KeyboardRender.native.js
'use strict';

import Key from './Key';

import React, {
  StyleSheet,
  View
} from 'react-native';

export default function () {
  return (
    <View style={styles.keyboard}>

      <View style={styles.row}>
        <Key keyType='number' keyValue='1' keySymbol='1' />
        <Key keyType='number' keyValue='2' keySymbol='2' />
        <Key keyType='number' keyValue='3' keySymbol='3' />
      </View>
      ... ...
    </View>
  );
}

// KeyboardRender.js
'use strict';

import Key from './Key';

import React from 'react';

export default function () {
  return (
    <div className='keyboard'>
      <div className='keyboard-row'>
        <Key keyType='number' keyValue='1' keySymbol='1' />
        <Key keyType='number' keyValue='2' keySymbol='2' />
        <Key keyType='number' keyValue='3' keySymbol='3' />
      </div>
      ... ...
    </div>
  );
}

同样的一个UI组件Keyboard,需要2份实现,一份是RN的实现,一份是Web的实现,两份组件的结构高度相似,而且共用了Key.js这个共同的模块。这种方式优点很明显,可以随心所欲的写React组件,而不用担心因为RN的部分组件无法通过RNW转换成Web组件而妥协,甚至可以结合Web自身的特点,只复用业务逻辑的部分,而将Web的UI和App的UI单独设计成两套,用户体验会更好。当然,缺点就是增大了工作负担。我觉得如果是团队开发的话,这个方案其实蛮好的,既可以强迫你剥离出业务逻辑和UI组件,让你不得不做到数据和显示分离,也可以让整个产品在不同的平台上拥有更好的用户体验。

  1. 第二种方式,就是利用非常流行的react-native-web,将RN项目转换成可以在web运行的代码。我本人就是采用这种方式来开发跨Web平台的App应用的。这种方式的好处,显而易见,一次编写就可以覆盖多个平台,减轻了工作量。所以,如果一个人开发,实际上采用这种方式,会大大减轻开发者的思维负担,但是也因为Web的UI也是复用App的UI模板,所以在Web上的用户体验就会差点意思。由于Web应用和App应用本身的设计理念是不一样的,所以如果一模一样照搬App的所有UI/UX到Web端,有些设计会看起来水土不服,后期需要专门针对Web进行微调整,但是即使这样,它的工作量也小于开发两套模板。

在RN项目中配置RNW

重点来了,如何在RN项目中配置RNW才能达到一套RN代码,输出三个平台的效果呢?

因为RN是在不断的迭代更新的,所以我们有时候网上搜索的很多方案,发现都解决不了我们的我本地的问题,原因很大就是因为本地的项目使用的是最新的RN和RNW,而网上的方案是来自之前的一些版本。所以,这里我要明确下,我下面的这些配置,是基于哪两个版本的:

    "react-native": "0.70.2",
    "react-native-web": "0.18.9",

未来随着时间的推移,我的方案也不一定好用了哈,特此说明。

Web项目的编译,首选的Webpack以及它的各种plugin是少不了的,先列出所有需要用到的相关的包:

    "@svgr/webpack": "6.4.0",
    "babel-loader": "8.2.5",
    "babel-plugin-react-native-web": "0.18.9",
    "html-webpack-plugin": "5.5.0",
    "url-loader": "4.1.1",
    "webpack": "5.74.0",
    "webpack-cli": "4.10.0",
    "webpack-dev-server": "4.11.1"

接下来就是配置webpack.config.js了,直接上代码

// webpack.config.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const appDirectory = path.resolve(__dirname)
const { presets, plugins } = require(`${appDirectory}/babel.config.js`)
const compileNodeModules = [
  // 这个位置,要加上所有的你用到的RN的库,因为需要把这些RN的库,都通过RNW编译成Web的代码
  // 这里为了更好的提供实例,我列出了我的项目用到的RN的库
  // 你的内容和我的不一样,这一段不要照抄,要根据自己的情况自己来改,
  '@react-navigation/bottom-tabs',
  '@react-navigation/native',
  '@react-navigation/native-stack',
  'react-native-linear-gradient',
  'react-native-safe-area-context',
  'react-native-safe-area-view',
  'react-native-screens',
  'react-native-vector-icons',
].map((moduleName) => path.resolve(appDirectory, `node_modules/${moduleName}`))
const babelLoaderConfiguration = {
  test: /\.(js|jsx)$/,
  // 所有需要编译的文件都要在include里列出来,上面写的compileNodeModules也要加在这里面
  include: [
    path.resolve(__dirname, 'index.web.js'),
    path.resolve(__dirname, 'src'),
    ...compileNodeModules,
  ],
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true,
      presets,
      plugins,
    },
  },
}; const svgLoaderConfiguration = {
  test: /\.svg$/,
  use: [
    {
      loader: '@svgr/webpack',
    },
  ],
}
const imageLoaderConfiguration = {
  test: /\.(bmp|gif|jpe?g|png)$/,
  use: {
    loader: 'url-loader',
    options: {
      name: '[name].[ext]',
    },
  },
}

module.exports = {
  entry: {
    app: path.join(__dirname, 'index.web.js'),
  },
  output: {
    path: path.resolve(appDirectory, 'dist'),
    publicPath: '/',
    filename: 'rnw.bundle.js',
  },
  resolve: {
    extensions: ['.web.js', '.web.jsx', '.js', '.jsx'],
    alias: {
      'react-native$': 'react-native-web',
    },
  },
  module: {
    rules: [
      babelLoaderConfiguration,
      imageLoaderConfiguration,
      svgLoaderConfiguration,
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'index.html'),
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.DefinePlugin({
      // 此处特别说明,是为了兼容RN和RNW之间的一个全局变量而特别加上的
      // 具体可以看RNW的作者Necolas的说明 
      // <https://github.com/necolas/react-native-web/issues/349>
      __DEV__: JSON.stringify(true),
    }),
  ],
}

我们注意到,在webpack.config.js中,涉及到两个文件,分别是index.web.js和index.html,它们是Web项目的入口文件。具体代码如下:

//index.web.js
import { AppRegistry } from 'react-native'
import App from './src/App'
import { name as appName } from './app.json'

AppRegistry.registerComponent(appName, () => App)
AppRegistry.runApplication(appName, {
  initialProps: {},
  rootTag: document.getElementById('app-root')
})
// index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Flower As World</title>
    <style>
      #app-root {
        display: flex;
        flex: 1 1 100%;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <div id="app-root"></div>
  </body>
</html>

一般来说,一个RN项目中,有且仅有一个index.html,其余所有的实现,都是通过js来完成的。而对于Web项目中必不可少的css,在RN项目中,也是非必要不写,而对于必要的css,也仅仅是全局一个而已。

在RN项目中.web .native .ios .android .windows 等都是有实际用处的关键字,并不是随便起的。简单来说,如果后缀是.web.js的文件,那么在native编译时,就会忽略它。

如果已经准备好了上述所有,接下来就该在RN项目中启动Web了,在package.json中加上这一段:

"scripts": {
    "web": "webpack serve --mode=development --config webpack.config.js"
  },

在terminal里,运行yarn web启动项目。

我经过实践证明了,不需要新项目,对于已经开发了一段时间的老的RN项目,按照以上步骤,也是可以顺利引入RNW的。下图就是我的个人项目在三个平台上运行的效果。

image.png

从左到右依次为Web,IOS,Android,其中IOS是通过windows虚拟机安装的MacOS系统搭建的IOS环境,具体做法参见Windows上搭建MacOS系统为IOS开发做准备

另外,上图中引入了字体文件和部分CSS,具体做法参见如何在React Native中引入CSS和字体

结束语

我从2022年9月22日利用业余时间,从零开始学做App,10月8日写下第一行代码。至今已经。。。。。。额,好吧,还不到一个月。貌似也没有资格在这感慨。这都不重要,如果您也想和我一样,从零开始,打造一个属于自己的跨平台的App,那么请关注我后续的文章,看看这件事情,到底有没有可能成功呢?