跨端技术下的鸿蒙应用:文件资源访问策略与实践

411 阅读7分钟

背景

作为跨端框架,原生 web 组件中直接访问本地应用文件资源和远程跨域文件资源是一个常见且必要的应用场景。如双线程技术或混合开发模式下的 web 组件中对用户文件、应用资源、远程跨域资源的预览。因此,在鸿蒙系统上,同样也需支持以上功能,从而提升跨端应用的开发效率和用户体验。

然而,目前鸿蒙的 web 组件中并不直接支持用户文件、应用资源和远程跨域资源的预览,此时我们就需要寻求一种解决方案,以实现在鸿蒙系统上对这些资源的有效预览。

文件基础

在鸿蒙系统中,本地文件主要包括应用文件、用户文件、系统文件。

  • 应用文件:文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。
  • 用户文件:文件所有者为登录到该终端设备的用户,包括用户的相册、文档等。
  • 系统文件:与应用和用户无关的其它文件,包括公共库、设备文件、系统资源文件等。

在应用内,应用文件与部分系统文件组合成为一个应用沙箱。应用文件在应用内可以被进行保存和处理;在沙箱内的系统文件在文件应用内是只读的,沙箱外的系统文件是不可访问的;若想在应用内访问用户文件,则还需要通过特定的api,经过用户的相应授权才能进行。

web 组件内访问本地应用资源

在鸿蒙系统的应用中可直接通过 file 模块访问和处理应用文件,用户文件则需要通过 file.picker 模块的相关 api 获取到文件的 uri,然后通过 file 模块的 open 功能获取该 uri 文件内容。

而在鸿蒙系统的 web 组件中直接访问本地文件资源则是无效的,一般可通过以下两种方式在应用进程中将本地文件资源的内容代理到 web 组件中。

创建通道方式

通过创建应用与 web 通信通道,在应用中读取本地资源内容,向 web 传递读取的内容实现。在通过此种方式传递文件内容时,传递的数据一般为 ArrayBuffer ,当经过通道进行传递时,该ArrayBuffer 对应的内存缓冲区将会被传递到 web 中对应的 ArrayBuffer 对象所拥有,应用内创建的 Arraybuffer 将会被分离(byteLength 为 0)

此种方式需要注意的是创建 html 侧端口流程需要在 html 侧监听 message 事件之后,否则无法在 web 侧监听到端口创建,也就无法接收到到端口相关的信息。另外,此方式不适用于太大的文件传输,毕竟会在系统侧创建一个内存缓冲区。

由于此种方式需要读取文件内容,在文件内容(ArrayBuffer)未被使用完成时,不可关闭文件内容的读取,否则在 Web 侧将无法获取到创建的 ArrayBuffer 对象对应的缓冲区的内容。但当文件不再被使用时,需要及时关闭文件,避免后续出现系统问题。因此,可以在当前页面关闭时同时关闭该文件,并且关闭端口。

// 读取文件内容
getArrayBuffer(filePath: string): ArrayBuffer | undefined {
  try{
    const file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY)
    const fileStat = fs.statSync(filePath)
    // 读取源文件内容并写入至 ArrayBuffer
    let bufSize = fileStat.size;
    let readSize = 0;
    let buf = new ArrayBuffer(bufSize);
    let readOptions: ReadOptions = {
      offset: readSize,
      length: bufSize
    };
    fs.readSync(file.fd, buf, readOptions);
    this.fileFd = file.fd
    // 此时不能直接关闭文件,需要在页面关闭时关闭
    return buf
  }catch(err) {
    return
  }
}

// 页面关闭时
aboutToDisappear(): void {
  try {
    // 关闭文件
    fs.closeSync(this.fileFd)
    // 关闭端口
    this.nativePort?.close()
    this.fileFd = null
  } catch (err) {
  }
}

拦截请求方式

通过 web 组件的 onInterceptRequest ,可以拦截网页内对本地资源的请求,返回本地资源对应的文件句柄(file.fd)使 web 组件中对应请求直接请求到本地资源。

对于标签上的资源访问和 fetch 的访问有些不同,若在 web 组件中通过 <image> 等资源类标签通过 file 协议访问应用资源时,由于它会从本地文件系统加载资源,不会跨域,所以可以在onInterceptRequest 中进行拦截。但对于 web 组件内直接 fetch 网络请求一个file协议的本地资源时,则由于浏览器的安全限制,浏览器通常会阻止这个请求,导致无法触发拦截。

