哈喽大家好,我是九弓子
我又来参加 Cocos 征稿活动啦。
随着鸿蒙 Next 星河版系统的推进,相信很多写前端写 JS/TS 的小伙伴都跃跃欲试在新的操作系统中写自己的 App 啦。
那么作为 Cocos 的常年老粉丝,
不请自来的分享一波鸿蒙 Web 组件+Cocos3D 项目混合开发的示例。
(如果你不想阅读···文末有视频~)
先来看一些我之前已经做好的鸿蒙 webview 的例子吧。
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 年购买的华为手机,都能下载和安装
我们开始新建项目吧。
需要注意的是,在新建项目的时候【Model】模型选择
Stage 模型
到这一步继续点击 Finish,说明你已经具备鸿蒙 app 的开发环境了。
具体鸿蒙应用开发入门,参考官方文档:
这里就不展开了。
2.添加 Web 组件
搭建 ArkUI+Cocos 混合开发热更新工作流程
在这里我们在 Index 页面中,添加了 Web 组件(webview)。
我们为其设置了宽度 100%,高度 50%的矩形区域。
在鸿蒙中的 Web 组件其实就是我们在安卓 IOS 等客户端软件开发中的 weview 能力。
即为在屏幕中空出一块区域去显示 Chromium 内核展示的网页,
所以我们在 Web 组件的 src 参数中输入 Cocos Creator 的预览地址,
就能在真机中看到 Cocos 项目了。
一些需要注意的点:
1.webview 导包
import webview from '@ohos.web.webview';
2.webview 控制类
webController: webview.WebviewController = new webview.WebviewController();
具体的使用文档:
实现以上操作,我们在电脑端操作 ccc 点击保存等刷新操作的时候。
手机 App 也一样能够接收到 creator 的热更新推送,
cocos creator NB!
3.打包 cocos 项目导入鸿蒙项目
这里打包 cocos 项目要注意,我们期望的是手机页面内的一部分区分显示 cocos 游戏图形。
所以我们选择打包的发布平台为:Web 手机端
哦对了,手机中的交互均为触摸。
将 ccc 打包完的项目包直接复制粘贴到鸿蒙项目路径下:
entry\src\main\resources\rawfile
如图所示:
在这里的文件将可以所谓鸿蒙项目的资源直接调用,
后续我们会详细操作这些鸿蒙本地资源。
注意:
如果你的 DevEco Studio 版本是 3.1,并且你的 ccc 项目体积特别大。
比如我的示例项目中有一个 blender 用随机数据制作的城市模型,80mb 左右。
那么你需要等待 DevEco Studio 读取完这些文件。
等待上图中的进度走完才能继续操作,否则无法打包 hap 到手机。
这个问题已经在 DevEco Studio 5.x 中得到解决,
开发鸿蒙 Next 星河版手机应用可以说是非常快啦。
4.了解 webview 请求拦截防止 cors 跨域
聪明的小伙伴通过官方文档,可能已经想通过资源文件路径打开 web 项目了。
比如这样?
这样是无法运行的,因为现代前端项目的资源请求基本不可以通过 file://协议直接打开或运行。
这是因为 cors 跨域拦截,需要我们将资源做服务端部署。
但是我们目标是 App 内部资源本地部署啊,
所以鸿蒙 webview 组件提供了请求拦截事件。
我们可以通过 onInterceptRequest 事件,拦截 http 请求中的每一个细节。
从而返回我们需要的 Web 资源数据。
官方示例中,我们看到可以自定义 html 字符串数据做返回信息。
并且在这里我们还可以伪造几乎所有 HTTP 所需要的 Response 头信息。
返回我们 DevEco 编辑器,仔细查看。
关于 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。
具体用法:
将上述脚本保存到 ccc 项目中的 build 文件夹,
然后执行 node filepath.cjs
我们就得到了一个名为 webResourceList.ts 的文件。
我们将 webResourceList.ts 文件中的内容全部复制到鸿蒙项目中
如果你的数据中,path 字段中有 web-mobile 文件夹名称。
批量替换如图即可。
我们要做的很简单,就是要获取在 resources\rawfile 下面所有文件的相对路径。
方便我们后续通过鸿蒙的文件读取 api 去获取 fd 数据
6.网页资源初始化
在页面 build 函数之外,我们可以利用的生命周期 aboutToAppear 中去提前读取所有文件的 fd 存入到一个列表中。
这个列表我们定义在了 Index 页面组件中:
CocosResourceList:WebResource[] = webResourceList //
图中所有代码的类型和数据来源,就是我们上一步利用脚本产出的文件。
注意
aboutToAppear 是鸿蒙应用开发框架 ArkUI 提供给页面的生命周期之一。
完整的生命周期如下:
可知,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"/>
8.总结
我们提前写好的主页路由:
src: "http://xxx.dweb.club/rawfile/index.html"
//xxx.dweb.club这个域是不存在的
//我们全部自定义了请求拦截,rawfile作为拦截信息用来分割
如果你完全按照上述操作,并理解。你应该看到了如下日志:
恭喜,我们完成了cocos creator项目在鸿蒙ArkUI中的webview本地部署
现在我们只是完成了项目的部署,只是可以运行了而已。
并没有真正实现Web组件与鸿蒙操作系统的混合开发。
后续还有更多鸿蒙应用开发Web组件的混合开发详细内容:
1.JSBridge脚本注入 (类似Electron的上下文桥)
2.ArkUI与Web项目双向通信
小伙伴们可以通过官方文档来了解:
当然啦,我也有不少实战经验,比如:
ArkUI+Vue+Cocos的混合开发,
如果小伙伴们需要的话,留言评论三连啦!