跳到主要内容

Cloudflare Workers:从本地 API 到存储与上线准备

Cloudflare Workers 可以接收 HTTP 请求、运行代码并返回结果,适合小型 API、请求转发、权限检查,以及静态网站旁的动态功能。你负责请求处理逻辑,Cloudflare 负责运行它;无需先配置一台 Linux 服务器。

本文先做一个只在本机运行的 TypeScript API:检查服务状态,接收名字并返回问候语。随后再加入可选的本地数据库查询,解释存储、域名、安全和费用。基础示例不需要登录、部署、云资源或密钥;创建项目时会下载开发依赖。熟悉文件编辑、终端和基本 JavaScript 即可开始。

1. 先理解代码在哪里运行

Workers 使用 V8 isolate:同一运行时内相互隔离的 JavaScript 执行环境,而不是给每个应用启动一台虚拟机。workerd 是 Workers 的运行时,本地 Wrangler 开发也使用它。一次 HTTP 请求到来后,平台调用导出的 fetch 处理函数;函数返回响应。参见运行模型本地开发说明

处理函数的几个参数各有用途:

  • request 是 Web Request,包含 URL、方法、请求头和请求体。
  • 返回的 Web Response 包含状态码、响应头和响应体。
  • env 提供配置值和绑定。绑定是平台交给代码的资源接口,例如 env.DB,不只是数据库地址字符串。
  • 可选的 ctx 提供请求生命周期工具,例如安排响应后的短时工作。它不能把任务变成永久后台进程。
  • 函数名 fetch 表示处理入站请求;在函数内调用全局 fetch(url) 则会发出另一个 HTTP 请求。

这个模型不要求你调用 listen() 管理端口。模块顶层变量可能被多个请求复用,也可能随 isolate 回收而消失。不要用它保存登录用户、订单或需要可靠持久化的计数;即使可用某些文件系统 API,也不要把它当作服务器上的持久磁盘。

兼容日期与 Node.js

compatibility_date 选择运行时的行为兼容基线,不是 Node 版本,也不是部署日期。新项目按官方建议使用当前日期;以后调整日期时,先读变更并重跑测试。它不能固定 npm 依赖,依赖仍需锁文件管理。兼容日期说明

截至 2026-09-12,Node.js 兼容文档说明:兼容日期从 2026-08-04 起默认启用 nodejs_compatnodejs_compat_v2 对应的行为,新配置无需重复添加这两个标志;日期在 2024-09-23 至 2026-08-03 之间则需用 nodejs_compat 主动启用。这仍只是文档列出的 Node API 和补充实现,部分模块只有可导入的桩实现,调用时会报错。能安装 npm 包不等于它能完整运行;依赖原生扩展、操作系统服务或常驻进程的程序应单独评估。下面的 API 只使用 Web API,不依赖 Node 兼容能力。

2. Workers、静态网站、容器还是 VPS?

下表是按应用形态给出的选择建议,不是成本或延迟排名。Pages 官方页面目前建议新项目使用 Workers;这不等于现有 Pages 网站必须迁移。

需求可先考虑需要承担或核实的部分
小型 HTTP API、转发、请求入口鉴权Workers运行时兼容性、CPU 和内存预算、下游存储
HTML、CSS、图片和少量 API 放在一起Workers Static Assets静态资源路由与 Worker 调用规则,不能假定每个文件请求都经过鉴权代码
已有 Pages 网站和稳定构建流程继续用 Pages,按实际需求评估迁移不必仅因新项目选 Workers 就迁移现有站点
已有容器镜像或以容器为单位的 Web 服务Cloud Run容器启动、服务配置、依赖和扩缩容;其执行单位不是 V8 isolate
需要完整系统控制、自定义守护进程和服务器软件VPS系统更新、网络、进程管理、备份与故障恢复

比如,只需验证表单并返回 JSON,可以从 Worker 开始;已有带原生依赖的容器服务,先评估 Cloud Run 更直接。请求执行位置和数据库位置都会影响延迟,不能据此断言 Workers 总比 VPS 更快或更便宜。

3. 创建纯本地项目

准备工具

