React Native三端同构在黑湖HD中的实践

3,514 阅读11分钟

随着移动互联网的兴起和发展,目前市面上「端」的形态多种多样,iOS、Android 、H5、Web前端等各种端层出不穷,结合实际业务场景和客户诉求,同一个系统、产品、业务需求往往需要在多端上呈现。在端融合技术发展之前,基本会按照不同的端各自组织开发团队,各自开发相关功能,但是其工作量显然是庞大的,并且无论是代码架构还是最终实现,都无法保持所有端很好地统一。黑湖移动端团队致力于跨端跨平台开发实践,在最新的HD项目中落地实现Android、iOS、Web前端三端同构的能力。

一、背景

黑湖智造是一款云端智能制造协同系统,主要为制造业提供计划排产、生产执行、质量管理、物流管理、物料管理、可视化数据分析等协同制造功能。应用场景用于计划调度、生产派单及过程管控、生产进度的实施把控、物流及物料的协同配合、管理层对生产数据的分析等。其中,生产、质量、仓储、设备等等相关执行侧的功能均由APP手机应用程序来承载。

黑湖智造是一款ToB的工业互联网APP,其目标客群是制造业企业客户,因为一些历史原因,客户现场配置的设备五花八门,有Android系统的工业一体机,有工人自带或者企业集体采购的Android手机、平板电脑,有便于红外扫码、键盘输入等功能的PDA,有windows系统的电脑,还有管理层使用偏多的iphone系列手机等等。这些设备呈现出来的特性包括:1、系统多样:涵盖了目前终端设备最主流的Android、iOS、Windows三大系统;2、屏幕尺寸各异:小到手持PDA,中到平板电脑、大到工业一体机、台式电脑。

为了提升产品交互体验和更多横屏设备的视觉体验,黑湖科技提出了打造一款以横屏交互为基础的全新应用程序,我们命名为HD版本。黑湖智造移动端一直以来采用的技术栈都是React Native,综合其产品特性、开发人员效率、频繁的热更新诉求,React Native是相对比较适合的移动端技术栈。另外一方面,移动端团队耗时半年开发了黑湖移动端组件库项目,将常用的UI控件、业务组件抽象出来,单独项目维护,目前已经稳定运行在黑湖智造APP中。

首先需要考虑的问题是如何满足三大主流操作系统同时上线功能,无外乎三种方式:1、移动端开发一套,Web前端开发一套,各自维护相应的功能。该方式的缺点有:工作量翻倍,并且无法保证移动端和Web端的代码架构和实现上的一致性,后续迭代极容易出现分叉等情况。2、使用H5开发,同时运行于移动端和Web端。该方式的缺点有:在H5端无法很好地完成外部硬件对接,比如蓝牙打印机、电子秤等,H5在移动端上的体验还是无法与原生应用相媲美,存在一定程度的产品体验牺牲。3、使用React Native开发,利用相关工具将代码兼容Web前端。该方案既可以最大程度保留移动端体验,也可以最大程度提高研发人员效率。

二、RN&Web同构的思路与方案

2.1 同构的关键能力:

1. 一套代码兼容多端

一套代码 iOS / Android / Web 三端运行,通过 React Native 为客户端提供动态化能力,并利用React Native Web框架同构生成Web前端网页。做好相应平台的特性适配,产品、设计、研发皆只需投入一份资源,即可完成功能的多端覆盖。

Android

iOS

Web

2. 样式组件

React Native的style尺寸转换成CSS,这部分工作React Native Web框架已经帮我们处理好了。针对不同尺寸屏幕,我们需要做相应的适配,以1920*1080屏幕尺寸为基准,根据屏幕宽度进行等比缩放,超过1920宽度的屏幕以1920为准。封装p2d方法供业务方使用,该方法既支持传入具体的尺寸,也支持传入style样式,这样在业务开发中,每个页面只需要将所有的style样式通过stylesheet.create创建出来,外层用p2d包裹,使用该方式可以最大程度避免p2d的反复使用。

代码实例:

