- 存储
- 网络
- 通信
【免费下载链接】js-ipfs
IPFS implementation in JavaScript
本篇技术指南围绕 packages/ipfs-message-port-protocol/README.md 展开,深入讲解 js-IPFS 中通过
MessagePort在浏览器多线程(Worker/iframe)之间搭建 IPFS 客户端与服务端通信所需的线协议编解码器(wire protocol codecs):CID、DAGNode、AsyncIterable 与 Callback。读完本文,你将掌握这些类型为何无法被结构化克隆(structured cloning)直接传输、如何通过transfer列表零拷贝搬移底层内存,以及如何在两端对称地进行编码与解码,从而在自己的多线程 IPFS 应用中落地可复用的跨线程数据通道。
背景:为什么 IPFS 跨线程通信需要专门的协议层
浏览器提供了 MessageChannel 与 MessagePort 作为线程间通信的底层通道,postMessage默认使用结构化克隆算法(structured cloning algorithm)复制数据。然而 IPFS 核心数据类型并不总能被结构化克隆正确还原:
- CID(Content Identifier)依赖原型链上的
asCID、/等属性与继承自multiformats/cid的方法,克隆后只是一堆失去类身份的普通对象; - DAG 节点可能嵌套任意深度的对象、数组与 CID 实例;
- AsyncIterable(如
ipfs.cat、ipfs.addAll的返回值)本质上是运行时对象,根本无法被克隆,只能以「远程迭代器」的方式跨越线程逐个拉取数据项; - 回调函数(如
ipfs.add的progress选项)同样无法被克隆。
ipfs-message-port-protocol包(版本0.15.1,见 package.json)正是为解决这一系列问题而设计:它为上述类型提供成对的encode*/decode*函数,所有编码器都接受一个可选的transfer集合——若提供,编码器会把值中所有Transferable字段加入其中,从而让这些底层内存块在跨线程时被“转移”而非“复制”,实现零拷贝。
需要说明:本包属于已被 Helia 取代的 js-IPFS 项目。仓库 README.md 及本包 README 开头均声明该项目已弃用(deprecated)、不再提供安全修复。本文面向仍在维护/迁移旧代码的开发者,介绍其协议实现原理,可作为理解 Helia 同类消息端口方案的参考。
安装与模块入口
npm 安装
$ npm i ipfs-message-port-protocol包以 ESM 模块("type": "module")发布,要求 Node.js>=16.0.0、npm>=7.0.0。
浏览器<script>标签
通过 script 标签加载时,包会将导出暴露为全局命名空间IpfsMessagePortProtocol:
<script src="https://unpkg.com/ipfs-message-port-protocol/dist/index.min.js"></script>子路径导入(subpath exports)
从 package.json 的exports字段可见,本包按功能拆分了多个独立子路径,每个子路径只暴露对应的 codec,方便按需加载:
| 子路径 | 入口文件 | 导出的编解码器 |
|---|---|---|
ipfs-message-port-protocol | src/index.js | 汇总入口 |
ipfs-message-port-protocol/cid | src/cid.js | encodeCID/decodeCID |
ipfs-message-port-protocol/dag | src/dag.js | encodeNode/decodeNode |
ipfs-message-port-protocol/core | src/core.js | encodeIterable/decodeIterable、encodeCallback/decodeCallback |
ipfs-message-port-protocol/block | src/block.js | encodeBlock |
ipfs-message-port-protocol/error | src/error.js | encodeError/decodeError |
核心依赖仅两个:multiformats(CID 实现)与ipfs-core-types(类型定义),见 package.json。
线协议编解码器(Wire protocol codecs)
本模块为结构化克隆算法无法支持的类型提供编码/解码函数:发送端先把这类值编码成可克隆的表示,通过消息通道postMessage过去,接收端再解码还原。所有编码器的共同约定是:可选地接收一个transfer参数,一旦提供,编码器就会把所有Transferable字段(底层ArrayBuffer)加入其中,使这些内存在线程间被转移而非复制。
CID编解码
CID 是 IPFS 的标识核心。本包的 CID codec 依赖multiformats/cid,完整实现见 src/cid.js。
使用示例
import { CID, encodeCID, decodeCID } from 'ipfs-message-port-protocol/cid' const cid = CID.parse('bafybeig6xv5nwphfmvcnektpnojts33jqcuam7bmye2pb54adnrtccjlsu') const { port1, port2 } = new MessageChannel() // 复制底层内存(默认行为) port1.postMessage(encodeCID(cid)) // 转移底层内存(发送后本线程的 cid 将失效) const transfer = [] port1.postMessage(encodeCID(cid, transfer), transfer) // 接收线程侧 port2.onmessage = ({ data }) => { const cid = decodeCID(data) data instanceof CID // => true }源码原理
encodeCID的实现非常轻量——严格说它并未重新编码,而是利用结构化克隆本来就会复制普通属性的事实,直接把 CID 对象本身发出去;唯一额外动作是:若传入了transfer,就把cid.multihash.bytes.buffer(存放 multihash 字节的ArrayBuffer)加入转移列表:
export const encodeCID = (cid, transfer) => { if (transfer) { transfer.add(cid.multihash.bytes.buffer) } return cid }decodeCID则相反,负责把克隆后“失去类身份”的普通对象重新还原为 CID 实例。它做了一系列原型修复:
- 若
cid.asCID不存在,则定义 getter 返回自身; - 若
cid['/']不存在,则定义 getter 返回cid.bytes; - 通过
Object.setPrototypeOf把multihash.digest、multihash.bytes、bytes重新挂回Uint8Array.prototype; - 最后把整个对象挂回
CID.prototype,使其重新拥有 CID 类的方法。
DAGNode 编解码
DAG 节点是ipfs.dag.putAPI 接受的数据结构——任意 JSON 兼容值,内部可能嵌套任意层次的 CID 链接。其编解码实现见 src/dag.js。
使用示例
import { CID } from 'multiformats/cid' import { encodeNode, decodeNode } from 'ipfs-message-port-protocol/dag' const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') const dagNode = { hi: 'hello', link: cid } const { port1, port2 } = new MessageChannel() // 复制底层内存 port1.postMessage(encodeNode(dagNode)) // 转移底层内存(本线程 dagNode.link 将失效) const transfer = [] port1.postMessage(encodeNode(dagNode, transfer), transfer) // 接收线程侧 port2.onmessage = ({ data }) => { const dagNode = decodeNode(data) dagNode.link instanceof CID // true }源码原理:收集式编码
encodeNode并不逐字段重写节点,而是用collectNode递归遍历整个节点(见 src/dag.js#L64-L90):
- 遇到
CID.asCID(value)为真的对象,把它压入cids数组,并调用encodeCID转移其底层 buffer; - 遇到
ArrayBuffer或其视图(typed array),若提供了transfer则将其buffer加入转移列表; - 遇到数组则遍历成员,遇到普通对象则遍历
Object.values,递归继续。
最终返回{ dagNode, cids }这样的两段式结构:dagNode是可直接被结构化克隆的纯 JSON 表示,cids是其中所有 CID 的收集清单。
export const encodeNode = (dagNode, transfer) => { const cids = [] collectNode(dagNode, cids, transfer) return { dagNode, cids } }decodeNode则只需遍历cids清单逐个调用decodeCID恢复原型,无需再次遍历节点本体——这正是收集式编码的意义:把“查找并还原 CID”的成本从 O(节点大小) 的遍历降为 O(CID 数量) 的清单迭代:
export const decodeNode = ({ dagNode, cids }) => { for (const cid of cids) { decodeCID(cid) } return dagNode }AsyncIterable 编解码(远程迭代器)
AsyncIterable 是 IPFS API 最常见的返回形态(如ipfs.cat、ipfs.addAll、ipfs.ls)。它无法被结构化克隆,因此本包把它编码为一个RemoteIterable:一个自带MessagePort的远程迭代器描述,接收端通过反复发送next消息逐项拉取数据。实现见 src/core.js#L53-L156。
与其他编码器不同,AsyncIterable 的transfer参数是必填的——因为异步迭代器被编码成只能转移、不能复制的MessagePort。
使用示例
import { encodeIterable, decodeIterable } from 'ipfs-message-port-protocol/core' const content = ipfs.cat('/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') const { port1, port2 } = new MessageChannel() // 每个 chunk 复制到接收线程 { const transfer = [] port1.postMessage( encodeIterable(content, chunk => chunk, transfer), transfer ) } // 每个 chunk 转移到底层内存(本线程数据失效) { const transfer = [] port1.postMessage( encodeIterable( content, (chunk, transfer) => { transfer.push(chunk.buffer) return chunk }, transfer ), transfer ) } // 接收线程侧 port2.onmessage = async ({ data }) => { for await (const chunk of decodeIterable(data)) { chunk instanceof Uint8Array // true } }源码原理
encodeIterable(iterable, encode, transfer)内部新建一个MessageChannel,把port1留在本地作为服务端、port2(即remote)加入用户的transfer列表返回{ type: 'RemoteIterable', port: remote }。服务端监听next/return两类消息:
next:调用iterator.next()推进迭代器;若结束则回发{ done: true }并关闭端口;否则用itemTransfer(预先分配并循环复用的Set,见 src/core.js#L104-L129)承载当前项的可转移字段,调用用户提供的encode(value, itemTransfer)编码该项后回发;若迭代器抛错,则回发{ type: 'throw', error: encodeError(error) };return:关闭端口并调用iterator.return()触发清理。
decodeIterable(remote, decode)则是一个异步生成器:每次yield前向端口发送{ method: 'next' },用一个receive变量暂存 Promise 的 resolve,port.onmessage到达后兑现(见 src/core.js#L53-L90)。finally块中,若迭代提前结束(未done)则发送{ method: 'return' },并最终port.close()关闭通道。toIterator同时兼容同步迭代器与异步迭代器(见 src/core.js#L163-L175)。
配套的RemoteYield<T>/RemoteDone<T>/RemoteError类型把每次next的响应建模为「产出 / 完成 / 抛错」三种形态(见 src/core.js#L18-L45),错误在传输时经encodeError展平成普通对象、接收时经decodeError按name还原为对应的RangeError/TypeError/Error等原生类型(见 src/error.js)。
Callback 编解码(远程回调)
IPFS 的部分 API 接受回调参数,最典型的是ipfs.add的progress进度回调。本包将其编码为RemoteCallback(同样自带MessagePort),接收端解码得到的函数每次调用都会把参数回传给另一端的真实回调。与 AsyncIterable 相同,transfer参数必填。实现见 src/core.js#L182-L206。
使用示例
import { encodeCallback, decodeCallback } from 'ipfs-message-port-protocol/core' const { port1, port2 } = new MessageChannel() const progress = (value) => console.log(value) const transfer = [] port1.postMessage(encodeCallback(progress, transfer), transfer) // 接收线程侧 port2.onmessage = ({ data }) => { const progress = decodeCallback(data) // 将调用另一端的 progress(20) progress(20) }源码原理
encodeCallback(callback, transfer)新建MessageChannel,让本地端口监听onmessage并把收到的参数数组data通过callback.apply(null, data)派发给真实回调,同时把远端端口加入transfer并返回{ type: 'RemoteCallback', port: remote }。decodeCallback({ port })返回一个包装函数,调用时通过postMessage(port, args, transfer)把参数数组发回服务端(也支持可选的transfer一并转移参数中的可转移对象)。
从源码视角看整体协议设计
ipfs-message-port-protocol不只是编解码函数集,它还定义了跨线程 RPC 的类型骨架,供上层的客户端/服务端包共享:
- src/data.ts 定义了
JSONValue、EncodedError等基础表示; - src/rpc.ts 通过 TypeScript 条件类型推导出
ServiceQuery、Remote、MultiService等泛型结构,把「命名空间 + 方法 + 输入 + 结果」的 RPC 调用模型类型化; - src/files.ts 与 src/root.ts 声明了 MFS 与 add 相关接口的编码形态(如
EncodedAddInput、EncodedIPFSEntry、EncodedStat),其中文件内容即RemoteIterable<ArrayBufferView>形态的异步可迭代数据。
这套协议的实际消费方是同仓库的 packages/ipfs-message-port-client 与 packages/ipfs-message-port-server。例如客户端 packages/ipfs-message-port-client/src/block.js 在调用block.put/block.rm时用encodeCID编码 CID、用decodeCID还原结果;packages/ipfs-message-port-client/src/dag.js 用encodeNode/decodeNode处理 DAG 节点;packages/ipfs-message-port-client/src/core.js 则组合使用encodeIterable/decodeIterable/encodeCallback实现cat、addAll、ls等异步迭代 API 与进度回调的跨线程封装。测试端可通过aegir test(含 node、browser、webworker 多种目标,见 package.json)验证协议在真实 Worker 环境下的行为。
实战要点小结
- 复制还是转移:不传
transfer则所有底层内存被复制(安全但开销大);传入transfer则零拷贝转移(快但发送端数据即刻失效)。跨线程传大数据(如文件块、大 DAG)务必走转移路径。 transfer是必填还是可选:CID / DAGNode / Block 的编码器可选;AsyncIterable 与 Callback 因为要传MessagePort,必填。- 成对使用:
encode*与decode*必须配套,且解码端使用的decode函数要与编码端为每项提供的encode函数语义一致(如均按Uint8Array处理)。 - 端口生命周期:AsyncIterable 传输结束后端口自动关闭(
next到达末尾或提前return),接收端的for await...of正常退出即可,无需手动关闭;回调型端口在不再需要时应由上层负责关闭。 - 错误传递:迭代器中的异常经
encodeError展平后跨线程传输,解码端按错误名还原为对应原生错误类型,调用方可用try/catch正常捕获。
License 与参与方式
本包以 Apache 2.0 与 MIT 双许可发布(见 LICENSE-APACHE 与 LICENSE-MIT),除非明确说明,所有有意提交的贡献均按 Apache-2.0 定义以双重许可纳入。参与贡献请遵循仓库的 CONTRIBUTING.md 与 IPFS 社区行为准则。
- 存储
- 网络
- 通信
【免费下载链接】js-ipfs
IPFS implementation in JavaScript
相关推荐
Typer 开发指南:用 Docker 容器分步测试 Bash/Zsh/Fish/PowerShell 补全功能
Typer 开发指南:用 Docker 容器分步测试 Bash/Zsh/Fish/PowerShell 补全功能 导读 本文基于 Typer 仓库的 docs/
存储网络通信js-IPFS gRPC 协议包 ipfs-grpc-protocol 深度解析:Protobuf 定义、消息结构与生成流程
js IPFS gRPC 协议包 ipfs grpc protocol 深度解析:Protobuf 定义、消息结构与生成流程 导读 ipfs grpc prot
存储网络通信ipfs-message-port-server 实战指南:通过 MessageChannel 将 js-IPFS 节点暴露给多客户端
ipfs message port server 实战指南:通过 MessageChannel 将 js IPFS 节点暴露给多客户端 ipfs message
存储网络通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考