安装处于 Node.js Current、Active LTS 或 Maintenance LTS 阶段的版本和 npm,并准备 curl。以 Wrangler 安装政策为准,不要沿用旧入门页上的过时 Node 最低版本。Node 用来运行开发工具,部署后的 Worker 并不会因此变成 Node 服务器。

在新的工作目录中执行官方 C3 创建流程

npm create cloudflare@latest -- workers-local-api
cd workers-local-api

C3 是项目生成器;Wrangler 是开发、配置和发布 Workers 的命令行工具。选择最小 Worker 示例和 TypeScript,拒绝部署。提示文字可能变化;若流程要求登录或授权,先停止,不要把授权当成本地练习的一步。保留 C3 生成的兼容依赖、锁文件和 TypeScript 配置,无需再额外升级一次 Wrangler。

配置文件

wrangler.jsonc 设置为下列内容。JSONC 允许注释,也是官方推荐的配置形式;示例里的 APP_ENV 只是普通配置值。Wrangler 配置

{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "workers-local-api",
"main": "src/index.ts",
"compatibility_date": "2026-09-12",
"vars": {
"APP_ENV": "local"
}
}

日期是本教程的行为基线,不是 Wrangler 发布号。如果本地工具警告不支持该日期,不要忽略警告或声称已验证该基线;检查安装版本及其更新说明,再有意识地调整工具或日期并重测。

完整的请求处理代码

src/index.ts 替换为下列代码。EnvExportedHandler 由下一步生成的 Workers 类型提供,不要另安装一套不匹配的运行时类型。

function json(value: unknown, status = 200, extra: HeadersInit = {}) {
const headers = new Headers(extra);
headers.set("content-type", "application/json; charset=utf-8");
headers.set("cache-control", "no-store");
return new Response(JSON.stringify(value), { status, headers });
}

function error(status: number, code: string, extra: HeadersInit = {}) {
return json({ error: code }, status, extra);
}

async function readSmallBody(request: Request): Promise<string | null> {
if (!request.body) return "";
const reader = request.body.getReader();
const chunks: Uint8Array[] = [];
let size = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > 4096) {
await reader.cancel();
return null;
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const path = new URL(request.url).pathname;
try {
if (path === "/health") {
if (request.method !== "GET") {
return error(405, "method_not_allowed", { Allow: "GET" });
}
return json({ ok: true, environment: env.APP_ENV });
}
if (path !== "/api/greet") return error(404, "not_found");
if (request.method !== "POST") {
return error(405, "method_not_allowed", { Allow: "POST" });
}
const type = request.headers.get("content-type")
?.split(";", 1)[0].trim().toLowerCase();
if (type !== "application/json") {
return error(415, "unsupported_media_type");
}
const text = await readSmallBody(request);
if (text === null) return error(413, "payload_too_large");
let payload: unknown;
try {
payload = JSON.parse(text);
} catch {
return error(400, "invalid_json");
}
if (typeof payload !== "object" || payload === null ||
Array.isArray(payload) || !("name" in payload) ||
typeof payload.name !== "string") {
return error(400, "invalid_name");
}
const name = payload.name.trim();
if (name.length < 1 || name.length > 80) {
return error(400, "invalid_name");
}
return json({ message: `Hello, ${name}!` });
} catch {
console.error("request_failed", { method: request.method });
return error(500, "internal_error");
}
},
} satisfies ExportedHandler<Env>;

接口只接受 JSON 对象,name 去除两端空白后须有 1–80 个 JavaScript 字符串码元,额外字段忽略。码元不等于可见字符数,例如部分表情会占两个码元。请求体按实际读取的字节限制为 4096 字节,不信任客户端提供的 Content-Length;这能限制缓存的输入大小,但不能独自防住慢速客户端或滥用请求。

已知路径使用错误方法时返回 405 和 Allow;未知路径返回 404。错误响应不泄露异常细节,响应统一禁止缓存。示例严格区分方法,没有隐式支持 HEAD 或 OPTIONS,也不添加宽泛的 CORS 响应头。它没有敏感操作,不能直接改成匿名写数据库接口后就用于生产。

生成类型并启动

Wrangler 根据配置、绑定、兼容日期和标志生成类型。先运行:

npx wrangler types
npx tsc --noEmit
npx wrangler dev --local

检查 C3 的 tsconfig.jsoncompilerOptions.types 应包含 ./worker-configuration.d.ts,保留其他仍有用途的条目。若 EnvExportedHandler 找不到,先核对生成文件和该配置;不要用 any 掩盖问题。配置有变化后重新生成。TypeScript 指南

默认本地地址是 http://localhost:8787,以终端实际显示为准。若端口被占用,可用 npx wrangler dev --local --port 8788,并相应修改下文 URL。

用 curl 检查成功和失败路径

保持开发进程运行,在项目目录的第二个终端执行:

npx wrangler types --check
npx tsc --noEmit
curl -i http://localhost:8787/health
curl -i -X POST http://localhost:8787/api/greet \
-H 'Content-Type: application/json' --data '{"name":"Ada"}'
curl -i -X POST http://localhost:8787/api/greet \
-H 'Content-Type: application/json' --data '{"name":" "}'
curl -i -X POST http://localhost:8787/api/greet \
-H 'Content-Type: application/json' --data '{'
curl -i http://localhost:8787/api/greet
curl -i -X POST http://localhost:8787/api/greet --data 'name=Ada'
curl -i http://localhost:8787/missing
node -e 'process.stdout.write(JSON.stringify({name:"a".repeat(4097)}))' | \
curl -i -X POST http://localhost:8787/api/greet \
-H 'Content-Type: application/json' --data-binary @-

下表给出代码应产生的结果,便于对照本机运行;类型检查本身不能证明 HTTP 行为正确。

请求顺序状态响应体及必要响应头
健康检查200{"ok":true,"environment":"local"}
有效名字200{"message":"Hello, Ada!"}
空白名字400{"error":"invalid_name"}
损坏的 JSON400{"error":"invalid_json"}
GET 问候接口405{"error":"method_not_allowed"}Allow: POST
表单媒体类型415{"error":"unsupported_media_type"}
未知路径404{"error":"not_found"}
超过 4096 字节413{"error":"payload_too_large"}

连接被拒绝时先确认 Wrangler 是否仍运行以及端口是否一致;415 通常是漏了 JSON 请求头;400 则检查语法和字段。不要把所有错误都归因于平台权限。

4. 代码在本地,不代表资源也在本地

本地开发文档区分代码执行位置与资源位置:普通本地开发运行 workerd,绑定可以使用本地模拟资源;配置 remote: true 可以让本地代码访问真实资源,wrangler dev --remote 则会上传代码并远程执行。

本教程使用 --local 强制本地绑定,不配置远程资源。它不是网络沙箱:代码里的外部 fetch() 仍然可能调用真实 API、发送数据或产生费用。Workers AI 等始终远程的能力不属于这个练习。

普通配置放在 vars,密钥不要放进去。需要本地密钥时,在配置旁使用 .dev.vars .env,并确认 Git 忽略这些文件;它们是本地明文文件。.dev.vars.<env> 替代基础 .dev.vars,而 .env 系列按优先级合并,不能混为同一种加载规则。密钥说明

特别留意:npx wrangler secret put KEY 会创建版本并立即部署;npx wrangler versions secret put KEY 创建版本但不部署。两者都会修改云端,均不是本地配置步骤。本例不需要任何密钥。

5. 按数据需求选择绑定

绑定名须是有效 JavaScript 标识符,例如配置中的 DB 对应代码里的 env.DB。绑定提供访问资源的能力,但不会替你判断当前用户有没有权读取某条数据。绑定配置

资源适用情况容易误用的地方
KV读多写少、允许旧值的配置或缓存最终一致,跨位置传播可能需 60 秒或更久;未命中也会缓存,不能做锁或可靠原子计数器
D1基于 SQLite 的关系数据、SQL 查询与约束按数据库能力设计表与查询,单独核实容量和并发限制
R2图片、附件、备份等按键存取的对象对象存储不是关系数据库,也不是共享文件系统
Durable Objects聊天房间、单实体状态协调、需要强一致存储的处理将相关请求送到同一个对象身份;不是自动串行化整个应用
Queues可异步处理、需要重试的工作默认至少一次投递,可能重复;不要假定恰好一次或按顺序处理
Service bindingsWorker 调用另一个 Worker 的函数或 HTTP 处理逻辑不必暴露公共 URL;仍需设计调用权限与被调用服务的公开入口

“最终一致”意味着一次写入后,不同位置可能暂时读到不同值;KV 的 cacheTtl 还可能延长旧值的可见时间。因此库存扣减不能靠“读 KV、减一、写回”实现可靠并发控制。

队列消费者则需要幂等:同一个事件执行多次,不应重复扣款或重复创建业务记录。可以给事件分配唯一 ID,并把数据库效果与该 ID 的唯一约束记录放在同一事务中;调用外部支付等服务时,用对方支持的幂等键。单纯“先查是否处理,再执行”仍会有并发竞争。

6. 可选:用 D1 读取本地便笺

这一节适合已有专用练习数据库身份信息的读者。数据库创建本身是云端操作,此处不创建资源,也不借用生产数据库。不具备这个前提时,停留在上一节的 API 即可;不要编造数据库 UUID 来假装完成配置。

将配置替换为下面的完整版本,并把占位符换成专用练习数据库的真实 ID。它不是可原样运行的配置。带 --local 的执行使用独立本地数据库,不会初始化远程数据。D1 入门本地开发

{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "workers-local-api",
"main": "src/index.ts",
"compatibility_date": "2026-09-12",
"vars": { "APP_ENV": "local" },
"d1_databases": [
{
"binding": "DB",
"database_name": "workers-tutorial-db",
"database_id": "REPLACE_WITH_DEDICATED_DATABASE_ID"
}
]
}

在项目根目录建立 schema.sql

CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
INSERT OR IGNORE INTO notes (id, title) VALUES (1, 'Local D1 note');

停止原开发进程,然后执行本地初始化和查询:

npx wrangler d1 execute workers-tutorial-db --local --file=./schema.sql
npx wrangler d1 execute workers-tutorial-db --local --command='SELECT id, title FROM notes;'
npx wrangler types

查询应显示 ID 为 1、标题为 Local D1 note 的记录。生成的 Env 此时会包含 DB。在原处理函数的外层 try 内、if (path !== "/api/greet") 之前插入:

if (path === "/api/note") {
if (request.method !== "GET") {
return error(405, "method_not_allowed", { Allow: "GET" });
}
const id = new URL(request.url).searchParams.get("id");
if (!id || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id))) {
return error(400, "invalid_id");
}
const result = await env.DB
.prepare("SELECT id, title FROM notes WHERE id = ?")
.bind(Number(id))
.run();
return json({ notes: result.results });
}

SQL 的 ? 是参数占位符,.bind() 把值与 SQL 结构分开,避免把输入拼进查询。这里仅开放本地示例数据的读取;换成用户笔记前要补充身份验证和逐条访问授权。

npx wrangler types --check
npx tsc --noEmit
npx wrangler dev --local

在第二个终端检查:

curl -i 'http://localhost:8787/api/note?id=1'
curl -i 'http://localhost:8787/api/note?id=2'
curl -i 'http://localhost:8787/api/note?id=bad'

依次应返回 200 与 {"notes":[{"id":1,"title":"Local D1 note"}]}、200 与 {"notes":[]}、400 与 {"error":"invalid_id"}no such table 通常说明没有在同一项目和本地状态位置执行 schema;检查命令的数据库名和 --local,不要改成远程执行来试错。本地数据会保留供下次开发使用,但不会自动上传,本地成功也不能证明生产迁移已经完成。

7. 上线前:路由与环境要分开检查

只做本地练习可以在这里停下。下面先解释路由和环境,再给出可选的云端发布步骤;它会登录账号、公开 API,并可能产生费用。创建数据库、上传密钥和修改域名不包含在这个最小发布示例中。

路由文档区分三种入口:

入口用途
workers.devCloudflare 提供的子域名,便于试用;官方建议业务关键的生产服务使用路由或自定义域名
Custom Domain在 Cloudflare zone 中的自有域名或子域名上,让 Worker 本身成为应用源站
Route按 zone 内的流量匹配规则运行 Worker,常用于放在已有源站前面

这里的 zone 是 Cloudflare 管理的 DNS 区域;源站是实际提供应用内容的服务。接管域名前要确认现有流量、DNS 和回退行为,不能把三种入口当成可随意互换的地址。若 Worker 承担安全检查,配额耗尽时“失败开放”到源站可能绕过检查;应按数据风险选择失败策略。限制与超限行为

为 staging 和 production 明确配置各自的变量和资源。Wrangler 的 vars 和绑定不由命名环境自动继承,不能只写一个环境名就假定数据库已隔离。APP_ENV: local 也只是标签,不会改变真实资源映射。环境配置

可以先只在本地添加 env.stagingvars,以及需要时的专用 D1 映射,再用 npx wrangler dev --local --env staging 检查。每次执行资源命令前,核对账户、环境、资源 ID 和本地/远程参数;不要让预发布环境指向生产数据库。

可选:发布一个专用练习 Worker

先阅读第 9 节费用说明,并确认账号允许这次发布。使用第 3 节的基础处理代码,不包含第 6 节插入的 D1 路由;若已做过 D1 练习,先恢复第 3 节的完整 src/index.ts。这个公开示例只返回健康状态和问候语,不保存用户数据,也不提供私有业务功能。

wrangler.jsonc 改为下面的完整配置,将 YOUR_UNUSED_WORKER_NAME 替换为账号中尚未使用的练习名称。不要复用线上 Worker 名称。这里没有数据库、域名路由或密钥绑定:

{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "YOUR_UNUSED_WORKER_NAME",
"main": "src/index.ts",
"compatibility_date": "2026-09-12",
"vars": { "APP_ENV": "local" },
"env": {
"tutorial": {
"workers_dev": true,
"vars": { "APP_ENV": "tutorial" }
}
}
}

命名环境规则,这个 Worker 使用顶层名称加 -tutorial 后缀。workers_dev 提供公开入口,不代表有访问控制。先生成类型、检查代码和试打包:

npx wrangler types
npx tsc --noEmit
npx wrangler deploy --env tutorial --dry-run

--dry-run编译但不发布到线上;它不是验证账号权限或线上行为,也不保证完全不写本地文件。通过后,登录会启动浏览器 OAuth 授权流程。确认是预期账号,再发布:

npx wrangler login
npx wrangler deploy --env tutorial

如账号关联多个账户,先按 CLI 提示确认目标。使用发布输出中的实际 HTTPS 地址,不猜测子域名;把下面的占位地址替换后检查:

export WORKER_URL="https://YOUR_DEPLOYED_WORKER_HOST"
curl -i "$WORKER_URL/health"
curl -i -X POST "$WORKER_URL/api/greet" \
-H 'Content-Type: application/json' --data '{"name":"Ada"}'

预期分别是 HTTP 200、{"ok":true,"environment":"tutorial"}{"message":"Hello, Ada!"}。这里的 tutorial 有助于发现请求是否打到了错误环境,但它不能代替核对 URL。修改代码后先重跑本地检查,再执行相同的带环境发布命令。需要观察真实请求时,可用 npx wrangler tail --env tutorial;它连接云端日志,高流量下可能采样。按 Ctrl-C 停止查看日志并不会停掉已部署的 Worker。

8. 安全、日志与故障恢复

暴露真实功能前补齐什么

  • 身份与权限:验证会话或令牌,还要判断此用户能否操作该资源。知道 URL、绑定名或对象 ID 都不构成授权。
  • CORS:它是浏览器的跨源访问规则,不是身份验证。确需跨源时,只允许预期来源,明确处理 OPTIONS;动态返回已验证的来源时添加 Vary: Origin,不要把通配来源与凭据组合使用。curl 不执行浏览器的 CORS 检查。
  • SSRF:不要接受任意用户 URL 再直接 fetch()。这种服务端请求伪造风险应通过固定上游或严格允许列表、协议限制及重定向检查来控制。本例没有外发请求。
  • 密钥与日志:凭据不要放入源代码、vars、URL、前端包、错误响应或日志。日志只保留排障所需的信息,避免请求体、Cookie、令牌和个人数据。
  • 滥用与预算:限制输入和调用频率,为昂贵操作设置应用额度。平台 CPU 限制不能代替完整的费用控制。

