演示如何在 HarmonyOS 上用 Cordova 容器封装 Web 前端,完成一个可运行、可发布的混合应用。文章聚焦于落地工程方法与关键桥接点,代码与文字比例约 4:6,所有代码片段均来自本项目真实文件。
为什么选择混合方案
对于已有 Web 小游戏(例如 2048),混合方案的优势在于:
- 快速复用:直接复用现有 HTML/CSS/JS 代码。
- 低成本发布:通过容器封装,适配 HarmonyOS 分发生态。
- 渐进增强:可按需接入原生能力(导航、触觉、性能监测等)。
在本项目中,我们用 @magongshou/harmony-cordova 提供的 MainPage 承载 WebView 并管理页面生命周期,从而将 rawfile/www 下的前端项目原样装入。
工程结构与加载关系
- Harmony 原生模块(ArkTS):
entry/src/main/ets/pages/Index.ets通过MainPage组件加载 Web 入口页面,并将生命周期/返回键事件桥接给 Cordova。
- Web 前端模块:
www/index.html为 Web 入口文件,引用外部样式css/style.css与脚本js/app.js。
关键关系:ArkTS 的 MainPage 默认加载 rawfile/www/index.html,并把 pageShowEvent / pageHideEvent / pageBackPress 这类事件透传给 Cordova 容器,容器再与 WebView 协同管理路由与回退。
加载流程图
flowchart LR
A[Index.ets (ArkTS)] --> B[MainPage(容器)] --> C[WebView]
C --> D[rawfile/www/index.html]
D --> E[css/style.css]
D --> F[js/app.js]
A -. pageShow/pageHide .-> B
A -. pageBackPress .-> B
B -. 生命周期/路由管理 .-> C
ArkTS 侧容器与桥接点
文件:entry/src/main/ets/pages/Index.ets
import {
MainPage,
pageBackPress,
pageHideEvent,
pageShowEvent,
PluginEntry
} from '@magongshou/harmony-cordova/Index';
@Entry
@Component
struct Index {
/**
* ArkTS 侧的自定义插件配置
* 配置插件名称和对象,详见自定义插件开发部分
*/
cordovaPlugs: Array<PluginEntry> = [];
aboutToAppear() {
}
/** 页面显示生命周期:通知 Cordova 页面已显示 */
onPageShow() {
pageShowEvent();
}
/** 返回键拦截:交由 Cordova 处理返回栈 */
onBackPress() {
pageBackPress();
return true; // 返回 true 拦截默认行为
}
/** 页面隐藏生命周期:通知 Cordova 页面已隐藏 */
onPageHide() {
pageHideEvent();
}
/** 构建页面 UI,装载 WebView(rawfile/www/index.html) */
build() {
RelativeContainer() {
MainPage({
isWebDebug: false, // 是否开启 Web 调试
cordovaPlugs: this.cordovaPlugs // 传入插件配置(当前为空,可按需扩展)
});
}
.height('100%')
.width('100%')
}
}
要点说明:
MainPage是混合容器核心;默认载入rawfile/www/index.html。pageShowEvent/pageHideEvent/pageBackPress把原生生命周期与返回键事件交给 Cordova,由容器协调 Web 页面的路由与回栈。cordovaPlugs为空数组时,不注册自定义原生插件;后续可按需加入自定义插件对象,完成更深的 JS↔原生能力互通(例如文件系统、设备信息、振动、截图等)。
桥接机制详解:Index.ets → Cordova → WebView
- 生命周期透传
onPageShow()调用pageShowEvent()→ Cordova 接收并可通知 Web 层“页面前台”,便于暂停/恢复音效或计时。onPageHide()调用pageHideEvent()→ Cordova 接收并可通知 Web 层“页面后台”,便于持久化状态。
- 系统返回键托管
onBackPress()内部pageBackPress()交给 Cordova 决定回退行为(如:Web 历史后退、关闭弹窗、二次确认退出等)。ArkTS 返回true表示已拦截系统默认行为。
- 插件注册入口
cordovaPlugs: Array<PluginEntry>是 JS↔原生能力扩展的注册点;本项目现阶段保持为空,专注于封装与运行。
- 时序说明
- ArkTS/Index.ets 触发生命周期 →
page*方法将事件投递给 Cordova →- Cordova 根据事件类型与页面状态,协调 WebView 或(若存在)分发到注册的原生插件。
时序流程图
Cordova 插件基类与注册接口
文件:cordova/src/main/ets/components/CordovaPlugin.ets
import { CordovaInterface } from './CordovaInterface'
import { CallbackContext } from './CallbackContext'
import { CordovaWebView } from './CordovaWebView'
import { HashMap } from '@kit.ArkTS'
import { CordovaPreferences } from './CordovaPreferences'
export class CordovaPlugin {
protected webId?:string;
protected cordovaWebView?: CordovaWebView;
protected preferences?:CordovaPreferences;
protected cordovaInterface?: CordovaInterface;
protected serviceName?: string;
protected mapWebIdToCustomPlugins?:HashMap<string, HashMap<string, CordovaPlugin>>;
protected mapWebIdToWebView?:HashMap<string, CordovaWebView>;
protected mapWebIdToInterface?:HashMap<string, CordovaInterface>;
protected mapWebIdToWebTag?:HashMap<string, string>;
privateInitialize(webId:string,
serviceName: string,
preferences:CordovaPreferences,
cordovaInterface: CordovaInterface,
cordovaWebView:CordovaWebView,
mapWebIdToCustomPlugins:HashMap<string, HashMap<string, CordovaPlugin>>,
mapWebIdToWebView:HashMap<string, CordovaWebView>,
mapWebIdToInterface:HashMap<string, CordovaInterface>,
mapWebIdToWebTag:HashMap<string, string>): void {
this.webId = webId;
this.serviceName = serviceName;
this.preferences = preferences;
this.cordovaInterface = cordovaInterface;
this.cordovaWebView = cordovaWebView;
this.mapWebIdToCustomPlugins = mapWebIdToCustomPlugins;
this.mapWebIdToWebView = mapWebIdToWebView;
this.mapWebIdToInterface = mapWebIdToInterface;
this.mapWebIdToWebTag = mapWebIdToWebTag;
this.initialize(cordovaInterface, cordovaWebView);
}
initialize(cordovaInterface: CordovaInterface, cordovaWebView:CordovaWebView):void{
return;
}
execute(action: string, args: ESObject[], callbackContext: CallbackContext):boolean {
return true;
}
onMessage(id:string, data:string):void {
}
onDestroy():void {
}
onPause(multitasking:boolean):void {
}
onResume(multitasking:boolean):void {
}
onStart():void {
}
onPageStart():void {
}
onPageEnd():void {
}
}
export interface PluginEntry {
pluginName: string
pluginObject:CordovaPlugin
}
补充说明:
PluginEntry定义了原生插件的注册结构(服务名与实例对象)。Index.ets中的cordovaPlugs: Array<PluginEntry>即为注册入口。execute(action, args, callbackContext)是 JS 调用原生时的统一入口;onMessage用于容器向插件广播消息;其余生命周期钩子可根据需要覆写。
代码结构图
classDiagram
class CordovaPlugin {
- webId?: string
- cordovaWebView?: CordovaWebView
- preferences?: CordovaPreferences
- cordovaInterface?: CordovaInterface
- serviceName?: string
- mapWebIdToCustomPlugins?: HashMap<string, HashMap<string, CordovaPlugin>>
- mapWebIdToWebView?: HashMap<string, CordovaWebView>
- mapWebIdToInterface?: HashMap<string, CordovaInterface>
- mapWebIdToWebTag?: HashMap<string, string>
+ initialize(cordovaInterface: CordovaInterface, cordovaWebView: CordovaWebView): void
+ execute(action: string, args: ESObject[], callbackContext: CallbackContext): boolean
+ onMessage(id: string, data: string): void
+ onDestroy(): void
+ onPause(multitasking: boolean): void
+ onResume(multitasking: boolean): void
+ onStart(): void
+ onPageStart(): void
+ onPageEnd(): void
}
class PluginEntry {
+ pluginName: string
+ pluginObject: CordovaPlugin
}
class CordovaInterface
class CordovaWebView
class CordovaPreferences
class CallbackContext
class HashMap
CordovaPlugin --> CordovaInterface
CordovaPlugin --> CordovaWebView
CordovaPlugin --> CordovaPreferences
CordovaPlugin ..> CallbackContext : uses
PluginEntry --> CordovaPlugin
CordovaPlugin ..> "mapWebIdToCustomPlugins: HashMap<string, HashMap<string, CordovaPlugin>>"
CordovaPlugin ..> "mapWebIdToWebView: HashMap<string, CordovaWebView>"
CordovaPlugin ..> "mapWebIdToInterface: HashMap<string, CordovaInterface>"
CordovaPlugin ..> "mapWebIdToWebTag: HashMap<string, string>"
Web 侧入口
文件:cordova/src/main/resources/rawfile/www/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>2048</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div class="wrap">
<div class="head">
<h1>2048</h1>
<div class="stats">
<div class="badge"><small>分数</small><b id="score">0</b></div>
<div class="badge"><small>最佳</small><b id="best">0</b></div>
</div>
</div>
<div class="toolbar">
<button id="newGame">新游戏</button>
<span class="tip">使用键盘方向键(↑↓←→)进行移动。</span>
</div>
<div class="board">
<div id="grid" class="grid"></div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>
要点说明:
- 页面只关心 UI 与游戏逻辑;不直接感知容器。
- 容器负责加载此页面、托管生命周期与返回键。
样式片段
文件:cordova/src/main/resources/rawfile/www/css/style.css
.board { background: var(--board); padding: 10px; border-radius: 10px; position: relative; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.cell {
background: var(--cell); border-radius: 6px; aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 30px; color: var(--dark);
}
.n-0 { background: var(--cell); color: transparent; }
.n-2 { background: var(--tile-2); }
.n-4 { background: var(--tile-4); }
.n-8 { background: var(--tile-8); color: var(--light); }
.n-16 { background: var(--tile-16); color: var(--light); }
.n-32 { background: var(--tile-32); color: var(--light); }
.n-64 { background: var(--tile-64); color: var(--light); }
.n-128 { background: var(--tile-128); color: var(--light); font-size: 26px; }
.n-256 { background: var(--tile-256); color: var(--light); font-size: 26px; }
.n-512 { background: var(--tile-512); color: var(--light); font-size: 24px; }
.n-1024 { background: var(--tile-1024); color: var(--light); font-size: 20px; }
.n-2048 { background: var(--tile-2048); color: var(--light); font-size: 20px; }
要点说明:
- 使用 CSS Grid 快速布局棋盘。
- 通过不同数值 class(如
n-2,n-4,n-8)映射不同底色与字号,提升信息密度与辨识度。
JS 核心算法与输入
文件:cordova/src/main/resources/rawfile/www/js/app.js(节选)
// 初始化网格 DOM
function buildGridDOM() {
elGrid.innerHTML = '';
for (let i = 0; i < size * size; i++) {
const d = document.createElement('div');
d.className = 'cell n-0';
elGrid.appendChild(d);
}
}
function freshUI() {
const cells = elGrid.children;
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const v = board[r][c];
const idx = r * size + c;
const cell = cells[idx];
cell.textContent = v ? String(v) : '';
cell.className = 'cell n-' + (v || 0);
}
}
elScore.textContent = String(score);
elBest.textContent = String(best);
}
function slideAndMerge(line) {
// 移除 0
const filtered = line.filter(v => v !== 0);
let moved = filtered.length !== line.length;
const out = [];
for (let i = 0; i < filtered.length; i++) {
if (i < filtered.length - 1 && filtered[i] === filtered[i + 1]) {
const merged = filtered[i] * 2;
out.push(merged);
score += merged;
i++;
moved = true;
} else {
out.push(filtered[i]);
}
}
while (out.length < size) out.push(0);
// 检查是否有变化
for (let i = 0; i < size; i++) {
if (out[i] !== line[i]) moved = true;
}
return { line: out, moved };
}
function moveLeft() {
let movedAny = false;
for (let r = 0; r < size; r++) {
const { line, moved } = slideAndMerge(board[r]);
board[r] = line;
movedAny = movedAny || moved;
}
return movedAny;
}
// 键盘输入与触摸滑动
window.addEventListener('keydown', (e) => {
let moved = false;
switch (e.key) {
case 'ArrowLeft': e.preventDefault(); moved = moveLeft(); break;
case 'ArrowRight': e.preventDefault(); moved = moveRight(); break;
case 'ArrowUp': e.preventDefault(); moved = moveUp(); break;
case 'ArrowDown': e.preventDefault(); moved = moveDown(); break;
default: return;
}
afterMove(moved);
});
要点说明:
slideAndMerge是核心:过滤 0 → 合并相邻 → 末尾补 0 → 返回是否发生变更。- 四个方向移动通过“行列变换 + 反转”技巧复用同一套逻辑,保证实现简洁且易测。
- 输入层兼容键盘与触摸(移动端滑动阈值 30px)。
JS↔原生桥接的现状与扩展路径
本项目当前桥接点主要体现在 ArkTS 对页面生命周期与返回键的透传(见第 3 节 pageShowEvent/pageHideEvent/pageBackPress)。这能确保:
- Web 页在容器内“正确地”响应前后台切换。
- 统一由 Cordova 管理返回栈,避免出现系统返回键导致 Web 页意外退出的情况。
当需要“JS 调用原生能力”与“原生回调 JS”时,推荐路径:
- 在 ArkTS 侧实现并注册自定义插件对象,加入
cordovaPlugs: Array<PluginEntry>数组。 - 在 Web 侧通过
cordova.exec(success, fail, service, action, [args])调用对应服务与方法;成功后容器回调success。
性能与体验建议
- 渲染层尽量减少 DOM 重建:本项目仅更新已有节点的
className与textContent。 - 动画建议采用
transform/opacity,避免频繁触发布局重排。 - 移动端设置合理的滑动阈值(本项目为 30px),减少误触。
- 按需降级:在低端设备上可暂时关闭复杂动画,优先保留核心玩法。
构建与运行
-
Harmony 工程启动后,
MainPage默认装载rawfile/www/index.html,无需额外配置即可运行 2048。 -
Web 端调试:直接双击
www/index.html也可运行,便于快速联调 UI 与逻辑。
小结与下一步
本文展示了将一个纯 Web 2048 游戏以最小成本封装到 HarmonyOS 的完整过程:
- ArkTS 用
MainPage容器装载前端页面,并桥接生命周期与返回键。 - Web 侧三件套(HTML/CSS/JS)保持纯净,专注 UI 与游戏核心算法。
- 当需要更强的原生能力时,再通过
cordovaPlugs与cordova.exec渐进式接入。

被折叠的 条评论
为什么被折叠?



