本文记录 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_hrefREST 自描述链接,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 SDKsdk/default,apiVersion 26(HarmonyOS 26.0.0.105)
HAP 兼容线compatibleSdkVersion 5.1.0(18),hvigor modelVersion 5.1.0
Node / npmDevEco 自带 24.14.1 / 11.11.0(系统 PATH 无 Node,绝对路径调用)
前端构建grunt 1.0.4、TypeScript 3.4.4、Babel 7、sass(现行 dart-sass 包名)
Python3.13.0(py 启动器建 venv)
服务端栈flask 0.12.4 + sqlalchemy 1.3.24 + flask-sqlalchemy 2.4.4(钉版组合见 3.2)
HAP 打包 JavaDevEco JBR 25.0.2(PackageHap 任务依赖)
设备工具hdc 3.2.0f(sdk/default/openharmony/toolchains/hdc.exe)
Bundleorg.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/imggrunt 跑通即得完整可用前端,无需改动上游打包结构
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.csstessera-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钉版失效原因
flask0.12.40.12.4(不变)—
werkzeug未钉0.16.1浮动到 3.x 与 flask 0.12 决裂
jinja2 / markupsafe未钉2.11.3 / 1.1.1jinja2 3.x 去 Markup;markupsafe ≥2.1 去 soft_unicode
itsdangerous / click未钉1.1.0 / 7.1.2新版 API 变更
flask-sqlalchemy2.12.4.42.1 用 time.clock
sqlalchemy1.3.01.3.241.3.0 Windows 分支用 time.clock
flask-migrate / alembic2.0.2 / 未钉2.0.2 / 1.4.2最新 alembic 要求 sqlalchemy ≥1.4
flask-cors3.0.23.0.2(不变)—
requests / urllib32.20.0 / 未钉2.31.0 / 1.26.20旧 urllib3 vendored six 失效;urllib3 必须显式钉,否则 pip 保留已装 1.24
six1.10.01.16.0six.moves 元路径导入器缺 find_spec
inflection0.3.10.3.1(不变)—
invoke / invocations / spec / nosedev-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.mdfork 首页(状态表、快速开始、Demo 覆盖)
README.upstream.md上游原文一字未动
README.OpenHarmony.md / _CN.md鸿蒙适配说明(英/中,中文 5078 字 19 节)
tessera-frontend/Gruntfile.jsfork 唯一前端代码改动:优先 require('sass')
tessera-frontend/package.json增 sass devDependency
tessera-server/etc/config.py本地运行配置(DEBUG=False / SECRET_KEY / GRAPHITE_URL)
example/harmonyApp/AppScope/app.json5bundleName org.terminator.ohos.tessera、0.11.0-ohos.1
example/harmonyApp/entry/src/main/module.json5Stage 模型 + ohos.permission.INTERNET
example/harmonyApp/entry/src/main/ets/pages/Index.ets连接向导 + Web 视图验收页
doc/本博客与效果图
.gitignore追加 harmonyApp 构建产物目录

4.3 关键接口对照

层次入口作用
ArkTStestConnection()握手:解析仪表盘数组、渲染状态行
ArkTSopenDashboards() / goHome()Web 加载 / controller.loadUrl 回首页
ArkTSpreferences服务地址持久化(重启保留)
HTTPGET /api/dashboard/仪表盘列表元数据(握手数据源)
HTTPGET /、/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/*.json25 个文件全部成功
导入后 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 前台运行

真机功能用例:

  1. 首次启动设置面板展开,默认地址 http://192.168.1.100:5000,说明文字四条齐全;
  2. 默认地址无服务端时点「测试连接」→ 5 秒超时 → 状态行给出「连接失败:…请确认服务端已启动、手机与电脑同网段」,应用不崩、面板可用(错误通道真机验证,见第七节效果图);
  3. 填入可达服务端地址后「测试连接」→「连接成功 · 25 个仪表盘,首个:Billboard」(服务端联调在本机 curl 侧同口径验证:同一接口返回同构数组);
  4. 「打开仪表盘」→ Web 视图加载首页,列表/搜索/点入 Billboard 正常;
  5. GRAPHITE_URL 手机可达时 Flot 出图;不可达时图区空、布局/文本/编辑正常(上游设计);
  6. 杀进程重启 HAP,地址保留(preferences 持久化);
  7. 「首页」按钮 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 踩坑复盘

  1. npm dart-sass 包停在 1.25.x 且无 math.div,而 ^3.4.1 浮动的 bootstrap-sass 3.4.9 需要它——「旧包名 + 新语法」两头对不上,换 sass 包一行解决;
  2. puppeteer 1.14 的 postinstall 拉 2019 年 Chromium,PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 跳过(它只服务截图脚本,不进构建链);
  3. 后台脚本里 python 是 Microsoft Store 别名,静默失败 rc 49 且不建 venv——用 py 启动器;
  4. PyPI 直连 12 分钟装不进一个包,换清华镜像后分钟级完成;
  5. sqlalchemy 1.3.0 / flask-sqlalchemy 2.1 在 Windows 分支用 time.clock(py3.8 移除)——升同线末版 1.3.24 / 2.4.4;
  6. six 1.10 的 six.moves 元路径导入器缺 find_spec(py3.12 移除 find_module)——升 1.16.0;
  7. requests 2.20 捆绑的 urllib3 1.24 vendored six 同病,且 urllib3 必须显式钉:升 requests 后 pip 认为已装的 1.24 满足约束不会动它;
  8. 安装 invoke 相关包时 jinja2 被连带升到 3.x,与 flask 0.12 决裂(cannot import name 'Markup')——钉回 2.11.3 + markupsafe 1.1.1,卸掉多余包;
  9. invoke 0.14 用 imp(py3.12 移除)、1.7 vendored six 同样不可用——弃用 invoke,直调 tessera.main 入口;
  10. flask DEBUG=True 的 werkzeug reloader 与 python -c 的 argv 机制冲突(Restarting with stat 后崩)——etc/config.py 钉 DEBUG=False;
  11. hvigorw assembleHap 在 --mode project 下报 Task not found,正确姿势 --mode module -p module=entry@default;
  12. PackageHap 报 00308018 spawn java ENOENT——命令行打包必须把 DevEco JBR 前置进 PATH;
  13. ArkTS 严格模式:API 18 的枚举成员是 HorizontalAlign.Start 不是 START;JSON.parse 结果须 as 到显式 interface;
  14. 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.js 4.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 三条经验

  1. 适配路线由项目形态决定,不由家族惯例决定。 KMP/CMP 库的正确答案是「KN .so + C ABI + N-API」,Web 系统的正确答案是「服务端照旧 + ArkWeb 客户端」——硬给 tessera 造一层 NAPI 没有任何收益。判断依据是运行期契约:tessera 的契约本来就是 HTTP + 浏览器。
  2. 老栈复活的单位是「逐条报错钉一个版本」,不是「整体升级」。 每个钉版都对应一条本机实际报错(time.clock / six.moves / Markup / imp),组合因此可审计、可复现、可回滚;整体升级则把适配变成改写。
  3. 握手要穿透业务层,兜底要能指路。 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