一码多端

一、项目背景

c端页面存在native、rn、h5三类页面

  • native页面,性能最优,但效率较低。从开发到测试,都需要维护两个平台,都需要双倍人力的投入。所以只适合于业务已基本定型,功能相对稳定的页面。该类页面很少迭代,或几乎不迭代。

  • rn页面性能优于h5,h5要逐步改造成rn。

选秒开率作为衡量指标:rn比h5提升56%以上。计算公式:(rn秒开率-h5秒开率) / h5秒开率

  • h5页面 不含容器初始化秒开率 60%,容器初始化~750ms。包含容器时间预计秒开率<50%

  • rn页面 秒开率78%

  • RN化过程中,还无法完全放弃H5的维护,当前维护成本较高,主APP < (支持RN的版本) 版本整体占比 ~3%

  • 部分页面需要端外投放,同时开发rn和h5两套代码,效率低下。

通过上述描述,可以看到,性能提升的方案,似乎会带来效率的降低。

我们是否可以实现提升页面性能的同时也解决掉由于低版本客户端的兼容和端外投放开发两套代码导致的效率降低的问题?

二、问题分析

仔细分析上述内容,可以发现原来需要写一套h5代码就能完成的工作,在引入rn后,现在需要同时写rn和h5两套代码,带来了页面性能提升的同时影响了整体的工作效率。如何在提升性能又不影响开发效率的目标,简单看来,只需要避免开发两套代码就可以了。

那有没有可能有一种方案既可以在提升性能的同时,不影响开发效率?

针对该疑问,业界已经给出了解决方案:一码多端,即编写一套代码,分发到不同的平台上运行。

一码多端示意图:

image.png

RN已经打通,小程序本次不考虑。本次解决端内h5和WebView的打通,

三、项目目标

通过一码多端的方式,实现下述目标:

  • 性能目标:**端启动RN化进程,提高用户体验,秒开率从现在的 ~ 50%达到 ~ 70%;

  • 效率目标:整体研发效率提升30%;

研发效率提升计算公式: (rn总人日+h5总人日-一码多端总人日)/(rn总人日+h5总人日)

四、方案调研

调研了业界内几个比较知名的一码多端方案。包括Rax、taro、uni-app、react-native-web。

总体概括:

方案/框架 技术方向 团队 跨端 特点
Rax 运行时 阿里 小程序/weex/h5 类react语法;客户端基于weex
taro 静态脚本编译 京东 小程序/rn/h5 类react语法;客户端基于rn
uni-app 静态脚本编译 DCloud 小程序/weex/h5 vue语法;客户端基于weex
react-native-web 静态组件替换 Twitter rn/h5 rn语法;客户端基于rn

4.1 RAX

Rax 是阿里巴巴应用最广泛的跨端解决方案,支持开发者通过类 React DSL 编写 Web、小程序、Flutter 等不同容器的跨端应用。

原理图:

image

通过不同的driver,处理对不同平台的拓展。

特点

  • 1、抽象出driver层,处理业务代码到不同容器的适配,拓展性好。

  • 2、React 是一种标准,Rax 是对该标准的一个实现。

  • Rax 是一个面向无线端的解决方案,因此自身的体积对于性能来讲就显得非常重要。Rax 压缩 + gzip 后的体积是 8.0kb, 相比 React 的 43.7kb, 对于无线端友好了很多。

代码示例:

import { createElement, Component, render, createRef } from "rax";
import DriverUniversal from "driver-universal";
import View from "rax-view";
import Text from "rax-text";
import TextInput from "rax-textinput";
const styles = {
  root: {
    width: '750rpx',
    paddingTop: '20rpx'
  },
  container: {
    padding: '20rpx',
    borderStyle: "solid",
    borderColor: "#dddddd",
    borderWidth: '1rpx',
    marginLeft: '20rpx',
    marginRight: '20rpx',
    marginBottom: '10rpx'
  },
  default: {
    borderWidth: '1rpx',
    borderColor: "#0f0f0f",
    flex: 1
  },
  eventLabel: {
    margin: '3rpx',
    fontSize: '24rpx'
  },
  multiline: {
    borderWidth: '1rpx',
    borderColor: "#0f0f0f",
    flex: 1,
    fontSize: '26rpx',
    height: '100rpx',
    padding: '8rpx',
    marginBottom: '8rpx'
  },
  hashtag: {
    color: "blue",
    margin: '10rpx',
    fontWeight: "bold"
  }
};
class TextAreaDemo extends Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "Hello #World , Hello #Rax , Hello #天天好心情"
    };
  }

  render() {
    let delimiter = /\s+/;
    // split string
    let _text = this.state.text;
    let token,
      index,
      parts = [];
    while (_text) {
      delimiter.lastIndex = 0;
      token = delimiter.exec(_text);
      if (token === null) {
        break;
      }
      index = token.index;
      if (token[0].length === 0) {
        index = 1;
      }
      parts.push(_text.substr(0, index));
      parts.push(token[0]);
      index = index + token[0].length;
      _text = _text.slice(index);
    }
    parts.push(_text);

    let hashtags = [];
    parts.forEach(text => {
      if (/^#/.test(text)) {
        hashtags.push(
          
            {text}
          
        );
      }
    });

    return (
      
         {
            this.setState({ text });
          }}
        />
        
          {hashtags}
        
      
    );
  }
}

