在 web 环境运行 react-native 页面

背景

近两年来react-native构造原生应用异常火爆,在app中用来替代H5页面可以明显提升用户体验,但是在一些场景是需要配套web版本的,比如分享、seo或者react-native报错时的降级方案等。如果适配web再去实现一套H5的页面会增加开发和维护成本,同一套代码能不能跑在浏览器了? 由于react-native的页面都是基于react-native基础组件和API或者自己实现的module,react-native页面的代码是完全可以复用的。 web端实现同样的基础组件和API,webpack打包js文件时做好组件映射,这样同一套业务代码可以运行在三端。

WEB配套react-native基础组件&API

业内也有这方面的实践,淘宝和和Twritter都开源了web组件和API代码就不需要自己去实现了,我选用的是淘宝的React-web,详情见https://github.com/taobaofed/react-web 这个git项目官方差不多停止维护,自己拷贝了一份来维护。

实践&解决问题

项目目录结构,index.web.js为web项目的入口文件,index.ios.js和index.android.js分别为ios和android打包入口文件。

react组件的代码大概这样

import {Component} from 'react'
import {StyleSheet, View, Text} from 'react-native'
import { connect } from 'react-redux'
import ChorusItem from './ChorusItem'
import Gotobar from './Gotobar'
class Chorus extends Component {
       constructor(props) {
         super(props);
       }    
    render(params) {
        const {  chorus, dispatch } = this.props;
        return (
            <View>
                <Gotobar route={'/chorus'} dispatch={this.props.dispatch}>{'合唱推荐'}</Gotobar>
                <View style={styles.itemColumn}>
                    {chorus.map((item, index) => <ChorusItem item={item} dispatch={dispatch} from={'indexPage.chorus'} key={index} index={index + 1}></ChorusItem>)}
                </View>          
            </View>
        )
    }
}
var styles = StyleSheet.create({
    itemColumn: {
        justifyContent: 'flex-start'
    }
})
module.exports = connect((state)=>{
  return {
    chorus: state.indexPage.chorus
  }
})(Chorus)

遇到web和react-native有差异时需要自己区分平台写差异代码,对于比较小的差异可以通过Platform来区分例如

_onScroll(e) {
    let event = e.nativeEvent, y;
    if (Platform.OS == 'web') {
          common.event.emit('scroll.change', event.target.scrollTop)
          common.event.emit('scroll.report', event.target.scrollTop)
    }
  }

差异较大的建议区分平台抽象为组件,通过webpack打包时映射到对应的web组件上,例如路由组件web用的是RouterContext.web.js, native用RouterContext.js

实践过程中有遇到些问题,列举两个影响和改动较大的问题

1 . web为了保持和react-native布局保持一致,页面固定一屏高度采用absolute + overflow:scoll局部滚动布局,IOS下滚动到页面顶部或者底部有回弹效果这时如果再向相反方向滚动页面导致页面无法滚动,如下图:

解决方案:去掉固定一屏高度和局部滚动的布局,采用常规的布局。这样会影响固定顶部、底部、遮罩层的布局,web端需要增加position:fixed样式,和native端的样式需要区分开。

2 .flex兼容问题,react-native采用flex布局,web端flex分为3个版本,2009、2012、final。 2009版本主要是兼容安卓4.4以下的设备,需要对flex属性兼容例如flex属性的映射和补充(flexWrap缺失)以及添加厂商前缀(-webkit)。详情见http://caniuse.com/#search=flex-wrap


//2009 flex属性和值映射
var flexboxProperties = {
      flex: "WebkitBoxFlex",
      order: "WebkitBoxOrdinalGroup",
      // https://github.com/postcss/autoprefixer/blob/master/lib/hacks/flex-direction.coffee
      flexDirection: "WebkitBoxOrient",
      // https://github.com/postcss/autoprefixer/blob/master/lib/hacks/align-items.coffee
      alignItems: "WebkitBoxAlign",
      // https://github.com/postcss/autoprefixer/blob/master/lib/hacks/justify-content.coffee
      justifyContent: "WebkitBoxPack",
      flexWrap: null,
      alignSelf: null,
};
var oldFlexboxValues = {
      flex-end: "end",
      flex-start: "start",
      space-between: "justify",
      space-around: "distribute",
};    
//flexWrap属性补充,组件增加兼容低版本样式_olderStyle属性, 例如
 _renderRow(rowData, sectionID, rowID){
     return (
         <SingleItem item={rowData} from={'single'} _olderStyle={{display: 'inline-block', 'paddingLeft':1}}/>
     )
}
//react-web处理样式生成jsx的style时添加_olderStyle里面的样式
if (flexboxSpec == '2009' && _olderStyle) {
    for (var key in _olderStyle) {
        var value = _olderStyle[key];
          if (!isValidValue(value)) {
            continue;
          }
          result[key] = value;
    }
  }