鸿蒙的 ArkWeb 内核不允许 file 协议或者 resource 协议访问URL上下文中来自跨域的请求。因此,对于网页内通过 fetch 等网络请求的方式请求一个 file 协议或 resource 协议的本地资源时,若不做任何处理,则会存在响应跨域或无法拦截的问题。解决上述问题可以通过创建同域环境来解决,主要的场景有以下两种。

  1. 本地 web 网页

对于访问内置的 Web 页面,可以配置私有域名来创建同域环境。实现 Web 页面中对应用资源的请求

如: 应用内 Web 页面是 rawfile 下的 index.html, 我们可以创建一个私有域名 domain(尽量不要和网络上真实存在的域名冲突),访问 rawfile 下的文件都设置为 domain + '/rawfile/',访问本地资源的链接都设置为 domain + '/localfile/',这样在Web 组件中拦截时就可以针对性拦截,匹配到 domain + '/rawfile/' 的返回 rawfile 下的文件,匹配到 domain + '/localfile/' 的读取本地文件并返回响应

let domain = 'http://gmu.pdfviwer.com'
let rawfileDir = domain + '/rawfile/'
let localfileDir = domain + '/localfile/'

// 组件中拦截
Web({
  src: rawfileDir + 'index.html',
  conroller: this.webController
}).onInterceptRequest((event: IRequestIntercept): WebResourceResponse | null => {
  if(event) {
    const url = event.request.getRequestUrl()
    if (url.startWith(rawfileDir) || url.startWith(localfileDir)) {
      if(url.startWith(rawfileDir)) {
        const rawfilePath = url.replace(rawfileDir, '')
        // 返回rawfile下的文件
        this.responseWeb.setResponseData($rawfile(rawfilePath));
        this.responseWeb.setResponseEncoding('utf-8');
      }
      if(url.startWith(localfileDir)) {
        const localfilePath = url.replace(localfileDir, '')
        // 读取文件并返回应用文件
        const file = fs.openSync(localfilePath, fs.OpenMode.READ_ONLY)
        this.responseWeb.setResponseData(file.fd);
      }
      // 获取文件的后缀,响应对应的MimeType
      const MimeType = this.getMimeType(url)
      this.responseWeb.setResponseMimeType(MimeType)
      this.responseWeb.setResponseCode(200);
      this.responseWeb.setReasonMessage('OK');
      this.responseWeb.setResponseIsReady(true);
      return this.responseWeb;
    }
    return null
  }
})

以上主要用于应用内部创建的 Web 组件,Web 的 index.html 来自于应用内部 rawfile 下的文件,创造同域环境来访问应用内的 rawfile 下的文件和应用中的应用文件和应用外的用户文件。

  1. 远程 web 网页

应用内通过 Web 组件访问的大多数是外部的 http 协议的网页,此时则无需创造同域环境。此时我们可以利用现成的域名,创造访问应用内标准。比如我们产品,在 Web 组件内注入了应用的目录路径 env.USER_DATA_PATH,则在 web 内可通过访问远程 web 网页的 origin + env.USER_DATA_PATH 的方式访问应用内文件。

USER_DATA_PATH这个标识主要用于区分远程网页内发起的请求是本身的资源,还是其所在应用的应用资源,因此具有此功能的任意可识别和用于区分的标识都可以。此处使用应用的目录路径。

如:在 http://www.example.com/pagea 页面中获取到了应用内 pdf 文件的 file 协议地址 file:///data/storage/xxx.pdf,在网页中通过 env.USER_DATA_PATH 获取到 lightreource/files。 在通过 fetch请求访问该应用文件时,则可通过访问 http://www.example.com/lightreource/files/data/storage/xxx.pdf 来创造能够被拦截的环境,在拦截中匹配网页的 origin + USER_DATA_PATH(即http://www.example.com/lightreource/files/ )来获取到最终应用内的文件句柄,然后进行响应即可实现在远程 web 网页中访问应用内文件资源了。

web 组件内访问跨域资源

对于跨端框架或者混合开发模式中,在应用的 Web 组件中访问跨域资源是一个比较常见的场景,而由于同源策略,在 web 浏览器内是不支持响应跨域资源的请求的。因此,在应用内的 Web 中若需要支持跨域资源的请求,可以结合应用的特性,先下载到应用,再发起本地资源请求的方式,结合上述web组件内访问本地应用资源的方式实现。

总结

通过以上的技术,我们能够在双线程技术下的web组件内直接访问到用户设备上的资源或远程跨域资源,并能够在web 组件内直接进行预览。同时,可以结合原生接口与 web 组件的交互,实现文件内容的快速上传。对于跨端技术实现的鸿蒙应用,也能够更好的适配用户设备上的多媒体资源的上传与预览和一些协议文件预览及签约场景,为用户提供更好的体验。