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

2,641 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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渲染富文本的方案介绍完了,有写的不好以及错误的地方欢迎大家指出。