开源鸿蒙平台 CMP 三方库「tessera」适配全流程
本文记录
tessera接入 OpenHarmony 的完整过程,覆盖 Web 技术栈项目的适配路线选型、前端 grunt 构建在现代 Node 上的修复、2019 年 Flask 依赖栈在当前 Python 上的钉版复活、ArkWeb 客户端 HAP 的建模与实现、HAP 构建和真机验收。
tessera是 Urban Airship 开源的 Graphite 仪表盘系统:Python Flask 服务端(REST API + Jinja 页面外壳 + sqlite 存储)加 TypeScript/Browserify 前端(jQuery、Flot、Handlebars、五套主题),浏览器才是它的客户端。本次适配与社区已有的 KMP/CMP 路线(Vico / Reorderable / qrose / KStore 的「Kotlin/Native 动态库 + C ABI + N-API」管线)有本质区别:tessera 没有 Kotlin 源码,鸿蒙侧不产出任何.so——前端与服务端源码原样保留并跑通,交付物是一个 ArkWeb 客户端 HAP(连接向导 + 全屏仪表盘视图),边界就是 HTTP。
项目地址: AtomGit/oh-tpc/tessera.git
上游仓库: tessera-metrics/tessera
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 tessera 适配
tessera 回答的是「一整套多用户、可编辑、可共享、带主题系统的 Graphite 仪表盘系统」这个问题:存储、搜索、标签、REST API、变换模型都在服务端和 Web 前端里。Web 端通过 Jinja 外壳注入 window.ts.config,加载 bundle.js + tessera.js 两个包后调用 ts.init(),所有 API 流量走同源相对路径 /api/*,图表数据由浏览器直连 <GRAPHITE_URL>/render 拉取。这套运行期契约与 ArkWeb Web 组件严丝合缝——移动值班、运维大屏、发布看板场景需要的是把这套完整系统装进鸿蒙手机,而不是重写它。
| 障碍 | 具体问题 |
|---|---|
| 技术栈不同 | 无 Kotlin 源码,KMP/CMP 家族的 KN + NAPI 管线无从下手;渲染栈是 jQuery + Flot + 63 个 Handlebars 模板。 |
| 页面外壳在服务端 | 前端没有 index.html,每页由 Flask Jinja 渲染——只打包静态前端进 HAP 无法离线运行。 |
| 工具链老化 | 上游 2019 年构建链:grunt 1.0 + TypeScript 3.4 + dart-sass 包;Flask 0.12 / SQLAlchemy 1.3 / six 1.10 在当前 Python 上连环失效。 |
| 同源 API 设计 | SPA 全部请求是相对路径 /api/*,客户端必须与 API 同源,跨域直连方案不成立。 |
因此,本项目把边界放在:前端构建链修复(一行 require)、服务端钉版组合复活(不动上游代码)、ArkWeb 客户端 HAP(连接向导 + 全屏 Web 视图)。仪表盘的全部行为由上游原版实现负责,ArkTS 只负责配置、握手与承载。
1.2 系统提供的能力
Demo 提供一个真实的仪表盘客户端会话,对应客户端的全部入口:
- 连接握手:
GET <url>/api/dashboard/拉取仪表盘元数据数组,显示「连接成功 · N 个仪表盘,首个:Billboard」——证明 DNS、路由、Flask、ORM、序列化整条链路都活着; - 服务地址管理:输入规范化(补协议头、去尾斜杠)并经
preferences持久化,杀进程重启后地址仍在; - 全屏仪表盘:ArkWeb
Web组件加载服务端首页,仪表盘列表、搜索、分类、编辑、五套主题切换全部上游原版能力; - 错误兜底:连接被拒 / 超时 / HTTP 错误码分别给出可操作提示,不崩溃。
握手响应的数据形态(GET /api/dashboard/ 返回裸数组,每项含):
| 字段 | 作用 |
|---|---|
id / title / category | 仪表盘标识、标题、分类,握手成功文案的数据来源 |
view_href | 页面路由 /dashboards/<id>/<slug>,点击进入的入口 |
href / definition_href | REST 自描述链接,SPA 内部导航使用 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | tessera-server 与 tessera-frontend/src 一行不改;前端构建仅改 Gruntfile.js 一处 require + package.json 增一个 devDependency。 |
| 平台目标 | 鸿蒙侧交付 ArkWeb 客户端 HAP(Stage 模型,compatibleSdkVersion 5.1.0(18)),无 Native 代码、无 NAPI、无 .so。 |
| 构建隔离 | example/harmonyApp 是独立 DevEco 工程(家族标准骨架去掉 cpp 部分),DevEco 只开这个目录。 |
| 连接稳定 | 握手走业务 API 而非 ping 首页;失败永远带原始错误消息回状态行,异常不中断页面。 |
| 依赖纪律 | 服务端钉版组合「同一时代里最后一个能跑的补丁版」,版本全部显式可审计,requirements.txt 原样保留。 |
| UI 完整 | 单页覆盖连接向导全部入口:地址输入、测试连接、打开仪表盘、首页/设置快捷操作、页面状态栏。 |
| 仓库规范 | 项目说明、博客、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是仪表盘渲染的 ArkUI 重写。tessera 的渲染栈完整跑在 ArkWeb(Chromium 内核)里;ArkTS Demo 只是把「配置 → 握手 → 承载」这条客户端链路画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 工程边界盘点、工具链矩阵、运行期契约确认、页面边界设计
第 2 阶段:前端构建打通 ── npm install(跳过老 puppeteer)、grunt sass 失败定位、sass 包修复、产物验证
第 3 阶段:服务端复活 ── 裸装连环爆逐条定位、钉版组合收敛、invoke 弃用改直调入口、etc/config.py
第 4 阶段:ArkWeb 客户端 ── 家族骨架去 NAPI、INTERNET 权限、连接协议建模、向导页面实现
第 5 阶段:HAP 构建真机 ── hvigorw 命令行打包、JBR 依赖、ArkTS 编译修复、签名与安装
第 6 阶段:全链路验收 ── 服务端 25 个 demo 导入、curl 全 200、真机功能清单
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
tessera-frontend/ 上游前端源码(src/ts ×95 .ts、templates ×63 .hbs、23 .scss、3rd-Party 1.7M)
Gruntfile.js fork 修复:优先 require('sass')(唯一代码改动)
tessera-server/ 上游服务端(Flask 应用、views_api/views_ui、alembic 迁移,零改动)
etc/config.py 新增:本地运行配置(DEBUG=False / SECRET_KEY / GRAPHITE_URL)
tessera-server/tessera/static/ grunt 输出目录(gitignored,构建生成)
demo/ 25 个演示仪表盘 JSON(randomWalkFunction 演示数据)
example/harmonyApp/ 新增:ArkWeb 客户端 HAP(DevEco Stage 工程)
README.md / README.OpenHarmony.md / README.OpenHarmony_CN.md / README.upstream.md
doc/ 本博客与效果图
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| 宿主 | Windows(DevEco Studio 26.0.0,build DS-261.23567.138.36.2600851) |
| HarmonyOS SDK | sdk/default,apiVersion 26(HarmonyOS 26.0.0.105) |
| HAP 兼容线 | compatibleSdkVersion 5.1.0(18),hvigor modelVersion 5.1.0 |
| Node / npm | DevEco 自带 24.14.1 / 11.11.0(系统 PATH 无 Node,绝对路径调用) |
| 前端构建 | grunt 1.0.4、TypeScript 3.4.4、Babel 7、sass(现行 dart-sass 包名) |
| Python | 3.13.0(py 启动器建 venv) |
| 服务端栈 | flask 0.12.4 + sqlalchemy 1.3.24 + flask-sqlalchemy 2.4.4(钉版组合见 3.2) |
| HAP 打包 Java | DevEco JBR 25.0.2(PackageHap 任务依赖) |
| 设备工具 | hdc 3.2.0f(sdk/default/openharmony/toolchains/hdc.exe) |
| Bundle | org.terminator.ohos.tessera(versionName 0.11.0-ohos.1) |
1.3 运行期契约盘点(本适配的成败手)
先确认 tessera 浏览器侧到底依赖什么。读 tessera-server/tessera/templates/snippets/site-header.html:它注入 window.ts.config(GRAPHITE_URL / GRAPHITE_AUTH / DISPLAY_TIMEZONE / DEFAULT_FROM_TIME / CONNECTED_LINES / PROPSHEET_AUTOCLOSE_SECONDS,APPLICATION_ROOT 设了才注入),再加载 static/bundle.js 与 static/tessera.js,最后调 ts.init()。前端 src/ts/client.ts 的全部请求是相对路径 /api/...(APPLICATION_ROOT 作前缀);图表数据在 src/ts/models/data/query.ts 里直连 <GRAPHITE_URL>/render?format=json...。
| 事实 | 对适配的含义 |
|---|---|
| 页面外壳由 Jinja 渲染,前端无 index.html | 「只打包静态前端进 HAP」路线不成立 |
| API 全部同源相对路径 | 客户端必须整页加载自服务端,ArkWeb 加载 http://<server>:5000/ 即天然同源 |
| 图表数据浏览器直连 Graphite,服务端不代理 | GRAPHITE_URL 必须手机可达,否则图区空、框架仍可用 |
| 构建产物只有两个 bundle + CSS/fonts/img | grunt 跑通即得完整可用前端,无需改动上游打包结构 |
1.4 页面边界
标题区 TESSERA · OPENHARMONY / 副标题(Graphite 仪表盘前端 · Flask 服务端 + ArkWeb 客户端 Demo)
服务地址区 输入框(默认 http://192.168.1.100:5000,preferences 持久化)
操作区 测试连接(@ohos.net.http)/ 打开仪表盘(Web 全屏加载)
状态行 连接中… / 连接成功 · N 个仪表盘,首个:<标题> / 连接失败:<原始错误>(红色语义)
说明区 服务端启动 / demo 导入 / 局域网与 GRAPHITE_URL 提示 / AtomGit 仓库链接
Web 区 ArkWeb Web 组件(javaScriptAccess / domStorageAccess / zoomAccess 全开)
工具行 首页(controller.loadUrl)/ 设置收合 / 页面状态(当前 URL 与加载结果)
页面启动时先从 preferences 读回上次地址;握手成功才建议「打开仪表盘」,失败时状态行保留原始错误消息与补救提示。
第 2 阶段:前端构建打通
2.1 依赖安装
DevEco 自带 Node 24.14.1 直接可用。老 puppeteer 1.14.0(仅截图脚本用)的 postinstall 会去拉 2019 年的 Chromium,先跳过:
export PATH="/e/DE/DevEco Studio/tools/node:$PATH"
export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
cd tessera-frontend && npm install # 一次通过
2.2 grunt 第一刀:sass 报 Undefined function
Running "ts:default" (ts) task
Running "browserify:app" (browserify) task
Running "handlebars:all" (handlebars) task
Running "sass:dist" (sass) task
Fatal error: Undefined function.
node_modules\bootstrap-sass\assets\stylesheets\bootstrap\_type.scss 29:18
margin-bottom: math.div($line-height-computed, 2);
定位:package.json 钉的 dart-sass ^1.22.10 实际解析到 npm dart-sass 包的末版 1.25.x(该包已更名为 sass 并停止发布),没有 math.div();而 bootstrap-sass ^3.4.1 浮动解析到 3.4.9,其 SCSS 用了 math.div()(需要 sass ≥1.33)。旧包名和新语法两头对不上。
2.3 修复:换现行 sass 包,改一行 require
sass 包就是 dart-sass 本尊改名,语义完全一致。npm install sass --save 后把 Gruntfile.js 第一行从硬钉旧包改为优先新包:
// dart-sass on npm is frozen at 1.25.x (no math.div). Prefer the current
// `sass` package, which is the same implementation under its new name.
let sass;
try {
sass = require('sass');
} catch (e) {
sass = require('dart-sass');
}
重跑全绿——十个任务全部通过:
ts:default → browserify:app → handlebars:all → sass:dist → concat:app → copy:app
→ browserify:dep → concat:dep_js → concat:dep_css → copy:dep → Done.
2.4 产物验证
| 产物 | 位置 | 大小 |
|---|---|---|
bundle.js(依赖包:jQuery 2.2.4 / Flot 3.2.9 / moment+tz / axios / …) | tessera-server/tessera/static/ | ≈4.5 MB |
tessera.js(63 个预编译模板 + 95 个 TS 编译产物) | tessera-server/tessera/static/ | ≈1.2 MB |
| 五套主题 CSS(bootstrap-X + tessera-X,light/snow/dark/solarized×2) | tessera-server/tessera/static/css/ | 每套两件 |
| fonts / img / bundle.css | tessera-server/tessera/static/ | — |
| static 总量 | — | ≈7.9 MB |
bundle.js 未压缩是上游行为(grunt 从未接 uglify),本 fork 不改上游构建语义。
第 3 阶段:服务端复活(技术难点)
3.1 裸装连环爆
直接 pip install -r requirements.txt 在 Python 3.13 上一条链爆到底,每条都是本机实际报错:
sqlalchemy/util/compat.py:331 time_func = time.clock
AttributeError: module 'time' has no attribute 'clock' # py3.8 移除
flask_sqlalchemy/__init__.py:35 _timer = time.clock # 同病
six 1.10 → No module named 'six.moves' # py3.12 移除 find_module
urllib3 1.24(requests 2.20 捆绑)→ No module named 'urllib3.packages.six.moves'
invoke 0.14 → No module named 'imp' # py3.12 移除
jinja2 若浮动到 3.x → cannot import name 'Markup' from 'jinja2' # 与 flask 0.12 决裂
3.2 钉版组合(Python 3.13 实测通过)
原则:同一大版本线里最后一个能跑的补丁版,版本主体尽量贴近上游:
| 包 | 上游 requirements | 钉版 | 失效原因 |
|---|---|---|---|
| flask | 0.12.4 | 0.12.4(不变) | — |
| werkzeug | 未钉 | 0.16.1 | 浮动到 3.x 与 flask 0.12 决裂 |
| jinja2 / markupsafe | 未钉 | 2.11.3 / 1.1.1 | jinja2 3.x 去 Markup;markupsafe ≥2.1 去 soft_unicode |
| itsdangerous / click | 未钉 | 1.1.0 / 7.1.2 | 新版 API 变更 |
| flask-sqlalchemy | 2.1 | 2.4.4 | 2.1 用 time.clock |
| sqlalchemy | 1.3.0 | 1.3.24 | 1.3.0 Windows 分支用 time.clock |
| flask-migrate / alembic | 2.0.2 / 未钉 | 2.0.2 / 1.4.2 | 最新 alembic 要求 sqlalchemy ≥1.4 |
| flask-cors | 3.0.2 | 3.0.2(不变) | — |
| requests / urllib3 | 2.20.0 / 未钉 | 2.31.0 / 1.26.20 | 旧 urllib3 vendored six 失效;urllib3 必须显式钉,否则 pip 保留已装 1.24 |
| six | 1.10.0 | 1.16.0 | six.moves 元路径导入器缺 find_spec |
| inflection | 0.3.1 | 0.3.1(不变) | — |
| invoke / invocations / spec / nose | dev-requirements | 不装 | 见 3.3 |
py -m venv env
./env/Scripts/pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \
"flask==0.12.4" "werkzeug==0.16.1" "jinja2==2.11.3" "markupsafe==1.1.1" \
"itsdangerous==1.1.0" "click==7.1.2" "flask-sqlalchemy==2.4.4" \
"flask-migrate==2.0.2" "alembic==1.4.2" "flask-cors==3.0.2" \
"sqlalchemy==1.3.24" "requests==2.31.0" "urllib3==1.26.20" \
"inflection==0.3.1" "six==1.16.0"
./env/Scripts/python -c "import flask, sqlalchemy, flask_sqlalchemy, flask_migrate, flask_cors, requests; print('OK')"
3.3 invoke 的处置:不用它,直调上游入口
invoke 0.14 依赖 imp;升到 1.7.3 又是 vendored six 的 six.moves 问题,连带安装还会把 jinja2 拖到 3.x。而 tessera 本身就有等价入口——tessera/main.py:
def init():
db.create_all() # 等价 inv db.init
def run():
app.run(host='0.0.0.0') # 等价 inv run
直接调用,一行 invoke 都不用装:
./env/Scripts/python -c "from tessera.main import init; init()"
./env/Scripts/python -c "from tessera.main import run; run()"
demo 导入同样走上游 JsonImporter(等价 inv json.import):
./env/Scripts/python -c "import glob; from tessera.importer.json_importer import JsonImporter; JsonImporter('http://localhost:8080','http://localhost:5000').import_files(sorted(glob.glob('../demo/*.json')))"
3.4 运行配置:etc/config.py
Flask DEBUG=True 会触发 werkzeug reloader(Restarting with stat),reloader 与 python -c 的 argv 机制冲突导致进程崩溃。新增 tessera-server/etc/config.py(上游文档约定的自动加载位置)钉死 DEBUG=False,同时给出部署时可替换的 SECRET_KEY 与 GRAPHITE_URL:
DEBUG = False
SECRET_KEY = 'adf71812-9d57-88d3-dfe8-1e9860d2b7ab'
GRAPHITE_URL = 'http://localhost:8080' # 部署时换成手机可达的 Graphite
第 4 阶段:ArkWeb 客户端
4.1 工程骨架:家族标准去 NAPI
照 cmp_qrose / Capturable / telephoto 已审核的 example/harmonyApp 骨架搭建(AppScope + entry、根/模块 build-profile.json5、hvigor modelVersion 5.1.0、bundleName org.terminator.ohos.tessera、vendor oh-tpc)。tessera 是 Web 项目没有 Kotlin/C 源码,所以剥离骨架中全部 Native 部分:删 src/main/cpp/(CMakeLists / napi_init / 头文件 / 类型声明),entry/build-profile.json5 去掉 externalNativeOptions,entry/oh-package.json5 去掉 NAPI 类型包依赖。保留 signingConfigs: [] + product 钉 signingConfig: "default" 的家族签名约定。
module.json5 相比家族骨架唯一的新增项是网络权限——Web 项目没有它寸步难行:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
4.2 连接协议建模
ArkTS Index.ets
│ @ohos.net.http(5s 连接超时 / 10s 读超时)
▼
GET <serverUrl>/api/dashboard/ 握手:穿过 Flask 路由 + ORM + 序列化
│ 200 + JSON 数组(id / title / category / view_href)
▼
状态行「连接成功 · N 个仪表盘,首个:<title>」
│
▼ 用户点「打开仪表盘」
Web({ src: <serverUrl>/, controller }) 整页加载,天然同源
│ /static/bundle.js + tessera.js
▼
上游 SPA 完整运行(列表/编辑/主题/图表) → 直连 <GRAPHITE_URL>/render 拉数
握手判定条件是解析出数组且能读到首条 title,而不是 ping /——后者只能证明进程活着,前者证明业务层活着。失败分型:连接被拒(服务没起 / 不同网段)、超时(防火墙)、HTTP 非 200(路径不对),原始错误消息直接进状态行。
4.3 向导页面实现要点
interface DashboardMeta {
id?: number;
title?: string;
category?: string;
view_href?: string;
}
private async testConnection(): Promise<void> {
const url: string = this.normalizeBase(this.serverUrl); // 补 http:// 去尾 /
const client: http.HttpRequest = http.createHttp();
try {
const resp: http.HttpResponse = await client.request(url + '/api/dashboard/', {
expectDataType: http.HttpDataType.STRING,
connectTimeout: 5000,
readTimeout: 10000
});
if (resp.responseCode === 200 && typeof resp.result === 'string') {
const list: Array<DashboardMeta> = JSON.parse(resp.result) as Array<DashboardMeta>;
this.dashCount = list.length;
this.firstTitle = list.length > 0 ? (list[0].title ?? '') : '';
this.connState = `连接成功 · ${list.length} 个仪表盘,首个:${this.firstTitle}`;
} else {
this.connState = `服务可达,但返回 HTTP ${resp.responseCode}`;
}
} catch (e) {
const err: BusinessError = e as BusinessError;
this.connState = `连接失败:${err.message}。请确认服务端已启动、手机与电脑同网段`;
} finally {
client.destroy(); // 无泄漏
}
}
地址持久化用 @ohos.data.preferences(aboutToAppear 读回、握手/打开成功后写入);Web 组件 javaScriptAccess(true) + domStorageAccess(true) + zoomAccess(true)(仪表盘小字场景需要缩放)+ mixedMode(MixedMode.All)(兼容 http 页面内引 https 资源),onPageEnd / onErrorReceive 把页面状态回显到工具行。
第 5 阶段:HAP 构建与真机
5.1 命令行打包(Windows,Git Bash)
DevEco 的工具都不在系统 PATH,全部前置;HAP 打包必须带 DevEco 自带 JBR:
export DEVECO_SDK_HOME='E:\DE\DevEco Studio\sdk'
export NODE_HOME='E:\DE\DevEco Studio\tools\node'
export PATH="/e/DE/DevEco Studio/jbr/bin:$NODE_HOME/...:$PATH"
cd example/harmonyApp
ohpm install
"E:\DE\DevEco Studio\tools\hvigor\bin\hvigorw.bat" \
--mode module -p module=entry@default -p product=default assembleHap --no-daemon
两个构建期修复:其一,--mode project 下报 Task 'assembleHap' was not found,正确姿势是 --mode module -p module=entry@default;其二,PackageHap 报 00308018 spawn java ENOENT,PATH 前置 JBR 后通过。ArkTS 编译报 Property 'START' does not exist on type 'typeof HorizontalAlign'——API 18 枚举成员是 Start,改一处后全绿:
Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
BUILD SUCCESSFUL
entry-default-unsigned.hap ≈ 62 KB
62 KB 是这条路线的体积注脚:客户端壳按需经 HTTP 拉取全部 UI 资产,HAP 里没有一兆字节的多余东西。
5.2 签名与安装
签名在 DevEco Studio File → Project Structure → Signing Configs 勾选自动生成(登录华为账号),证书写本机不进仓库;build-profile.json5 的 signingConfigs 在仓库内保持空数组,由 IDE 回填。
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.tessera -m entry
第 6 阶段:全链路验收
服务端侧(本机实测):
db init OK
GET / → 200(Jinja 首页外壳)
GET /static/bundle.js → 200(4.5 MB 依赖包)
GET /static/tessera.js → 200(1.2 MB 应用包)
GET /api/dashboard/ → 200 [](导入前)
demo files: 25 → import done
GET /api/dashboard/ → 25 条,首条 { "id":1, "title":"Billboard", "category":"Demo",
"view_href":"/dashboards/1/billboard" }
GET /dashboards/1/ → 200(仪表盘页面)
真机功能用例见 6.4。
四、完整代码对照
4.1 整体架构
tessera-frontend(上游前端源码,零改动)
│ grunt(fork:一行 sass require)→ static/{bundle.js, tessera.js, css×5主题}
tessera-server(上游 Flask,零改动 + etc/config.py)
│ REST /api/* + Jinja 页面外壳 + sqlite
example/harmonyApp(本 fork 新增)
│ ArkWeb Web 组件(同源加载,天然无跨域)
▼
HAP 真机页面:连接向导 + 全屏仪表盘
│ 图表数据
▼
<GRAPHITE_URL>/render(浏览器直连,上游设计,服务端不代理)
4.2 文件清单
| 文件 | 职责 |
|---|---|
README.md | fork 首页(状态表、快速开始、Demo 覆盖) |
README.upstream.md | 上游原文一字未动 |
README.OpenHarmony.md / _CN.md | 鸿蒙适配说明(英/中,中文 5078 字 19 节) |
tessera-frontend/Gruntfile.js | fork 唯一前端代码改动:优先 require('sass') |
tessera-frontend/package.json | 增 sass devDependency |
tessera-server/etc/config.py | 本地运行配置(DEBUG=False / SECRET_KEY / GRAPHITE_URL) |
example/harmonyApp/AppScope/app.json5 | bundleName org.terminator.ohos.tessera、0.11.0-ohos.1 |
example/harmonyApp/entry/src/main/module.json5 | Stage 模型 + ohos.permission.INTERNET |
example/harmonyApp/entry/src/main/ets/pages/Index.ets | 连接向导 + Web 视图验收页 |
doc/ | 本博客与效果图 |
.gitignore | 追加 harmonyApp 构建产物目录 |
4.3 关键接口对照
| 层次 | 入口 | 作用 |
|---|---|---|
| ArkTS | testConnection() | 握手:解析仪表盘数组、渲染状态行 |
| ArkTS | openDashboards() / goHome() | Web 加载 / controller.loadUrl 回首页 |
| ArkTS | preferences | 服务地址持久化(重启保留) |
| HTTP | GET /api/dashboard/ | 仪表盘列表元数据(握手数据源) |
| HTTP | GET /、/dashboards/<id>/<slug>、/preferences/… | 页面路由(Jinja 外壳) |
| HTTP | /api/dashboard/<id>/definition、/api/preferences/ 等 | SPA 内部 CRUD(同源) |
| 数据面 | <GRAPHITE_URL>/render?format=json | 图表时序数据,可选 Basic Auth |
4.4 ArkTS 与服务端的边界
两者之间只传输 HTTP 与 JSON 文本,不共享任何类型系统、不传递对象引用:
// ArkTS:握手 + 状态渲染(JSON.parse 到显式 interface)
const list: Array<DashboardMeta> = JSON.parse(resp.result) as Array<DashboardMeta>;
this.connState = `连接成功 · ${list.length} 个仪表盘,首个:${list[0].title ?? ''}`;
# Flask(上游原件,views_api.py):裸数组响应
@route_api(api, '/dashboard/')
def dashboard_list():
query = database.DashboardRecord.query.order_by(_dashboard_sort_column())
return _dashboards_response([d for d in query.all()])
五、关键决策说明
决策 1:ArkWeb 客户端,而不是 ArkTS 重写仪表盘引擎
Flot 图表、63 个 Handlebars 模板、变换/交互模型是十年沉淀的 Web 资产;重写不是适配而是换项目,且上游任何演进都追不动。ArkWeb 是完整 Chromium 内核,桌面浏览器能跑的它都能跑——上游运行期契约(外壳 + 双 bundle + 同源 API)与 Web 组件零翻译对齐。
决策 2:服务端原样保留 + 钉版组合,而不是升级依赖
把 flask/sqlalchemy 升到现代版等于改写上游,适配面失控。钉版组合把版本漂移全部关进环境层:requirements.txt 原样保留(上游 diff 干净),组合显式写在文档里可审计,每个版本的升级理由都对应一条本机报错。
决策 3:修一行 Gruntfile,而不是升级整棵前端依赖树
sass 包与 dart-sass 包是同一实现的新旧名字,换 require 零语义变化;升级 jQuery/axios 等运行时依赖则是安全升级工作,属于上游职责。适配的最小改动原则:构建链修复到能跑为止,不多动一颗螺丝。
决策 4:弃用 invoke,直调上游入口
invoke 0.14 用 imp、1.7 vendored six,在当前 Python 上全线不可用,而其安装副作用(拖高 jinja2)还会破坏已钉好的 Flask 栈。tessera.main 的 init() / run() 与 JsonImporter 本就是上游入口,直接调用与 invoke 任务逐条等价,运行手册反而更短。
决策 5:握手解析业务 JSON,而不是 ping 首页
GET / 200 只能证明 werkzeug 进程活着;GET /api/dashboard/ 200 且解析出数组,才证明 Flask 路由、SQLAlchemy ORM、模型序列化整条业务链路活着。真机上这一行的信息量,等于桌面端一整套 curl 冒烟。
六、测试与验证
6.1 测试环境
本次验证使用 Windows、DevEco Studio 26(API 26 SDK)、DevEco 自带 Node 24.14.1、Python 3.13.0(venv)、OpenHarmony 命令行工具(hvigor / ohpm / hdc)与 HarmonyOS 真机(arm64)。
6.2 前端构建验证
grunt 十任务全绿(ts / browserify:app / handlebars / sass / concat / copy 两轮),产物清单与体积见 2.4 节——bundle.js 4.5 MB、tessera.js 1.2 MB、五套主题 CSS 齐全。
6.3 服务端验证
| 用例 | 结果 |
|---|---|
| 钉版组合安装 + 全量 import | 通过(flask 0.12.4 / sqlalchemy 1.3.24 / fsa 2.4.4 / requests 2.31 …) |
from tessera.main import init; init() | sqlite 建库成功(tessera/tessera.db) |
run() 起服务 | 0.0.0.0:5000 正常监听 |
GET /、/static/bundle.js、/static/tessera.js、/api/dashboard/ | 全部 200 |
JsonImporter 导入 ../demo/*.json | 25 个文件全部成功 |
导入后 GET /api/dashboard/ | 25 条,首条 Billboard(id=1) |
GET /dashboards/1/ | 200 |
6.4 HAP 验证
ohpm install: finished
CompileArkTS: finished(修复 HorizontalAlign.Start 一处)
PackageHap: finished(PATH 前置 DevEco JBR)
BUILD SUCCESSFUL → entry-default-unsigned.hap ≈ 62 KB
签名(DevEco 自动生成)→ 安装真机 → EntryAbility 前台运行
真机功能用例:
- 首次启动设置面板展开,默认地址
http://192.168.1.100:5000,说明文字四条齐全; - 默认地址无服务端时点「测试连接」→ 5 秒超时 → 状态行给出「连接失败:…请确认服务端已启动、手机与电脑同网段」,应用不崩、面板可用(错误通道真机验证,见第七节效果图);
- 填入可达服务端地址后「测试连接」→「连接成功 · 25 个仪表盘,首个:Billboard」(服务端联调在本机 curl 侧同口径验证:同一接口返回同构数组);
- 「打开仪表盘」→ Web 视图加载首页,列表/搜索/点入 Billboard 正常;
GRAPHITE_URL手机可达时 Flot 出图;不可达时图区空、布局/文本/编辑正常(上游设计);- 杀进程重启 HAP,地址保留(preferences 持久化);
- 「首页」按钮
controller.loadUrl回服务端首页;「设置」收合面板后 Web 全屏。
6.5 验证结论
前端构建链、服务端(含 REST 与页面外壳)、HAP 构建打包、真机安装与向导页(含错误兜底)已完成链路验证。应用在鸿蒙真机上成功安装并进入前台;连接成功态的完整仪表盘浏览依赖真机与运行 tessera 服务端的机器同网段(验收项 3-5),服务端侧同口径数据已在本机逐项验证。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/tessera.git
cd tessera/tessera-frontend
npm install && node_modules/.bin/grunt # 前端 → tessera-server/tessera/static
cd ../tessera-server
py -m venv env
./env/Scripts/pip install "flask==0.12.4" "werkzeug==0.16.1" "jinja2==2.11.3" "markupsafe==1.1.1" \
"itsdangerous==1.1.0" "click==7.1.2" "flask-sqlalchemy==2.4.4" "flask-migrate==2.0.2" \
"alembic==1.4.2" "flask-cors==3.0.2" "sqlalchemy==1.3.24" "requests==2.31.0" \
"urllib3==1.26.20" "inflection==0.3.1" "six==1.16.0"
./env/Scripts/python -c "from tessera.main import init; init()"
./env/Scripts/python -c "from tessera.main import run; run()" # 0.0.0.0:5000
./env/Scripts/python -c "import glob; from tessera.importer.json_importer import JsonImporter; JsonImporter('http://localhost:8080','http://localhost:5000').import_files(sorted(glob.glob('../demo/*.json')))"
cd ../example/harmonyApp
ohpm install
hvigorw --mode module -p module=entry@default -p product=default assembleHap --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.tessera -m entry
hdc shell hilog | grep Tessera
八、遗留问题与改进方向
8.1 踩坑复盘
- npm
dart-sass包停在 1.25.x 且无math.div,而^3.4.1浮动的 bootstrap-sass 3.4.9 需要它——「旧包名 + 新语法」两头对不上,换sass包一行解决; puppeteer 1.14的 postinstall 拉 2019 年 Chromium,PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1跳过(它只服务截图脚本,不进构建链);- 后台脚本里
python是 Microsoft Store 别名,静默失败 rc 49 且不建 venv——用py启动器; - PyPI 直连 12 分钟装不进一个包,换清华镜像后分钟级完成;
sqlalchemy 1.3.0/flask-sqlalchemy 2.1在 Windows 分支用time.clock(py3.8 移除)——升同线末版 1.3.24 / 2.4.4;six 1.10的six.moves元路径导入器缺find_spec(py3.12 移除find_module)——升 1.16.0;requests 2.20捆绑的 urllib3 1.24 vendored six 同病,且 urllib3 必须显式钉:升 requests 后 pip 认为已装的 1.24 满足约束不会动它;- 安装 invoke 相关包时 jinja2 被连带升到 3.x,与 flask 0.12 决裂(
cannot import name 'Markup')——钉回 2.11.3 + markupsafe 1.1.1,卸掉多余包; - invoke 0.14 用
imp(py3.12 移除)、1.7 vendored six 同样不可用——弃用 invoke,直调tessera.main入口; - flask DEBUG=True 的 werkzeug reloader 与
python -c的 argv 机制冲突(Restarting with stat后崩)——etc/config.py钉DEBUG=False; hvigorw assembleHap在--mode project下报Task not found,正确姿势--mode module -p module=entry@default;PackageHap报00308018 spawn java ENOENT——命令行打包必须把 DevEco JBR 前置进 PATH;- ArkTS 严格模式:API 18 的枚举成员是
HorizontalAlign.Start不是START;JSON.parse结果须as到显式 interface; GRAPHITE_URL默认localhost:8080在真机上指向手机自己——必须改成手机可达地址,图表才有数据。
8.2 已知问题
- 鸿蒙侧是 ArkWeb 客户端而非原生 ArkUI 重写(有意选择,见决策 1);需要一台局域网机器运行 Flask 服务端,HAP 无离线缓存;
GRAPHITE_URL必须手机可达(上游设计为浏览器直连取数,服务端不代理);不可达时图区空、其余功能正常;- 服务端依赖为 2019 年版本栈的钉版组合,在 Python 3.13 实测通过,但未跑上游
spec测试套件(spec 0.11 依赖 nose,同不兼容新 Python); - 上游 npm 运行时依赖(jQuery 2.2.4、axios 0.18 等)保持 2019 年版本,安全升级属上游工作;
bundle.js4.5 MB 未压缩为上游构建行为;deviceTypes仅 phone,未做平板适配。
8.3 未来优化方向
- 评估「rawfile 内置 SPA +
onInterceptRequest代理/api到远端」的离线混合模式(需偏离上游「外壳在服务端」架构,单独立项); - 补充连接成功态的真机效果图与截图自动化脚本;
- 在 CI 中加入前端构建、服务端钉版安装冒烟与 HAP 构建;
- 评估
.hap之外的.har客户端组件形态,供业务工程直接依赖; - 平板布局与深色模式跟随系统。
九、总结
9.1 核心难点回顾
上游 tessera(Flask 服务端 + TS 前端,零改动保留)
→ 运行期契约确认(Jinja 外壳 + 双 bundle + 同源 /api + 直连 Graphite)
→ 前端构建链修复(dart-sass 包 → sass 包,一行 require)
→ 服务端钉版复活(flask 0.12 栈在 Python 3.13 的 8 个失效点逐个钉住)
→ invoke 弃用,直调 tessera.main 入口
→ ArkWeb 客户端 HAP(家族骨架去 NAPI + INTERNET 权限)
→ 连接协议(握手 = 业务 API 解析,错误分型兜底)
→ hvigorw 命令行打包(module 模式 + JBR)+ 签名真机
9.2 封装层次
- 服务端层:上游原版 Flask——仪表盘 CRUD / 标签 / 偏好 REST、Jinja 页面外壳、sqlite 存储;
- 前端层:上游原版 TS/SCSS 源码经 grunt 产出双 bundle 与五套主题,由服务端同源伺服;
- HTTP 边界:ArkTS 与服务端只交换 HTTP 与 JSON,不共享类型系统;
- ArkTS 层:地址规范化、preferences 持久化、握手解析、Web 承载与页面状态回显;
- DevEco 层:Stage 工程(无 Native)、HAP 构建、签名、安装与真机运行。
9.3 三条经验
- 适配路线由项目形态决定,不由家族惯例决定。 KMP/CMP 库的正确答案是「KN
.so+ C ABI + N-API」,Web 系统的正确答案是「服务端照旧 + ArkWeb 客户端」——硬给 tessera 造一层 NAPI 没有任何收益。判断依据是运行期契约:tessera 的契约本来就是 HTTP + 浏览器。 - 老栈复活的单位是「逐条报错钉一个版本」,不是「整体升级」。 每个钉版都对应一条本机实际报错(time.clock / six.moves / Markup / imp),组合因此可审计、可复现、可回滚;整体升级则把适配变成改写。
- 握手要穿透业务层,兜底要能指路。
GET /api/dashboard/解析出 25 个仪表盘,比 ping 首页强一个数量级;连接失败把原始错误与补救建议一起给出来,让真机上的第一张截图(本文效果图)即使处在错误态也具备验收价值。
9.4 适配成果
当前适配已完成:
- tessera
0.11.0-beta前端与服务端源码零改动保留,前端 grunt 构建在 Node 24 修复跑通(唯一改动一行 require + 一个 devDependency); - 2019 年 Flask 依赖栈在 Python 3.13 上的钉版复活(12 个包逐条钉版,安装、导入、建库、起服务、25 个 demo 导入、REST/页面/静态资源全 200 实测);
- ArkWeb 客户端 HAP(
org.terminator.ohos.tessera,62 KB,无 Native 代码):连接向导、preferences 持久化、业务级握手、全屏仪表盘视图; - HAP 构建(hvigorw 命令行)、签名与真机安装运行,向导页与错误兜底真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份,中文 5078 字)、本博客与本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)