@ 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。 。 。 . |
hasSpaFallback | true | 服务 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都没有.