本文记录 cookie 2.0.1 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在终端中通过真实示例展示 Cookie 请求头解析、Cookie 请求头序列化、Set-Cookie 属性解析、响应头生成、自定义编解码和 Cookie 删除等能力。
项目地址: AtomGit/nutpi_cj/cookie
上游仓库: jshttp/cookie
开发工具: 华为云码道
一、背景
1.1 为什么适配 cookie
cookie 是 HTTP 服务端常用的 Cookie 请求头和 Set-Cookie 响应头解析、序列化库。JavaScript 版本提供 parseCookie、stringifyCookie、parseSetCookie 和 stringifySetCookie 等 API,能够处理 RFC 6265 规定的 Cookie 名称、值和属性。
JavaScript 版本使用动态对象表示 Cookie 集合,使用 undefined 表示缺失的值,并使用 Date 表示 Expires。仓颉适配需要把这些动态边界转换成稳定的静态类型,同时保留 URL 编解码、空值、重复键、属性校验和 Set-Cookie 的完整调用习惯。
本项目使用 HashMap<String, String> 表示请求 Cookie 集合,使用 SetCookie 结构体表示 Set-Cookie 属性,并通过仓颉函数值提供自定义 encode/decode 能力。这样熟悉原库的开发者可以在仓颉项目中继续使用相同的 Cookie 处理流程。
1.2 跨端三方库的适配边界
| 边界 | 仓颉实现 |
|---|---|
| 包管理 | cjpm.toml、cjpm.lock、cjpm build 和静态库输出 |
| Cookie 请求头 | parseCookie、stringifyCookie 和 HashMap<String, String> |
| Set-Cookie 响应头 | parseSetCookie、stringifySetCookie 和 SetCookie |
| URL 编解码 | encodeCookie、decodeCookie,保留 Cookie 安全字符并处理百分号编码 |
| 属性处理 | Max-Age、Expires、Domain、Path、HttpOnly、Secure、Partitioned、Priority、SameSite |
| 错误模型 | 通过 IllegalArgumentException 报告名称、值和属性校验错误 |
| 交付形态 | 仓颉静态库 libcookie_cangjie.a 和接口文件 cookie_cangjie.cjo |
| 验证方式 | test/main.cj 编译验证和 example/example.cj 真实业务流程展示 |
这里没有 HAP 构建、ArkTS 桥接或真机系统能力验收。Cookie 解析和序列化属于纯字符串与字节处理,只依赖仓颉标准库,可以被提供兼容仓颉 SDK 的服务端、网关和跨端业务项目直接复用。
1.3 适配后的能力
当前适配导出 9 个顶层公开名称,覆盖以下能力:
- Cookie 集合:
parseCookie、stringifyCookie、parse和stringify; - Set-Cookie 模型:
SetCookie结构体、parseSetCookie和stringifySetCookie; - 编解码:
encodeCookie和decodeCookie; - RFC 6265 字符范围校验、空值处理、重复键保留第一次出现的值;
- Max-Age、Expires、Domain、Path、HttpOnly、Secure、Partitioned、Priority 和 SameSite 属性;
- 自定义值编码函数和自定义值解码函数;
- Cookie 删除场景中的
Max-Age=-1输出。
二、实现路线图
text
第 1 阶段:盘点 API ── 对照 cookie 的四个核心函数和 Set-Cookie 属性模型
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射 ── 用 struct、HashMap 和显式字段替换动态 Cookie 对象
第 4 阶段:实现解析 ── 完成 OWS、分号、等号、百分号编解码和属性序列化
第 5 阶段:测试与核对 ── 编译测试入口,检查请求头和响应头行为
第 6 阶段:终端演示 ── 运行真实业务流程,输出解析数据和生成的 Header
每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 src/cookie.cj,验证阶段得到可执行的 test/main.cj,最终示例则模拟服务端接收 Cookie、修改 Cookie 并生成 Set-Cookie 响应头。
三、逐步实现过程
第 1 阶段:盘点原库 API 和仓颉工程边界
1.1 识别动态 API
原库的请求 Cookie API 接受字符串并返回动态对象,Set-Cookie API 则返回带有可选字段的对象。仓颉没有 JavaScript 风格的 undefined,因此适配选择以下明确规则:请求 Cookie 的值全部是 String;Set-Cookie 中未出现的字符串属性使用空字符串;未设置 Max-Age 时使用 maxAgeSet=false,从而可以区分缺失值和显式 -1。
自定义 encode 和 decode 在仓颉中映射为 (String) -> String 函数值,并通过命名可选参数传入。解析器会先去掉 RFC 允许的空格和制表符,再按分号和等号定位字段。
1.2 固定仓颉项目结构
以下目录树按照当前 cookie 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 TypeScript 源码、测试、脚本和原有配置保持原位置:
text
cookie/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│ └── cookie.cj
├── test/
│ └── main.cj
└── example/
├── cjpm.toml
├── cjpm.lock
└── example.cj
源码集中在 src/cookie.cj。测试入口位于 test/main.cj,示例工程位于 example/。示例使用 src-dir = ".",但 target-dir 放在 example 外部的 .cjpm-target/cookie_cangjie_example,避免构建产物进入源码扫描范围。
第 2 阶段:创建 cjpm 工程
根目录 cjpm.toml 使用静态库输出类型:
toml
[package]
cjc-version = "1.1.3"
name = "cookie_cangjie"
version = "2.0.1"
output-type = "static"
src-dir = "src"
target-dir = "target"
示例工程使用本地路径依赖,并把构建目录放在 example 外部:
toml
[package]
cjc-version = "1.1.3"
name = "cookie_cangjie_example"
version = "1.0.0"
output-type = "executable"
src-dir = "."
target-dir = "../.cjpm-target/cookie_cangjie_example"
[dependencies]
cookie_cangjie = { path = ".." }
发布到远端后,业务项目可以把本地路径替换为 AtomGit 依赖:
toml
[dependencies]
cookie_cangjie = { git = "https://atomgit.com/nutpi_cj/cookie.git", branch = "master" }
根包构建成功后,静态库和接口文件位于 target/release/cookie_cangjie/;示例构建产物写入 ../.cjpm-target/cookie_cangjie_example。这两个构建目录都不提交到仓库。
第 3 阶段:把 JavaScript 动态对象映射为仓颉类型
3.1 用 HashMap 表达 Cookie 请求头
请求头中的 Cookie 名称和值使用 HashMap<String, String>:
cangjie
let cookies = parseCookie("session=abc%20123; theme=dark")
println(cookies["session"])
let header = stringifyCookie(HashMap<String, String>([
("session", "abc 123"),
("theme", "light")
]))
parseCookie 会处理 OWS、空值、重复键和百分号编码;同名 Cookie 只保留第一次出现的值。stringifyCookie 会按 Cookie 名称和值的字符规则校验并自动编码不安全字符。
3.2 用 SetCookie 表达响应属性
Set-Cookie 的可选属性集中在 SetCookie 结构体中:
cangjie
let cookie = SetCookie(
"session", "abc 123", maxAge: 3600,
domain: "example.com", path: "/account",
httpOnly: true, secure: true, partitioned: true,
priority: "high", sameSite: "lax")
let header = stringifySetCookie(cookie)
maxAgeSet 用于区分缺省的 Max-Age 和删除 Cookie 时显式传入的 -1。Expires 在仓颉结构体中保留为 HTTP 日期字符串,解析和序列化都不会把日期转换成平台特定的对象。
3.3 编解码函数
默认编码保留 RFC 6265 Cookie 安全字符,其余 UTF-8 字节使用百分号编码。业务可以传入自定义函数:
cangjie
let encoded = stringifyCookie(
HashMap<String, String>([("profile", "admin")]),
encode: { value => "app-${value}" })
let decoded = parseCookie(encoded,
decode: { value => if (value.startsWith("app-")) { value[4..] } else { value } })
仓颉适配保留了原库的编解码扩展点,同时保证自定义编码结果仍会经过 Cookie 值合法性校验。
第 4 阶段:实现源码
4.1 文件 src/cookie.cj
源码按 Cookie 生命周期组织:先定义 Cookie 名称、值、路径和域名的字符校验,再实现 UTF-8 百分号编解码;之后解析请求 Cookie 的键值对,最后解析和生成 Set-Cookie 的属性列表。
parseCookie 使用分号和等号的字节位置处理带空格的请求头;parseSetCookie 先读取首个名称和值,再遍历 HttpOnly、Secure、Domain、Path、Max-Age、Expires、Partitioned、Priority 和 SameSite 属性。未知属性会被忽略,非法 Max-Age、Priority、SameSite 和日期不会覆盖已经解析出的有效值。
stringifyCookie 和 stringifySetCookie 在生成 Header 前执行名称、值、域、路径和属性校验,校验失败时抛出 IllegalArgumentException,避免把无效字符直接写入 HTTP 头。
4.2 真实业务流程示例
example/example.cj 模拟一个服务端处理流程:
- 接收
session=abc%20123; theme=dark; language=zh-CN; - 调用
parseCookie,读取 session 和 theme; - 修改 theme,并调用
stringifyCookie生成下一次请求使用的 Cookie Header; - 调用
stringifySetCookie生成带 Max-Age、Expires、Domain、Path、HttpOnly、Secure、Partitioned、Priority 和 SameSite 的响应头; - 再调用
parseSetCookie展示服务端拿到的全部属性; - 生成
Max-Age=-1删除 Cookie 响应,并展示parse、stringify兼容别名。
示例打印的是库调用返回的实际字符串和结构体字段,不是预先写死的测试报告。
4.3 RFC 6265 和平台边界
当前实现覆盖 RFC 6265 Cookie 名称、Cookie 值、Domain 和 Path 的字符规则,以及常用 Set-Cookie 属性。它是纯仓颉字符串处理库,不绑定 HTTP Server、浏览器存储、网络栈或 UI 框架。
Cookie 的 Max-Age 在结构体中使用 Int64;Expires 保留为 HTTP 日期文本。仓颉适配没有引入 JavaScript Date、动态对象或浏览器 document.cookie,调用方负责把库输出的 Header 交给自己的 HTTP 服务端或客户端。
第 5 阶段:建立自动化测试
test/main.cj 是一个直接链接仓颉静态库的可执行测试入口,覆盖以下行为:
| 测试组 | 示例 |
|---|---|
| 请求 Cookie 解析 | 普通键值、空值、重复键、空格和 URL 解码 |
| 请求 Cookie 序列化 | 多值、空 map、默认编码、往返解析和自定义解码 |
| Set-Cookie 解析 | 名称、值、Max-Age、Domain、Path、布尔属性和 SameSite |
| Set-Cookie 序列化 | Header 属性顺序、负 Max-Age 和自定义编码 |
| 编解码 | 安全字符保留、百分号编码和异常转义回退 |
测试入口用于自动化断言;完整业务结果由 example/example.cj 直接打印展示。构建测试入口时,测试程序从 target/release/cookie_cangjie 链接 .a 静态库和 .cjo 接口文件。
第 6 阶段:核对 cookie 适配 API
对 src/cookie.cj 的顶层 public 声明进行核对,当前公开入口为:
text
Cookie Cangjie public API: 9
Missing documented entries: 0
这 9 个入口包括 SetCookie、decodeCookie、encodeCookie、parseCookie、stringifyCookie、parseSetCookie、stringifySetCookie、parse 和 stringify。其中 parse 和 stringify 是方便迁移的短别名,其余 API 对应 Cookie 适配的主要功能面。
四、完整验证步骤
以下命令均以仓库根目录 cookie/ 为当前目录。先确认本机已经安装仓颉 SDK:
bash
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
构建静态库:
bash
cjpm build
编译并运行测试入口:
bash
cjc test/main.cj \
--import-path target/release/cookie_cangjie \
--library-path target/release/cookie_cangjie \
--library cookie_cangjie \
--output-type exe \
-o /tmp/cookie-cangjie-test
DYLD_LIBRARY_PATH="$CANGJIE_HOME/runtime/lib/darwin_aarch64_cjnative" \
/tmp/cookie-cangjie-test
再构建并运行独立的终端示例工程:
bash
cd example
cjpm check
cjpm run
示例使用 target-dir = "../.cjpm-target/cookie_cangjie_example",不会把构建输出目录放进 src-dir = "." 的扫描范围。运行结束后清理构建产物:
bash
cjpm clean
cd ..
test ! -e .cjpm-target/cookie_cangjie_example
test ! -e target
五、运行效果
下面的效果图来自 example/example.cj 的终端运行,展示 parseSetCookie 返回的字段、删除 Cookie 生成的 Header、Priority 和 SameSite 属性变体,以及 demo 完成信息。

