React Native展示富文本(HTML标签)最佳解决方案

需求背景

我们最近要做一个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比实际高度大很多。
在这里插入图片描述
这个问题困扰了我整整两天,偶然一次调试中,高度居然正确了。这次调试我做了什么呢?我只是把webview的初始高度由100改成了0啊!
在这里插入图片描述
webview高度居然就算对了!
在这里插入图片描述
为什么初始高度会影响offsetHeight的值,我这里还不清楚,了解的小伙伴可以给我留意。


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

最后

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

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

支持拖拽 复制 截图 excel ----------------------------------------------------------------------------------------------------------------------------------------------复制代码 /** * Created by zhanglei on 2017/5/23. */ import React, { Component, PropTypes } from 'react'; import { Icon,Modal,message } from 'antd'; import ContentEditable from 'react-contenteditable' import './edit.less' export default class Editor extends Component { static propTypes = { className: PropTypes.string, value:PropTypes.string, editColor:PropTypes.string, }; constructor(props){ super(props); ['inputTextChange','onchangefile','onpaste','ondrop','onParseOrDrop'].map(item=>this[item]=this[item].bind(this)); this.state={ value:null, tableData:[], linkModel:false, visible:false, isColor:false, myDisabled:false, isEdit:true, isFace:false, isBackground:false, linkValue:null, editStatus:[ {label:'加粗',value:'bold',icon:'zitijiacu'}, {label:'斜体',value:'italic',icon:'zitixieti'}, {label:'下划线',value:'underline',icon:'xiahuaxian'}, {label:'链接',value:'createLink',icon:'lianjie'} ], fontSizeData:[ {title:'大号',value:'h1',icon:'H1'}, {title:'中号',value:'h2',icon:'h2'}, {title:'正常',value:'h3',icon:'h3-copy-copy'}, {title:'小号',value:'h4',icon:'h4'} ], isSent:true, colorData:[ 'red','orange','yellow','#01FF01','#98F5FF','#8686FF','rgb(216, 154, 255)', '#fff', '#DE1607','#E49402','#E2E205','#04DE04','rgb(71, 237, 255)','#6363F9','rgb(204, 123, 255)', 'rgb(206, 205, 205)', '#C10303','#D08702','#C5C503','#07C307','rgb(0, 221, 245)','#4C4CFB','rgb(184, 70, 255)', 'rgb(183, 183, 183)', '#960505','#AB7005','#ABAB03','#02A902','rgb(6, 171, 189)','#3333FF','rgb(167, 25, 255)', 'rgb(148, 148, 148)', '#710303','#989805','#989805','#059C05','rgb(9, 138, 152)','blue','#A020F0', 'rgb(76, 75, 75)', '#5D0404',' #757504','#757504','green','rgb(2, 99, 109)','blue','#A020F0', '#000','rgb(56, 2, 2)' ], } }; componentDidMount(){ document.addEventListener('click',this.documentClick); }; componentWillReceiveProps(nextProps){ if('value' in nextProps&&this;.state.editValue !== nextProps.value){ this.setState({editValue:nextProps.value}) } } //全局取消隐藏颜色框 documentClick=(e)=>{ const {isColor,isBackground} = this.state; if(isColor||isBackground){ let en = e.srcElement||e.target; const name = '.color-content'; while(en){ if(en.className&&en;.className === name.replace('.','')){ return; } en = en[removed]; } this.setState({isColor:false,isBackground:false}); } }; //卸载颜色框 componentWillUnmount(){ document.removeEventListener('click',this.documentClick) } /* * <粘贴功能> * @param onParseOrDrop 通用方法
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值