<

{{title}}

{{file.summary.functions.pct}}%

Functions
{{file.summary.functions.covered}}/{{file.summary.functions.total}}

{{file.summary.branches.pct}}%

Branches
{{file.summary.branches.covered}}/{{file.summary.branches.total}}

{{file.summary.lines.pct}}%

Lines
{{file.summary.lines.covered}}/{{file.summary.lines.total}}
          
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
                
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() {} 
} 

              
Code coverage generated by bjc at {{date}}