@charcuterie/server

中文文档
0.5.1发布于 2026/08/28MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2483

机队的节点服务器工具包——Hono静态资产处理器,出局后HTTP缓存政策,以及MQTT cmd/resp.

中文使用文档
文档目录15 节
  1. 为什么会这样
  2. 使用量
  3. 在开放标签中部署更新
  4. MQTT cmd/ resp 数据
  5. 外出 HTTP 缓存和节流
  6. 终身
  7. QQ A 304节省了带宽. 它不节省预算.
  8. 这四把礼节斧头是四样东西
  9. 也处理过
  10. 你得到的东西
  11. 两个缓存桶
  12. 选项
  13. 挂载一个生活在别处的目录
  14. 移动手卷处理器
  15. 同伴依赖关系

@ charcuterie/ 服务器

舰队的节点服务器包: 预压缩字节,诚实的缓存头,以及 一个304 帮助,加上MQTT cmd/* 编号 : resp/* 与家庭助理交谈. 生成压缩字节的 Vite 插件生活在静态处理器旁边 因为两分之一是一份合同 把它们分开是它们如何漂移的. MQTT 是一个单独的子路径, 所以一个静态的应用程序永远不会解决 mqtt。 。 。 .

为什么会这样

机队的6个应用程序服务着来自Hono服务器的Vite SPA. 所有六个手卷起来,一个 2026年8月的审计发现: 没有压缩任何东西穆克马吉克是 最坏的情况和导致这个软件包的情况: 它服务于一个1.02 MB 捆绑的未压缩 (290克B击出) Cache-Control: no-cache, no-store, must-revalidate 在一个 内容隐藏 文件名 。 每次访问,每次重装,每个客户端导航 被重新下載了1.2 MB,可能已经是318 kB并可能永远被缓存.

失败的不是任何人不小心。 是"保存目录"看起来像 20行,20行 省去压缩,缓存政策,流出, Range, (中文(简体) ), MIME 类型、逆向防御以及缺失路径和缺失路径之间的区别 资产.

使用量

两分之一 双线.

// vite.config.ts — the build half
import { precompressAssets } from "@charcuterie/server/vite"
import { createViteConfig } from "@charcuterie/vite-config"

export default createViteConfig({
  plugins: [react(), precompressAssets()],
})
// server — the serve half
import { createStaticHandler } from "@charcuterie/server"

app.route("/api", apiApp)
app.use("*", createStaticHandler({ rootDir: webDistDir }))

按先后顺序收养是安全的:没有 .br编号 :.gz 处理器磁盘上的兄弟姐妹 和兄弟姐妹是惰性的 直到找到他们.

在开放标签中部署更新

createStaticHandler 并在 /__charcuterie/deployment 和 SSE 标记流 /__charcuterie/deployment/events标记是部署的SHA-256散列 index.html。一个 Vite 构建在命名新资产集时更改了 shell.

一个 EventSource 更换容器后重新连接。 它的首次活动来自新的 容器中包含新的标记。 对齐服务器 useDeploymentUpdate 从 @charcuterie/ui; 它报告 isUpdateAvailable 并给应用程序一个 reload 行动。 这个 钩子 没有 自己重新装入标签,因为标签可以有未保存的工作.

已经拥有 SSE 连接的应用程序可以避免第二流 :

const { checkForUpdate, isUpdateAvailable, reload } = useDeploymentUpdate({
  isEventSourceEnabled: false,
})

// In the existing SSE source's successful reconnect callback:
void checkForUpdate()

当 isUpdateAvailable 是真的,打电话 reload 从此 按钮。 静态外壳已经发出 Cache-Control: no-cache,因此重载将获取 新外壳及其新的不可变资产 URL。 设置两者 deploymentPath 财务报告和审定财务报表 deploymentEventsPath 改为 false 仅用于没有浏览器客户端的仅资产源.

MQTT cmd/ resp 数据

唯节. 导入 @charcuterie/server/mqtt,而不是主桶,并添加 mqtt 作为 应用程序的依赖性。 命令和响应主题为 从未保留 - 一个经纪人 重放不能在夜间重放 。 拒绝同一动作的重叠命令 与 { ok: false, reason: "already-running" }。 。 。 .

import { createMqttService } from "@charcuterie/server/mqtt"

const mqtt = await createMqttService({
  base: "board-game-picker",
  host: process.env.MQTT_HOST,
  password: process.env.MQTT_PASS,
  username: process.env.MQTT_USER,
})

mqtt.handleCommand("sync", async (payload) => {
  // run the work
  return { ok: true, payload }
})
// board-game-picker/cmd/sync  →  board-game-picker/resp/sync

TLS 默认端口为时 8883 (单位:千美元)mqtt.octen.dev) (中文(简体) ). 过 isTls 以覆盖. truenas-mqtt是因为 trigger编号 :status 树是一个遗留的特例——新应用 cmd编号 :resp这样做 没有 属于 @charcuterie/streams (浏览器,只推.

外出 HTTP 缓存和节流

与上面所有东西相反的方向:这是针对应用程序的反应 接收 从别人的服务器。 导入 @charcuterie/server/http零点 依赖关系,无 fetch 属于自己的.

图书馆从不拥有存储器. 5个应用程序在机队缓存第三方HTTP和所有 五个选择了不同的底片——两个SQLite表,两个JSON文件目录,一个 内存 Map 写成一个文件,每个文件都坐在那里。 什麽 重复为 政策因此,这就是移动在这里: 多久的答案仍然是真实的, 并允许我们问多少次.

import {
  createHttpCache,
  createThrottle,
  ONE_DAY_MS,
} from "@charcuterie/server/http"

const cache = createHttpCache<string>({
  fetchFromOrigin: async ({ etag, key }) => {
    const response = await fetch(key, {
      headers: etag == null ? {} : { "if-none-match": etag },
    })

    if (response.status === 304) return { outcome: "unchanged" }
    if (response.status === 404) return { outcome: "missing" }
    if (!response.ok) return { outcome: "unavailable" }

    return {
      etag: response.headers.get("etag"),
      outcome: "payload",
      payload: await response.text(),
    }
  },
  lifetime: ONE_DAY_MS,
  store: {
    // Whatever the app already has. Synchronous is fine.
    read: ({ key }) => selectRow(key),
    write: ({ key, record }) => upsertRow(key, record),
  },
  throttle: createThrottle({ minIntervalMs: 1_000 }),
})

const { payload, source } = await cache.get({ key: url })

终身

"immutable" | "none" | number。 。 。 . "immutable" 是一个 关于 URL 的主张没有暗示.. 在一个 40 个字符的 hash 中的文件不能成为另一个文件. "none" 保持一个 完全读出商店。 一个数字是毫秒,它设置在呼叫站点, 因为只有呼叫网站才知道 答案的速度.

QQ A 304节省了带宽. 它不节省预算.

2026-08-24日以活的 GitHub API 测量,未经认证:每个条件 304请求又来了 x-ratelimit-remaining 仍然跌倒一个。 GitHub 文档 a 304作为自由反对 小学 费率限制和未经校正的地址限制 这不是这个限度。 一个 ETag 买回尸体,不拿预算. 只有一辈子 未用完节省的预算因为唯一的自由请求是从未发出过的请求.

这四把礼节斧头是四样东西

选项手段
minIntervalMs最小的间隙从两个开始.
maxConcurrent有多少人可能同时在逃.
maxPerWindow + 键 windowMs预算可以补充.
cooldownMs失败后一切都停止了.

一个缺口不是预算:每秒一个请求允许3600小时,60小时 预算允许在第一秒为三. run 槽的队列; tryRun 返回时 null 而不是排队, 对于一个呼叫者 在民意调查循环 宁可放弃工作 然后把它捡起来 下一滴.

也处理过

  • 阴相克取. missLifetime 记得"起源没有这种东西" 一个应用已经取决于。 默认为 "none"。 。 。 .
  • 重现状态. isStaleWhileRevalidate 立即返回已淡化的正文,并 后面很新鲜 默认关闭.
  • 单飞. 同时读取一个键崩溃成一个请求 .
  • 失败就是价值. 一个 "unavailable" 源代码永远不会被写入,为 僵尸 开始冷却 存储错误可降解为错失并转到 onError;缓存从来不是页面无法渲染的原因.

名字已经定了.. fetchedAt 编号 : expiresAt 编号 : etag,没有 Ms 后缀在一个 *At 字段, Ms 保持每个持续时间 (单位:千美元)决定) (中文(简体) ).

你得到的东西

压缩.br 编号 : .zst 编号 : .gz 兄弟姐妹们,谈判反对 Accept-Encoding,与 Vary 设置。 在构建时间由Brotli质地11所写出——字节从不改变,因此在用户等待时根据请求推导出烧伤CPU.
缓存/assets/* → public, max-age=31536000, immutable. 其他一切 * no-cache。 。 。 .
调整ETag + 键 If-None-Match 304个没有尸体,适用 仅限 页:1 no-cache 桶头.
流线createReadStream所以大字节从不落地.
正确性Range 请求,MIME搜索, ../ 逆向防御 和失踪 .js 404 而不是返回 HTML.

两个缓存桶

维特只有两种文件 dist/,他们想要相反的头.

内容隐藏 文档(/assets/index-D7e1J0tu.js永远不能改变他们的名字后面, 所以他们是 immutable - 不仅仅是 max-age,仍然允许重新认证 轮回再装入.

其他的都一样 —— 说 index.html任何内容 public/ - 在整个部署期间保持其名称 每次必须重新确认. no-cache 并不意味着“ 不缓存 ” ; 这意味着 "cache,然后在再利用前重新验证". 佩尔戴着ETAG 通常的答案是304 没有尸体.

no-store 是表示“不要缓存”的页眉,放在被散列的资产上就是 此软件包存在以删除.

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}Bucketing is by 路径前缀而不是由散列形状的正反面。 维特的 assetsDir 是一个建筑 保证; “ 此文件名看起来散列” 是双向错误的猜测 (单位:千美元)vendor-legacy.js 不是散发的, logo-v2.png 也没有。 覆盖 immutablePathPrefixes 如果您需要的话 assetsDir 没有 assets,或者添加自己的 内容地址目录 :

createStaticHandler({
  immutablePathPrefixes: ["/assets/", "/images/"],
  rootDir: webDistDir,
})

名单 替换 默认而不是扩展它,所以一个重新命名的应用程序 assetsDir 忘记说,得到安全答案——重新确认——而不是一年 刺痛的骨骼.

选项

选项默认
rootDir所需经费构建输出的绝对路径 。 相对路径解析 process.cwd() - 这就是 serveStatic行为,不是我们的.
immutablePathPrefixes["/assets/"]可能永远被缓存的请求路径前缀 .
index"index.html"SPA外壳,相对于 rootDir。 。 。 .
hasSpaFallbacktrue服务 index 用于匹配无文件的无扩展路径。 关闭纯粹的资产来源.
rewriteRequestPath—— 说将请求路径映射到 rootDir 在查找前,用于显示其 URL 前缀不是真实目录的挂载.

挂载一个生活在别处的目录

board-games 服务 /images/* 调出 $BOARD_GAMES_IMAGES,它远未靠近 网络根 :

app.use("/images/*", createStaticHandler({
  immutablePathPrefixes: ["/images/"],
  rewriteRequestPath: (path) => path.replace(/^\/images/, ""),
  rootDir: imagesDirectory(),
}))

缓存桶还在决定上 请求 路径,而不是重写的一个,所以 immutablePathPrefixes 继续命名 URL , 因为一个呼叫者看到它们 。 重写是在哪里 字节活在磁盘上;缓存是关于浏览器承诺的内容. SPA的倒计时是 不受影响 : index 反对 rootDir 直接,所以重写不能误导 壳.

precompressAssets() 带 algorithms (默认) ["br", "gz"]和 thresholdBytes (默认) 1024) (中文(简体) ). zst 被支持,但作为第三: 操作者更喜欢 布罗特利,每个说zstd的浏览器 也会说布罗特利, Gzip 已经是地板了 对那些不会说话的人来说.

移动手卷处理器

舰队中的每个版本都是三个形状之一.

这个 readFileSync 循环 (点 -- -- 市场、邮件过滤器、画廊下载器、Mux-Magic) - 内容类型地图, a Cache-Control 特纳里,一个SPA倒置, 和一个穿梭的警卫. 删除全部内容,包括 CONTENT_TYPES 常数和 isWithinRoot 帮手.

-app.use("*", async (context) => {
-  const filePath = /* …twenty lines… */
-  context.header("Content-Type", CONTENT_TYPES[ext] ?? "application/octet-stream")
-  context.header("Cache-Control", "no-cache, no-store, must-revalidate")
-  return context.body(readFileSync(filePath))
-})
+app.use("*", createStaticHandler({ rootDir: webDistDir }))

