第 15 章 前端与后端集成模式
Walrus 应用通常不是只选 HTTP API 或只选 SDK,而是按读写责任组合:
- 公开下载:aggregator HTTP API,加 CDN 或多 aggregator fallback。
- 后端托管上传:publisher HTTP API,服务端统一付费和治理。
- 用户自付上传:TypeScript SDK,钱包签名,必要时接 upload relay。
- 批量小文件:SDK
writeFiles或 HTTP quilt。 - 浏览器大文件:SDK flow + upload relay + 持久化进度。
本章给出几种可以直接落地的集成方式。
模式一:后端代理 publisher
适合内容平台、管理员上传、CI 发布、AI 任务产物归档。用户不直接接触 Sui/WAL,费用由服务端账户或 publisher 承担。
sequenceDiagram
participant UI as Web UI
participant API as App Backend
participant Pub as Walrus Publisher
participant Agg as Walrus Aggregator
UI->>API: POST /api/uploads(file)
API->>API: 鉴权、限额、扫描、记录任务
API->>Pub: PUT /v1/blobs?epochs=...
Pub-->>API: blobId / objectId
API-->>UI: blobId / downloadUrl
UI->>Agg: GET /v1/blobs/{blobId}
后端接口示例:
export async function uploadToWalrus(file: Uint8Array, contentType: string) {
const url = new URL('/v1/blobs', process.env.PUBLISHER);
url.searchParams.set('epochs', '5');
url.searchParams.set('permanent', 'true');
const response = await fetch(url, {
method: 'PUT',
headers: {
'content-type': contentType,
},
body: file,
});
if (!response.ok) {
throw new Error(`publisher failed: ${response.status} ${await response.text()}`);
}
const result = await response.json();
const blobId = result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId;
const objectId = result.newlyCreated?.blobObject?.id;
if (!blobId) {
throw new Error(`publisher response missing blobId: ${JSON.stringify(result)}`);
}
return { blobId, objectId, raw: result };
}
这个模式最容易运营,但 publisher 是写入信任边界。你需要限制文件大小、内容类型、用户额度和保存周期,避免开放接口被刷成存储账单。
模式二:用户钱包直写
适合 NFT 元数据、用户拥有的数据资产、去中心化网盘、付费数据集。用户钱包签名,用户地址拥有 Blob 对象。
sequenceDiagram
participant UI as Browser
participant Wallet as Sui Wallet
participant Relay as Upload Relay
participant Sui as Sui
UI->>UI: encode file
UI->>Wallet: sign register tx
Wallet->>Sui: execute register
UI->>Relay: upload blob bytes/slivers
UI->>Wallet: sign certify tx
Wallet->>Sui: execute certify
关键点:
- 不要把私钥放进浏览器代码;使用钱包适配器签名。
- 注册和认证分成两个用户动作,避免钱包弹窗被浏览器拦截。
- 大文件使用 upload relay,减少浏览器直接访问 storage nodes 的请求数量。
- 保存
lastStep到 IndexedDB 或后端,用户刷新页面后可以恢复。
Vite WASM 配置
Walrus SDK 需要 WASM 编解码。Vite 中导入 WASM URL 后传给 walrus():
import { walrus } from '@mysten/walrus';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url';
export const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
walrus({
wasmUrl: walrusWasmUrl,
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
max: 1_000,
},
},
}),
);
如果构建系统不能解析 ?url,可以自托管 WASM 文件,或固定 CDN 版本:
walrus({
wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@0.2.0/web/walrus_wasm_bg.wasm',
});
不要在生产代码里使用 @latest CDN URL。书中固定版本是为了可复现。
Next.js 配置
Next.js 服务端 API routes 或 server actions 使用 Walrus SDK 时,配置外部包:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['@mysten/walrus', '@mysten/walrus-wasm'],
};
export default nextConfig;
如果你只在浏览器组件使用 SDK,仍然要确认 WASM 文件能被打包或从固定 URL 加载。服务端上传更推荐 HTTP publisher 或 Node SDK;浏览器上传更推荐钱包 flow。
模式三:服务端签名 SDK
适合你不想运行 publisher,但希望服务端自己直接控制 Walrus 写入。服务端保存 WALRUS_PRIVATE_KEY,用 SDK 写 blob:
const result = await client.walrus.writeBlob({
blob: fileBytes,
epochs: 7,
deletable: false,
signer,
onStep: (step) => uploadStore.save(uploadId, step),
resume: await uploadStore.load(uploadId),
});
相比 publisher,这个模式更灵活:
- 可以接 upload relay,也可以直连 storage nodes。
- 可以把 flow step 写进自己的任务表。
- 可以统一处理 retry、审计和业务状态。
代价是服务端需要管理 Sui/WAL 余额、私钥安全和并发上传。
模式四:读路径多 aggregator fallback
Blob 是内容寻址的,读路径天然适合 fallback:
const aggregators = [
'https://aggregator.walrus-testnet.walrus.space',
'https://walrus.example.com',
];
export async function readWithFallback(blobId: string) {
let lastError: unknown;
for (const baseUrl of aggregators) {
try {
const response = await fetch(`${baseUrl}/v1/blobs/${blobId}`);
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
return new Uint8Array(await response.arrayBuffer());
} catch (error) {
lastError = error;
}
}
throw lastError;
}
公共资产可以把 aggregator 放在 CDN 后面。注意缓存键要包含完整 URL;如果使用 strict_consistency_check 或 skip_consistency_check,查询参数也是缓存键的一部分。
前端展示下载链接
最简单的下载 URL:
export function walrusDownloadUrl(blobId: string) {
return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/${encodeURIComponent(blobId)}`;
}
对象 ID 下载:
export function walrusObjectDownloadUrl(objectId: string) {
return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/by-object-id/${objectId}`;
}
Quilt patch 下载:
export function walrusPatchDownloadUrl(patchId: string) {
return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/by-quilt-patch-id/${patchId}`;
}
浏览器会尝试推断内容类型,但 aggregator 会阻止危险可执行类型的嗅探。需要稳定文件名、下载行为和 MIME 时,用对象属性或自己的后端代理设置响应头。
安全边界
Walrus blob 是公开可发现的。不要直接上传密钥、未加密个人数据、访问令牌、内部日志或商业敏感数据。需要保密时先在应用层加密,再上传密文。
后端代理必须做:
- 鉴权:谁能上传、能保存多久。
- 限额:单文件大小、每日总量、并发数。
- 内容治理:MIME 白名单、病毒扫描、业务审核。
- 成本保护:按用户、项目或 API key 计量。
- 结果校验:保存 blob ID、对象 ID、epoch、交易 digest 和响应原文。
前端钱包直写必须做:
- 明确展示费用、epochs、deletable/permanent。
- 拆分签名步骤。
- 保存恢复状态。
- 给出 aggregator 下载验证入口。
推荐默认架构
大多数产品可以从这个组合开始:
- 管理员和后端任务上传:HTTP publisher。
- 普通用户下载:HTTP aggregator + CDN。
- 用户自拥有资产上传:SDK
writeFilesFlow+ upload relay。 - 小文件批量:quilt。
- 大文件任务:SDK flow + 后端任务表 + 重试。
这个架构把复杂度放在需要控制的地方,把普通读写交给 HTTP 接口,后续再按业务要求逐步替换成更底层的 SDK flow。