需求背景

我们最近要做一个React Native的产品详情页,里面有一部分内容展示的是服务端下发的html标签(运营人员编写的,内容不固定,且很随意)。需求点是:1、展示富文本,包括imgspandiv等基本html标签;2、控制富文本的整体样式,比如业务配置的图片大小不一,我们要统一已屏幕宽度为基准,高度自适应。

第三方组件react-native-htmlview

要实现这个需求,我第一想到的就是react-native-htmlview,我们之前断断续续的RN页面里的富文本展示需求用的都是这个组件。而且这个组件有一个好处就是不用传入高度,能根据富文本内容自适应高度。使用也很简单:

import HTMLView from 'react-native-htmlview'
<HTMLView 
	value={wrapper}
/>

只需要把富文本传给value就行了,可是实际展示效果令人堪忧。


文本虽然展示比较差,但图片展示还不错。


于是我就着手去支持文本标签,最终效果如图:


还不错吧。如果你的需求就只是展示下富文本(图片、文本),那这个开源组件就可以满足你的需求了。先说下我是如何把文本样式正确展示出来的:

<HTMLView 
	value={wrapper}
	renderNode={renderNode}
/>

function renderNode(node, index, siblings, parent, defaultRenderer) {
			if(node.name == 'div') {
				const View1 = styled.View`
				${ node.attribs.style };
			  `;
		  
			  return (
				  <View1 key={index}>
					{defaultRenderer(node.children, parent)}
				  </View1>
			  );
			} else if (node.name == 'p' || node.name == 'span') {
		  
			  const Text1 = styled.Text`
				${ node.attribs.style };
			  `;
		  
			  return (
				  <Text1 key={index}>
					{defaultRenderer(node.children, parent)}
				  </Text1>
			  );
			}else if(node.name == 'strong'){
			  const Text2 = styled.Text`
				${ node.attribs.style };
				font-weight:bold;
			  `;
		  
			  return (
				  <Text2 key={index}>
					{defaultRenderer(node.children, parent)}
				  </Text2>
			  );
			}
}

renderNode这个接口提供了让我们自定义解析html标签的方法,这里我用到了styled-components,它能去读取html标签的css样式,然后转换成RN支持的样式代码,如上述的Text1Text2
好,现在能展示文本和图片了,然而我们的需求不止这么简单,我们还要统一控制样式:图片不能有大有小,必须自适应屏幕宽度。
这里,我对img标签做了处理,如果是img标签,输出的是可以根据宽度,按照比例拿到高度的图片。

if(node.name == 'img'){
				return <AutoSizedImage
					key={index}
					source={{uri: node.attribs.src}}
					style={{width:330, height:0}}
				/>
}

然而,ios上可以了,android上输出的图片只能是默认大小,即使我已经重新给高度赋值了。

import React, { PureComponent, Component } from 'react';
import {
  Image,
  View,
} from 'react-native';

const baseStyle = {
  backgroundColor: '#0f0',
};

export default class AutoSizedImage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      width: this.props.style.width || 1,
      height: this.props.style.height || 1,
    };
  }

  componentDidMount() {
    Image.getSize(this.props.source.uri, (w, h) => {
      console.log('setState')
      this.setState({ width: w, height: h });
    });
  }

  render() {
    const finalSize = {};

    finalSize.width = this.props.style.width;
    const ratio = this.props.style.width / this.state.width;
    finalSize.height = Math.floor(this.state.height * ratio);

    console.log('height='+finalSize.height)
    return <Image style={{width: finalSize.width, height: finalSize.height}} source={this.props.source} />
  }
}

而且,我们要控制的样式有很多,如果每个都自定义一个组件,可维护性太差,故暂时放弃。

第三方组件react-native-render-html

接下来,我用到了网上很火的react-native-render-html,使用方法如下:

import HTML from 'react-native-render-html'
<HTML
	html={wrapper}
	tagsStyles
/>

这里又遇到了问题,ios上展示的图片很模糊,而且也存在第一个组件的问题:要想全局控制整体样式,还是要根据每个标签做适配,开发成本有些大。

原生组件WebView

至此,我们要想用webView,那就必须根据webView内容控制高度。

import {
	WebView
} from 'react-native';

<WebView
						style={{
              				width: Dimensions.get('window').width,
              				height: this.state.height
						}}
						injectedJavaScript={injectedJs}
						automaticallyAdjustContentInsets={true}
						source={{html: `<!DOCTYPE html><html> <style type="text/css">
						.tour_product_explain img{ display: block!important; vertical-align: top!important; width: 100%!important;}
						.tour_product_explain{ padding: 0 15px 20px 15px;}
						.tour_product_explain *{text-align: left!important;
							font-size: 14px!important;
							line-height: 1.3!important;
							font-family: Arial,"Lucida Grande",Verdana,"Microsoft YaHei",hei!important;
							float: none!important;
							padding: 0!important;
							position: static!important;
							height: auto!important}
						</style><body><div class='tour_product_explain' id='content'>${this.state.value}</div></body></html>`}}
						scalesPageToFit={true}
						javaScriptEnabled={true} // 仅限Android平台。iOS平台JavaScript是默认开启的。
						domStorageEnabled={true} // 适用于安卓a
						scrollEnabled={false}
						onMessage={(event)=>{
							 console.log(event.nativeEvent.data )
                			this.setState({height: +event.nativeEvent.data})
						}}
/>

const injectedJs = 'setInterval(() => {window.parent.postMessage(document.getElementById("content").clientHeight)}, 500)'

这里,富文本可以完全按照我们的需求展示,但是,webview的高度太大了,拿到的offsetHeight比实际高度大很多。

JAVA编写HTML富文本 html 富文本_react native


这个问题困扰了我整整两天,偶然一次调试中,高度居然正确了。这次调试我做了什么呢?我只是把webview的初始高度由100改成了0啊!

JAVA编写HTML富文本 html 富文本_webview_02


webview高度居然就算对了!

JAVA编写HTML富文本 html 富文本_高度_03


为什么初始高度会影响offsetHeight的值,我这里还不清楚,了解的小伙伴可以给我留意。


2019.6.24
webview的方案在后来的开发过程中又被我放弃了,因为webview加载富文本太慢了。我这边测试下来,是要等所有图片加载完成才能给出回调,这样就要等的时间太长了。
最终我还是决定用react-native-render-html,而它确实不能很好地满足我们的需求,目前我的做法是把不满足我们需求的功能,对源码做了些修改。

最后

至此,react native里展示富文本就彻底解决了。原理是给富文本注入一段JS代码,让其可以把父div的高度回调给我,我再去setState设置webview高度。
至于上面介绍的两个开源组件,这两天我也去读了里面的实现:它们其实是把html标签对应渲染成RN中的Text或者View,这其实是让我没法理解的:要先去解析html标签,再展示成原生组件,那为什么不直接让webView直接去解析呢?
如果你的业务场景只是要正确展示出富文本,那这两个开源组件还是推荐的,只是我这里要全局统一控制样式,webView在这个业务场景下是更适合我的。

这里简单记录一下,希望能给遇到这个问题的小伙伴一些帮助。