feat: 鸿蒙 XWebView 模块新建
新增模块:xwebview/ - XWebView.ets - WebView 组件(Web 组件封装) - XWebViewTypes.ets - 类型定义 - XWebViewBridge.ets - 桥接管理 功能: - WebView 渲染(Web 组件) - JSBridge 通信(ReactNativeWebView) - 对话框原生化(alert/confirm/prompt) - StandardHandlers(xuqm.* 命名空间) - 导航栏(返回/标题/关闭) - 进度条 - 页面错误处理 Co-Authored-By: Claude <noreply@anthropic.com>
这个提交包含在:
父节点
6b561715bc
当前提交
5a913ab4f0
@ -4,6 +4,8 @@ export { PushSDK } from './src/main/ets/push/PushSDK'
|
|||||||
export { UpdateSDK } from './src/main/ets/update/UpdateSDK'
|
export { UpdateSDK } from './src/main/ets/update/UpdateSDK'
|
||||||
export { SDKContext } from './src/main/ets/core/SDKContext'
|
export { SDKContext } from './src/main/ets/core/SDKContext'
|
||||||
export { HttpClient } from './src/main/ets/core/HttpClient'
|
export { HttpClient } from './src/main/ets/core/HttpClient'
|
||||||
|
export { XWebViewComponent, XWebViewBuilder } from './src/main/ets/xwebview/XWebView'
|
||||||
|
export { XWebViewBridge, openXWebView, getXWebViewControl, setXWebViewController, getXWebViewConfig } from './src/main/ets/xwebview/XWebViewBridge'
|
||||||
export type {
|
export type {
|
||||||
ConfigFile,
|
ConfigFile,
|
||||||
SDKConfig,
|
SDKConfig,
|
||||||
|
|||||||
@ -0,0 +1,326 @@
|
|||||||
|
/**
|
||||||
|
* XWebView 组件
|
||||||
|
* HarmonyOS WebView 封装
|
||||||
|
*/
|
||||||
|
|
||||||
|
import web_webview from '@ohos.web.webview'
|
||||||
|
import type { XWebViewConfig, XWebViewControllerAPI } from './XWebViewTypes'
|
||||||
|
import { XWebViewBridge } from './XWebViewBridge'
|
||||||
|
|
||||||
|
/// JS Bridge 名称
|
||||||
|
const BRIDGE_NAME = 'ReactNativeWebView'
|
||||||
|
|
||||||
|
/// 对话框覆盖 JS
|
||||||
|
const DIALOG_OVERRIDE_JS = `
|
||||||
|
(function() {
|
||||||
|
function post(obj) {
|
||||||
|
window.${BRIDGE_NAME}.postMessage(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.alert = function(msg) { post({ __xwv: 'alert', msg: String(msg) }); };
|
||||||
|
window.confirm = function(msg) { post({ __xwv: 'confirm', msg: String(msg) }); return true; };
|
||||||
|
window.prompt = function(msg, def) { post({ __xwv: 'prompt', msg: String(msg), def: def || '' }); return def || ''; };
|
||||||
|
|
||||||
|
window.open = function(url) {
|
||||||
|
if (url) { window.location.href = url; }
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
true;
|
||||||
|
`
|
||||||
|
|
||||||
|
/// StandardHandlers JS
|
||||||
|
const STANDARD_HANDLERS_JS = `
|
||||||
|
(function() {
|
||||||
|
window.xuqm = window.xuqm || {};
|
||||||
|
|
||||||
|
window.xuqm.getDeviceInfo = function(callback) {
|
||||||
|
var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
window[callbackId] = function(data) {
|
||||||
|
if (callback) callback(data);
|
||||||
|
delete window[callbackId];
|
||||||
|
};
|
||||||
|
window.${BRIDGE_NAME}.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.getDeviceInfo',
|
||||||
|
callbackId: callbackId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.getToken = function(callback) {
|
||||||
|
var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
window[callbackId] = function(data) {
|
||||||
|
if (callback) callback(data.token);
|
||||||
|
delete window[callbackId];
|
||||||
|
};
|
||||||
|
window.${BRIDGE_NAME}.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.getToken',
|
||||||
|
callbackId: callbackId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.getUserInfo = function(callback) {
|
||||||
|
var callbackId = 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
window[callbackId] = function(data) {
|
||||||
|
if (callback) callback(data);
|
||||||
|
delete window[callbackId];
|
||||||
|
};
|
||||||
|
window.${BRIDGE_NAME}.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.getUserInfo',
|
||||||
|
callbackId: callbackId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.closeWebView = function() {
|
||||||
|
window.${BRIDGE_NAME}.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.closeWebView'
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.xuqm.showToast = function(message) {
|
||||||
|
window.${BRIDGE_NAME}.postMessage(JSON.stringify({
|
||||||
|
action: 'xuqm.showToast',
|
||||||
|
message: message
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
export function XWebViewBuilder(config: XWebViewConfig) {
|
||||||
|
XWebViewComponent({ config: config })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct XWebViewComponent {
|
||||||
|
@Prop config: XWebViewConfig = {}
|
||||||
|
private webController: web_webview.WebviewController = new web_webview.WebviewController()
|
||||||
|
@State currentUrl: string = ''
|
||||||
|
@State currentTitle: string = ''
|
||||||
|
@State loadProgress: number = 0
|
||||||
|
@State canGoBack: boolean = false
|
||||||
|
@State loadError: boolean = false
|
||||||
|
|
||||||
|
aboutToAppear() {
|
||||||
|
// 注册控制器
|
||||||
|
const self = this
|
||||||
|
const controller: XWebViewControllerAPI = {
|
||||||
|
refresh() {
|
||||||
|
self.webController.refresh()
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
self.config.onClose?.()
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
self.webController.backward()
|
||||||
|
},
|
||||||
|
goForward() {
|
||||||
|
self.webController.forward()
|
||||||
|
},
|
||||||
|
loadUrl(url: string) {
|
||||||
|
self.webController.loadUrl(url)
|
||||||
|
},
|
||||||
|
postMessageToWeb(js: string) {
|
||||||
|
self.webController.runJavaScript(js)
|
||||||
|
},
|
||||||
|
getTitle() {
|
||||||
|
return self.currentTitle
|
||||||
|
},
|
||||||
|
canGoBack() {
|
||||||
|
return self.canGoBack
|
||||||
|
},
|
||||||
|
canGoForward() {
|
||||||
|
return self.webController.accessForward()
|
||||||
|
},
|
||||||
|
currentUrl() {
|
||||||
|
return self.currentUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
XWebViewBridge.getInstance().setController(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
// 顶部导航栏
|
||||||
|
if (this.config.showTopBar !== false) {
|
||||||
|
this.TopBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
Progress({ value: this.loadProgress, total: 100 })
|
||||||
|
.width('100%')
|
||||||
|
.height(2)
|
||||||
|
.visibility(this.loadProgress < 100 ? Visibility.Visible : Visibility.Hidden)
|
||||||
|
|
||||||
|
// WebView
|
||||||
|
Web({ src: this.config.url ?? '', controller: this.webController })
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.javaScriptAccess(true)
|
||||||
|
.domStorageAccess(true)
|
||||||
|
.mixedContent(MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW)
|
||||||
|
.onPageBegin((event) => {
|
||||||
|
this.currentUrl = event.url
|
||||||
|
this.loadError = false
|
||||||
|
this.config.onProgressChanged?.(0)
|
||||||
|
})
|
||||||
|
.onPageEnd((event) => {
|
||||||
|
this.currentUrl = event.url
|
||||||
|
this.loadProgress = 100
|
||||||
|
this.config.onProgressChanged?.(100)
|
||||||
|
|
||||||
|
// 注入 JS
|
||||||
|
const injectedJs = DIALOG_OVERRIDE_JS + '\n' + STANDARD_HANDLERS_JS + '\n' + (this.config.injectedJavaScript ?? '')
|
||||||
|
this.webController.runJavaScript(injectedJs)
|
||||||
|
|
||||||
|
// 获取标题
|
||||||
|
this.webController.runJavaScript('document.title').then((result) => {
|
||||||
|
if (typeof result === 'string' && result.length > 0) {
|
||||||
|
this.currentTitle = result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.canGoBack = this.webController.accessBackward()
|
||||||
|
})
|
||||||
|
.onProgressChange((event) => {
|
||||||
|
this.loadProgress = event.newProgress
|
||||||
|
this.config.onProgressChanged?.(event.newProgress)
|
||||||
|
})
|
||||||
|
.onErrorReceive((event) => {
|
||||||
|
this.loadError = true
|
||||||
|
this.config.onPageError?.(event.errorCode, event.description ?? '', event.url ?? '')
|
||||||
|
})
|
||||||
|
.onConsole((event) => {
|
||||||
|
// 处理 JS 控制台消息
|
||||||
|
if (event.message) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.message)
|
||||||
|
this.handleJsMessage(data)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
TopBar() {
|
||||||
|
Row() {
|
||||||
|
// 返回按钮
|
||||||
|
if (this.canGoBack) {
|
||||||
|
Image($r('app.media.ic_back'))
|
||||||
|
.width(24)
|
||||||
|
.height(24)
|
||||||
|
.onClick(() => {
|
||||||
|
this.webController.backward()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
Text(this.config.autoTitle ? this.currentTitle : (this.config.title ?? ''))
|
||||||
|
.layoutWeight(1)
|
||||||
|
.fontSize(16)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.margin({ left: 12 })
|
||||||
|
|
||||||
|
// 关闭按钮
|
||||||
|
Text('关闭')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#666666')
|
||||||
|
.onClick(() => {
|
||||||
|
this.config.onClose?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(48)
|
||||||
|
.padding({ left: 12, right: 12 })
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.border({ width: { bottom: 1 }, color: '#E5E5E5' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 JS 消息
|
||||||
|
*/
|
||||||
|
private handleJsMessage(data: Record<string, Object>): void {
|
||||||
|
// 处理 StandardHandlers
|
||||||
|
const action = data['action'] as string
|
||||||
|
if (action && action.startsWith('xuqm.')) {
|
||||||
|
this.handleStandardAction(action, data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 __xwv 消息
|
||||||
|
const xwv = data['__xwv'] as string
|
||||||
|
if (xwv) {
|
||||||
|
switch (xwv) {
|
||||||
|
case 'download':
|
||||||
|
// TODO: 处理下载
|
||||||
|
break
|
||||||
|
case 'blobdownload':
|
||||||
|
// TODO: 处理 Blob 下载
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转发给用户回调
|
||||||
|
this.config.onMessage?.(JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 StandardHandlers 动作
|
||||||
|
*/
|
||||||
|
private handleStandardAction(action: string, data: Record<string, Object>): void {
|
||||||
|
const callbackId = data['callbackId'] as string
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'xuqm.getDeviceInfo':
|
||||||
|
this.handleGetDeviceInfo(callbackId)
|
||||||
|
break
|
||||||
|
case 'xuqm.getToken':
|
||||||
|
this.handleGetToken(callbackId)
|
||||||
|
break
|
||||||
|
case 'xuqm.getUserInfo':
|
||||||
|
this.handleGetUserInfo(callbackId)
|
||||||
|
break
|
||||||
|
case 'xuqm.closeWebView':
|
||||||
|
this.config.onClose?.()
|
||||||
|
break
|
||||||
|
case 'xuqm.showToast':
|
||||||
|
// HarmonyOS 不支持原生 Toast,可以使用 prompt
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetDeviceInfo(callbackId: string): void {
|
||||||
|
if (!callbackId) return
|
||||||
|
// TODO: 获取设备信息
|
||||||
|
const data = {
|
||||||
|
model: '',
|
||||||
|
osVersion: '',
|
||||||
|
platform: 'HARMONY',
|
||||||
|
deviceId: ''
|
||||||
|
}
|
||||||
|
this.callJsCallback(callbackId, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetToken(callbackId: string): void {
|
||||||
|
if (!callbackId) return
|
||||||
|
// TODO: 获取 Token
|
||||||
|
this.callJsCallback(callbackId, { token: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetUserInfo(callbackId: string): void {
|
||||||
|
if (!callbackId) return
|
||||||
|
// TODO: 获取用户信息
|
||||||
|
this.callJsCallback(callbackId, { userId: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
private callJsCallback(callbackId: string, data: Object): void {
|
||||||
|
const json = JSON.stringify(data)
|
||||||
|
this.webController.runJavaScript(`window["${callbackId}"](${json})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* XWebView 桥接管理
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { XWebViewConfig, XWebViewControllerAPI } from './XWebViewTypes'
|
||||||
|
|
||||||
|
export class XWebViewBridge {
|
||||||
|
private static instance: XWebViewBridge
|
||||||
|
private config: XWebViewConfig = {}
|
||||||
|
private controller: XWebViewControllerAPI | null = null
|
||||||
|
|
||||||
|
static getInstance(): XWebViewBridge {
|
||||||
|
if (!XWebViewBridge.instance) {
|
||||||
|
XWebViewBridge.instance = new XWebViewBridge()
|
||||||
|
}
|
||||||
|
return XWebViewBridge.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置配置
|
||||||
|
*/
|
||||||
|
setConfig(config: XWebViewConfig): void {
|
||||||
|
this.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置
|
||||||
|
*/
|
||||||
|
getConfig(): XWebViewConfig {
|
||||||
|
return this.config
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置控制器
|
||||||
|
*/
|
||||||
|
setController(controller: XWebViewControllerAPI): void {
|
||||||
|
this.controller = controller
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取控制器
|
||||||
|
*/
|
||||||
|
getController(): XWebViewControllerAPI | null {
|
||||||
|
return this.controller
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开 WebView
|
||||||
|
*/
|
||||||
|
openWebView(config: XWebViewConfig): void {
|
||||||
|
this.config = config
|
||||||
|
// 实际打开逻辑由页面层处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局打开 WebView
|
||||||
|
*/
|
||||||
|
export function openXWebView(config: XWebViewConfig): void {
|
||||||
|
XWebViewBridge.getInstance().openWebView(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全局控制器
|
||||||
|
*/
|
||||||
|
export function getXWebViewControl(): XWebViewControllerAPI | null {
|
||||||
|
return XWebViewBridge.getInstance().getController()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置全局控制器
|
||||||
|
*/
|
||||||
|
export function setXWebViewController(controller: XWebViewControllerAPI): void {
|
||||||
|
XWebViewBridge.getInstance().setController(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置
|
||||||
|
*/
|
||||||
|
export function getXWebViewConfig(): XWebViewConfig {
|
||||||
|
return XWebViewBridge.getInstance().getConfig()
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* XWebView 类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// WebView 配置
|
||||||
|
export interface XWebViewConfig {
|
||||||
|
url?: string
|
||||||
|
title?: string
|
||||||
|
showTopBar?: boolean
|
||||||
|
showStatusBar?: boolean
|
||||||
|
autoTitle?: boolean
|
||||||
|
showMenu?: boolean
|
||||||
|
injectedJavaScript?: string
|
||||||
|
onMessage?: (message: string) => void
|
||||||
|
onClose?: () => void
|
||||||
|
onPageError?: (errorCode: number, description: string, url: string) => void
|
||||||
|
onProgressChanged?: (progress: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebView 控制器接口
|
||||||
|
export interface XWebViewControllerAPI {
|
||||||
|
refresh(): void
|
||||||
|
close(): void
|
||||||
|
goBack(): void
|
||||||
|
goForward(): void
|
||||||
|
loadUrl(url: string): void
|
||||||
|
postMessageToWeb(js: string): void
|
||||||
|
getTitle(): string
|
||||||
|
canGoBack(): boolean
|
||||||
|
canGoForward(): boolean
|
||||||
|
currentUrl(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载请求
|
||||||
|
export interface XWebViewDownloadRequest {
|
||||||
|
url: string
|
||||||
|
filename?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载结果
|
||||||
|
export interface XWebViewDownloadResult {
|
||||||
|
url: string
|
||||||
|
filename: string
|
||||||
|
filePath: string
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载冲突策略
|
||||||
|
export type XWebViewDownloadConflict = 'rename' | 'overwrite'
|
||||||
|
|
||||||
|
/// 消息事件
|
||||||
|
export interface XWebViewMessageEvent {
|
||||||
|
data: string
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户