V7投稿|首发鸿蒙ArkUI+Webview实战Cocos Web3D本地部署

哈喽大家好,我是九弓子

我又来参加 Cocos 征稿活动啦。

随着鸿蒙 Next 星河版系统的推进,相信很多写前端写 JS/TS 的小伙伴都跃跃欲试在新的操作系统中写自己的 App 啦。

那么作为 Cocos 的常年老粉丝,

不请自来的分享一波鸿蒙 Web 组件+Cocos3D 项目混合开发的示例。
(如果你不想阅读···文末有视频~)

先来看一些我之前已经做好的鸿蒙 webview 的例子吧。
混合开发示例2

WebView 是应用开发中非常重要的组件之一,

尤其是对于我们 Cocos 游戏开发者来说,如果能将 Cocos 打包的 Web 项目。

直接本地部署到客户端软件内,不需要远程资源请求。

就不再需要分包优化,网络优化这些工作,

也能让用户在最快速度启动我们的项目。

这让我们开发 Cocos 的 Web3D 项目时候,可以解放很多大体积的美术素材直接部署。


因为并不是所有小伙伴具备 Next 真机,

我准备的分享内容尽量靠近消费者版本。

必备清单:

1.HarmonyOS 4.0 及以上操作系统

2.DevEco Studio 3.1.1 Release

3.HarmonyOS SDK 3.1.0(API9)

4.Cocos Creator 3.8.1(可选) 5.任意一个 Cocos Creator 构建的 Web 项目都可以

以上清单相信手里只要有近 3 年购买的华为手机,都能下载和安装

我们开始新建项目吧。

image
image-1

需要注意的是,在新建项目的时候【Model】模型选择

Stage 模型

到这一步继续点击 Finish,说明你已经具备鸿蒙 app 的开发环境了。

具体鸿蒙应用开发入门,参考官方文档:

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/start-with-ets-stage-0000001477980905-V2

这里就不展开了。


2.添加 Web 组件

搭建 ArkUI+Cocos 混合开发热更新工作流程
image-3

在这里我们在 Index 页面中,添加了 Web 组件(webview)。

我们为其设置了宽度 100%,高度 50%的矩形区域。

在鸿蒙中的 Web 组件其实就是我们在安卓 IOS 等客户端软件开发中的 weview 能力。

即为在屏幕中空出一块区域去显示 Chromium 内核展示的网页,

所以我们在 Web 组件的 src 参数中输入 Cocos Creator 的预览地址,

就能在真机中看到 Cocos 项目了。

SVID_20240427_195624_1-1

一些需要注意的点:
image-4

1.webview 导包

import webview from '@ohos.web.webview';

2.webview 控制类

webController: webview.WebviewController = new webview.WebviewController();

具体的使用文档:

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V2/ts-basic-components-web-0000001477981205-V2

实现以上操作,我们在电脑端操作 ccc 点击保存等刷新操作的时候。

手机 App 也一样能够接收到 creator 的热更新推送,

cocos creator NB!


3.打包 cocos 项目导入鸿蒙项目

这里打包 cocos 项目要注意,我们期望的是手机页面内的一部分区分显示 cocos 游戏图形。

所以我们选择打包的发布平台为:Web 手机端

image-5

哦对了,手机中的交互均为触摸。

将 ccc 打包完的项目包直接复制粘贴到鸿蒙项目路径下:

entry\src\main\resources\rawfile

如图所示:
image-6

在这里的文件将可以所谓鸿蒙项目的资源直接调用,

后续我们会详细操作这些鸿蒙本地资源。

注意

如果你的 DevEco Studio 版本是 3.1,并且你的 ccc 项目体积特别大。

比如我的示例项目中有一个 blender 用随机数据制作的城市模型,80mb 左右。

那么你需要等待 DevEco Studio 读取完这些文件。

image-7

等待上图中的进度走完才能继续操作,否则无法打包 hap 到手机。

这个问题已经在 DevEco Studio 5.x 中得到解决,

开发鸿蒙 Next 星河版手机应用可以说是非常快啦。


4.了解 webview 请求拦截防止 cors 跨域

聪明的小伙伴通过官方文档,可能已经想通过资源文件路径打开 web 项目了。

image-9

比如这样?

这样是无法运行的,因为现代前端项目的资源请求基本不可以通过 file://协议直接打开或运行。

这是因为 cors 跨域拦截,需要我们将资源做服务端部署。

但是我们目标是 App 内部资源本地部署啊,

所以鸿蒙 webview 组件提供了请求拦截事件。

image-10

我们可以通过 onInterceptRequest 事件,拦截 http 请求中的每一个细节。

