← Cloudflare Workers / workers / testing / miniflare
Začínáme
Miniflare API vám umožňuje odesílat události workerům bez skutečných HTTP požadavků, simulovat spojení mezi Workery a pracovat s lokálními emulacemi úložných produktů, jako je KV, R2, a Durable Objects. Díky tomu se skvěle hodí pro psaní testů nebo jiné pokročilé případy použití, kde potřebujete jemnější kontrolu.
Instalace
Miniflare se instaluje pomocí npm jako vývojovou závislost:
npm i -D miniflarePoužití
Ve všech dalších příkladech budeme předpokládat, že Node.js běží v režimu ES modulů. Toho
dosáhnete nastavením type pole ve vašem package.json:
{
...
"type": "module"
...
}Chcete-li inicializovat Miniflare, importujte Miniflare třída z 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();zbytek této dokumentace se podrobněji věnuje konfiguraci konkrétních funkcí.
Skripty pro práci s řetězci a soubory
Všimněte si, že ve výše uvedeném příkladu nastavujeme script jako řetězec. Skript jsme stejně dobře
mohli umístit do souboru, jako je například worker.js, poté jste použili scriptPath
vlastnost místo toho:
const mf = new Miniflare({
scriptPath: "worker.js",
});Sledování, opětovné načítání a uvolňování
API Miniflare je určeno především pro testovací scénáře, kde sledování souborů obvykle není potřeba. Pokud potřebujete sledovat soubory, zvažte použití samostatného sledovače souborů, například fs.watch() ↗ nebo chokidar ↗, a volání setOptions() s vaší původní konfigurací při změně.
Chcete-li uklidit prostředky a přestat naslouchat požadavkům, měli byste dispose() vaše instance:
await mf.dispose();Skripty (hlavní i Durable Objects) a možnosti můžete také ručně znovu načíst voláním setOptions() původním konfiguračním objektem.
Aktualizace možností a globálního rozsahu
Můžete použít setOptions metoda pro aktualizaci možností existujícího
Miniflare instance. Přijímá stejný objekt options jako
new Miniflare konstruktor, použije tato nastavení a poté worker znovu načte.
const mf = new Miniflare({
script: "...",
kvNamespaces: ["TEST_NAMESPACE"],
bindings: { KEY: "value1" },
});
await mf.setOptions({
script: "...",
kvNamespaces: ["TEST_NAMESPACE"],
bindings: { KEY: "value2" },
});Odesílání událostí
getWorker odesílá fetch, queues, a scheduled události
postupně jednotlivým workerům:
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: []}Viz 📨 Události fetch a ⏰ Naplánované události pro další podrobnosti.
HTTP server
Miniflare automaticky spustí server HTTP. Chcete-li počkat, až bude připravený, použijte await ready vlastnost:
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");Request#cf Object
Ve výchozím nastavení Miniflare stáhne Request#cf objekt z důvěryhodného
endpointu Cloudflare a uložit jej do mezipaměti node_modules/.mf/cf.json. Toto chování můžete vypnout pomocí cf možnost:
const mf = new Miniflare({
cf: false,
});Vlastní objekt cf můžete také zadat prostřednictvím cesty k souboru:
const mf = new Miniflare({
cf: "cf.json",
});Toto chování můžete také ovládat pomocí systémové proměnné prostředí, což je užitečné, pokud nepoužíváte přímo Miniflare API (například při spouštění 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 devExplicitní cf možnost v API Miniflare má přednost před oběma proměnnými prostředí.
HTTPS server
Chcete-li místo toho spustit server HTTPS, nastavte https možnost. Pro použití výchozí sdílený self-signed certifikát ↗, nastavte https na true:
const mf = new Miniflare({
https: true,
});Chcete-li načíst existující certifikát ze souborového systému:
const mf = new Miniflare({
// These are all optional, you don't need to include them all
httpsKeyPath: "./key.pem",
httpsCertPath: "./cert.pem",
});Chcete-li místo toho načíst existující certifikát z řetězců:
const mf = new Miniflare({
// These are all optional, you don't need to include them all
httpsKey: "-----BEGIN RSA PRIVATE KEY-----...",
httpsCert: "-----BEGIN CERTIFICATE-----...",
});Pokud je pro volbu zadán jak řetězec, tak cesta (např. httpsKey a
httpsKeyPath), bude upřednostněn řetězec.
Protokolování
Standardně [mf:*] logy jsou při použití API vypnuté. Chcete-li je zapnout,
nastavte log vlastnost na instanci Log třída. Jejím jediným
parametrem je úroveň logování udávající, které zprávy se mají zaznamenávat:
import { Miniflare, Log, LogLevel } from "miniflare";
const mf = new Miniflare({
scriptPath: "worker.js",
log: new Log(LogLevel.DEBUG), // Enable debug messages
});Referenční informace
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