图中的 Set-Cookie、maxAge、expires、domain、path、httpOnly、secure、partitioned、priority 和 sameSite 都来自库实际生成或解析的结果。示例通过仓颉库展示完整业务数据,不使用预先写死的测试输出替代运行结果。
六、关键决策说明
6.1 为什么使用 HashMap<String, String>
Cookie 请求头本质上是名称和值组成的字符串集合,HashMap<String, String> 能在仓颉中提供足够清晰的静态类型边界。缺少等号的片段会被忽略,重复名称保留第一次出现的值,避免业务层收到不确定的覆盖结果。
6.2 为什么使用 SetCookie 结构体
Set-Cookie 的属性数量较多,使用结构体可以让调用方直接看到 Max-Age、Domain、Path、HttpOnly、Secure、Partitioned、Priority 和 SameSite。maxAgeSet 解决了"未设置"和"显式 -1 删除"在仓颉静态类型中的区分问题。
6.3 为什么把测试和示例分开
test/main.cj 负责断言和自动化回归;example/example.cj 负责模拟真实服务端流程并打印用户可以直接观察的 Cookie 数据和 HTTP Header。两者都使用同一个 cookie_cangjie 包,但用途不同:测试回答行为是否正确,示例展示功能运行后产生什么结果。
七、兼容边界和遗留问题
JavaScript cookie 与仓颉静态类型模型不同,以下限制需要在接入时提前了解:
- Cookie 值和属性以
String表示,Expires 不转换为仓颉日期对象;调用方如需日期运算,应在业务层完成。 - 请求 Cookie 返回
HashMap<String, String>,不提供 JavaScriptundefined或多值数组;同名 Cookie 按原库行为保留第一次出现的值。 - 自定义 decode 函数只能返回
String,不能用undefined表示跳过某个 Cookie。 - Set-Cookie 的未知属性会被忽略;非法 Max-Age、Priority、SameSite 和 Expires 不会写入结果。
- 该库只处理 Header 字符串,不负责 HTTP 服务端、浏览器 Cookie 存储、跨域策略、TLS 或网络请求本身。
- 不同仓颉 SDK 对异常打印的具体格式可能不同,业务代码应捕获
IllegalArgumentException并根据自己的日志规范处理。
这些限制属于适配边界,不影响当前请求头和响应头处理能力。只要 cjpm build 成功,测试入口能够链接静态库,示例能够输出真实 Cookie 和 Set-Cookie 数据,就说明当前版本已经完成本地编译和运行验收。
八、接入方式
在另一个仓颉项目中,可以通过 AtomGit 依赖使用当前适配包:
toml
[dependencies]
cookie_cangjie = { git = "https://atomgit.com/nutpi_cj/cookie.git", branch = "master" }
调用代码只需要导入仓颉包:
cangjie
import cookie_cangjie.*
import std.collection.*
main(): Int64 {
let requestCookies = parseCookie("session=abc%20123")
println(requestCookies["session"])
let response = stringifySetCookie(SetCookie(
"session", "abc 123", maxAge: 3600, httpOnly: true))
println(response)
return 0
}
跨端业务可以复用同一套 Cookie 解析、序列化和 Set-Cookie 属性处理 API;具体的 HTTP Header 读写、Cookie 存储和生命周期管理由宿主平台负责。
九、总结
本次适配完成了 cookie 到仓颉静态类型模型的迁移:请求 Cookie 使用 HashMap<String, String>,Set-Cookie 使用 SetCookie 结构体,百分号编解码使用仓颉字节数组处理,Cookie 名称和值以及响应属性都经过 RFC 规则校验。
验证链路包括 cjpm build、测试入口编译、真实业务流程示例、请求 Cookie 往返、Set-Cookie 全属性解析、Cookie 删除和自定义编解码。最终效果图来自 example/example.cj 的实际终端运行,能够直观看到库生成的 Cookie Header、Set-Cookie Header 和解析后的属性结果。
对于 JavaScript undefined、Date 对象和多值动态对象等无法一比一映射的部分,项目通过 maxAgeSet、字符串日期和静态 HashMap 规则保持行为可解释,方便后续在仓颉目标平台中继续扩展。
适配过程 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抓取运行日志; - 优先复现最小用例,确认是否为平台差异或时序问题。