从而返回我们需要的 Web 资源数据。

官方示例:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V2/ts-basic-components-web-0000001477981205-V2#ZH-CN_TOPIC_0000001523968730__oninterceptrequest9

官方示例中,我们看到可以自定义 html 字符串数据做返回信息。

image-11

并且在这里我们还可以伪造几乎所有 HTTP 所需要的 Response 头信息。

返回我们 DevEco 编辑器,仔细查看。

image-12

关于 setResponseData 这个关键 api 的参数,我们不仅可以放入字符串。

我们还可以放入文件的 fd 描述用的 number 整型。

所以我们需要在整个 Web 组件启动请求的那一刻之前,拿到所有 cocos 项目文件在鸿蒙 hap 资源中的 fd 文件描述。

但是问题就来了,cocos creator 打包的项目文件很多啊。

所以我们需要先写一个文件夹遍历脚本


5.Web 前端项目文件遍历脚本

废话不多说,直接贴脚本。

        const fs = require('fs');
const path = require('path');

function traverseDirectory(dir) {
const files = fs.readdirSync(dir);
const webResourceList = [];

for (const file of files) {
const filePath = path.join(dir, file);
const stats = fs.statSync(filePath);

if (stats.isDirectory()) {
  // 如果是文件夹,则递归遍历
  const subList = traverseDirectory(filePath);
  webResourceList.push(...subList);
} else {
  // 如果是文件,则处理路径并根据文件类型设置 mimeType
  const relativePath = filePath.replace('.\\web-mobile\\', ''); // 去掉前缀
  let mimeType = '';
  if (filePath.endsWith('.html')) {
    mimeType = 'text/html';
  } else if (filePath.endsWith('.css')) {
    mimeType = 'text/css';
  } else if (filePath.endsWith('.js')) {
    mimeType = 'application/javascript';
  } else if (filePath.endsWith('.svg')) {
    mimeType = 'image/svg+xml';
  }else if (filePath.endsWith('.png')) {
    mimeType = 'image/png';
  }else if (filePath.endsWith('.jpg')) {
    mimeType = 'image/jpeg';
  }else if (filePath.endsWith('.map')) {
    mimeType = 'application/js';
  }else if (filePath.endsWith('.json')) {
    mimeType = 'application/json';
  }else if (filePath.endsWith('.png')) {
    mimeType = 'image/png';
  }else if (filePath.endsWith('.bin')) {
    mimeType = 'application/octet-stream';
  }else if (filePath.endsWith('.wasm')) {
    mimeType = 'application/wasm';
  }


  // 添加到 webResourceList
  webResourceList.push({
    path: relativePath.replace(/\\/g, '/'), // 将 \ 替换为 /
    fd: null, // 你可以根据需要设置文件描述符
    mimeType: mimeType,
  });
}
  }

  return webResourceList;
}

// 指定目标文件夹的路径
const targetDirectory = './web-mobile/';

// 生成 TypeScript 代码
const tsCode = `
export interface WebResource {
  path: string;
  fd: number | null;
  mimeType: "text/html" | "text/css" | "application/javascript" | "image/svg+xml" | "image/png" | "image/jpeg" | "application/json" | "application/octet-stream" | "application/wasm";
}

export const webResourceList: WebResource[] = ${JSON.stringify(traverseDirectory(targetDirectory), null, 2)};
`;

// 将 TypeScript 代码写入 .ts 文件
fs.writeFileSync('webResourceList.ts', tsCode);

// 打印结果
console.log('webResourceList.ts 文件已生成');

在这里这个临时脚本的意思就是遍历目标文件夹下的所有文件,

然后生产一个结构为这样的列表数据:

export interface WebResource {
  path: string;
  fd: number | null;
  mimeType: "text/html" | "text/css" | "application/javascript" | "image/svg+xml" | "image/png" | "image/jpeg" | "application/json" | "application/octet-stream" | "application/wasm";
}

方便我们后续在鸿蒙项目中,读取 ccc 文件资源的 fd。

具体用法:

image-13

将上述脚本保存到 ccc 项目中的 build 文件夹,

然后执行 node filepath.cjs

我们就得到了一个名为 webResourceList.ts 的文件。

我们将 webResourceList.ts 文件中的内容全部复制到鸿蒙项目中

image-14

如果你的数据中,path 字段中有 web-mobile 文件夹名称。

批量替换如图即可。

我们要做的很简单,就是要获取在 resources\rawfile 下面所有文件的相对路径。

方便我们后续通过鸿蒙的文件读取 api 去获取 fd 数据


6.网页资源初始化

image-15

在页面 build 函数之外,我们可以利用的生命周期 aboutToAppear 中去提前读取所有文件的 fd 存入到一个列表中。

