Miniflare API 允许你在不发起实际 HTTP 请求的情况下向 Worker 分发事件、模拟 Worker 之间的连接,并与 KV、R2 和 Durable Objects 等存储产品的本地模拟进行交互。这使其非常适合编写测试,或其他需要更细粒度控制的高级用例。
Miniflare 通过 npm 作为开发依赖安装:
npm i -D miniflareyarn add -D miniflarepnpm add -D miniflarebun add -d miniflare在后续所有示例中,我们假设 Node.js 以 ES 模块模式运行。你可以通过在 package.json 中设置 type 字段来实现:
{
...
"type": "module"
...
}要初始化 Miniflare,从 miniflare 导入 Miniflare 类:
import { Miniflare } from "miniflare";
const mf = new Miniflare({
modules: true,
script: `
export default {
async fetch(request, env, ctx) {
return new Response("Hello Miniflare!");
}
}
`,
});
const res = await mf.dispatchFetch("http://localhost:8787/");
console.log(await res.text()); // Hello Miniflare!
await mf.dispose();其余文档 详细介绍了如何配置 特定功能。
请注意,在上面的示例中,我们将 script 指定为字符串。我们也可以
将脚本放在 worker.js 等文件中,然后改用 scriptPath
属性:
const mf = new Miniflare({
scriptPath: "worker.js",
});Miniflare 的 API 主要用于测试场景,通常不需要文件监听。如果你需要监听文件,请考虑使用独立的文件监听器,如 fs.watch() ↗ 或 chokidar ↗,并在变更时调用 setOptions() 并传入原始配置。
要清理并停止监听请求,应对实例调用 dispose():
await mf.dispose();你也可以通过使用原始配置对象调用 setOptions() 来手动重载脚本(主脚本和 Durable Objects 脚本)和选项。
你可以使用 setOptions 方法更新现有
Miniflare 实例的选项。它接受与
new Miniflare 构造函数相同的选项对象,应用这些选项,然后重载 Worker。
const mf = new Miniflare({
script: "...",
kvNamespaces: ["TEST_NAMESPACE"],
bindings: { KEY: "value1" },
});
await mf.setOptions({
script: "...",
kvNamespaces: ["TEST_NAMESPACE"],
bindings: { KEY: "value2" },
});getWorker 分别向 Worker 分发 fetch、queues 和 scheduled 事件:
import { Miniflare } from "miniflare";
const mf = new Miniflare({
modules: true,
script: `
let lastScheduledController;
let lastQueueBatch;
export default {
async fetch(request, env, ctx) {
const { pathname } = new URL(request.url);
if (pathname === "/scheduled") {
return Response.json({
scheduledTime: lastScheduledController?.scheduledTime,
cron: lastScheduledController?.cron,
});
} else if (pathname === "/queue") {
return Response.json({
queue: lastQueueBatch.queue,
messages: lastQueueBatch.messages.map((message) => ({
id: message.id,
timestamp: message.timestamp.getTime(),
body: message.body,
bodyType: message.body.constructor.name,
})),
});
} else if (pathname === "/get-url") {
return new Response(request.url);
} else {
return new Response(null, { status: 404 });
}
},
async scheduled(controller, env, ctx) {
lastScheduledController = controller;
if (controller.cron === "* * * * *") controller.noRetry();
},
async queue(batch, env, ctx) {
lastQueueBatch = batch;
if (batch.queue === "needy") batch.retryAll();
for (const message of batch.messages) {
if (message.id === "perfect") message.ack();
}
}
}`,
});
const res = await mf.dispatchFetch("http://localhost:8787/", {
headers: { "X-Message": "Hello Miniflare!" },
});
console.log(await res.text()); // Hello Miniflare!
const worker = await mf.getWorker();
const scheduledResult = await worker.scheduled({
cron: "* * * * *",
});
console.log(scheduledResult); // { outcome: "ok", noRetry: true });
const queueResult = await worker.queue("needy", [
{ id: "a", timestamp: new Date(1000), body: "a", attempts: 1 },
{ id: "b", timestamp: new Date(2000), body: { b: 1 }, attempts: 1 },
]);
console.log(queueResult); // { outcome: "ok", retryAll: true, ackAll: false, explicitRetries: [], explicitAcks: []}更多详情请参阅 📨 Fetch 事件 和 ⏰ Scheduled 事件。
Miniflare 会自动启动 HTTP 服务器。要等待其就绪,请 await ready 属性:
import { Miniflare } from "miniflare";
const mf = new Miniflare({
modules: true,
script: `
export default {
async fetch(request, env, ctx) {
return new Response("Hello Miniflare!");
})
}
`,
port: 5000,
});
await mf.ready;
console.log("Listening on :5000");默认情况下,Miniflare 会从受信任的
Cloudflare 端点获取 Request#cf 对象,并将其缓存到 node_modules/.mf/cf.json。你可以使用 cf 选项禁用此行为:
const mf = new Miniflare({
cf: false,
});你也可以通过文件路径提供自定义 cf 对象:
const mf = new Miniflare({
cf: "cf.json",
});当你未直接使用 Miniflare API 时(例如运行 wrangler dev 时),还可以使用系统环境变量控制此行为:
# Disable cf fetching entirely (uses fallback data)
export CLOUDFLARE_CF_FETCH_ENABLED=false
npx wrangler dev
# Use a custom cache location for cf.json
export CLOUDFLARE_CF_FETCH_PATH=/tmp/.cf-cache.json
npx wrangler devMiniflare API 中的显式 cf 选项优先于这两个环境变量。
要改为启动 HTTPS 服务器,请设置 https 选项。要使用默认共享自签名证书 ↗,将 https 设置为 true:
const mf = new Miniflare({
https: true,
});从文件系统加载现有证书:
const mf = new Miniflare({
// These are all optional, you don't need to include them all
httpsKeyPath: "./key.pem",
httpsCertPath: "./cert.pem",
});改为从字符串加载现有证书:
const mf = new Miniflare({
// These are all optional, you don't need to include them all
httpsKey: "-----BEGIN RSA PRIVATE KEY-----...",
httpsCert: "-----BEGIN CERTIFICATE-----...",
});如果某个选项同时指定了字符串和路径(例如 httpsKey 和
httpsKeyPath),将优先使用字符串。
默认情况下,使用 API 时 [mf:*] 日志处于禁用状态。要
启用这些日志,请将 log 属性设置为 Log 类的实例。其唯一
参数是日志级别,用于指示应记录哪些消息:
import { Miniflare, Log, LogLevel } from "miniflare";
const mf = new Miniflare({
scriptPath: "worker.js",
log: new Log(LogLevel.DEBUG), // Enable debug messages
});import { Miniflare, Log, LogLevel } from "miniflare";
const mf = new Miniflare({
// All options are optional, but one of script or scriptPath is required
log: new Log(LogLevel.INFO), // Logger Miniflare uses for debugging
script: `
export default {
async fetch(request, env, ctx) {
return new Response("Hello Miniflare!");
}
}
`,
scriptPath: "./index.js",
modules: true, // Enable modules
modulesRules: [
// Modules import rule
{ type: "ESModule", include: ["**/*.js"], fallthrough: true },
{ type: "Text", include: ["**/*.text"] },
],
compatibilityDate: "2021-11-23", // Opt into backwards-incompatible changes from
compatibilityFlags: ["formdata_parser_supports_files"], // Control specific backwards-incompatible changes
upstream: "https://miniflare.dev", // URL of upstream origin
workers: [{
// reference additional named workers
name: "worker2",
kvNamespaces: { COUNTS: "counts" },
serviceBindings: {
INCREMENTER: "incrementer",
// Service bindings can also be defined as custom functions, with access
// to anything defined outside Miniflare.
async CUSTOM(request) {
// `request` is the incoming `Request` object.
return new Response(message);
},
},
modules: true,
script: `export default {
async fetch(request, env, ctx) {
// Get the message defined outside
const response = await env.CUSTOM.fetch("http://host/");
const message = await response.text();
// Increment the count 3 times
await env.INCREMENTER.fetch("http://host/");
await env.INCREMENTER.fetch("http://host/");
await env.INCREMENTER.fetch("http://host/");
const count = await env.COUNTS.get("count");
return new Response(message + count);
}
}`,
},
}],
name: "worker", // Name of service
routes: ["*site.mf/worker"],
host: "127.0.0.1", // Host for HTTP(S) server to listen on
port: 8787, // Port for HTTP(S) server to listen on
https: true, // Enable self-signed HTTPS (with optional cert path)
httpsKey: "-----BEGIN RSA PRIVATE KEY-----...",
httpsKeyPath: "./key.pem", // Path to PEM SSL key
httpsCert: "-----BEGIN CERTIFICATE-----...",
httpsCertPath: "./cert.pem", // Path to PEM SSL cert chain
cf: "./node_modules/.mf/cf.json", // Path for cached Request cf object from Cloudflare
liveReload: true, // Reload HTML pages whenever worker is reloaded
kvNamespaces: ["TEST_NAMESPACE"], // KV namespace to bind
kvPersist: "./kv-data", // Persist KV data (to optional path)
r2Buckets: ["BUCKET"], // R2 bucket to bind
r2Persist: "./r2-data", // Persist R2 data (to optional path)
durableObjects: {
// Durable Object to bind
TEST_OBJECT: "TestObject", // className
API_OBJECT: { className: "ApiObject", scriptName: "api" },
},
durableObjectsPersist: "./durable-objects-data", // Persist Durable Object data (to optional path)
cache: false, // Enable default/named caches (enabled by default)
cachePersist: "./cache-data", // Persist cached data (to optional path)
cacheWarnUsage: true, // Warn on cache usage, for workers.dev subdomains
sitePath: "./site", // Path to serve Workers Site files from
siteInclude: ["**/*.html", "**/*.css", "**/*.js"], // Glob pattern of site files to serve
siteExclude: ["node_modules"], // Glob pattern of site files not to serve
bindings: { SECRET: "sssh" }, // Binds variable/secret to environment
wasmBindings: { ADD_MODULE: "./add.wasm" }, // WASM module to bind
textBlobBindings: { TEXT: "./text.txt" }, // Text blob to bind
dataBlobBindings: { DATA: "./data.bin" }, // Data blob to bind
});
await mf.setOptions({ kvNamespaces: ["TEST_NAMESPACE2"] }); // Apply options and reload
const bindings = await mf.getBindings(); // Get bindings (KV/Durable Object namespaces, variables, etc)
// Dispatch "fetch" event to worker
const res = await mf.dispatchFetch("http://localhost:8787/", {
headers: { Authorization: "Bearer ..." },
});
const text = await res.text();
const worker = await mf.getWorker();
// Dispatch "scheduled" event to worker
const scheduledResult = await worker.scheduled({ cron: "30 * * * *" })
const TEST_NAMESPACE = await mf.getKVNamespace("TEST_NAMESPACE");
const BUCKET = await mf.getR2Bucket("BUCKET");
const caches = await mf.getCaches(); // Get global `CacheStorage` instance
const defaultCache = caches.default;
const namedCache = await caches.open("name");
// Get Durable Object namespace and storage for ID
const TEST_OBJECT = await mf.getDurableObjectNamespace("TEST_OBJECT");
const id = TEST_OBJECT.newUniqueId();
const storage = await mf.getDurableObjectStorage(id);
// Get Queue Producer
const producer = await mf.getQueueProducer("QUEUE_BINDING");
// Get D1 Database
const db = await mf.getD1Database("D1_BINDING")
await mf.dispose(); // Cleanup storage database connections and watcher