const styles = StyleSheet.create(
  p2d({
    inputContainer: {
      width: 935,
      paddingVertical: 100,
      alignSelf: 'center',
    },
    input: {
      backgroundColor: BlColor.white,
      marginBottom: 48,
    },
  }),
);

const HardwarePage = () => {
    return <></>
}

3. 组件库兼容

在手机端,Viewport 和 Screen 的尺寸大小相差不大,甚至在不少手机上是一致的,但是在浏览器中,差别是比较明显的,需要正确处理视窗大小和屏幕大小;Web 中 css 比 native 中强大很多,比如 Web 中可以通过 css 来实现渐变功能,native 端一般依赖 react-native-linear-gradient 实现,为了在使用时不去写两套代码,并且同时满足双端需求,可以针对不同平台封装两个组件,分别以 .tsx.web.tsx 结尾,metro 的打包配置无需做更改,默认获取 .tsx结尾文件;web 端使用 webpack 打包的话,配置 extensions 属性:

extensions: ['.web.tsx', '.tsx', '.ts', '.web.js', '.js'], // .web.tsx 文件在前,会先被匹配到

如果组件库原来仅为 native 端使用,需为 web 做兼容,那样式需要做的更改很少,因为 web端 css 支持了更多属性;如果是 web 端项目需要为 web 端做兼容,那样式可能做的更改比较多,比如 native 不支持fixed 定位、rem单位,不支持 css 选择器,不支持媒体查询 @media query ,不支持 animation 动画等等;

2.2 同构的具体方案:

image.png

通过三端同构样式组件和兼容组件库实现 RN 业务代码的快速编写。

移动端部分:通过 React Native 生成双端代码,并为 App 提供动态化能力。

Web部分:通过 React Native Web 将 RN 代码编译为 Web,使用webpack灵活配置打包。

三、三端同构的实现

3.1. React Native Web的介绍

React-Native-Web是由 前 Twitter 现 Facebook 工程师 Nicolas Gallagher 实现并维护的开源项目,是一个使 React Native 组件和 API 能运行在 Web 上的库,其和 React Native Windows,ReactNative macOS 等库将 React Native 拓展到一个又一个新的平台。目前推特、expo、大联盟足球、Flipkart、优步、《泰晤士报》、DataCamp 等产品都在生产中使用了 react-native-web。Chrome、Firefox、Edge,Safari 7 +、IE 10+都支持通过 react-native-web 构建的 web 应用。当然值得注意的是,官方文档明确表示不支持 React Native 中不推荐使用的组件和 API,因此如果项目中的某些功能依赖第三方库,可能那部分的功能在 web 端同构时需要额外处理。

浅显地认为react-native-web就是把React Native的组件和API都用适用于Web的标签和API再适配实现一遍,使其在Web上的行为和在原生应用上尽量保持一致。

3.2. RN项目的web同构改造

1. 路由

@react-navigation/native 本身提供了 web 端、App 端的兼容,但是也需要针对 web 端的体验做部分修改:

  • 为 NavigationContainer 配置 linking 属性,否则 web 端的路由都是同一个 url 地址,刷新页面后保存在路由中的状态会丢失;
  • 为 NavigationContainer 配置 title 属性,可以通过 formatter 方法,根据页面参数构造正确的页面标题;
  • 参数传递:在native中,因为可以避免刷新操作,路由参数传递没有太多限制;在 web 中,url 中的参数如果传递对象、数组等非基础类型,刷新页面后会无法获取;即使传递的是数字,在刷新页面后获取,数字类型也变成字符串类型,可能在进行 === 严格操作时判断错误;所以参数传递的时候,一般需要经过 encode/decode

2. 移动端原生能力处理

  1. 打印功能:native 端通过下载原始文件后,发送给打印机进行下载;web 端提供了print API,但是在交互、后端不作变更的情况下,直接使用 print 并不能满足需求:
  • 后端直接返回下载流,浏览器打开链接的话,是去下载文件而不是预览;
  • native 端的交互为点击下载按钮直接下载,无预览操作;