这个列表我们定义在了 Index 页面组件中:

CocosResourceList:WebResource[] = webResourceList //

图中所有代码的类型和数据来源,就是我们上一步利用脚本产出的文件。

image-16

注意

aboutToAppear 是鸿蒙应用开发框架 ArkUI 提供给页面的生命周期之一。

完整的生命周期如下:
image-17

官方文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-page-custom-components-lifecycle-0000001524296665-V2

可知,aboutToAppear 执行在了用户看到页面之前。

也符合我们的需要,我们同时利用 async+await 的异步等待方法可以阻塞该流程。保证我们的资源在应用启动的时候,完成资源的加载。

当然你可以在 onPageShow 之后做用户交互事件去触发,具体的这个实现是自由的。

7.自定义资源请求拦截

贴代码:

Web({ src: "http://xxx.dweb.club/rawfile/index.html",
    controller: this.webController })
    .width("100.1%")
    .height("50%")
    .backgroundColor("#2B2F3B")
    .visibility(Visibility.Visible)
    .position({ x: 0, y: 0 })
    .onTitleReceive(() => {
      console.log("----web title receive----")
    })
    .onResourceLoad(async (event) => {
      console.log('onResourceLoad: ' + event.url)
      if (event.url == "http://xxx.dweb.club/favicon.ico") {
        this.webController.refresh()
      }
    })
    .onInterceptRequest((event) => {
      console.log('onInterceptRequest: ' + event.request.getRequestUrl());
      let path = new url.URL(event.request.getRequestUrl());
      let rawFilePath = path.pathname;
      rawFilePath = path.pathname.replace("rawfile/", "");
      rawFilePath = rawFilePath.replace("/", "");
      console.log(JSON.stringify(rawFilePath));

      // 创建一个 WebResourceResponse 对象
      const webResourceResponse = new WebResourceResponse();
      this.CocosResourceList.forEach((item,index)=>{
        if (item.path == rawFilePath){
          console.log(JSON.stringify(item))
          webResourceResponse.setResponseData(item.fd);
          webResourceResponse.setResponseMimeType(item.mimeType);
        }
      })
      webResourceResponse.setResponseEncoding("UTF-8");
      webResourceResponse.setResponseCode(200);
      return webResourceResponse
    });

以上就是完整的自定义本地资源请求拦截返回的Web组件写法,

这里可以看到,我们可以利用的事件还有很多。

比如:onTitleReceive onResourceLoad

这些都可以在后续开发的复杂场景中,进行更多的混合开发操作。

这里有一个坑,注意

新的web内核在请求网页的时候,如果页面没有定义标题栏的ico。

那么它会自动请求根目录下的,favicon.ico。

而ccc编译的web项目,默认并没有这个ico,所以我们需要自定义新增这个资源。然后在html中,最好再多加一行。

<link rel="icon" type="image/x-icon" href="favicon.ico"/>

image-18

8.总结

我们提前写好的主页路由:

src: "http://xxx.dweb.club/rawfile/index.html"
//xxx.dweb.club这个域是不存在的
//我们全部自定义了请求拦截,rawfile作为拦截信息用来分割

如果你完全按照上述操作,并理解。你应该看到了如下日志:
image-19

恭喜,我们完成了cocos creator项目在鸿蒙ArkUI中的webview本地部署


现在我们只是完成了项目的部署,只是可以运行了而已。

并没有真正实现Web组件与鸿蒙操作系统的混合开发。

后续还有更多鸿蒙应用开发Web组件的混合开发详细内容:

1.JSBridge脚本注入 (类似Electron的上下文桥)

2.ArkUI与Web项目双向通信

小伙伴们可以通过官方文档来了解:

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/web-component-overview-0000001508249461-V2

当然啦,我也有不少实战经验,比如:

ArkUI+Vue+Cocos的混合开发,
混合开发示例2

如果小伙伴们需要的话,留言评论三连啦!

4赞

鸿蒙ArkUI代码地址:https://gitee.com/sugarnine/arkui-webview-study
cocos示例项目地址:https://github.com/412845222/ccc38-particle-demo

2赞

大概是理解了,随便写了一个远程地址,然后由onInterceptRequest拦截之后,改成resources下的静态地址,其他的呢。

  1. vue工程也是这样处理的吗;
  2. vewview嵌入的怎么进行消息交互呢,postMessage吗

WebResourceList.ets应该也不用写,应该可以直接根据文件后缀,判断mimeType是啥,写了WebResourceList文件之后,以后每次重新编译工程还是个麻烦事儿,尤其前端的编译结果还携带了hash值