从 0 到 1:用 CJMP 开发「每日早报」鸿蒙应用完整实战
从 0 到 1:用 CJMP 开发「每日早报」鸿蒙应用完整实战
本文记录使用 CJMP(仓颉跨平台框架)+ 仓颉语言 + HarmonyOS,从空白工程到实现一个调用 ALAPI「每日早报」接口的鸿蒙应用,并在真机上跑通的完整过程,包含所有编译报错与启动闪退的排查思路。
一、目标与背景
我们要做一个小应用:打开后自动请求 ALAPI 的每日早报接口,把当天的 15 条新闻简报和微语展示在屏幕上,支持手动刷新。
API 信息:
- 接口:
POST https://v3.alapi.cn/api/zaobao - 参数:
token(必填)、format=json - 返回:
code、data.date、data.news(字符串数组)、data.weiyu
技术栈:
- CJMP SDK v0.2.2(跨平台 UI 引擎 Keels)
- 仓颉语言(HarmonyOS Cangjie 1.1.0)
- ArkUI 声明式组件 +
@State状态管理 ohos.net.http(HarmonyOS 网络 HTTP Kit)
二、环境准备(简略版)
macOS(Apple Silicon)+ DevEco Studio 6.1 + OpenHarmony SDK 26.0.0 + CJMP SDK v0.2.2 + 鸿蒙真机(Mate 60 Pro)。
要点回顾:
git clone --depth 1 -b open-sdk-mac-v0.2.2 https://atomgit.com/CJMP/OpenSDK.git ~/cjmp-sdk~/.zshrc配置CJMP_SDK_HOME与 PATH- 从 DevEco Studio 的 CJMP 插件包中提取
build-tools与api(命令行构建 ohos 需要) xattr -dr com.apple.quarantine解除 Gatekeeper 拦截- 构建时设置
DEVECO_CANGJIE_PATH/DEVECO_CANGJIE_PLUGIN_ENABLED
详细过程可参考上一篇文章《从零搭建 CJMP 开发环境并运行到鸿蒙设备》。
三、创建项目
keels create ~/Desktop/cjpm/demo --name demo
cd ~/Desktop/cjpm/demo
生成的三端壳工程中,我们只关心 lib/(仓颉跨平台逻辑)和 hos/(鸿蒙壳):
demo/
├── project.conf # Keels 项目配置
├── lib/ # 仓颉源码(三端共享)
│ ├── main_ability.cj # UIAbility 入口,loadContent("EntryView")
│ ├── index.cj # ⭐ 主界面组件 EntryView
│ ├── ability_mainability_entry.cj
│ └── cjpm.toml
└── hos/ # HarmonyOS 壳工程
└── entry/src/main/module.json5 # ⭐ 模块配置(权限)
main_ability.cj 负责把名为 EntryView 的组件加载到窗口:
class MainAbility <: UIAbility {
public override func onWindowStageCreate(windowStage: WindowStage): Unit {
windowStage.loadContent("EntryView")
}
}
我们要写的全部 UI 和网络逻辑都在 lib/index.cj 的 EntryView 里。
四、界面设计:ArkUI 声明式写法
仓颉中的 ArkUI 组件用法与 ArkTS 几乎一致:@Entry @Component 标注组件类,@State 标注响应式变量,build() 里用声明式语法搭界面。
@Entry
@Component
class EntryView {
@State
var newsContent: String = "正在获取今日早报..."
@State
var isLoading: Bool = true
func build() {
Row {
Column {
Text("📰 每日早报")
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin(20)
if (isLoading) {
LoadingProgress().width(40).height(40)
}
Scroll {
Text(newsContent)
.fontSize(16)
.lineHeight(28)
.padding(16)
}.layoutWeight(1).scrollBar(BarState.Auto)
Button("刷新")
.width(200).height(44).margin(20)
.onClick({ evt => loadNews() })
}.width(100.percent)
}.height(100.percent)
}
public override func aboutToAppear(): Unit {
loadNews() // 页面首次出现即拉取数据
}
}
仓颉语法小贴士:
- 属性链式调用与 ArkTS 相同:
.fontSize(28).fontWeight(...) - 点击回调是无参风格:
.onClick({ evt => loadNews() }) - 尺寸用数字 + 隐式单位;百分比用
100.percent margin/padding只传单个数值即可四边生效,不支持{ top: 20 }这种对象字面量- 生命周期回调是
aboutToAppear(),不是onAppear()(后者会报"没有可覆写的方法")
五、网络请求:Cangjie 调用 HarmonyOS HTTP Kit
5.1 先看 API 声明文件
CJMP SDK 里每个预编译模块都带一份 .cj.d 声明文件(相当于头文件),写代码前先读它:
~/cjmp-sdk/cjmp-tools/api/modules/linux_ohos_aarch64_cjnative/ohos/ohos.net.http.cj.d
从中确认关键 API:
- 顶层函数
public func createHttp(): HttpRequest(注意:不是类方法) HttpRequest.request(url, options, callback),需要ohos.permission.INTERNET- 回调类型
AsyncCallback<HttpResponse>,即(Option<BusinessException>, Option<HttpResponse>) -> Unit HttpRequestOptions里有method、header、timeout、expectDataTypeHttpResponse.result是枚举HttpData(StringData(String)/ArrayData(Array<Byte>))- HTTP 方法枚举成员是
RequestMethod.Post(首字母大写驼峰)
5.2 踩坑一:必须"逐符号导入"
这是编译期最大的坑。三种导入方式:
// ❌ 模块导入:符号全部解析不到(undeclared)
import ohos.net.http
// ❌ 通配符导入:同样解析不到(undeclared)
import ohos.net.http.*
// ✅ 逐个符号显式导入:正常可用
import ohos.net.http.createHttp
import ohos.net.http.HttpRequest
import ohos.net.http.HttpRequestOptions
import ohos.net.http.HttpResponse
import ohos.net.http.RequestMethod
import ohos.net.http.HttpData
import ohos.net.http.HttpDataType
排查技巧:写一个只引用某个符号的最小文件单独编译,可以快速确认符号是否可用。实测用「单符号导入」即可编译通过。
5.3 请求代码
func loadNews(): Unit {
isLoading = true
newsContent = "正在获取今日早报..."
let httpRequest = createHttp()
let options = HttpRequestOptions()
options.method = RequestMethod.Post
options.connectTimeout = 10000
options.readTimeout = 10000
options.header = HashMap<String, String>()
options.header["Content-Type"] = "application/x-www-form-urlencoded"
options.expectDataType = HttpDataType.StringValue // 强制字符串返回
let url = "https://v3.alapi.cn/api/zaobao?token=${API_TOKEN}&format=json"
try {
httpRequest.request(url, options, {
err: Option<BusinessException>, data: Option<HttpResponse> =>
// ... 回调处理,见 5.4
})
} catch (e: BusinessException) {
newsContent = "请求异常: ${e.message}"
isLoading = false
}
}
注意 request() 声明为 throwexception: true,同步调用也要包 try/catch。
5.4 回调里的状态更新必须回主线程
回调参数是 Option 包裹的,先 match 解包。但直接在这里给 @State 变量赋值会闪退,原因见第七节。最终写法是把所有状态更新包进 ohos.base.launch({...}):
httpRequest.request(url, options, {
err: Option<BusinessException>, data: Option<HttpResponse> =>
// HTTP 回调在后台线程,必须切回主线程再更新 @State
launch({
=>
match (err) {
case Some(e) =>
newsContent = "请求失败: ${e.message}"
isLoading = false
case None =>
match (data) {
case Some(resp) =>
if (resp.responseCode == 200) {
let text = extractText(resp.result)
match (text) {
case Some(s) => newsContent = extractNews(s)
case None => newsContent = "返回数据格式不是字符串"
}
} else {
newsContent = "请求失败,状态码: ${resp.responseCode}"
}
case None => newsContent = "请求失败:无返回数据"
}
isLoading = false
}
})
})
关键导入: import ohos.base.launch(同样是逐符号导入)。launch 的官方注释是 “Submit the task to the main thread for execution”——它就是 ArkUI 状态管理的主线程调度入口。
六、启动闪退排查实录
第一版代码编译通过、安装成功,但一点击应用图标就闪退。排查工具与思路:
6.1 抓崩溃日志
hdc shell "hilog -x" | grep -iE 'cjerror|BusinessException|Uncaught exception|Faultlog'
看到系统写入了 Cangjie 错误日志:
SaveFaultLogToFile: create log cjerror-com.example.demo-20021078-xxx.log
Reason:ohos.business_exception:BusinessException
/data/log/faultlog/faultlogger/ 目录 shell 直接 cat 会 Permission denied,但可以用 hdc file recv 拉到本地:
hdc file recv /data/log/faultlog/faultlogger/cjerror-com.example.demo-xxx.log /tmp/cjerror.log
6.2 根因一:缺少 INTERNET 权限
崩溃日志显示 BusinessException,而 HttpRequest.request() 的声明里明确写着:
permission: "ohos.permission.INTERNET", throwexception: true
即:没申请网络权限就发起请求 → 抛权限异常(错误码 201)→ 未捕获 → 闪退。
修复:在 hos/entry/src/main/module.json5 的 module 里加权限声明:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
],
6.3 根因二:@State 不能在后台线程更新
加上权限后重新安装,仍然闪退。再抓一次日志,堆栈直接指到问题代码:
Exception info: State updates are not performed on the main thread.
at ohos.arkui.state_management.ObservedProperty<...>::set(...)
at EntryView::newsContent::set(.../lib/index.cj:25)
at EntryView::loadNews::lambda.0()(.../lib/index.cj:99) <-- HTTP 回调
原因:HTTP 异步回调运行在后台线程(FFI 回调线程),而 ArkUI 规定 @State 属性只能在 UI 主线程修改,否则抛异常闪退。
修复:把回调里所有状态更新包进 ohos.base.launch({...}) 切回主线程(见 5.4 代码)。
七、数据解析优化:两处"非预期"
修完闪退,界面能稳定显示了,但先后出现两个数据问题。
7.1 “返回数据格式不是字符串”
现象:界面显示兜底文案「返回数据格式不是字符串」。
排查:HttpResponse.result 是 HttpData 枚举,此前只处理了 StringData。实际响应命中 ArrayData(字节数组)。
修复分两层:
- 源头:请求时设置
options.expectDataType = HttpDataType.StringValue,告诉框架返回字符串。 - 兜底:无论哪种形态都能解析——新增
extractText():
func extractText(result: HttpData): Option<String> {
match (result) {
case HttpData.StringData(text) => Some(text)
case HttpData.ArrayData(bytes) => Some(String.fromUtf8(bytes)) // Byte == UInt8 别名
case _ => None // 枚举有第三个成员,必须补全分支否则编译报 non-exhaustive
}
}
注意:Cangjie 里
Byte就是UInt8的别名(public type Byte = UInt8),所以字节数组可以直接传给String.fromUtf8,无需手动转换。
7.2 “未获取到新闻内容”
现象:界面有日期、有微语,但新闻列表是空的。
排查:先在 Mac 上用 curl 实测接口真实返回:
curl -s -X POST "https://v3.alapi.cn/api/zaobao?token=xxx&format=json"
发现关键差异——返回的 news 是字符串数组,不是对象数组:
"data": {
"date": "2026-09-04",
"news": ["1、青海海西州发生5.1级地震…", "2、世界气象组织…"],
"weiyu": "【微语】…"
}
而第一版解析器按 [{"title":…,"content":…}] 的对象结构去匹配 "title":",自然永远匹配不到。
修复:按真实结构解析——定位 "news":[" 后逐条截取到下一个 ",跳过 "," 分隔符继续;最后再单独提取 weiyu 字段拼到末尾。同时给每条加序号前缀方便阅读。
let newsKey = "\"news\":[\""
let keyPos = safeIndexOf(jsonStr, newsKey, 0)
if (keyPos >= 0) {
var textFrom = keyPos + newsKey.size // 直接指向首条文本
var index = 0
while (itemCount < 40) {
let closeQ = safeIndexOf(jsonStr, "\"", textFrom)
if (closeQ <= textFrom) { break }
let itemText = jsonStr[textFrom .. closeQ]
if (itemText.size == 0) { break }
index += 1
content = content + "${index}. ${itemText}\n\n"
itemCount += 1
// 条间分隔 ",":跳过 ',' 与下一项的开引号
let nextOpenQ = safeIndexOf(jsonStr, "\"", closeQ + 1)
if (nextOpenQ < 0) { break }
textFrom = nextOpenQ + 1
}
}
经验教训:客户端解析前先 curl 看真实返回,别凭文档/直觉猜结构;字符串数组 ≠ 对象数组。
八、构建、安装与真机验证
8.1 构建 HAP
export CJMP_SDK_HOME=$HOME/cjmp-sdk
export PATH=$CJMP_SDK_HOME/cjmp-tools/bin:$CJMP_SDK_HOME/cjmp-tools/build-tools/tools/bin:$HOME/Library/OpenHarmony/Sdk/26.0.0/toolchains:$PATH
export DEVECO_CANGJIE_PATH=$HOME/cjmp-sdk/cjmp-tools
export DEVECO_CANGJIE_PLUGIN_ENABLED=true
cd ~/Desktop/cjpm/demo
rm -rf hos/.hvigor # 清缓存,避免增量误判
keels build hap -v # → BUILD SUCCESSFUL
产物在:hos/entry/build/default/outputs/default/entry-default-signed.hap。
8.2 安装与启动
keels run 只支持 Android(走 adb),鸿蒙设备要用 hdc 手动装:
export PATH=$HOME/Library/OpenHarmony/Sdk/26.0.0/toolchains:$PATH
hdc uninstall com.example.demo # 每次先卸干净,权限配置才生效
hdc install hos/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.demo
验证进程存活、无新崩溃:
hdc shell "pidof com.example.demo" # 有输出 = 存活
hdc shell "hilog -x" | grep -E 'cjerror|Uncaught exception' # 无输出 = 没崩
8.3 用 UI 树验证数据真的显示了
肉眼确认之外,还能用 uitest 导出界面控件树,程序化检查 Text 内容:
hdc shell "uitest dumpLayout -p /data/local/tmp/layout.json"
hdc file recv /data/local/tmp/layout.json /tmp/layout.json
解析 JSON 后就能看到界面上的真实文本——最终验证结果:
📰 每日早报
📅 2026-09-04
1. 青海海西州发生5.1级地震,中国地震局启动四级应急响应…
2. 世界气象组织确认厄尔尼诺已形成…
…(共 15 条)
💬 【微语】愿你在平凡的日子里,也活出自己的光…

九、总结与避坑清单
从零到真机运行,最大的成本不在写功能,而在理解 Cangjie 与 HarmonyOS Kit 的对接约定。核心避坑清单:
| # | 坑 | 一句话解法 |
|---|---|---|
| 1 | import ohos.net.http 后符号 undeclared | 逐符号导入:import ohos.net.http.HttpRequest |
| 2 | 回调参数报错/类型对不上 | 回调是 AsyncCallback:(Option<BusinessException>, Option<HttpResponse>) |
| 3 | 请求抛异常闪退 | request() 声明 throwexception,需 try/catch |
| 4 | BusinessException 闪退 | 先查 module.json5 是否声明 INTERNET 权限 |
| 5 | “State updates are not performed on the main thread” | 网络回调里更新 @State 要包 ohos.base.launch({...}) |
| 6 | 枚举成员写错 | RequestMethod.Post(驼峰),不是 POST |
| 7 | 响应是字节数组 | options.expectDataType = HttpDataType.StringValue,兜底 String.fromUtf8 |
| 8 | match 报 non-exhaustive | HttpData 有第三个成员,补 case _ => None |
| 9 | 解析结果为空 | 先 curl 真实返回——news 是字符串数组不是对象数组 |
| 10 | keels run 不支持鸿蒙 | 用 hdc install + hdc shell aa start |
方法论沉淀:
- 先读
.cj.d声明文件再写代码——每个 API 的签名、权限、异常都写在注释里,比猜 API 高效得多。 - 崩溃先抓日志——
hilog -x+hdc file recv拉 cjerror 日志,堆栈会精确指到出错的那一行源码。 - 解析前先 curl——接口真实返回永远比文档更可信。
- 状态更新必须回主线程——这是 ArkUI 状态管理的铁律,网络层回调默认在后台线程。
希望这篇实战记录能帮你少走弯路。完整的「每日早报」应用虽然简单,但它把 Cangjie + ArkUI + HTTP + 权限 + 线程 + JSON 解析这些鸿蒙原生开发的要点串了起来,是一份很好的起步样例。
相关链接:
- CJMP 组织:https://atomgit.com/CJMP
- CJMP SDK:https://atomgit.com/CJMP/OpenSDK
- CJMP 文档:https://atomgit.com/CJMP/Docs
- ALAPI 每日早报接口文档:https://v3.alapi.cn/api/zaobao
- 项目地址:https://atomgit.com/jianguoxu/cjmp-demo
更多推荐



所有评论(0)