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>
这个提交包含在:
XuqmGroup 2026-06-19 15:01:01 +08:00
父节点 6b561715bc
当前提交 5a913ab4f0
共有 4 个文件被更改,包括 464 次插入0 次删除

查看文件

@ -4,6 +4,8 @@ export { PushSDK } from './src/main/ets/push/PushSDK'
export { UpdateSDK } from './src/main/ets/update/UpdateSDK'
export { SDKContext } from './src/main/ets/core/SDKContext'
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 {
ConfigFile,
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
}