diff --git a/harmony/rn_webview/index.ets b/harmony/rn_webview/index.ets index 8b2c9b830..4fee26358 100644 --- a/harmony/rn_webview/index.ets +++ b/harmony/rn_webview/index.ets @@ -21,6 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +import {RNCWebView as WebView} from './src/main/ets/RNCWebView' + +export { WebView } export * from './src/main/ets/RNCWebView' diff --git a/harmony/rn_webview/src/main/ets/RNCWebView.ets b/harmony/rn_webview/src/main/ets/RNCWebView.ets index 6b46f98b4..3d44df710 100644 --- a/harmony/rn_webview/src/main/ets/RNCWebView.ets +++ b/harmony/rn_webview/src/main/ets/RNCWebView.ets @@ -24,6 +24,7 @@ import { Descriptor, RNComponentContext, ViewBaseProps } from '@rnoh/react-native-openharmony'; import webview from '@ohos.web.webview'; +import { url as OSUrl } from '@kit.ArkTS'; import { RNC } from '@rnoh/react-native-openharmony/generated'; import Logger from './Logger'; @@ -42,6 +43,12 @@ export class WebViewNewSource { baseUrl?: string } +interface AlertEvent { + url: string; + message: string; + result: JsResult; +} + const TAG = "WebView" export const WEB_VIEW = "RNCWebView" @@ -131,7 +138,6 @@ export struct RNCWebView { html: "", baseUrl: "" } - @State descriptor: WebViewViewDescriptor = {} as WebViewViewDescriptor html: string | undefined = "" url: string | Resource = "" controller: webview.WebviewController = new webview.WebviewController(); @@ -146,43 +152,65 @@ export struct RNCWebView { controllerAttached: boolean = false; renderMode: RenderMode = RenderMode.SYNC_RENDER; scrollEnabled = true; + nestedScroll = NestedScrollMode.SELF_FIRST; headers: Array = [] + allowPageStartInProgress = true; + @State webviewWidth: number = 0 + @State webviewHeight: number = 0 private unregisterDescriptorChangesListener?: () => void = undefined private cleanupCommandCallback?: () => void = undefined private eventEmitter: RNC.RNCWebView.EventEmitter | undefined = undefined private cleanUpCallbacks: (() => void)[] = [] private descriptorWrapper: RNC.RNCWebView.DescriptorWrapper = {} as RNC.RNCWebView.DescriptorWrapper + private onDescriptorWrapperChange(descriptorWrapper: RNC.RNCWebView.DescriptorWrapper) { + this.initVariable() + this.descriptorWrapper = descriptorWrapper + if (this.html != "" && this.html != this.source.html) { + Logger.debug(TAG, "[RNOH] html is update") + this.html = this.source.html + if (this.controllerAttached) { + try { + this.controller.loadData( + this.source.html, + "text/html", + "UTF-8", + this.source.baseUrl, + " " + ); + } catch (error) { + Logger.error(TAG, "error: " + error) + } + } + } else if (this.source.uri != "" && this.url != this.source.uri) { + Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri); + this.url = this.source.uri as string; + if (this.controllerAttached) { + this.controller.loadUrl(this.descriptorWrapper.props.newSource.uri, this.headers) + } + } + if (this.controllerAttached) { + this.controller?.setScrollable(this.descriptorWrapper.props.scrollEnabled) + } + } + aboutToAppear() { this.eventEmitter = new RNC.RNCWebView.EventEmitter(this.ctx.rnInstance, this.tag) - this.onDescriptorWrapperChange(this.ctx.descriptorRegistry.findDescriptorWrapperByTag(this.tag)!) + this.initVariable() + this.url = this.source.uri as string; + + webview.WebviewController.setWebDebuggingAccess(this.descriptorWrapper.props.webviewDebuggingEnabled) this.cleanUpCallbacks.push(this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag, (_descriptor, newDescriptorWrapper) => { this.onDescriptorWrapperChange(newDescriptorWrapper! as RNC.RNCWebView.DescriptorWrapper) } )) - if (this.descriptorWrapper.props.overScrollMode === 'always') { - this.overScrollMode = OverScrollMode.ALWAYS - } else if (this.descriptorWrapper.props.overScrollMode === 'never') { - this.overScrollMode = OverScrollMode.NEVER - } - this.scrollEnabled = this.descriptorWrapper.props.scrollEnabled; - this.javaScriptEnable = this.descriptorWrapper.props.javaScriptEnabled; - this.cacheMode = - this.descriptorWrapper.props.cacheEnabled ? - this.transCacheMode(this.descriptorWrapper.props.cacheMode as CACHE_MODE) : CacheMode.Online; - this.source = this.descriptorWrapper.props.newSource - this.html = this.source.html - if (this.source.uri && this.source.uri.toString().startsWith("asset://")) { - this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest())); - } - this.url = this.source.uri as string; - this.overScrollMode = this.descriptorWrapper.props.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER this.registerCommandCallback() if (this.hasIncognito()) { this.cacheMode = CacheMode.Online; } + if (this.source.headers) { this.source.headers.forEach(item => { if (item.name && item.value) { @@ -196,6 +224,68 @@ export struct RNCWebView { return this.descriptorWrapper.props.incognito && this.descriptorWrapper.props.incognito === true } + private initVariable() { + this.descriptorWrapper = this.ctx.descriptorRegistry.findDescriptorWrapperByTag(this.tag)! + this.javaScriptEnable = this.descriptorWrapper.props.javaScriptEnabled; + this.cacheMode = + this.descriptorWrapper.props.cacheEnabled ? this.transCacheMode(this.descriptorWrapper.props.cacheMode as CACHE_MODE) : CacheMode.Online; + this.source = { + uri: this.descriptorWrapper.props.newSource.uri ?? "", + method: this.descriptorWrapper.props.newSource.method ?? "", + body: this.descriptorWrapper.props.newSource.body ?? "", + html: this.descriptorWrapper.props.newSource.html ?? "", + baseUrl: this.descriptorWrapper.props.newSource.baseUrl ?? "", + headers: this.descriptorWrapper.props.newSource.headers ?? [] + } + if (this.source.headers) { + this.source.headers.forEach(item => { + if (item.name && item.value) { + this.headers.push({ headerKey: item.name, headerValue: item.value }) + } + }) + } + this.html = this.source.html + this.webviewWidth = this.descriptorWrapper.width + this.webviewHeight = this.descriptorWrapper.height + this.scrollEnabled = this.descriptorWrapper.props.scrollEnabled; + // 当禁止滚动时,使用父组件滚动 + this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST; + if (this.source.uri && this.source.uri.toString().startsWith("asset://")) { + this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest())); + } + + if(this.descriptorWrapper.props.overScrollMode === 'always') { + this.overScrollMode = OverScrollMode.ALWAYS + } else if(this.descriptorWrapper.props.overScrollMode === 'never') { + this.overScrollMode = OverScrollMode.NEVER + } + + this.overScrollMode = this.descriptorWrapper.props.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER; + } + + private registerPostMessage() { + if (this.messagingEnabled == this.descriptorWrapper.props.messagingEnabled) { + return; + } + this.messagingEnabled = this.descriptorWrapper.props.messagingEnabled; + if (this.messagingEnabled) { + let bridge: RNCWebViewBridge = { + postMessage: (data: string) => { + Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`); + if (this.controller != null) { + let result: WebViewEventParams = this.createWebViewEvent("onMessage") + result.data = data.toString() + result.lockIdentifier = 0 + this.eventEmitter!.emit("message", result as ResultType); + } + } + }; + this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"]) + this.controller.refresh() + this.hasRegisterJavaScriptProxy = true + } + } + createWebViewEvent(param: string): WebViewEventParams { let result: WebViewEventParams = new WebViewEventParams(param); result.loading = this.progress != 100 @@ -372,6 +462,26 @@ export struct RNCWebView { }) } + onProgressChange() { + // 修复单页面应用切换路由时 onPageBegin 未触发的问题 + if (this.controller.getUrl() !== this.url && this.progress < 100 && this.allowPageStartInProgress) { + this.allowPageStartInProgress = false; + this.onLoadingStart(); + } + this.eventEmitter!.emit('loadingProgress', { + url: this.controller.getUrl(), + loading: this.progress != 100, + title: this.controller.getTitle(), + canGoBack: this.controller.accessBackward(), + canGoForward: this.controller.accessForward(), + lockIdentifier: 0, + progress: this.progress / 100 + }) + if (this.progress === 100) { + this.allowPageStartInProgress = true; + } + } + onLoadingFinish() { this.eventEmitter!.emit('loadingFinish', { url: this.controller.getUrl(), @@ -412,18 +522,6 @@ export struct RNCWebView { }) } - loadingProgress(progress: number) { - this.eventEmitter!.emit('loadingProgress', { - url: this.controller.getUrl(), - loading: false, - title: this.controller.getTitle(), - canGoBack: this.controller.accessBackward(), - canGoForward: this.controller.accessForward(), - lockIdentifier: 0, - progress: progress - }) - } - scroll(x: number, y: number) { this.eventEmitter!.emit('scroll', { contentInset: { @@ -483,11 +581,66 @@ export struct RNCWebView { } } + getJsDialogTitle() { + const url = this.controller.getUrl() + // 对于 data: URL,只显示 'JavaScript',类似于 Chrome + let title = 'JavaScript'; + const isDataUrl = (url !== null) && url.startsWith("data:"); + + + if (!isDataUrl) { + try { + const dialogUrl = OSUrl.URL.parseURL(url); + title = `网址为”${dialogUrl.protocol}//${dialogUrl.host}“的网页显示:`; + } catch (ex) { + title = url; + } + } + + return title; + } + + onJavascriptAlert(event?: AlertEvent) { + if (event) { + AlertDialog.show({ + title: this.getJsDialogTitle(), + message: event.message, + alignment: DialogAlignment.Center, + secondaryButton: { + value: '确定', + action: () => event.result.handleConfirm() + }, + cancel: () => event.result.handleCancel(), + }) + } + return true + } + + onJavascriptConfirm(event?: AlertEvent) { + if (event) { + AlertDialog.show({ + title: this.getJsDialogTitle(), + message: event.message, + alignment: DialogAlignment.Center, + secondaryButton: { + value: '确定', + action: () => event.result.handleConfirm() + }, + cancel: () => event.result.handleCancel(), + primaryButton: { + value: '取消', + action: () => event.result.handleCancel() + }, + }) + } + return true + } + build() { Stack() { Web({ src: "", controller: this.controller, renderMode: this.renderMode }) - .width("100%") - .height("100%") + .width(this.webviewWidth) + .height(this.webviewHeight) .constraintSize({ minHeight: 1 }) .overScrollMode(this.overScrollMode) .backgroundColor(Color.Transparent) @@ -498,12 +651,12 @@ export struct RNCWebView { .textZoomRatio(this.descriptorWrapper.props.textZoom) .cacheMode(this.cacheMode) .domStorageAccess(this.descriptorWrapper.props.domStorageEnabled) - .zoomAccess(this.descriptorWrapper.props.scalesPageToFit)// nestedScrollFlag 为true 表示可以在嵌套滚动中,web自己能滚动 - .overScrollMode(this.overScrollMode)// .layoutMode(this.nestedScrollFlag ? WebLayoutMode.NONE : WebLayoutMode.FIT_CONTENT) + .zoomAccess(this.descriptorWrapper.props.scalesPageToFit) + .overScrollMode(this.overScrollMode) .onProgressChange((event) => { if (event) { - this.progress = event.newProgress / 100 - this.loadingProgress(this.progress) + this.progress = event.newProgress + this.onProgressChange() } }) .onScroll((event) => { @@ -511,6 +664,10 @@ export struct RNCWebView { this.scroll(event.xOffset, event.yOffset) } }) + .nestedScroll({ + scrollForward: this.nestedScroll, + scrollBackward: this.nestedScroll, + }) .onPageBegin(() => { this.onLoadingStart() this.controller.setScrollable(this.scrollEnabled) @@ -562,6 +719,14 @@ export struct RNCWebView { if (!this.hasRegisterJavaScriptProxy) { this.registerPostMessage() } + try { + // 修复一些页面下无法将鸿蒙userAgent识别为手机的问题 + let userAgent = this.controller.getUserAgent() + ' iPhone'; + this.controller.setCustomUserAgent(userAgent); + + } catch (error) { + Logger.error(TAG, "setUA error: " + error) + } if (this.descriptorWrapper.props.userAgent) { try { this.controller.setCustomUserAgent(this.descriptorWrapper.props.userAgent); @@ -571,79 +736,14 @@ export struct RNCWebView { } } }) + .onAlert((event) => this.onJavascriptAlert(event)) + .onConfirm((event) => this.onJavascriptConfirm(event)) } - .width('100%') - .height('100%') + .width(this.webviewWidth) + .height(this.webviewHeight) .position({ x: this.descriptorWrapper.positionRelativeToParent.x, y: this.descriptorWrapper.positionRelativeToParent.y }) } - - private onDescriptorWrapperChange(descriptorWrapper: RNC.RNCWebView.DescriptorWrapper) { - this.descriptorWrapper = descriptorWrapper - - Logger.debug(TAG, `[RNOH] newDescriptor props uri, ${JSON.stringify(this.descriptorWrapper.props.newSource.uri)}`); - this.cacheMode = - this.descriptorWrapper.props.cacheEnabled ? - this.transCacheMode(this.descriptorWrapper.props.cacheMode as CACHE_MODE) : CacheMode.Online; - this.javaScriptEnable = this.descriptorWrapper.props.javaScriptEnabled; - this.source = this.descriptorWrapper.props.newSource - if (this.source.headers) { - this.source.headers.forEach(item => { - if (item.name && item.value) { - this.headers.push({ headerKey: item.name, headerValue: item.value }) - } - }) - } - if (this.html != "" && this.html != this.source.html) { - Logger.debug(TAG, "[RNOH] html is update") - this.html = this.source.html - if (this.controllerAttached) { - try { - this.controller.loadData( - this.source.html, - "text/html", - "UTF-8", - this.source.baseUrl, - " " - ); - } catch (error) { - Logger.error(TAG, "error: " + error) - } - } - } else if (this.source.uri != "" && this.url != this.source.uri) { - Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri); - this.url = this.source.uri as string; - if (this.controllerAttached) { - this.controller.loadUrl(this.descriptorWrapper.props.newSource.uri, this.headers) - } - } - if (this.controllerAttached) { - this.controller?.setScrollable(this.descriptorWrapper.props.scrollEnabled) - } - } - - private registerPostMessage() { - if (this.messagingEnabled == this.descriptorWrapper.props.messagingEnabled) { - return; - } - this.messagingEnabled = this.descriptorWrapper.props.messagingEnabled; - if (this.messagingEnabled) { - let bridge: RNCWebViewBridge = { - postMessage: (data: string) => { - Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`); - if (this.controller != null) { - let result: WebViewEventParams = this.createWebViewEvent("onMessage") - result.data = data - result.lockIdentifier = 0 - this.eventEmitter!.emit("message", result as ResultType); - } - } - }; - this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"]) - this.controller.refresh() - this.hasRegisterJavaScriptProxy = true - } - } } \ No newline at end of file diff --git a/harmony/rn_webview/ts.ts b/harmony/rn_webview/ts.ts index 5fff7c3b0..93f51702e 100644 --- a/harmony/rn_webview/ts.ts +++ b/harmony/rn_webview/ts.ts @@ -21,5 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +import {RNCWebViewPackage as WebViewPackage} from './src/main/ets/RNCWebViewPackage' + +export {WebViewPackage}; export * from './src/main/ets/RNCWebViewPackage' \ No newline at end of file diff --git a/src/WebView.harmony.tsx b/src/WebView.harmony.tsx index 99bf50f8d..6bd37f01c 100644 --- a/src/WebView.harmony.tsx +++ b/src/WebView.harmony.tsx @@ -51,7 +51,7 @@ const shouldStartLoadWithLockIdentifier:( lockIdentifier: Double ) => void = () => {} -const WebViewComponent = forwardRef<{}, IOSWebViewProps>( +const WebViewComponent = forwardRef<{}, IOSWebViewProps & {scalesPageToFit: boolean}>( ( { fraudulentWebsiteWarningEnabled = true, @@ -237,6 +237,7 @@ const WebViewComponent = forwardRef<{}, IOSWebViewProps>( key="webViewKey" {...otherProps} scrollEnabled={otherProps.scrollEnabled ?? true} + scalesPageToFit={otherProps.scalesPageToFit ?? true} fraudulentWebsiteWarningEnabled={fraudulentWebsiteWarningEnabled} javaScriptEnabled={javaScriptEnabled} cacheEnabled={cacheEnabled}