react-native渲染富文本的几种方案

凌博实
2023-12-01

前言

在日常开发中,经常需要用到富文本编辑器来编辑业务内容,例如新闻、论坛等,然后在用到的地方进行渲染,一般在web端可以直接在html中载入编辑好的内容(一般是一段html),不过在react-native的app中不能直接把html渲染进去,接下来我介绍几种渲染html的方案。

正文

1、使用已有的组件

既然已经有人造好了轮子,我们就直接拿来用就可以了。可以在网上搜 react-native 富文本组件,可以有几个选择方案,我这边以react-native-render-html,npm地址:www.npmjs.com/package/rea…

使用方法:

import RenderHTML , {IMGElementContainer, useIMGElementProps, useIMGElementState, IMGElement} from "react-native-render-html";

// 其他代码 ...
_previewImg = (src) => {const images = [{ uri : src}];Overlay.show((<Overlay.PopViewcontainerStyle={{flex: 1}}overlayOpacity={1}ref={v => this.fullImageView = v}><AlbumViewstyle={{flex: 1}}control={true}images={images}defaultIndex={0}onPress={() => {this.fullImageView && this.fullImageView.close()}}/></Overlay.PopView>));
};

_imagesNode = (props) => {const imgElementProps = useIMGElementProps(props);return (<IMGElementonPress={this._previewImg.bind(this, imgElementProps.source.uri)}source={imgElementProps.source}contentWidth={imgElementProps.contentWidth - 15}/>);
};

render() {const styles = this.styles;const Props = this.props;return (<RenderHTMLsource={{html: this._handleContent(Props.data)}}renderers={{img: this._imagesNode}}tagsStyles={{p: {marginVertical: 5}}}{...Props}/>);
}
// 其他代码 ... 

这个组件的原理就是把富文本的html标签一个个的解析出来,转换成react-native的标签,再进行渲染就可以了。

注意:如果需要点击放大预览的话,图片需要单独处理。

2、使用webview

我们除了直接使用已有的组件之外,还有可以使用webview进行渲染。webview就相当于嵌在App里面的浏览器一样,可以直接访问和渲染html代码。其中webview载入html资源还有两种方法:

(1)直接在source上写html,然后把内容(data)放进去:

import WebView from 'react-native-webview'
// 其他代码...
 render() {const Props = this.props;const { webHeight } = this.state;let data = this._handleContent(Props.data);return <WebViewsource={{html: `<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><style>html, body, *{margin:0; padding: 0;}p, pre {margin: 1em 0;}body img{max-width: 100% !important;}</style></head><body>${data}</body></html>`, baseUrl:Platform.OS === "ios" ? undefined : ''}}style={{height: webHeight}}contentInset={{top:0,left:0}} ></WebView>}
// 其他代码... 

(2)使用资源文件载入:

具体实现是,先新建一个html文件,在里面写好初始化方法init,提供入参data(html内容)和fn(需要执行的方法),代码如下:

<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>测试富文本</title></head><style>html, body, *{margin:0; padding: 0;}p, pre {margin: 1em 0;}body img{max-width: 100% !important;height: auto !important}</style><body id="height-wrapper"><script>function init(data, fn) {var wrapper = document.getElementById('height-wrapper');wrapper.innerHTML = data;fn && eval(fn);}</script></body>
</html> 

然后再webview引入这个html,注意ios安卓平台引入的路径问题,代码如下:

// 其他代码...
bootstrapJS() {let data = this._handleContent(this.props.data);let fun =`(function () { console.log("我是预留的方法");} ())`;return `init(${JSON.stringify(data)}, ${fun})`
}
render() {const Props = this.props;const { webHeight } = this.state;const source = (Platform.OS == 'ios') ? require('../../../html/renderHtml.html') : { uri: 'file:///android_asset/html/renderHtml.html' };return <View>{!!Props.data && <WebViewsource={source}style={{height: webHeight}}contentInset={{top:0,left:0}}injectedJavaScript={this.bootstrapJS()}scalesPageToFit={false}/>}</View>
}
// 其他代码... 

(3)webview高度自适应问题

以上两种方法都能满足App渲染富文本html的需求,但是高度得需要写死,这样肯定是没有达到预期的,所以我们需要根据内容自适应。webview有个参数是onNavigationStateChange:当导航状态发生变化的时候调用。

