Cookie 跨端三方库仓颉适配全流程

本文记录 cookie 2.0.1 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在终端中通过真实示例展示 Cookie 请求头解析、Cookie 请求头序列化、Set-Cookie 属性解析、响应头生成、自定义编解码和 Cookie 删除等能力。

项目地址: AtomGit/nutpi_cj/cookie

上游仓库: jshttp/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 动态对象映射为仓颉类型

请求头中的 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 模拟一个服务端处理流程:

  1. 接收 session=abc%20123; theme=dark; language=zh-CN;
  2. 调用 parseCookie,读取 session 和 theme;
  3. 修改 theme,并调用 stringifyCookie 生成下一次请求使用的 Cookie Header;
  4. 调用 stringifySetCookie 生成带 Max-Age、Expires、Domain、Path、HttpOnly、Secure、Partitioned、Priority 和 SameSite 的响应头;
  5. 再调用 parseSetCookie 展示服务端拿到的全部属性;
  6. 生成 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 接口文件。

对 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 与仓颉静态类型模型不同,以下限制需要在接入时提前了解:

  1. Cookie 值和属性以 String 表示,Expires 不转换为仓颉日期对象;调用方如需日期运算,应在业务层完成。
  2. 请求 Cookie 返回 HashMap<String, String>,不提供 JavaScript undefined 或多值数组;同名 Cookie 按原库行为保留第一次出现的值。
  3. 自定义 decode 函数只能返回 String,不能用 undefined 表示跳过某个 Cookie。
  4. Set-Cookie 的未知属性会被忽略;非法 Max-Age、Priority、SameSite 和 Expires 不会写入结果。
  5. 该库只处理 Header 字符串,不负责 HTTP 服务端、浏览器 Cookie 存储、跨域策略、TLS 或网络请求本身。
  6. 不同仓颉 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 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
万粉变现经纪人1 小时前
如何解决 pip install 安装报错 检测到 yanked 版本需更换可用版本 问题
人工智能·python·scrapy·numpy·pandas·pyqt·pip
香蕉大香蕉(06敬畏市场)1 小时前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习
JustNow_Man1 小时前
【Harness】本地文本向量化搜索解决方案--qmd
git·python·算法
小林ixn1 小时前
用 FastAPI + SQLAlchemy 从零手搓一个博客系统:一套能落地、能讲清楚的 RESTful 后端
后端·python·fastapi
小年糕是糕手1 小时前
【AI】用 Python 做一个 AI 截图检索助手:把收藏夹里的截图找回来
linux·人工智能·python·chatgpt·agent·codex·deepseek
2601_967980111 小时前
wxPython 入门教程.pdf
python·wxpython·gui·教程·移植性
计算机毕业编程指导师2 小时前
2027计算机毕设选题指南:网站AI化、大数据预测+算法(XGBoost/LightGBM)、深度学习三方向57题+选题方法论
大数据·python·计算机·毕业设计·课程设计·毕设选题·1024程序员节
李航19832 小时前
DeepSeek AI建模(二)让AI建第一个模型
人工智能·python·计算机视觉·ai·ai编程
小嘉丶学长2 小时前
Day06 · 数据类型与类型检测
开发语言·python·学习·1024程序员节