为了与 native 端保持一致,web 端的操作:

  • 在页面中构造两个隐藏div,记为 pdfContainer 和 imgContainer,用以展示pdf 或者图片;
  • 根据后端返回的 url 后缀,判断文件类型是图片还是 pdf:如果是图片,在 imgContainer 中构建 img标签,在图片下载完成后,将图片元素放入宽和高都为0的 iframe 中,在 iframe 执行打印操作;如果返回的是 pdf 链接,通过 react-pdf 的 Document来通过canvas进行预览,在 onLoadSuccess 事件触发后,将 Document 元素中的 canvas 元素转换为图片,之后的流程与打印图片一致;

需要注意的是,web 端目前无法正确地获得打印状态,afterprint事件触发的条件,可能是打印成功,也可能是下载成功或者取消打印。

  1. 扫码枪功能

    扫码枪在 web 端,是通过触发 keydown 事件来进行交互(具体需要查看不同扫码枪的协议),需要特殊处理 event.keyEnterShift 的情况;

import { useEffect, useRef } from 'react';
import { DeviceEventEmitter, Platform } from 'react-native';

export const useScan = () => {
  const scanResult = useRef('');
  const onScan = (event: KeyboardEvent) => {
    if (event.key === 'Enter') {
      DeviceEventEmitter.emit('CodeScanned', { code: scanResult.current });
      scanResult.current = '';
    } else if (event.key !== 'Shift') {
      scanResult.current += event.key;
    }
  };
  useEffect(() => {
    if (Platform.OS !== 'web') {
      return;
    }
    document.addEventListener('keydown', onScan);
    () => document.removeEventListener('keydown', onScan);
  }, []);
};
  1. 设备唯一ID功能

原理和实现可以参照这篇文章。需要注意的是,通过 canvas 绘图的方式来实现,如果不同设备的配置完全一致,那么生成的“唯一ID”大概率也是一样的。

3. webpack打包配置

Webpack 的打包配置与纯 web 的项目差异并不是太大,但也有些需要注意的点:

  • index.html 尽量别放在根目录下,否则在使用 Flipper 进行native 调试的时候,可能被识别为 web 项目,导致无法调试;
  • React Native Web 兼容了 web 和 native 的大部分功能;遇到需要分别编写双端代码时,一般有两种方式:一是通过环境判断,根据Platform.OS 来执行对应代码,因为 Platform.OS 运行时才知道,这种方式的缺点是可能会导致包变大(比如 web 环境打包了部分原生环境才会执行的代码);一是单独为 web 环境下执行的代码创建文件,并且以 .web.tsx为后缀,webpack 的 resolve.extensions 配置为 ['.web.tsx', '.tsx', '.ts', '.web.js', '.js'],这种方式的弊端是需要维护两套代码;
  • 有些原生包未经过打包,并且使用的是 flow 语法,所以需要为 babel 配置 flow 插件,并且将这些包加入到打包流程中:
{
  test: /.(js)x?$/,
  include: [
    path.resolve(appDirectory, 'node_modules/包路径'),
  ],
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true,
      presets: [
        'module:metro-react-native-babel-preset',
        '@babel/preset-env',
        '@babel/preset-react',
        '@babel/preset-typescript',
        '@babel/preset-flow', // 解决 flow 语法报错
      ],
      plugins: ['react-native-web', '@babel/plugin-proposal-export-default-from'],
    },
  },
}

4. 是否需要考虑 SEO

可以根据项目情况,判断是否有 SEO 方面的需求;如果没有的话,那运维需要进行的配置与普通的 react 项目没有差异;如果有的话,则可以参照 Next 官方仓库做出更改,同时兼容 react native web 和 Next.js。

四、同构的开发测试发布流程

image.png

五、展望

  1. 智能生成兼容代码:业务开发者将无需关心三端同构的处理逻辑,借助工具自动生成兼容代码,处理兼容逻辑。
  2. 兼容平台的拓展:除了满足Android、iOS、Web端的兼容,如果业务需求有需要的话,需要探索h5、小程序、VR等更多平台的兼容方案。
  3. 覆盖更加全面的单元测试,保证在多端兼容层面的高可用性。
  4. 开发脚手架工具,为后期其他项目一键生成三端同构架构代码打下基础。