我们可以在html里面写个方法,把body的高度写到html 的title标签上,这样就会触发导航状态变化,然后再把高度设置到webview样式上就可以了。具体实现:

import WebView from 'react-native-webview'
// 其他代码...
 render() {const Props = this.props;const { webHeight } = this.state;let data = this._handleContent(Props.data);return <WebViewsource={{html: `<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><style>html, body, *{margin:0; padding: 0;}p, pre {margin: 1em 0;}body img{max-width: 100% !important;}</style></head><body>${data}<script>window.onload=function(){document.title = document.body.scrollHeight;}</script></body></html>`, baseUrl:Platform.OS === "ios" ? undefined : ''}}style={{height: webHeight}}contentInset={{top:0,left:0}}onNavigationStateChange={(event)=>{if(event.title && !Props.webHeight) {if (this.uuid === event.target) {this.setState({webHeight:((isNaN(parseInt(event.title)) ? 0 : parseInt(event.title)))})}}}}></WebView>}
// 其他代码... 

html的高度不确定多数来源于图片的加载,如果有多个图片的话,onload方法里面拿高度并不一定能得到最后的高度,因为可能有图片没有加载出来。所以需要不断的监听body高度的变化,再设置回去,所以我们可以在onload方法中写,每加载完一个图片就执行一次changeHeight方法:

var height = null;
function changeHeight() {if (document.body.scrollHeight != height) {document.title = document.body.scrollHeight;}
}

setTimeout(function(){let images = document.querySelectorAll("img");for (let i = 0; i < images.length; i++) {images[i].onload = function() {changeHeight();}}
},300) 

(4)图片预览及链接跳转问题

使用webview进行渲染html的话,就不能直接操作里面的图片及链接(a标签)了,不过webview提供了一个与App通信的功能,也就是onMessage

onMessage:在 webview 内部的网页中调用 window.postMessage 方法时可以触发此属性对应的函数,从而实现网页和 RN 之间的数据交换。

所以我们可以在html上的onload方法写图片的点击事件,然后发送给react-native这边,react-native在用预览图片的方法进行预览或者对链接的跳转。

html代码:

 // 图片处理let imgArr = document.querySelectorAll("img");for(let i = 0; i < imgArr.length; i ++){imgArr[i].onclick = function() {window.postMessage(JSON.stringify({type: "img", url: imgArr[i].getAttribute("src")}));}}// a标签处理let aArr = document.querySelectorAll("a");for(let i = 0; i < aArr.length; i ++){let elem = aArr[i];let url = elem.getAttribute("href");elem.onclick = function() {window.postMessage(JSON.stringify({type: "a", url: url}));};elem.setAttribute("href", "javascript: void(0)");} 

react-native代码:

 // 其他代码...
_onLinkPress = (url) => {Linking.openURL(url);
};

_previewImg = (url) => {let images = [];if (url) {images = [{uri: url}];} Overlay.show((<Overlay.PopViewcontainerStyle={{flex: 1}}overlayOpacity={1}ref={v => this.fullImageView = v}><AlbumViewstyle={{flex: 1}}control={true}images={images}defaultIndex={0}onPress={() => {this.fullImageView && this.fullImageView.close()}}/></Overlay.PopView>));
};
_onMessage = (event) => {let data = JSON.parse(decodeURIComponent(decodeURIComponent(event.nativeEvent.data))) || {};data.type === "img" && this._previewImg(data.url);data.type === "a" && this._onLinkPress(data.url);
};
 // 其他代码... 

(5)通过postMessage设置高度

所以我们也可以使用postMessage来发送html的高度,html中的changeHeight方法可以改成以下代码:

function changeHeight() {if (document.body.scrollHeight != height) {height = document.body.scrollHeight;window.postMessage(JSON.stringify({type: 'setHeight',height: height,}))}
} 

可以监听设置高度的 react-native代码:

_onMessage = (event) => {let data = JSON.parse(decodeURIComponent(decodeURIComponent(event.nativeEvent.data))) || {};data.type === "img" && this._previewImg(data.url);data.type === "a" && this._onLinkPress(data.url);try {if (data.type === 'setHeight' && data.height > 0) {this.setState({ webHeight: data.height })}} catch (error) {// ...}
}; 

至此,react-native渲染富文本的方案介绍完了,有写的不好以及错误的地方欢迎大家指出。

 类似资料: