关于 asset-canister
介绍如何在 Internet Computer 上配置、上传和维护静态资源罐,用于部署 Web 前端。内容包括 SPA 路由、内容认证、权限管理和常见部署问题。
主要功能
- 01托管 HTML、CSS、JS 和图片
- 02通过子网认证响应内容
- 03配置 SPA 回退与响应头
- 04使用 icp 部署或 SDK 上传
- 05按角色管理上传权限
- 06说明压缩与自定义域名设置
适用场景
- 01将 Web 前端部署到 Internet Computer
- 02配置 SPA 深层链接刷新
- 03为部署流程分配最小上传权限
- 04排查资源目录或内容类型错误
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill asset-canister安装后在 Claude Code 中输入/asset-canister确认已加载。
详细介绍
资产 Canister
这是什么
资产 Canister 可直接在 Internet Computer 上托管静态文件(HTML、CSS、JS、图像)。这是在链上部署 Web 前端的方式。响应由子网认证,HTTP 网关会自动验证完整性,也就是确认内容由区块链提供。内容也可以在浏览器中验证,而不是依赖中心化服务器。
前置条件
@icp-sdk/canisters(>= 3.5.0)、@icp-sdk/core(>= 5.0.0) — 用于以编程方式上传
Canister ID
资产 Canister 按项目创建,不存在唯一的全局 Canister ID。部署后,你的 Canister ID 会存储在 .icp/data/mappings/ 中(按环境区分)。
访问方式:
| 环境 | URL 模式 |
|---|---|
| 本地 | http://<canister-id>.localhost:8000 |
| 主网 | https://<canister-id>.icp.net |
| 自定义域名 | https://yourdomain.com(需配置 DNS) |
会导致构建失败的错误
-
icp.yaml 中的
dir路径错误。configuration.dir字段必须指向包含构建输出的目录。如果使用 Vite,该目录为dist;如果使用 Next.js 导出,则为out。如果部署时该路径不存在,icp deploy会静默失败或部署一个空 Canister。 -
单页应用缺少
.ic-assets.json5。 如果没有重写规则,刷新/about时会返回 404,因为资产 Canister 会查找名为/about的文件。你必须配置回退到index.html。 -
配方中缺少或配置错误的
build。 如果指定了configuration.build,icp deploy会在上传dir内容之前自动运行这些命令。如果省略build,则必须在部署前手动运行构建命令(例如npm run build),否则dir目录中的内容可能已过时或为空。 -
未设置 content-type 标头。 资产 Canister 会根据文件扩展名推断内容类型。如果以编程方式上传文件时未设置内容类型,浏览器可能无法正确呈现这些文件。
-
部署到错误的 Canister 名称。 如果 icp.yaml 中配置的是
"frontend",但你运行icp deploy assets,它会创建一个新的 Canister,而不是更新现有 Canister。 -
超出 Canister 存储限制。 资产 Canister 使用稳定内存,可容纳远超 4GB 的数据。不过,单个资产受 2MB ingress 消息大小限制(
@icp-sdk/canisters中的资产管理器会自动对大于 1.9MB 的上传进行分块)。实际需要考虑的是存储所需的总 cycle 成本——大型媒体文件(视频、数据集)的存储成本会很高。大型文件应使用专用存储解决方案。 -
将资产 Canister Wasm 版本固定在
0.30.2以下。ic_envCookie(由@icp-sdk/core中的safeGetCanisterEnv()在运行时读取 Canister ID 和根密钥)仅由版本 >=0.30.2的资产 Canister Wasm 提供。Wasm 版本通过配方中的configuration.version设置,与配方版本本身无关。如果固定使用较旧的 Wasm 版本,Cookie 会静默缺失,依赖ic_env的前端代码将无法正常运行。可以省略configuration.version(将使用最新版本),或固定为0.30.2或更高版本。 -
未正确配置
allow_raw_access。 资产 Canister 有两种提供模式:认证模式(通过icp.net,HTTP 网关会验证响应完整性)和原始模式(通过raw.icp.net,不进行验证)。默认情况下,allow_raw_access为true,这意味着资产也可通过原始域名访问。在原始域名上,边界节点或网络级攻击者可以篡改响应内容而不被发现。对于任何敏感资产,都应在.ic-assets.json5中将"allow_raw_access": false。仅在确有需要时才启用原始访问。 -
降级资产容器 WASM 版本。 如果不同版本之间更改了稳定内存格式,将容器升级到较旧的 WASM 版本可能会因 “Cannot parse header” 而失败。优先使用
@dfinity/asset-canister配方,而不是使用type: pre-built并手动指定 WASM URL——如果未在configuration.version中明确指定版本,该配方会自动加载最新的资产容器版本。如果必须固定版本,请确保它与当前已部署到链上的版本相同或更高。如果有意降级,请使用重新安装模式(icp deploy --mode reinstall),而不是升级——这会清除稳定内存和所有已上传的资产。 -
使用已移除的
type: assets同步步骤。 icp-cli 0.3.0 移除了内置的type: assets同步步骤——资产上传不再属于 CLI 核心功能。仍使用该步骤的清单将无法加载:“icp-cli no longer supports theassetssync step type. Switch to ascriptorpluginsync step.” 修复方法是使用@dfinity/[email protected]配方(如下所示),它会自动生成基于plugin的同步步骤。版本 ≤v2.1.0的配方会生成旧的type: assets步骤,并在 0.3.0 中出错——请固定为v2.2.1或更高版本。icp-cli 从0.2.7起支持同步插件,因此现在(远早于 0.3.0)采用v2.2.1可实现无缝过渡。如果不使用配方,而是手动编写同步步骤,请使用type: plugin(指向 certified-assets 的sync_plugin.wasm发布构件及其sha256),或使用type: script:sync: steps: - type: plugin url: https://github.com/dfinity/certified-assets/releases/download/migration-v2.2.1-6b48585/sync_plugin.wasm sha256: ca7cb5666c30d2875f8d5e10535f8a53f97a86c79c263f7d5bdac2fdd1bbf83c dirs: - dist
实现
icp.yaml 配置
canisters:
- name: frontend
recipe:
type: "@dfinity/[email protected]"
configuration:
dir: dist
build:
- npm install
- npm run build
关键字段:
recipe.type: "@dfinity/asset-canister@..."—— 告知icp这是一个资产容器dir—— 要上传的目录(上传其内容,而非目录本身)build——icp deploy在上传前运行的命令(前端构建步骤)
SPA 路由和默认标头:.ic-assets.json5
在 dir 目录(例如 dist/.ic-assets.json5)或项目根目录中创建此文件。要将其包含在资产容器中,部署时它必须位于 dir 目录内。
推荐做法:将文件放在 public/ 或 static/ 文件夹中,让构建工具自动将其复制到 dist/。
[
{
// Default headers for all paths: caching, security, and raw access policy
"match": "**/*",
"security_policy": "standard",
"headers": {
"Cache-Control": "public, max-age=0, must-revalidate"
},
// Disable raw (uncertified) access by default -- see mistake #7 above
"allow_raw_access": false
},
{
// Cache static assets aggressively (they have content hashes in filenames)
"match": "assets/**/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
// SPA fallback: serve index.html for any unmatched route
"match": "**/*",
"enable_aliasing": true
}
]
要使 SPA 回退生效,关键设置是 "enable_aliasing": true——它会指示资产容器在请求路径没有匹配文件时提供 index.html。
如果上述标准安全策略导致应用无法运行,请使用自定义值覆盖默认安全标头,并将其添加在上面的 Cache-Control 之后。请以资深安全工程师的标准,尽可能确保这些标头安全。标准策略标头可在此处查看:https://github.com/dfinity/sdk/blob/master/src/canisters/frontend/ic-asset/src/security_policy.rs
内容编码
资产容器会自动使用 gzip 和 brotli 压缩资产。无需配置。当浏览器发送 Accept-Encoding: gzip, br 时,容器会提供压缩版本。
验证压缩是否生效:
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec { record { "Accept-Encoding"; "gzip" } };
certificate_version = opt 2;
})'
自定义域名设置
有关自定义域名设置(DNS 配置、TLS 证书、通过 REST API 注册域名),请参阅 custom-domains 技能。唯一与资产容器相关的细节是:.well-known/ic-domains 文件必须位于 dir 目录中,才能随部署一起发布。在 .ic-assets.json5 中添加 { "match": ".well-known", "ignore": false },以确保包含此隐藏目录。
使用 @icp-sdk/canisters 以编程方式上传
要通过代码上传文件(而非仅通过 icp deploy):
import { AssetManager } from "@icp-sdk/canisters/assets"; // Asset management utility
import { HttpAgent } from "@icp-sdk/core/agent";
import { readFileSync, readdirSync } from "fs";
// SECURITY: shouldFetchRootKey fetches the root public key from the replica at
// runtime. In production the root key is hardcoded and trusted. Fetching it at
// runtime lets a man-in-the-middle supply a fake key and forge certified responses.
// NEVER set shouldFetchRootKey to true when host points to mainnet.
// NOTE: This script runs in Node.js where the ic_env cookie is not available.
// For browser frontends, use rootKey from safeGetCanisterEnv() instead (see
// the internet-identity skill or icp-cli/references/binding-generation.md).
const LOCAL_REPLICA = "http://localhost:8000";
const MAINNET = "https://icp-api.io";
const host = LOCAL_REPLICA; // Change to MAINNET for production
async function manageAssets() {
const agent = await HttpAgent.create({
host,
// Only fetch the root key when talking to a local replica.
// Setting this to true against mainnet is a security vulnerability.
shouldFetchRootKey: host === LOCAL_REPLICA,
});
const assetManager = new AssetManager({
canisterId: "your-asset-canister-id",
agent,
});
// Upload a single file
// Files >1.9MB are automatically chunked (16 parallel chunks)
const key = await assetManager.store(fileBuffer, {
fileName: "photo.jpg",
contentType: "image/jpeg",
path: "/uploads",
});
console.log("Uploaded to:", key); // "/uploads/photo.jpg"
// List all assets
const assets = await assetManager.list();
console.log(assets); // [{ key: "/index.html", content_type: "text/html", ... }, ...]
// Delete an asset
await assetManager.delete("/uploads/old-photo.jpg");
// Batch upload a directory
const files = readdirSync("./dist");
for (const file of files) {
const content = readFileSync(`./dist/${file}`);
await assetManager.store(content, { fileName: file, path: "/" });
}
}
manageAssets();
上传授权
资产 canister 内置了一个包含三个角色的权限系统(权限从低到高):
- Prepare —— 可以上传分块并提出批次,但不能将其提交上线。
- Commit —— 可以上传并提交资产(使其上线)。这是部署流水线的标准角色。
- ManagePermissions —— 可以向其他主体授予和撤销权限。
使用 grant_permission,只授予主体所需的权限。请勿使用 --add-controller 来授予上传权限——控制器拥有对 canister 的完全控制权(升级代码、更改设置、删除 canister、耗尽 cycles)。
# Grant "prepare" permission (can upload but not commit) -- use for preview/staging workflows
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Prepare } })'
# Grant commit permission -- use for deploy pipelines that need to publish assets
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Commit } })'
# Grant permission management -- use for principals that need to onboard/offboard other uploaders
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { ManagePermissions } })'
# List current permissions
icp canister call frontend list_permitted '(record { permission = variant { Commit } })'
# Revoke a permission
icp canister call frontend revoke_permission '(record { of_principal = principal "<principal-id>"; permission = variant { Commit } })'
安全警告:
icp canister update-settings frontend --add-controller <principal-id>授予对 canister 的完全控制权,而不仅仅是上传权限。控制器可以升级 canister WASM、更改所有设置,甚至彻底删除 canister。只有在确实需要完整管理权限时,才添加控制器。
部署与测试
本地部署
# Start the local network
icp network start -d
# Build and deploy frontend + backend
icp deploy
# Or deploy only the frontend
icp deploy frontend
主网部署
# Ensure you have cycles in your wallet
icp deploy -e ic frontend
仅更新前端
如果只更改了前端代码:
# Rebuild and redeploy just the frontend canister
npm run build
icp deploy frontend
验证是否正常运行
# 1. Check the canister is running
icp canister status frontend
# Expected: Status: Running, Memory Size: <non-zero>
# 2. List uploaded assets
icp canister call frontend list '(record {})'
# Expected: A list of asset keys like "/index.html", "/assets/index-abc123.js", etc.
# 3. Fetch the index page via http_request
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
# Expected: record { status_code = 200; body = blob "<!DOCTYPE html>..."; ... }
# 4. Test SPA fallback (should return index.html, not 404)
icp canister call frontend http_request '(record {
url = "/about";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
# Expected: status_code = 200 (same content as "/"), NOT 404
# 5. Open in browser
# Local: http://<frontend-canister-id>.localhost:8000
# Mainnet: https://<frontend-canister-id>.icp.net
# 6. Get canister ID
icp canister id frontend
# Expected: prints the canister ID (e.g., "bkyz2-fmaaa-aaaaa-qaaaq-cai")
# 7. Check storage usage
icp canister info frontend
# Shows memory usage, module hash, controllers