赤裸裸的 serveStatic (板游戏)——已经流出,但没有压缩和 没有缓存策略 。 换个电话.

serveStatic + 键 onFound (板球游戏") /images/*———————— 这个是沉默的 今天断了. onFound 运行 之后 serveStatic 已经建立了反应,所以 它的片头被丢在地上 棋盘游戏 immutable 在盒子艺术从来没有 到达浏览器 。 反对 @hono/node-server 2.1.0: (中文(简体) )

serveStatic({ root, onFound: (_p, c) => c.header("Cache-Control", "immutable") })
// → response has no cache-control header at all

在中间软件中设置信头 在此之前 serveStatic,这就是这个包的工作.

同伴依赖关系

hono 财务报告和审定财务报表 @hono/node-server 需要同级软件——应用程序拥有这些版本. vite 实值 a 一个 选项 仅由 /vite 进入点,所以服务器永远不会解决 维特和一个建筑永远不会解决Hono. mqtt 是一个 选项 仅由 /mqtt 切入点,所以仅静态的应用程序永远不会解决经纪人客户端. 这个 /http 进入点有 完全没有依赖关系 - 连Hono都没有.

版本与发布记录

@charcuterie/server 共发布 6 个版本,首次发布于 2026/08/10;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.1latest2026/08/282 个月前
0.5.02026/08/282 个月前
0.4.02026/08/262 个月前
0.3.02026/08/162 个月前
0.2.02026/08/102 个月前
0.1.02026/08/102 个月前

关于 @charcuterie/server 的常见问题

@charcuterie/server 是什么?

机队的节点服务器工具包——Hono静态资产处理器,出局后HTTP缓存政策,以及MQTT cmd/resp.

如何安装 @charcuterie/server?

在项目目录运行 npm install @charcuterie/server,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@hono/node-server(>=1.13)、hono(>=4)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@charcuterie/server 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@charcuterie/server 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@charcuterie/server 可以商用吗?

@charcuterie/server 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@charcuterie/server 的下载量有多少?

根据 npm 官方下载统计,@charcuterie/server 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,483 次,日均约 83 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@charcuterie/server 还在维护吗?

最新版本 0.5.1 发布于 2026/08/28(2 个月前);至今共发布 6 个版本,其中过去 12 个月发布了 6 个;首次发布于 2026/08/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。