本文记录 http-errors 2.0.1 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在本地终端中通过真实的仓颉可执行程序验证 HTTP 错误构造、状态码分发、默认消息、自定义消息、错误暴露策略和错误识别能力。
项目地址: AtomGit/nutpi_cj/http-errors
上游仓库: jshttp/http-errors
开发工具: 华为云码道
一、背景
1.1 为什么适配 http-errors
http-errors 是 Node.js Web 服务中常用的 HTTP 错误构造库,用于把状态码、错误消息和客户端暴露策略统一封装成可检查的错误对象。JavaScript 版本通过动态对象、原型继承和状态码构造器提供 createError(404)、createError.NotFound 等调用方式,而仓颉强调静态类型、显式异常层次和跨平台可移植性。直接把 JavaScript 源码逐行改写成仓颉,难点不在语法转换,而在于把动态错误对象和状态码构造器重新组织成稳定的异常类型。
本项目保留 http-errors 的核心调用方向:使用 createError 按状态码生成错误,使用 NotFoundError、InternalServerError 等具名异常表达常见状态,使用 status、statusCode、message、expose 和 errorName 读取结果,并通过 isHttpError 判断一个值是否属于该异常层次。
1.2 跨端三方库的适配边界
当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架、Web 框架或操作系统。适配边界可以概括为:
| 边界 | 仓颉实现 |
|---|---|
| 包管理 | cjpm.toml、cjpm build 和静态库输出 |
| 基础错误 | HttpError <: Exception,保存状态码、消息和暴露标志 |
| 具名构造器 | 29 个 4xx 类和 12 个 5xx 类 |
| 工厂函数 | createError(status, message:),支持标准、未知和非法状态 |
| 状态消息 | statusMessage(status) 返回标准原因短语和范围回退 |
| 错误识别 | isHttpError(value) 检查仓颉异常层次 |
| 交付形态 | 仓颉静态库 libhttp_errors_cangjie.a 和接口文件 http_errors_cangjie.cjo |
| 验证方式 | test/main.cj 直接链接测试和 example/example.cj 完整终端结果 demo |
这里没有 HAP 构建、ArkTS 桥接、网络传输或系统权限。http-errors 本身只负责错误对象,HTTP 客户端、Web 框架和响应发送仍由调用方负责。只要目标平台提供兼容版本的仓颉 SDK,就可以按同一套源码和 cjpm 配置构建并消费这个库。
1.3 适配后的能力
当前仓颉实现对齐 http-errors 的错误构造和读取流程,公开提供以下能力:
- 基础异常:
HttpError,包含status、statusCode、message、expose和errorName; - 4xx 具名异常:
BadRequestError、UnauthorizedError、NotFoundError、TooManyRequestsError等; - 5xx 具名异常:
InternalServerError、BadGatewayError、GatewayTimeoutError等; - 工厂创建:
createError根据状态码自动选择具名异常,未知 4xx/5xx 保留状态码并使用范围默认消息; - 原因短语:
statusMessage返回标准 HTTP 状态消息; - 类型识别:
isHttpError判断一个值是否属于HttpError层次; - 结果输出:示例逐项打印具名构造器和工厂函数实际生成的名称、状态码、消息和
expose值。
二、实现路线图
第 1 阶段:盘点 API ── 对照 http-errors 的工厂、具名构造器和错误属性
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射 ── 用 Exception、class 和显式属性替换动态 Error 对象
第 4 阶段:实现错误模型 ── 完成状态消息、4xx/5xx 构造器和 createError 分发
第 5 阶段:测试与核对 ── 编译测试入口,执行状态码、消息和类型识别断言
第 6 阶段:终端演示 ── 输出所有库调用产生的真实结果并保留效果图
每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 .cjpm-target/http_errors_cangjie,验证阶段得到 ALL HTTP-ERRORS CANGJIE TESTS PASS,最终示例则输出 41 个具名构造器和所有工厂路径的实际结果。
三、逐步实现过程
第 1 阶段:盘点原库 API 和仓颉工程边界
1.1 识别动态 API
http-errors 的 JavaScript API 接受数字状态码、字符串消息、已有 Error 和动态属性对象,并根据状态码动态选择构造器。仓颉没有与 JavaScript 原型对象完全等价的动态错误容器,因此本适配先筛选可以稳定表达的能力:状态码使用 Int64,消息使用 String,错误类型使用 Exception 子类,公开属性使用明确的只读成员。
原库可以把任意已有 Error 重新装饰为 HTTP 错误;仓颉适配不修改任意异常的运行时原型,而是要求调用方使用 HttpError 或其具名子类。这样错误类型在编译期可见,调用方也能直接读取状态码和暴露策略。
1.2 固定仓颉项目结构
以下目录树按照当前 http-errors 项目的仓颉适配文件整理,只展示仓颉相关的文件;原始 JavaScript 源码、脚本和测试文件保持原位置:
http-errors/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│ └── http_errors.cj
├── test/
│ └── main.cj
└── example/
├── cjpm.toml
├── cjpm.lock
└── example.cj
源码集中在 src/http_errors.cj,按基础异常、状态消息、具名异常和工厂分发顺序组织。测试入口位于 test/main.cj,示例工程位于 example/。示例使用 http_errors_cangjie = { path = ".." } 引用本地包,且 src-dir = "." 时将 target-dir 放在 example/ 外部的 ../.cjpm-target/http_errors_example,避免构建输出被再次扫描为源码。
第 2 阶段:创建 cjpm 工程
根目录 cjpm.toml 使用静态库输出类型,并将构建目录放在源码目录之外:
[package]
cjc-version = "1.1.3"
name = "http_errors_cangjie"
version = "2.0.1"
output-type = "static"
src-dir = "src"
target-dir = ".cjpm-target/http_errors_cangjie"
示例工程使用本地路径依赖,并把构建目录放在 example/ 外部:
[package]
cjc-version = "1.1.3"
name = "http_errors_example"
version = "1.0.0"
output-type = "executable"
src-dir = "."
target-dir = "../.cjpm-target/http_errors_example"
[dependencies]
http_errors_cangjie = { path = ".." }
包版本沿用上游 http-errors 2.0.1,包名使用合法的仓颉标识符 http_errors_cangjie。构建成功后,最重要的两个库产物是:
.cjpm-target/http_errors_cangjie/release/http_errors_cangjie/libhttp_errors_cangjie.a
.cjpm-target/http_errors_cangjie/release/http_errors_cangjie/http_errors_cangjie.cjo
其中 .a 用于链接,.cjo 提供编译器需要的接口信息。根包的 target-dir 不在 src/ 扫描范围内,示例包的 target-dir 也不在 src-dir = "." 的目录范围内,因此运行 cjpm run 不会出现把构建输出目录当作源码扫描目录的警告。验证完成后清理 .cjpm-target,不提交构建产物。
第 3 阶段:把 JavaScript 动态错误映射为仓颉类型
3.1 用 HttpError 表示错误属性
JavaScript 错误对象的状态、消息和暴露标志在仓颉中变成显式的异常成员:
let error = HttpError(499, "custom base error",
errorName: "HttpError", expose: true)
println(error.status)
println(error.statusCode)
println(error.message)
println(error.expose)
status 和 statusCode 始终保存同一个状态码。message 复用仓颉 Exception 的消息属性,errorName 保存与 JavaScript 构造器名称对应的可读名称,expose 表示调用方是否可以把消息返回给客户端。
3.2 用具名异常替换动态构造器
JavaScript 版本会根据状态码动态选择 NotFound、InternalServerError 等构造器。仓颉适配把这些构造器实现为公开的 HttpError 子类:
let notFound = NotFoundError()
let timeout = GatewayTimeoutError()
println("${notFound.status}: ${notFound.message}")
println("${timeout.status}: ${timeout.message}")
4xx 子类默认 expose == true,5xx 子类默认 expose == false。每个具名类都有默认原因短语,也接受 message: 命名参数覆盖默认消息。
3.3 用 createError 保留工厂调用方向
调用方可以使用状态码工厂,不需要手动选择异常类:
let error = createError(404, message: "This video does not exist")
println("${error.errorName}: ${error.status} ${error.message}")
标准状态码返回对应的具名异常。未知 4xx 和 5xx 状态会保留数字状态码,并分别使用 Bad Request 和 Internal Server Error 作为回退消息;不属于 4xx/5xx 的状态会归一化为 500。
第 4 阶段:实现源码
4.1 文件 src/http_errors.cj
源码按错误生命周期组织:先由 statusMessage 提供状态原因短语,再由 4xx 和 5xx 具名类固定状态、名称和 expose,最后由 createError 按状态码分发到对应构造器。未知状态进入范围回退,isHttpError 使用仓颉运行时类型判断检查异常层次。
这种组织方式没有引入外部依赖,所有状态码和原因短语都在静态库中完成。调用方不需要初始化全局注册表,也不需要通过字符串查找异常构造器。
4.2 状态码与默认消息
statusMessage 覆盖上游 README 列出的 400 至 511 标准错误状态,包括 Im a Teapot、URI Too Long、Unavailable For Legal Reasons 和 Network Authentication Required。对未知的 4xx 和 5xx,函数分别返回范围默认消息;对 2xx、3xx 等非错误状态返回 HTTP Error。
4.3 错误识别和能力边界
isHttpError 只识别 HttpError 及其子类,不会把普通字符串、数字或其他仓颉异常误判为 HTTP 错误:
let error = createError(404)
println(isHttpError(error))
println(isHttpError("not an HTTP error"))
JavaScript 版本对外部 Error 的 duck typing 和任意属性扩展依赖动态对象模型,仓颉版本不伪造这部分行为,而是通过类型安全的异常层次明确适配边界。
第 5 阶段:建立自动化测试
test/main.cj 是一个直接链接仓颉静态库的可执行测试入口,覆盖以下行为:
| 测试组 | 示例 |
|---|---|
| 默认构造 | createError(404)、createError(500) |
| 自定义消息 | createError(500, message:) |
| 状态属性 | status、statusCode 同步 |
| 暴露策略 | 4xx 为 true,5xx 为 false |
| 未知状态 | 499 的状态保留和 4xx 回退消息 |
| 类型识别 | isHttpError 接受 HTTP 异常并拒绝普通值 |
断言失败会抛出异常并以非零状态退出;全部通过时输出固定标记,便于脚本或 CI 判断:
ALL HTTP-ERRORS CANGJIE TESTS PASS
完整的具名构造器和工厂输出由 example/example.cj 负责。示例会逐一调用全部 41 个具名异常,并打印库实际生成的名称、状态码、消息和 expose 值,而不是只打印预先写死的演示文本。
第 6 阶段:核对示例工程
示例工程位于 example/,通过本地路径依赖引用根包。example/example.cj 真实创建全部具名异常和工厂错误,输出自定义消息、未知状态、非法状态、statusMessage 和 isHttpError 结果。任一结果与预期不一致都会抛出 IllegalArgumentException,因此终端输出既是功能结果,也是可执行验收。
四、完整验证步骤
以下命令均以仓库根目录 http-errors/ 为当前目录。先确认本机已经安装仓颉 SDK:
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
然后检查并构建静态库:
cjpm check
cjpm build
编译并运行测试入口:
cjc test/main.cj \
--import-path .cjpm-target/http_errors_cangjie/release/http_errors_cangjie \
--library-path .cjpm-target/http_errors_cangjie/release/http_errors_cangjie \
--library http_errors_cangjie \
--static-libs --static-std \
-o /tmp/http-errors-cangjie-test
DYLD_LIBRARY_PATH="$CANGJIE_HOME/runtime/lib/darwin_aarch64_cjnative" \
/tmp/http-errors-cangjie-test
测试程序会输出:
ALL HTTP-ERRORS CANGJIE TESTS PASS
再构建并运行独立的终端示例工程:
cd example
cjpm check
cjpm build
cjpm run
示例不会访问网络,也不依赖预先写死的结果。它直接调用仓颉库并输出类似结果:
NotFoundError() => name=NotFoundError, status=404, statusCode=404, message="Not Found", expose=true
createError(500) => name=InternalServerError, status=500, statusCode=500, message="Internal Server Error", expose=false
createError(404, custom message) => name=NotFoundError, status=404, statusCode=404, message="The requested video does not exist", expose=true
createError(499) => name=HttpError, status=499, statusCode=499, message="Bad Request", expose=true
createError(200) => name=HttpError, status=500, statusCode=500, message="Internal Server Error", expose=false
statusMessage(200) => HTTP Error
isHttpError(createError(404)) => true
isHttpError(non-error string) => false
http-errors Cangjie demo passed
Generated results for 41 named constructors and all factory paths
示例使用 target-dir = "../.cjpm-target/http_errors_example",因此运行 cjpm run 时不会出现把 target 目录当作源码扫描目录的警告。验证结束后执行 find .cjpm-target -type f -delete 并删除空目录,确认 .cjpm-target、target 等构建目录未加入 Git。
五、运行效果
下面的效果图来自 example/example.cj 的实际终端运行,包含具名构造器、createError 工厂、状态消息、类型识别和最终完成标记。