观察什么

本地 console.error 输出会出现在开发终端。远程有 Workers Logs、实时日志等工具,但配置、采样、留存和收费应按选定方案核实;不要假定每条请求都永久保存。

上线后至少能回答:哪个路由的错误增加了?耗时来自计算还是等待数据库?队列是否持续重试?存储绑定是否失败?采用稳定的路由标签、状态码和关联 ID,避免直接记录可能包含个人信息的完整 URL。客户端看到通用 500 时,应通过受控日志定位原因,而不是把堆栈返回给它。

回滚与清理

回滚前确认完整的目标 Worker 名称;第 7 节的名称带有 -tutorial 后缀。把占位符换成已确认的名称后,npx wrangler rollback --name YOUR_VERIFIED_WORKER_NAME 是会立即改变线上流量的远程命令:选中的版本立即承接 100% 流量,可选择范围为最近 100 个已发布版本。已删除的绑定资源不会恢复,Durable Object 类的生命周期变更也可能阻止回滚。回滚说明

因此先保留可识别的上一版本和兼容的数据结构,在专用环境验证,再决定线上回滚。代码回滚不会撤销数据库写入;schema 变更需要单独的迁移和备份恢复方案。

本地结束时按 Ctrl-C 停止 Wrangler。保留练习项目和本地数据库即可;若要重置数据,先确认持久化目录及其归属,再只清理该练习的状态,不使用宽泛的递归删除命令。仅做本地章节时没有云端资源需要删除。若完成了第 7 节发布,在保留该练习配置的目录中先用 npx wrangler delete --env tutorial --dry-run 查看删除计划。只有确认 Worker 名称和关联资源均属于练习后,才执行 npx wrangler delete --env tutorial 并核对确认提示。删除命令可能同时删除关联的平台资源,不能拿这条命令清理复用了生产绑定的项目。

9. 费用与限制:CPU 时间不等于等待时间

以下是 2026-09-12 查阅的 Workers HTTP 额度与价格,不是对整个应用免费运行的承诺。正式估算前重新查看价格限制

项目文档所列值及含义
Free 请求额度每天 100,000 次请求
Free CPU 额度每次调用 10 ms CPU;不是 10 ms 的端到端响应期限
Paid 基础费用每账户每月至少 5 USD
Paid Standard 请求每月含 1000 万次,超出部分每百万次 0.30 USD
Paid Standard CPU每月含 3000 万 CPU-ms,超出部分每百万 CPU-ms 0.02 USD
内存每个 isolate 128 MB,可能由并发请求共享,不是每请求各有 128 MB
Paid HTTP CPU 上限默认 30 秒,可配置至 5 分钟;这不是每次请求应消耗的预算

CPU 时间统计代码主动计算的时间,等待 fetch、KV 或数据库通常不算 CPU。一次等待数据库 200 ms 的请求不一定消耗 200 CPU-ms,但请求和数据库操作仍可能计费。Workers 价格页不按持续时间收费,其他产品有各自账单。

HTTP 总历时与连接生命周期有关,不应据此设计永久运行任务。ctx.waitUntil() 在响应或断开后最多延长 30 秒,可靠的后台工作应交给适当的队列或任务机制。HTTP、队列和定时触发器的限制不能互相套用。执行限制

静态资源请求通常免费且不限次数,但价格页明确规定:启用 Workers Caching 后,缓存命中的请求也按请求计费,包括静态资源;未命中或绕过缓存的请求还会消耗 CPU。不要把“静态资源免费”直接推广到任意缓存和路由配置。

估算时分别统计请求量、CPU 分布、静态资源缓存配置、D1/KV/R2 的操作与存储、队列重试及日志。错误 1102 应检查 CPU 或内存耗尽;1027 则与 Free 每日请求额度耗尽有关。确认实际原因再优化或调整计划,升级套餐不能替代修复无限循环、过大缓冲和重复重试。

更多终端和服务器基础见工具与工作流;若最终需要完整系统控制,再转向 VPS 基础

探索关联打开关联网络