在 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 条评论
登录 后参与评论

相关文章

来自专栏代码GG之家

google 分屏 横屏模式 按home键界面错乱故障分析(一)

你确定你了解分屏的整个流程? ? 之前分析文章列表: Android 关机对话框概率没有阴影故障分析 android recent key长按事件弹起触发最近列...

1788
来自专栏编程微刊

HTML5中的Web Notification桌面通知(右下角提示)

1183
来自专栏ytkah

微信小程序开发教程第五章:微信小程序名片夹详情页开发

今天加了新干货!除了开发日志本身,还回答了一些朋友的问题。 闲话不多说,先看下「名片盒」详情页的效果图: ? ? 备注下大致需求:顶部背后是轮播图,二维码...

2948
来自专栏Sorrower的专栏

界面无小事(四):来写个滚动选择器吧!

512
来自专栏我和未来有约会

Silverlight中摄像头的运用—part2

Silverlight 4 中摄像头的运用—part1 将跟踪颜色视作输入 好了,我们能够跟踪到这个颜色了,那这么做的意义是什么呢?实际上,我们可以根据它的位...

1796
来自专栏Scott_Mr 个人专栏

React Native 系列(五) -- 组件间传值

33710
来自专栏菩提树下的杨过

Flash/Flex学习笔记(6):制作基于xml数据源的flv视频播放器

今天折腾了大半天,总算搞出了一个功能简单的视频播放器,可以向公司领导交差了 :) 步骤: 1.Flash CS4 中 先拖一个"FLVPlayback"组件到舞...

1935
来自专栏QQ音乐技术团队的专栏

iOS 视图,动画渲染机制探究

终端的开发,首当其冲的就是视图、动画的渲染,切换等等。用户使用 App 时最直接的体验就是这个界面好不好看,动画炫不炫,滑动流不流畅。UI就是 App 的门面,...

3179
来自专栏糊一笑

模拟制作网易云音乐(AudioContext)

记得好早前在慕课网上看到一款可视化音乐播放器,当前是觉得很是神奇,还能这么玩。由于当时刚刚转行不久,好多东西看得稀里糊涂不明白,于是趁着现在有时间又重新梳理了一...

3185
来自专栏xdecode

IE之页面加载慢.

场景 场景是大屏页面一张深色背景, 里面一些文本元素以及图表展示. 结果在IE下发现加载异常缓慢, 还有部分人员反馈页面卡死. ? 后台读写优化 默认处理图片逻...

1787

扫码关注云+社区