class App extends Component {
  state = {
    value: "I am value",
    curText: "",
    prevText: "",
    prev2Text: "",
    prev3Text: ""
  };

  inputRef = createRef();

  updateText = text => {
    this.setState(state => {
      return {
        curText: text,
        prevText: state.curText,
        prev2Text: state.prevText,
        prev3Text: state.prev2Text
      };
    });
  };

  render() {
    // define delimiter
    return (
      
        
           {
              this.updateText("onChange text: " + event.nativeEvent.text);
            }}
            style={styles.default}
          />

          
            {this.state.curText}
            {"\n"}
            (prev: {this.state.prevText}){"\n"}
            (prev2: {this.state.prev2Text}){"\n"}
            (prev3: {this.state.prev3Text})
          
        

        
           {
              this.setState({
                value: text
              });
            }}
          />

           {
              this.setState({
                value: e.nativeEvent.text
              });
            }}
            onClick={() => {
              this.setState({
                value: "I am value"
              });
            }}
          >
            Reset
          
        
        
      
    );
  }
}

render(, document.body, { driver: DriverUniversal });

4.2 taro

Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ 小程序 / H5 / RN 等应用。

原理图

image

通过不同的编译脚本,处理到不同平台的拓展

image

特点

  • 通过自定义不同平台的编译脚本,讲源码转译成目标平台的代码。

  • 适配平台多,覆盖native、小程序、h5。

  • 几乎完全兼容react语法。

代码示例:

import Taro from '@tarojs/taro'
import React from 'react'
import { View } from '@tarojs/components'
import { ThreadList } from '../../components/thread_list'
import api from '../../utils/api'

import './index.css'

class Index extends React.Component {
  config = {
    navigationBarTitleText: '首页'
  }

  state = {
    loading: true,
    threads: []
  }

  async componentDidMount () {
    try {
      const res = await Taro.request({
        url: api.getLatestTopic()
      })
      this.setState({
        threads: res.data,
        loading: false
      })
    } catch (error) {
      Taro.showToast({
        title: '载入远程数据错误'
      })
    }
  }

  render () {
    const { loading, threads } = this.state
    return (
      
        
      
    )
  }
}

export default Index

4.3 uni-app

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。

特点

  • 通过自定义不同平台的编译脚本,讲源码转译成目标平台的代码。

  • 适配平台多,覆盖native、小程序、h5。

  • vue语法。

原理图:

image

和taro类似,通过不同的编译脚本,处理到不同平台的拓展。区别是taro在native端使用的rn,uni-app使用的weex。

代码示例:

   
    

4.4 react-native-web

React Native for Web 是 React Native 组件和 API 在web端的实现,使得rn代码可与 React DOM 交互。react-native作为标准,react-native-web提供在web端的实现。

在介绍改方案的实现原理之前,先简单了解一下rn是怎么工作的,是怎么从代码显示到移动端设备的

jsx是react官方提供的React.createElement(type,[props],[...children])函数的语法糖,返回值是一个对象,即我们常说的vDOM。我们看一个例子:

源代码:

 
  
  
  Hello World!

经babel转换后的代码:

"use strict";

/*#__PURE__*/
React.createElement(View, {
  style: {
    flexDirection: "row",
    height: 100,
    padding: 20
  }
}, /*#__PURE__*/React.createElement(View, {
  style: {
    backgroundColor: "blue",
    flex: 0.3
  }
}), /*#__PURE__*/React.createElement(View, {
  style: {
    backgroundColor: "red",
    flex: 0.5
  }
}), /*#__PURE__*/React.createElement(Text, null, "Hello World!"));

