需求背景
我們APP里有個商品詳情頁,頁面上半部分是自己寫的界面,下半部分則要展示一段由后臺返回的html標簽,圖文混排的形式。由于WebView如果不給定一個高度,將無法展示內容,但html內容是運營人員編寫的,無法固定高度。我第一反應是由于WebView沒有獲取內容高度的api,所以有沒有好用的第三方組件呢?ps:最終方案可以直接看文章最后!
第三方組件
GitHub上一個高分組件,也是別人推薦的react-native-htmlview,其能渲染一段html標簽的字符串而無需給定高度,官方例子:
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ā)現在安卓里img渲染不出來,在GitHub的issues里也有相應的提問,解決辦法基本都是使用該組件的renderNode屬性,判斷是否為img標簽,然后給一個固定高度
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 }} />
);
}
}
但我們的圖片怎么能固定高度呢,圖片不就變形了嗎?最終這個方案被放棄了。
后來我們找了另一個組件react-native-render-html,該組件使用簡單點,提供了很多屬性,對圖片的最大寬度能用屬性設置,在安卓上表現很好,圖片正常顯示。官方例子:
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>
);
}
}
但……當我們換上一張尺寸較大的圖片時,ios端展示的圖片模糊了,無法忍受的那種。同樣GitHub的issues里也有相應的提問。有人給出了答案就是修改源碼,原因是圖片給了一個固定初始寬高,等圖片加載完后就變成stretched了,就模糊了;解決方法就是等圖片加載完后,設一個真實的寬高,這樣圖片就不模糊了。但這不是我心中完美的方法,并且后面發(fā)現其無法渲染span、em等標簽,所以還是放棄了。
原生組件WebView
發(fā)現第三方組件都會有點問題,正當無奈的時候,腦子開竅了。WebView沒有直接提供內容高度的屬性,不代表沒有間接獲取內容高度的屬性啊。百度一搜,各種答案,前面的那些折騰,簡直愚蠢啊。
方法一
使用WebView的onNavigationStateChange屬性。獲取高度原理是當文檔加載完后js獲取文檔高度然后添加到title標簽中。這時通過監(jiān)聽導航狀態(tài)變化的函數 onNavigationStateChange 來將title的值讀取出來賦值給this.state.height從而使webview的高度做到自適應。
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是一個uri呢,這種方法還是不夠靈活。
終極方法
使用WebView的injectedJavaScript和onMessage屬性。ps:在低版本的RN中無法使用onMessage屬性官方解釋:
injectedJavaScript string
設置在網頁加載之前注入的一段JS代碼。
onMessage function
在webview內部的網頁中調用`window.postMessage`方法時可以觸發(fā)此屬性對應的函數,從而實現網頁和RN之間的數據交換。 設置此屬性的同時會在webview中注入一個`postMessage`的全局函數并覆蓋可能已經存在的同名實現。
網頁端的`window.postMessage`只發(fā)送一個參數`data`,此參數封裝在RN端的event對象中,即`event.nativeEvent.data`。`data`只能是一個字符串。
思路是使用injectedJavaScript注入一段js代碼獲取網頁內容高度,然后調用window.postMessage方法把高度回調給onMessage方法,然后setState,改變webView高度,從而實現自適應。直接上代碼:
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ā)送過來的交互消息
*/
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平臺。iOS平臺JavaScript是默認開啟的。
domStorageEnabled // 適用于安卓
scrollEnabled={false}
onMessage={this.onMessage.bind(this)}
/>
</ScrollView>
)
}
}
export default RZWebView
這里有點小插曲,我們在BaseScript這段js字符串中,使用//寫了點注釋,結果安卓端onMessage方法就不被調用了。非常郁悶,最后查找資料發(fā)現這種//注釋方法是會導致這段js不被執(zhí)行的,正確的注釋方式是/**/。
最后完美解決問題,完成需求。這中間過程艱辛,希望本文的總結能幫到大家少走冤路。謝謝!