|
import webview from '@ohos.web.webview';
import common from '@ohos.app.ability.common';
import { TencentQianWebViewConfig } from './model/TencentQianWebViewConfig';
import { OnSignResult, OnSdkEvent } from './model/SignCallback';
import {
PageStartedEvent,
PageFinishedEvent,
PermissionGrantedEvent,
PermissionDeniedEvent,
LoadErrorEvent,
HttpErrorEvent,
SslErrorEvent,
RenderProcessGoneEvent
} from './model/SdkEvent';
import { WebSettingsApplier, WebSettingsSnapshot } from './internal/WebSettingsApplier';
import { UserAgentInjector } from './internal/UserAgentInjector';
import { CookieManagerHelper } from './internal/CookieManagerHelper';
import { JsDialogHandler } from './internal/JsDialogHandler';
import { DownloadHandler } from './internal/DownloadHandler';
import { NetworkStateInjector } from './internal/NetworkStateInjector';
import { FileChooserHandler } from './filechooser/FileChooserHandler';
import { RuntimePermissionRequester } from './permission/RuntimePermissionRequester';
import { SchemeInterceptor } from './scheme/SchemeInterceptor';
/**
* `javaScriptOnDocumentStart` 入参契约。@ohos.web.webview 未公开导出 ScriptItem 类型,
* 这里以最小子集声明,与 ArkUI 内置 ScriptItem 结构等价。
*/
export interface ScriptItemLike {
script: string;
scriptRules: string[];
}
/**
* ArkWeb 回调入参的最小契约接口集合。
*
* ArkTS 严格模式禁止使用对象字面量作为类型声明,所有匿名 `{ ... }` 类型必须先声明为命名 interface。
* 由于 @ohos.web.webview 没有公开导出每个回调的入参类型,这里以最小子集声明,
* 实际运行时 ArkWeb 传入的对象只要包含这些字段即可工作。
*/
export interface OnLoadInterceptEvent {
data: OnLoadInterceptData;
}
export interface OnLoadInterceptData {
getRequestUrl: () => string;
isMainFrame: () => boolean;
}
export interface OnPageEvent {
url: string;
}
export interface OnPermissionRequestEvent {
request: PermissionRequestLike;
}
export interface PermissionRequestLike {
getOrigin: () => string;
getAccessibleResource: () => string[];
grant: (resources: string[]) => void;
deny: () => void;
}
export interface OnShowFileSelectorEvent {
result: FileSelectorResultLike;
fileSelector: FileSelectorParamLike;
}
export interface FileSelectorResultLike {
handleFileList: (uris: string[]) => void;
}
export interface FileSelectorParamLike {
getAcceptType: () => string[];
isCapture: () => boolean;
getMode: () => number;
}
export interface OnJsDialogEvent {
url: string;
message: string;
result: JsResultLike;
}
/**
* `onPrompt` 事件入参。与 onAlert/onConfirm 不同,prompt 多出一个 `value` 默认值,
* 并且 `result` 需调 `handlePromptConfirm(value)` 返回用户输入。
*/
export interface OnJsPromptEvent {
url: string;
message: string;
value: string;
result: JsPromptResultLike;
}
export interface JsPromptResultLike {
handlePromptConfirm: (result: string) => void;
handleConfirm: () => void;
handleCancel: () => void;
}
export interface JsResultLike {
handleConfirm: () => void;
handleCancel: () => void;
}
export interface OnDownloadStartEvent {
url: string;
userAgent: string;
contentDisposition: string;
mimetype: string;
contentLength: number;
}
export interface OnErrorReceiveEvent {
request: WebRequestLike;
error: WebResourceErrorLike;
}
export interface OnHttpErrorReceiveEvent {
request: WebRequestLike;
response: WebResourceResponseLike;
}
export interface WebRequestLike {
getRequestUrl: () => string;
isMainFrame: () => boolean;
}
export interface WebResourceErrorLike {
getErrorCode: () => number;
getErrorInfo: () => string;
}
export interface WebResourceResponseLike {
getResponseCode: () => number;
getReasonMessage: () => string;
}
export interface OnSslErrorEvent {
handler: SslErrorHandlerLike;
error: number;
}
export interface SslErrorHandlerLike {
handleCancel: () => void;
}
export interface OnRenderExitedEvent {
renderExitReason: number;
}
export interface OnProgressChangeEvent {
newProgress: number;
}
/**
* 由 Configurator 返回给客户的事件处理器集合。
* 客户把这些处理器逐个绑定到自己 Web 组件的对应回调即可获得 SDK 全部能力。
*
* 所有事件回调入参类型为 `ESObject`(等价 any),用于绕过 ArkTS
* `arkts-no-structural-typing` 限制 —— ArkUI Web 回调对象无法与
* 我们的 `OnXxxEvent` interface 做结构性匹配,在 handler 内部再按字段取用。
*/
export interface TencentQianWebViewHandlers {
/** Web 组件的标准能力开关(JS / DomStorage / 缩放等),客户绑到链式属性上。 */
webSettings: Readonly<WebSettingsSnapshot>;
/** 内容混合模式名称: `'All'` 允许 https 页面加载 http 资源,`'None'` 禁止。 */
mixedMode: 'All' | 'None';
/** document_start 注入脚本集合,客户绑定到 `.javaScriptOnDocumentStart(scripts)`。 */
documentStartScripts: Array<ScriptItemLike>;
/** 网络状态注入器,客户在 `onControllerAttached` 中调 `attach(controller)`。 */
networkStateInjector: NetworkStateInjector;
/** controller attach 时机回调。客户在 Web 组件 `.onControllerAttached(...)` 中转调。 */
onControllerAttached: () => void;
/** URL 拦截。返回 `true` = 取消导航(如命中外部 scheme 或 `qianapp://`)。 */
onLoadIntercept: (event: ESObject) => boolean;
/** 页面开始加载。 */
onPageBegin: (event: ESObject) => void;
/** 页面加载结束。 */
onPageEnd: (event: ESObject) => void;
/** Web 端权限请求(摄像头 / 麦克风 / 定位等)。 */
onPermissionRequest: (event: ESObject) => void;
/** `<input type=file>` 文件选择。返回 `true` 表示由 SDK 接管。 */
onShowFileSelector: (event: ESObject) => boolean;
/** JS `alert`。返回 `true` 表示由 SDK 接管,弹出原生对话框。 */
onAlert: (event: ESObject) => boolean;
/** JS `confirm`。返回 `true` 表示由 SDK 接管,弹出原生对话框。 */
onConfirm: (event: ESObject) => boolean;
/** JS `prompt`。**必须返回 `true`**,否则 ArkWeb 默认拦截并返回 null。 */
onPrompt: (event: ESObject) => boolean;
/** 文件下载。SDK 走系统下载并通过 `onSdkEvent` 上报进度/结果。 */
onDownloadStart: (event: ESObject) => void;
/** 资源加载错误。 */
onErrorReceive: (event: ESObject) => void;
/** HTTP 错误响应(4xx / 5xx)。 */
onHttpErrorReceive: (event: ESObject) => void;
/** SSL 证书错误。SDK 默认全部 cancel,避免 MITM 风险。 */
onSslErrorEventReceive: (event: ESObject) => void;
/** 渲染进程退出。客户需自行决定 reload 或关闭页面。 */
onRenderExited: (event: ESObject) => void;
/** 加载进度变化。客户可在容器外侧绑定进度条 UI。 */
onProgressChange: (event: ESObject) => void;
}
/**
* 配置模式工具类(三种集成方式中的最灵活方式)。
*
* 客户已经有自己的 `Web` 组件 + `WebviewController` 实例,只想叠加 SDK 能力
* (UA 注入、scheme 拦截、权限处理、文件选择、JS 弹窗、下载、错误事件等)
* 而不替换原有 Web 组件时,使用本类。
*
* 调用 `apply()` 得到一组事件处理器,逐个绑定到自有 Web 组件的对应回调即可。
*/
export class TencentQianWebViewConfigurator {
/**
* 把 SDK 能力叠加到客户已有的 WebviewController 上,返回事件处理器集合。
*
* @param controller 客户自己创建的 WebviewController(SDK 不替换、不重建)
* @param context UIAbilityContext
* @param config 容器配置
* @param onSignResult 签署结果回调(必填)
* @param onSdkEvent SDK 事件回调(可选,用于日志/埋点)
* @returns handlers 集合,客户绑定到自己的 Web 组件回调
*/
public static apply(
controller: webview.WebviewController,
context: common.UIAbilityContext,
config: TencentQianWebViewConfig,
onSignResult: OnSignResult,
onSdkEvent?: OnSdkEvent
): TencentQianWebViewHandlers {
CookieManagerHelper.configure();
// Scheme 拦截器:主框架疑似下载链接强制走 DownloadHandler
const interceptor = new SchemeInterceptor(
context,
config,
onSignResult,
onSdkEvent,
(url: string): void => {
DownloadHandler.start(controller, context, url, '', '', onSdkEvent);
}
);
// document_start 注入与网络状态监听
const documentStartScripts: Array<ScriptItemLike> = [
{ script: NetworkStateInjector.getDocumentStartScript(), scriptRules: ['*'] }
];
const networkStateInjector: NetworkStateInjector = new NetworkStateInjector();
const handlers: TencentQianWebViewHandlers = {
webSettings: WebSettingsApplier.DEFAULTS,
mixedMode: WebSettingsApplier.mixedModeName(config.allowMixedContent),
documentStartScripts: documentStartScripts,
networkStateInjector: networkStateInjector,
onControllerAttached: (): void => {
UserAgentInjector.inject(controller, config, onSdkEvent);
networkStateInjector.attach(controller);
},
onLoadIntercept: (event: ESObject): boolean => {
try {
const e = event as OnLoadInterceptEvent;
const url: string = e.data.getRequestUrl();
if (!e.data.isMainFrame()) {
return false;
}
return interceptor.intercept(url);
} catch (_e) {
return false;
}
},
onPageBegin: (event: ESObject): void => {
if (onSdkEvent !== undefined) {
const e = event as OnPageEvent;
onSdkEvent(new PageStartedEvent(e.url));
}
},
onPageEnd: (event: ESObject): void => {
if (onSdkEvent !== undefined) {
const e = event as OnPageEvent;
onSdkEvent(new PageFinishedEvent(e.url));
}
},
onPermissionRequest: (event: ESObject): void => {
const e = event as OnPermissionRequestEvent;
const request = e.request;
const resources: string[] = request.getAccessibleResource();
const perms = RuntimePermissionRequester.mapWebResourcesToPermissions(resources);
const emit = (evt: PermissionGrantedEvent | PermissionDeniedEvent): void => {
if (onSdkEvent !== undefined) {
onSdkEvent(evt);
}
};
if (perms.length === 0) {
request.deny();
emit(new PermissionDeniedEvent(resources));
return;
}
RuntimePermissionRequester.request(context, perms, onSdkEvent).then((granted: boolean) => {
if (!granted) {
request.deny();
return;
}
request.grant(resources);
emit(new PermissionGrantedEvent(resources));
});
},
onShowFileSelector: (event: ESObject): boolean => {
const e = event as OnShowFileSelectorEvent;
const params = e.fileSelector;
const acceptTypes: string[] = params.getAcceptType();
const isCapture: boolean = params.isCapture();
const isMultiple: boolean = params.getMode() === 1;
FileChooserHandler.pick(
context,
acceptTypes,
isCapture,
isMultiple ? 'multiple' : 'single'
).then((uris: string[]) => {
e.result.handleFileList(uris);
});
return true;
},
onAlert: (event: ESObject): boolean => {
const e = event as OnJsDialogEvent;
JsDialogHandler.showAlert(context, e.url, e.message).then((_ok: boolean) => {
e.result.handleConfirm();
});
return true;
},
onConfirm: (event: ESObject): boolean => {
const e = event as OnJsDialogEvent;
JsDialogHandler.showConfirm(context, e.url, e.message).then((ok: boolean) => {
if (ok) {
e.result.handleConfirm();
} else {
e.result.handleCancel();
}
});
return true;
},
onPrompt: (event: ESObject): boolean => {
const e = event as OnJsPromptEvent;
JsDialogHandler.showPrompt(context, e.url, e.message, e.value).then((value: string | null) => {
if (value === null) {
e.result.handleCancel();
} else {
// ArkWeb 要求传回用户输入给 H5 prompt 的 resolve 值
e.result.handlePromptConfirm(value);
}
});
return true;
},
onDownloadStart: (event: ESObject): void => {
const e = event as OnDownloadStartEvent;
DownloadHandler.start(
controller,
context,
e.url,
e.mimetype,
e.contentDisposition,
onSdkEvent
);
},
onErrorReceive: (event: ESObject): void => {
const e = event as OnErrorReceiveEvent;
if (!e.request.isMainFrame()) {
return;
}
if (onSdkEvent !== undefined) {
onSdkEvent(new LoadErrorEvent(
e.request.getRequestUrl(),
e.error.getErrorCode(),
e.error.getErrorInfo()
));
}
},
onHttpErrorReceive: (event: ESObject): void => {
const e = event as OnHttpErrorReceiveEvent;
if (!e.request.isMainFrame()) {
return;
}
if (onSdkEvent !== undefined) {
onSdkEvent(new HttpErrorEvent(
e.request.getRequestUrl(),
e.response.getResponseCode(),
e.response.getReasonMessage()
));
}
},
onSslErrorEventReceive: (event: ESObject): void => {
const e = event as OnSslErrorEvent;
try {
e.handler.handleCancel();
} catch (_e) {
// ignore
}
if (onSdkEvent !== undefined) {
onSdkEvent(new SslErrorEvent('', e.error));
}
},
onRenderExited: (event: ESObject): void => {
const e = event as OnRenderExitedEvent;
const didCrash: boolean = e.renderExitReason === 0;
if (onSdkEvent !== undefined) {
onSdkEvent(new RenderProcessGoneEvent(didCrash, ''));
}
},
onProgressChange: (_event: ESObject): void => {
// 占位
}
};
return handlers;
}
// ArkTS 类禁止实例化
private constructor() {}
}
|