react-native的原理图:

image

代码执行后,根据render方法内写的标签,调用createElement函数,创建对应的vDOM,然后根据vDOM创建真实DOM,即native的视图。

下面再来看react-native-web的实现原理:

原理图:

image

使用静态编译的方式,完成不同平台的适配。和taro的区别在于,taro是通过不同的编译脚本,将业务代码转译成不同的平台的代码,使得代码可以运行到不同的平台。该方案是将rn的组件和api作为标准,提供在web的实现,然后在编译阶段,通过别名的方式,将组件替换成web端的,然后配合不同的代码入口,使组件在渲染时,native容器使用UIManager渲染,在web容器中,使用ReactDom渲染内容。

{
  "plugins": [
    ["module-resolver", {
      // 设置别名
      "alias": {
        "^react-native$": "react-native-web"
      }
    }]
  ]
}

特点:

  • 代码完全遵循react-native的规范,对rn代码无侵入。

代码示例:

import React, { Component } from "react";
import { View, Text } from "react-native";

class App extends Component {
  render() {
    return (
      
        
        
        Hello World!
      
    );
  }
}

export default App;

四、选型分析

4.1 方案对比

方案/框架 改造成本 影响范围
客户端 存量rn页面 h5
RAX 需要 需要 需要
taro 需要 需要 需要
uni-app 需要 需要 部分需要
react-native-web 不需要 不需要 需要

从方案调研中可以看到,调研的几个方案均可实现一码多端(Android/iOS/h5)的需求,但是考虑到当前业务及团队的现状,每个方案就分别变现出不同的匹配度。下面逐个对比各个方案的匹配度情况:

  • RAX

    • 使用类react语法,符合团队技术规划;

    • 客户端部分,采用weex方案,如果使用此方案需要客户端配合改造,不在可控范围内;

    • 语法层面不兼容rn语法,所有页面都需要重构,对线上业务影响范围较大,且时间成本较高。

    • 支持小程序。

  • taro

    • 使用类react语法,符合团队技术规划;

    • 客户端部分,采用rn方案,如果使用此方案需要客户端配合分析当前krn是否支持,是否需要改造,影响范围太大;

    • 语法层面不兼容rn语法,所有页面都需要重构,对线上业务影响范围较大,且时间成本较高。

    • 支持小程序

  • uni-app

    • 使用vue语法,不符合团队技术规划;

    • 客户端部分使用weex方案,需要客户端配合改造,不在可控范围内

    • 原有使用vue开发的页面,不需要重构,仅需要确保适配。现有rn页面需要重构,整体时间成本,相对较低。

    • 支持小程序

  • react-native-web

    • 使用rn语法,符合团队技术规划

    • 客户端部分使用rn,目前krn完全支持。

    • 现有rn页面,需要逐个验证h5适配,其他h5页面需要逐步改造成rn。时间成本相对较低

    • 不支持小程序

4.3 方案选型

通过上述方案的对比后,可以看出如下结论:

  • react-native-web相比其他方案,跨端数量较少,仅支持三端(Android,iOS,web),但是对现在业务影响较小。

  • 相较而言,RAX、taro、uni-app更适合在项目早期从0到1阶段的早期选型,后期项目改造成本较大。

如此看来,改造成本最低,能支持rn转h5的react-native-web为匹配度最高的选择。

4.4 拓展分析

现阶段该方案不支持小程序平台,后期如果需要扩展到小程序,经调研后确认,可以使用自定义渲染器的方式实现平台的扩展。详情参考remax实现原理。

拓展示意图:

image

五、项目方案

使用react-native结合react-native-web实现一码多端(Android/iOS/h5)。对现有业务影响较小,且完美支持react,可以拥抱整个react生态。

5.1 架构图

image

5.2项目依赖及升级方案

  • "react": "16.11.0",

  • "react-dom": "^17.0.2",

  • "react-native": "0.62.2",

  • "react-native-web": "^0.16.3"

核心库强依赖krn版本,在现阶段使用中,锁死上述版本,后续需随krn的升级一起升级,保持和krn版本一致。

5.3 示例

  • 页面:(应用中心页面) h5页面和iOS端页面展示

  • log:可以在web和终端内查看console输出内容

5.4 多平台适配

官方组件

目前react-native-web已实现绝大部分组件的兼容,可以开箱即用

