需求背景
我們APP里有個(gè)商品詳情頁(yè),頁(yè)面上半部分是自己寫(xiě)的界面,下半部分則要展示一段由后臺(tái)返回的html標(biāo)簽,圖文混排的形式。由于WebView如果不給定一個(gè)高度,將無(wú)法展示內(nèi)容,但html內(nèi)容是運(yùn)營(yíng)人員編寫(xiě)的,無(wú)法固定高度。我第一反應(yīng)是由于WebView沒(méi)有獲取內(nèi)容高度的api,所以有沒(méi)有好用的第三方組件呢?ps:最終方案可以直接看文章最后!
第三方組件
GitHub上一個(gè)高分組件,也是別人推薦的react-native-htmlview,其能渲染一段html標(biāo)簽的字符串而無(wú)需給定高度,官方例子:
import React from 'react';
import HTMLView from 'react-native-htmlview';
class App extends React.Component {
render() {
const htmlContent = `<p><a >♥ nice job!</a></p>`;
return (
<HTMLView
value={htmlContent}
stylesheet={styles}
/>
);
}
}
但我們很快發(fā)現(xiàn)在安卓里img渲染不出來(lái),在GitHub的issues里也有相應(yīng)的提問(wèn),解決辦法基本都是使用該組件的renderNode屬性,判斷是否為img標(biāo)簽,然后給一個(gè)固定高度
renderNode(node, index, siblings, parent, defaultRenderer) {
if (node.name == 'img') {
const { src, height } = node.attribs;
const imageHeight = height || 300;
return (
<Image
key={index}
style={{ width: width * PixelRatio.get(), height: imageHeight * PixelRatio.get() }}
source={{ uri: src }} />
);
}
}
但我們的圖片怎么能固定高度呢,圖片不就變形了嗎?最終這個(gè)方案被放棄了。
后來(lái)我們找了另一個(gè)組件react-native-render-html,該組件使用簡(jiǎn)單點(diǎn),提供了很多屬性,對(duì)圖片的最大寬度能用屬性設(shè)置,在安卓上表現(xiàn)很好,圖片正常顯示。官方例子:
import React, { Component } from 'react';
import { ScrollView, Dimensions } from 'react-native';
import HTML from 'react-native-render-html';
const htmlContent = `
<h1>This HTML snippet is now rendered with native components !</h1>
<img src="https://i.imgur.com/dHLmxfO.jpg?2" />
`;
export default class Demo extends Component {
render () {
return (
<ScrollView style={{ flex: 1 }}>
<HTML html={htmlContent} imagesMaxWidth={Dimensions.get('window').width} />
</ScrollView>
);
}
}
但……當(dāng)我們換上一張尺寸較大的圖片時(shí),ios端展示的圖片模糊了,無(wú)法忍受的那種。同樣GitHub的issues里也有相應(yīng)的提問(wèn)。有人給出了答案就是修改源碼,原因是圖片給了一個(gè)固定初始寬高,等圖片加載完后就變成stretched了,就模糊了;解決方法就是等圖片加載完后,設(shè)一個(gè)真實(shí)的寬高,這樣圖片就不模糊了。但這不是我心中完美的方法,并且后面發(fā)現(xiàn)其無(wú)法渲染span、em等標(biāo)簽,所以還是放棄了。
原生組件WebView
發(fā)現(xiàn)第三方組件都會(huì)有點(diǎn)問(wèn)題,正當(dāng)無(wú)奈的時(shí)候,腦子開(kāi)竅了。WebView沒(méi)有直接提供內(nèi)容高度的屬性,不代表沒(méi)有間接獲取內(nèi)容高度的屬性啊。百度一搜,各種答案,前面的那些折騰,簡(jiǎn)直愚蠢啊。
方法一
使用WebView的onNavigationStateChange屬性。獲取高度原理是當(dāng)文檔加載完后js獲取文檔高度然后添加到title標(biāo)簽中。這時(shí)通過(guò)監(jiān)聽(tīng)導(dǎo)航狀態(tài)變化的函數(shù) onNavigationStateChange 來(lái)將title的值讀取出來(lái)賦值給this.state.height從而使webview的高度做到自適應(yīng)。
constructor(props) {
super(props);
this.state={
height:500,
}
}
<View style={{height:this.state.height}}>
<WebView
source={{html: `<!DOCTYPE html><html><body>${htmlContent}<script>window.onload=function(){window.location.hash = 1;document.title = document.body.clientHeight;}</script></body></html>`}}
style={{flex:1}}
bounces={false}
scrollEnabled={false}
automaticallyAdjustContentInsets={true}
contentInset={{top:0,left:0}}
onNavigationStateChange={(title)=>{
if(title.title != undefined) {
this.setState({
height:(parseInt(title.title)+20)
})
}
}}
>
</WebView>
</View>
但是如果我的source是一個(gè)uri呢,這種方法還是不夠靈活。
終極方法
使用WebView的injectedJavaScript和onMessage屬性。ps:在低版本的RN中無(wú)法使用onMessage屬性官方解釋?zhuān)?/p>
injectedJavaScript string
設(shè)置在網(wǎng)頁(yè)加載之前注入的一段JS代碼。
onMessage function
在webview內(nèi)部的網(wǎng)頁(yè)中調(diào)用`window.postMessage`方法時(shí)可以觸發(fā)此屬性對(duì)應(yīng)的函數(shù),從而實(shí)現(xiàn)網(wǎng)頁(yè)和RN之間的數(shù)據(jù)交換。 設(shè)置此屬性的同時(shí)會(huì)在webview中注入一個(gè)`postMessage`的全局函數(shù)并覆蓋可能已經(jīng)存在的同名實(shí)現(xiàn)。
網(wǎng)頁(yè)端的`window.postMessage`只發(fā)送一個(gè)參數(shù)`data`,此參數(shù)封裝在RN端的event對(duì)象中,即`event.nativeEvent.data`。`data`只能是一個(gè)字符串。
思路是使用injectedJavaScript注入一段js代碼獲取網(wǎng)頁(yè)內(nèi)容高度,然后調(diào)用window.postMessage方法把高度回調(diào)給onMessage方法,然后setState,改變webView高度,從而實(shí)現(xiàn)自適應(yīng)。直接上代碼:
import React, { Component } from 'react'
import {
WebView,
Dimensions,
ScrollView
} from 'react-native'
const BaseScript =
`
(function () {
var height = null;
function changeHeight() {
if (document.body.scrollHeight != height) {
height = document.body.scrollHeight;
if (window.postMessage) {
window.postMessage(JSON.stringify({
type: 'setHeight',
height: height,
}))
}
}
}
setTimeout(changeHeight, 300);
} ())
`
const HTMLTEXT = `<h1>This HTML snippet is now rendered with native components !</h1>
<img src="https://i.imgur.com/dHLmxfO.jpg?2" />`
class AutoHeightWebView extends Component {
constructor (props) {
super(props);
this.state = ({
height: 0
})
}
/**
* web端發(fā)送過(guò)來(lái)的交互消息
*/
onMessage (event) {
try {
const action = JSON.parse(event.nativeEvent.data)
if (action.type === 'setHeight' && action.height > 0) {
this.setState({ height: action.height })
}
} catch (error) {
// pass
}
}
render () {
return (
<ScrollView>
<WebView
injectedJavaScript={BaseScript}
style={{
width: Dimensions.get('window').width,
height: this.state.height
}}
automaticallyAdjustContentInsets
source={{ html: HTMLTEXT }}// 這里可以使用uri
decelerationRate='normal'
scalesPageToFit
javaScriptEnabled // 僅限Android平臺(tái)。iOS平臺(tái)JavaScript是默認(rèn)開(kāi)啟的。
domStorageEnabled // 適用于安卓
scrollEnabled={false}
onMessage={this.onMessage.bind(this)}
/>
</ScrollView>
)
}
}
export default RZWebView
這里有點(diǎn)小插曲,我們?cè)?code>BaseScript這段js字符串中,使用//寫(xiě)了點(diǎn)注釋?zhuān)Y(jié)果安卓端onMessage方法就不被調(diào)用了。非常郁悶,最后查找資料發(fā)現(xiàn)這種//注釋方法是會(huì)導(dǎo)致這段js不被執(zhí)行的,正確的注釋方式是/**/。
最后完美解決問(wèn)題,完成需求。這中間過(guò)程艱辛,希望本文的總結(jié)能幫到大家少走冤路。謝謝!
參考文章
《ReactNative WebView高度自適應(yīng)》
《React-Native WebView 測(cè)量網(wǎng)頁(yè)高度》