持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
前言
在日常开发中,经常需要用到富文本编辑器来编辑业务内容,例如新闻、论坛等,然后在用到的地方进行渲染,一般在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.PopView
containerStyle={{flex: 1}}
overlayOpacity={1}
ref={v => this.fullImageView = v}
>
<AlbumView
style={{flex: 1}}
control={true}
images={images}
defaultIndex={0}
onPress={() => {
this.fullImageView && this.fullImageView.close()
}}
/>
</Overlay.PopView>
));
};
_imagesNode = (props) => {
const imgElementProps = useIMGElementProps(props);
return (
<IMGElement
onPress={this._previewImg.bind(this, imgElementProps.source.uri)}
source={imgElementProps.source}
contentWidth={imgElementProps.contentWidth - 15}
/>
);
};
render() {
const styles = this.styles;
const Props = this.props;
return (
<RenderHTML
source={{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 <WebView
source={{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 && <WebView
source={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 <WebView
source={{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.PopView
containerStyle={{flex: 1}}
overlayOpacity={1}
ref={v => this.fullImageView = v}
>
<AlbumView
style={{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渲染富文本的方案介绍完了,有写的不好以及错误的地方欢迎大家指出。