这张图可以作为文章或项目说明中的验收证据:终端同时展示了 4xx 和 5xx 的具名错误结果、状态码工厂结果、自定义消息、未知状态回退和 isHttpError 判断。输出由仓颉示例程序调用静态库生成,不是模拟页面或手工拼接的文本。
六、关键决策说明
6.1 为什么使用显式异常层次
仓颉适配的价值在于让调用方在编译期知道错误的数据形状。若所有错误都退化成 HashMap<String, Any>,虽然表面上接近 JavaScript,却会失去异常继承、状态属性和消息类型检查。因此项目使用 HttpError 作为公开基类,使用具名子类表达标准状态码,使用 Int64、String 和 Bool 固定属性类型。
6.2 为什么保留 http-errors 的方法和字段方向
保留 createError、HttpError、NotFoundError、InternalServerError、status、statusCode、message 和 expose 等名称,可以降低迁移成本。熟悉 Node.js http-errors 的开发者只需要适应仓颉的参数标签和异常捕获,不必重新记忆一套完全不同的 HTTP 错误 API。对于无法直接表达的动态属性装饰,适配通过明确文档边界保持行为可解释。
6.3 为什么把测试和示例分开
测试程序负责断言,适合自动化执行;示例工程负责展示,适合在终端中观察每个库调用生成的实际结果和截图。两者都使用同一个 http_errors_cangjie 仓颉静态库,因此效果图不是模拟输出,而是库的真实运行结果。
七、兼容边界和遗留问题
JavaScript http-errors 与仓颉的运行时模型不同,以下限制需要在接入时提前了解:
- 当前版本通过
HttpError异常层次表达 HTTP 错误,不修改任意外部Exception的原型,也不把普通对象动态装饰成 HTTP 错误。 createError支持状态码和消息参数;JavaScript 版本中用于附加任意属性的动态 properties 对象没有直接映射,调用方应在自己的业务结构体中保存额外上下文。- 状态码使用
Int64,标准状态提供具名异常;未知 4xx/5xx 会保留数值并使用范围回退消息,其他状态会归一化为 500。 statusMessage覆盖当前实现中的标准错误状态;它不负责解析自定义 HTTP 状态文本,也不连接网络或发送响应。isHttpError使用仓颉类型判断,只保证识别本包的HttpError及其子类,不实现 JavaScript duck typing 的外部 Error 检查。message和errorName使用字符串,expose使用布尔值;调用方仍需根据业务安全策略决定是否把错误消息发送给客户端。
这些不是构建错误。只要 cjpm build 成功、测试程序输出通过标记、示例输出所有库调用结果并显示 http-errors Cangjie demo passed,就说明当前版本已经完成本地编译和行为验收。
八、接入方式
在另一个仓颉项目中,可以将本仓库作为本地依赖或发布包使用。最直接的本地验证方式是先在本仓库根目录生成 .cjpm-target/http_errors_cangjie,再把相同的 --import-path、--library-path 和 --library 参数传给调用方的 cjc 命令。调用代码只需要导入:
import http_errors_cangjie.*
main(): Int64 {
let error = createError(404, message: "This video does not exist")
println("${error.errorName}: ${error.status} ${error.message}")
return 0
}
如果使用 cjpm 管理依赖,则将仓颉包加入项目依赖配置;示例工程采用 http_errors_cangjie = { path = ".." },发布到远程仓库后也可以替换为带版本的 AtomGit 依赖。业务工程负责捕获 HttpError、读取 status 和 message,并根据 expose 决定响应内容。跨端项目可以复用相同的错误构造和识别 API,具体的 HTTP 响应发送、日志和 UI 展示由宿主平台负责。
九、总结
本次适配完成了 http-errors 到仓颉静态异常模型的迁移:源码使用 HttpError 保存 HTTP 错误属性,使用 41 个具名异常覆盖标准 4xx/5xx 状态,使用 createError 统一分发状态码,使用 statusMessage 提供原因短语,使用 isHttpError 提供类型识别。
验证链路包括 cjpm check、cjpm build、测试入口独立编译运行和本地依赖示例工程运行。最终效果图来自真实的仓颉可执行程序,能够直观看到三方库在具名构造器、状态码工厂、默认消息、自定义消息、错误暴露策略和非法状态回退场景中的输出。对于无法一比一映射 JavaScript 动态对象装饰和任意 properties 的部分,项目通过静态签名和文档边界保持行为可解释,方便后续继续扩展。
适配过程 FAQ
以下问题来自仓颉工程创建、
cjpm依赖、与 HarmonyOS 设备联调的实际流程。
Q1:cjpm 依赖解析失败或拉取超时?
- 检查
cjpm.toml中的依赖坐标与版本号; - 配置可用的仓颉包仓库镜像或代理;
- 删除
target、cjpm.lock后重新执行cjpm update/cjpm build。
Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?
- 设置
CANGJIE_HOME指向仓颉 SDK 根目录; - 确认
cjc --version、cjpm --version与工程要求的版本一致; - 清理构建缓存后重新生成。
Q3:与 HarmonyOS 设备联调时安装失败?
- 用
hdc list targets确认设备在线; - 确认 HAP 已签名,
bundleName与签名 profile 一致; - 设备需加入调试 profile 的
deviceIds。
Q4:动态库或 FFI 符号找不到?
- 检查导出符号名称、ABI 与打包路径;
- 确认
.so已随 HAP 一起打包到libs/<abi>/; - 用
llvm-readelf -d检查依赖库是否齐全。
Q5:测试用例在设备上失败,但本机通过?
- 对比设备与主机的架构、SDK 版本和环境变量;
- 用
hdc hilog抓取运行日志; - 优先复现最小用例,确认是否为平台差异或时序问题。
十、参考资料
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2601_94997069/article/details/169057126