优化

react-web生成的页面在体验方面有些不太理想,比如js文件大小、首屏可见时间等,所以在某些做了些优化。

1 . 支持后端渲染直出提升首屏渲染可见时间,常规的静态页面渲染要经过js下载、执行,react组件渲染、数据加载、组件更新等耗时时间较长,如下图所示,在无缓存+wifi+笔记本i5+8g环境下,js大小为100kb,js下载+执行耗时300+ms

由于flex兼容判断是依赖浏览器环境,后端渲染需要去掉这些依赖补全全部的兼容样式,服务端渲染首屏主要耗时在后端渲染耗时较短200ms内基本可以返回html内容。

2 .按需加载组件减少不必要的依赖从而减少js文件大小 import {StyleSheet, View} from 'react-native' -> import View from 'react-native/View/View.web' import StyleSheet from 'react-native/View/View.web'

按需import前所有组件压缩后300kb 按需import后常用的组件压缩后80kb(stylesheet,view,text,image,touch,listview,scrollvie…)等

3 .常用组件+react+redux打包压缩后大小有300+kb依然不够理想, react+reactDom+redux占了160kb,可以用类react库替代react,从文件大小考虑最后用preact替换掉react,迁移也相对容易。 preact是react的规范的一种简单高效实现体积非常小,包含特性:vnode、component、lifecycle、context、props&state、Refs,精简掉的特性:PropType、 Synthetic Events。 由于preact去掉了合成事件,所有的事件都是绑定到dom上,对应的react-native的触摸手势事件需要用原生事件替代,组件上的手势事件prop改为原生的touch事件prop。

const EVENT_MAP = {
 'onStartShouldSetResponder': 'onTouchStart',
 'onMoveShouldSetResponder': 'onTouchMove',
 'onStartShouldSetResponderCapture': 'onTouchStartCapture',
 'onMoveShouldSetResponderCapture': 'onTouchMoveCapture',
 'onResponderGrant': 'onTouchStart',
 'onResponderTerminate': 'onTouchCancle',
 'onResponderMove': 'onTouchMove',
 'onResponderRelease': 'onTouchEnd'
}
//preact设置组件属性的时候会加上事件
if (name[0]=='o' && name[1]=='n') {
 let useCapture = name !== (name=name.replace(/Capture$/, ''));
 name = name.toLowerCase().substring(2);
 if (value) {
     if (!old) node.addEventListener(name, eventProxy, useCapture);
 }
 else {
     node.removeEventListener(name, eventProxy, useCapture);
 }
 (node._listeners || (node._listeners = {}))[name] = value;
}

preact的事件是直接绑定到DOM节点上的,当事件过多时建议采用事件代理来减少事件监听。 preact性能测试数据参考https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html

React+redux+reactDom打包压缩后的大小为160kb

Preact+preactcompat+redux打包压缩后大小为38kb

4 .react-web生成的页面样式都是内联到style属性上,这些样式属性可以从代码里提取出来生成css文件,这样就可以缓存页面的css也可以减少一些flex兼容的计算。

实现方式是编写webpack babel插件,利用静态抽象树AST来找出StyleSheet.create调用函数的参数,根据这个参数过滤出可以直接提取的样式对象并删除这些样式对应的AST节点,用过滤出来的样式对象生成classname样式文件,然后遍历jsx节点的style属性并给节点加上对应的className属性, 关于babel插件编写可参考https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md, 里面的 API和教程相当详细。

抽取css文件的主要流程如下图:(注:无法转化为样式字符串的style是指需要通过表达式计算得出的样式。) 举个例子:

转换前的style对象

let styles = StyleSheet.create({

    subtitleContainer: {

        flexDirection: 'row',

        justifyContent: 'flex-start',

        alignItems: 'center'

    },

    subtitleText: {

        fontSize: txt_1,

        color: '#F04F43',

    },

    iconRight : {

        height: 7,

        width: 7,

        borderTopWidth: 2,

        borderRightWidth: 2,

        borderColor: '#F04F43',

        //backgroundColor: '#F04F43',

        borderStyle: 'solid',

        transform: [{ 'rotate': '45deg' }]

    }

});

//JSX

<View style={styles.subtitleContainer}>

    <Text style={styles.subtitleText}>查看特权</Text>

        <View style={styles.iconRight}></View>

    </View>        

//转换完成后的CSS 类名经过hash处理

._17cytc2 {

    -webkit-box-orient: horizontal;

    -webkit-box-direction: normal;

    -webkit-flex-direction: row;

    flex-direction: row;

    -webkit-box-pack: start;

    -webkit-justify-content: flex-start;

    justify-content: flex-start;

    -webkit-box-align: center;

    -webkit-align-items: center;

    align-items: center;

}

