一码多端

2022-08-08  本文已影响0人  nextChallenger

一、项目背景

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

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

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

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

二、问题分析

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

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

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

一码多端示意图:

image.png

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

三、项目目标

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

研发效率提升计算公式: (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,处理对不同平台的拓展。

特点

代码示例:

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 key={text} style={styles.hashtag}>
            {text}
          </Text>
        );
      }
    });

    return (
      <View style={styles.container}>
        <TextInput
          multiline={true}
          numberOfLines={3}
          style={styles.multiline}
          value={this.state.text}
          onChangeText={text => {
            this.setState({ text });
          }}
        />
        <View style={{ flexDirection: "row", flexWrap: "wrap" }}>
          {hashtags}
        </View>
      </View>
    );
  }
}

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

  inputRef = createRef();

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

  render() {
    // define delimiter
    return (
      <View style={styles.root}>
        <View style={styles.container}>
          <TextInput
            autoCapitalize="none"
            placeholder="Enter text to see events"
            autoCorrect={false}
            onChange={event => {
              this.updateText("onChange text: " + event.nativeEvent.text);
            }}
            style={styles.default}
          />

          <Text style={styles.eventLabel}>
            {this.state.curText}
            {"\n"}
            (prev: {this.state.prevText}){"\n"}
            (prev2: {this.state.prev2Text}){"\n"}
            (prev3: {this.state.prev3Text})
          </Text>
        </View>

        <View style={styles.container}>
          <TextInput
            placeholder="Enter text to see events"
            value={this.state.value}
            ref={this.inputRef}
            style={{
              width: '600rpx',
              marginTop: '20rpx',
              borderWidth: "2rpx",
              borderColor: "#dddddd",
              borderStyle: "solid"
            }}
            onChangeText={text => {
              this.setState({
                value: text
              });
            }}
          />

          <View
            style={{
              marginTop: '20rpx'
            }}
            onFocus={e => {
              this.setState({
                value: e.nativeEvent.text
              });
            }}
            onClick={() => {
              this.setState({
                value: "I am value"
              });
            }}
          >
            <Text>Reset</Text>
          </View>
        </View>
        <TextAreaDemo />
      </View>
    );
  }
}

render(<App />, document.body, { driver: DriverUniversal });

4.2 taro

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

原理图

image

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

image

特点

代码示例:

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 (
      <View className='index'>
        <ThreadList
          threads={threads}
          loading={loading}
        />
      </View>
    )
  }
}

export default Index

4.3 uni-app

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

特点

原理图:

image

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

代码示例:

   <template>
        <view>
            <view>{{userName}}</view>
        </view>
    </template>
    <script>
        export default {
            data() {
                return {
                    "userName":"foo"
                }
            }
        }
    </script>

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。我们看一个例子:

源代码:

 <View
  style={{
    flexDirection: "row",
    height: 100,
    padding: 20
  }}
>
  <View style={{ backgroundColor: "blue", flex: 0.3 }} />
  <View style={{ backgroundColor: "red", flex: 0.5 }} />
  <Text>Hello World!</Text>
</View>

经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"
      }
    }]
  ]
}

特点:

代码示例:

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

class App extends Component {
  render() {
    return (
      <View
        style={{
          flexDirection: "row",
          height: 100,
          padding: 20
        }}
      >
        <View style={{ backgroundColor: "blue", flex: 0.3 }} />
        <View style={{ backgroundColor: "red", flex: 0.5 }} />
        <Text>Hello World!</Text>
      </View>
    );
  }
}

export default App;

四、选型分析

4.1 方案对比

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

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

4.3 方案选型

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

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

4.4 拓展分析

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

拓展示意图:

image

五、项目方案

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

5.1 架构图

image

5.2项目依赖及升级方案

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

5.3 示例

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个月,* 月* 日前完成

基建内容:

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

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

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

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

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

时间: 长期

上一篇 下一篇

猜你喜欢

热点阅读