一个普通标题 一个普通标题 一个普通标题
短文本 中等文本 稍微长一点的文本
稍微长一点的文本 短文本 中等文本
react-native组件(0.62) react-native-web(0.16.5) 备注
ActivityIndicator
Button
FlatList
Image Missing multiple sources (#515) and HTTP headers (#1019).
ImageBackground
KeyboardAvoidingView Mock. No equivalent web APIs.
Modal
RefreshControl Not started (#1027).
SafeAreaView
ScrollView Missing momentum scroll events (#1021).
SectionList
StatusBar Mock. No equivalent web APIs.
Switch
Text Missing onLongPress (#1011) support.
TextInput Missing rich text features (#1023), and auto-expanding behaviour (#795).
TouchableHighlight
TouchableOpacity
TouchableWithoutFeedback
View
VirtualizedList
DrawerLayoutAndroid(Android) Not started (#1024).
TouchableNativeFeedback(Android)
InputAccessoryView(iOS)

三方组件

待续

Api

绝大部分常用api,已实现适配。可以开箱即用

react-native api名称(0.62) react-native-web(0.16.5) 备注
AccessibilityInfo Mock. No equivalent web APIs.
Alert Not started (#1026).
Animated Missing useNativeDriver support.
Appearance
AppRegistry Includes additional support for server rendering with getApplication.
AppState
DevSettings DevSettings
Dimensions
Easing
InteractionManager
Keyboard Mock.
LayoutAnimation Missing translation to web animations.
Linking
PanResponder
PixelRatio
Share Only available over HTTPS. Read about the Web Share API.
StyleSheet
Systrace Systrace
Transforms
Vibration
BackHandler(Android) Mock. No equivalent web APIs.
PermissionsAndroid(Android) Android设备的权限管理
ToastAndroid(Android) Android设备的toast
ActionSheetIOS(iOS) iOS的弹窗
Settings(iOS) No equivalent web APIs.

六、项目规划

整体分为四个阶段

阶段一: 单一项目试点,跑通项目改造全流程

暂定【 ** 首页- ** 页面】

目标:

通过【 ** 首页- ** 页面】试点跑通整个方案从开发到上线的全部环节,梳理整个流程中出现的问题点。

时间:预计耗时半个月,日前完成

试点项目:** 页面

工作内容

1、【1pd】react-native-web集成

2、【1pd】调通h5编译

3、【2pd】项目内组件多端适配

4、【3pd】网络请求模块多端适配

5、【3pd】埋点模块多端适配

6、【1pd】修改rn降级页面为最新h5页面

7、【Xpd】qa测试

8、【1pd】创建对应的监控面板

9、【1pd】打包发布

阶段二:打造高质量的基础设施,为后续推广提供有力支撑

目标:

  • 1、分析梳理阶段一中暴露出的问题,针对性设计优化方案于解决方法

  • 2、制定基建计划,提供高质量的基建配套。

时间:预计耗时1~2个月,* 月* 日前完成

基建内容:

  • 网络库:支持端内和端外的使用
  • 文档阶段 (3-4pd)

  • 设计文档

  • 使用文档

  • 编码阶段 (2-3pd)

  • 日志和埋点系统: 支持端内和端外的使用
  • 文档阶段 (3-4pd)

  • 设计文档

  • 使用文档

  • 编码阶段 (2-3pd)

  • bridge模块: 支持端内和端外的使用
  • 文档阶段 ( 1-2pd)

  • 设计文档

  • 使用文档

  • 编码阶段 (3-4pd)

  • 跳转($go)模块: 支持端内和端外的使用
  • 文档阶段 ( 1-2pd)

  • 设计文档

  • 使用文档

  • 编码阶段 (3-4pd)

  • 降级:rn页面出现问题是能降级到h5页面
  • 文档阶段 (2-3pd)

  • 设计文档

  • 监控:异常报警、数据报表
  • 文档阶段(2-3pd)

  • 监控指标

  • 如何创建监控面板

  • 打包发布:一键发布rn和h5
  • 文档阶段 (3-5pd)

  • 打包流程说明文档

  • 打包发布操作文档

  • 编码阶段 (后期详评)

  • 组件库:兼容三端
  • 文档阶段

  • 设计文档

  • 建设计划

  • 开发文档(组件库开发人员使用)

  • 使用文档(业务开发人员使用)

  • 编码阶段(后期详评)

阶段三:一码多端,全面推广

目标:逐步推广,让更多业务接入

时间:视具体页面接入数量而定

阶段四:长期维护,为业务的迭代提供更好的支撑

目标: 支撑业务快速迭代。

时间: 长期

你可能感兴趣的:(一码多端)