近两年来react-native构造原生应用异常火爆,在app中用来替代H5页面可以明显提升用户体验,但是在一些场景是需要配套web版本的,比如分享、seo或者react-native报错时的降级方案等。如果适配web再去实现一套H5的页面会增加开发和维护成本,同一套代码能不能跑在浏览器了? 由于react-native的页面都是基于react-native基础组件和API或者自己实现的module,react-native页面的代码是完全可以复用的。 web端实现同样的基础组件和API,webpack打包js文件时做好组件映射,这样同一套业务代码可以运行在三端。
业内也有这方面的实践,淘宝和和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日开始 总资源加载耗时
页面开始导航到可交互耗时
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。