.osilsm {

    color: #F04F43;

}

._41z5ub {

    height: 7px;

    width: 7px;

    border-width: 0px;

    border-top-width: 2px;

    border-right-width: 2px;

    border-color: #F04F43;

    border-style: solid;

    -webkit-transform: rotate(45deg);

    transform: rotate(45deg);

}    

//转换过后的代码

var styles = __WEBPACK_IMPORTED_MODULE_4_react_native_StyleSheet_StyleSheet_web__["a" /* default */].create({

    subtitleText: {

        fontSize: txt_1

    }

});

//JSX

__WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement(

    __WEBPACK_IMPORTED_MODULE_1_react_native_View_View_web__["a" /* default */],

    {

        className: "_17cytc2",

        style: styles.subtitleContainer },

    __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement(

        __WEBPACK_IMPORTED_MODULE_2_react_native_Text_Text_web__["a" /* default */],

        {

            className: "osilsm",

            style: styles.subtitleText },

        "\u67E5\u770B\u7279\u6743"

    ),

    __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement(__WEBPACK_IMPORTED_MODULE_1_react_native_View_View_web__["a" /* default */], {

        className: "_41z5ub",

        style: styles.iconRight })

)

优化前后对比

环境为桌面chrome61 i7+wifi

1. 页面js加载和执行耗时如下

优化前

script加载和执行耗时168ms

优化后

script加载和执行耗时125ms 主要缩减react+reactweb组件大小, 大小从251kb缩减到117kb

2.组件渲染和首屏时间如下

优化前

优化后

组件渲染时长从105ms降到86ms,首屏可见事件从292ms提前到了230ms

线上数据

优化后页面是从9月29日开始 总资源加载耗时

页面开始导航到可交互耗时

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯AI实验室的专栏

腾讯AI Lab斩获知识图谱顶级赛事KBP 2017世界冠军

感谢阅读腾讯AI Lab微信号文章,恭喜知识图谱团队首次出战KBP大赛就获得实体发现与链接任务三语总分第一名好成绩!

18310
来自专栏社区的朋友们

机器学习概念总结笔记(一)

本部分介绍了机器学习算法的四大分类,即:监督学习、半监督学习、无监督学习和增强学习以及包括最小二乘回归、岭回归、LASSO回归、LARS回归在内的26大常见算法...

1.6K40
来自专栏Star先生的专栏

Tensorflow 术语表

本文主要简要介绍了广播操作、Graph(图)、Session(会话)、Tensor 等13个 Tensorflow 术语表。希望对大家了解学习 Tensorfl...

1.2K10
来自专栏腾讯AI实验室的专栏

腾讯AI Lab斩获知识图谱顶级赛事KBP 2017世界冠军

谢阅读腾讯AI Lab微信号文章,恭喜知识图谱团队首次出战KBP大赛就获得实体发现与链接任务三语总分第一名好成绩!

12110
来自专栏利炳根的专栏

学习笔记DL001 : 数学符号、深度学习的概念

深度学习是机器学习拉出的分支,它试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。本文主要介绍深度学习中的数学符号、数和数组。

59200
来自专栏吕晟的专栏

机器学习库初探之 Caffe

Caffe 是一个清晰而高效的深度学习框架,其作者是博士毕业于 UC Berkeley 的贾扬清,目前在 Facebook 工作。Caffe 是纯粹的 C++/...

1.5K20
来自专栏社区的朋友们

一个 RNN 调研引发的点评推荐血案

在这篇文章里,作者基于用户的评论内容,评估了十多种不同的 RNN 框架。这些 RNN 框架包括多层双向 GRU 和 LSTM,同时有 attention-bas...

1.8K00
来自专栏程飞翔的专栏

XGBoost 源码阅读笔记(2):树构造之 Exact Greedy Algorithm

本篇将继续向大家介绍 XGBoost 源码是如何构造一颗回归树,不过在分析源码之前,还是有必要先和大家一起推导下 XGBoost 的目标函数。

1.1K10
来自专栏腾讯移动品质中心TMQ的专栏

Tensorflow 的 word2vec 详细解释:basic篇

Word2Vec即Word to vector(词汇转向量)。我们希望词义相近的两个单词,在映射之后依然保持相近,词义很远的单词直接则保持很远的映射距离。

1.6K40
来自专栏织云平台团队的专栏

腾讯赵建春:AI浪潮下的高效运维思考及实践

腾讯 SNG 助理总经理、GOPS 金牌讲师赵建春老师受邀出席大会,并带来精彩演讲《AI 浪潮下的高效运维思考与实践》。

79410

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励