☰
Farm DevServer 与 HMR 完全指南:配置、中间件与热更新原理
2026/10/14 15:59:52 网站建设 项目流程
  • 前端构建
  • 构建工具
  • 开发工具

【免费下载链接】farm

Extremely fast Vite-compatible web build tool written in Rust

项目地址:https://gitcode.com/gh_mirrors/fa/farm
点击查看免费下载

Farm 默认在开发模式下提供DevServer并自动开启 HMR(Hot Module Replacement,热模块替换)。本文以官方文档 DevServer and HMR 为主线,结合@farmfe/core的源码实现,完整讲解 DevServer 的全部配置项(port、cors、proxy、hmr等)、connect 风格中间件的编写方式,以及 Vite 兼容的import.meta.hotAPI 的使用与底层更新链路,帮助你在开发环境中精确掌控服务器行为与热更新流程。

一、DevServer 是什么:先建立整体心智模型

Farm 的 DevServer 是一个连接"编译器"与"浏览器"的运行时载体,它承担了以下职责:

  • 监听指定端口,提供开发资源的 HTTP/HTTPS 服务;
  • 通过 WebSocket 通道接收 HMR 更新并广播给浏览器客户端;
  • 监听文件变化(watcher),触发增量重编译并推送结果;
  • 按顺序挂载一整套内置中间件(代理、静态资源、HTML fallback 等)。

从源码结构看,Server类定义在 server/index.ts,它封装了ws(WebSocket 服务)、hmrEngine(HMR 引擎)、watcher(文件监听)、middlewares(connect 实例)和compiler(编译器)五大核心组件。静态方法Server.createServer()在 server/index.ts 中按固定顺序完成初始化:解析 HTTPS 配置 → 处理 public 目录 → 创建 connect 中间件管道 → 创建 HTTP Server(middlewareMode下不创建)→ 初始化 HmrEngine → 创建 WebSocket Server → 注册 Vite invalidate 处理器 → 创建 watcher → 执行插件的configureServer钩子 → 挂载全部中间件。

这个初始化顺序解释了后文各配置项之间的联动关系(例如 HMR 端口同步、中间件排序),也是理解 DevServer 行为的最佳切入点。

二、基础配置:server 下的常用选项

所有 DevServer 选项都位于farm.config.ts的server字段下,完整选项列表见 Dev Server Options 文档:

import { defineConfig } from '@farmfe/core'; export default defineConfig({ server: { port: 9801, cors: true, proxy: { // ... }, open: true, } })

对应的类型定义UserServerConfig如下:

export interface UserServerConfig { headers?: OutgoingHttpHeaders | undefined; port?: number; https?: HttpsServerOptions; origin?: string; allowedHosts?: string[]; hmr?: boolean | HmrOptions; proxy?: Record<string, ProxyOptions>; strictPort?: boolean; open?: boolean; host?: string | boolean; cors?: boolean | any; appType?: 'spa' | 'mpa' | 'custom'; middlewares?: DevServerMiddleware[]; middlewareMode?: boolean; writeToDisk?: boolean; preview?: UserPreviewServerConfig; }

下面逐项说明各配置的默认值与行为(默认值取自 constants.ts 中的DEFAULT_DEV_SERVER_OPTIONS):

port

  • 默认值:9000(也可通过环境变量FARM_DEFAULT_SERVER_PORT覆盖)

DevServer 监听的端口。注意默认行为是"端口冲突自动递增":当9001被占用时会尝试9002,这与源码 http.ts 中httpServerStart的EADDRINUSE处理逻辑一致——捕获错误后执行httpServer.listen(++port, host)。

strictPort

  • 默认值:false

若设为true,端口被占用时直接抛出Port ${port} is already in use错误,而不是自动尝试下一个端口。适合 CI 或要求固定端口的调试场景。

cors

  • 默认值:false

是否启用 CORS。cors既可以是布尔值,也可以传入cors库的详细选项对象。在源码 server/index.ts 中可以看到,当cors为真时,中间件管道会挂载corsMiddleware(typeof cors === 'boolean' ? {} : cors)。

appType

  • 默认值:'spa'

决定未匹配路由的 HTML fallback 行为,影响开发服务器如何处理刷新时的 404 问题:

  • 'spa':单页应用。未匹配的路由回退到index.html,让前端路由接管;
  • 'mpa':多页应用。为每个路由查找对应的 HTML 文件;
  • 'custom':禁用自动 HTML fallback,适合你有自定义服务端逻辑的场景。

从源码看,只有appType为'spa'或'mpa'时,初始化中间件 才会挂载htmlFallbackMiddleware与notFoundMiddleware。

host

  • 默认值:'localhost'

Dev Server 监听的 host。设为true时通常表示监听所有网络接口,便于局域网或容器内访问。

open

  • 默认值:false

编译完成后自动用默认浏览器打开开发页面。实现见 open.ts,listen()流程中在首次编译完成后调用#openServerBrowser()打开resolvedUrls中的首个本地地址。

headers

  • 默认值:undefined

为 DevServer 的全部 HTTP 响应设置全局响应头:

import { defineConfig } from '@farmfe/core' export default defineConfig({ server: { headers: { 'Accept': 'xxxx' } } })

https

  • 默认值:undefined

启用 TLS + HTTP2。取值会传给 Node.js 的http2.createSecureServer,因此需要有效的证书(cert/key可传文件路径,会被 resolveHttpsConfig 自动读取为文件内容)。一个值得注意的实现细节:从 http.ts 的resolveHttpServer可以看出,如果同时配置了proxy,Farm 会降级使用node:https(HTTP/1.1),否则使用node:http2的createSecureServer并开启allowHTTP1: true——这是因为http-proxy对 HTTP/2 的支持存在已知问题。

origin / allowedHosts

  • origin默认值:''
  • allowedHosts默认值:[]

origin用于在响应中配置 DevServer 的源地址;allowedHosts配置后,只有Host请求头匹配列表内主机名的请求才会被响应,可用于限制可访问的域名。

writeToDisk

  • 默认值:false

默认情况下,编译产物存储在内存中并由中间件直接提供;设为true后,开发资源会被写入磁盘。源码中通过选择不同的中间件实现这一分支:初始化中间件 里,writeToDisk为真时挂载resourceDiskMiddleware,否则挂载内存版的resourceMiddleware。

middlewareMode

  • 默认值:false

设为true时,DevServer 不启动自己的 HTTP Server(server/index.ts 中httpServer直接置为null),而是暴露 connect 实例供你挂到现有的 Web 框架中,适合把 Farm 集成进已有服务。

proxy

  • 默认值:undefined

配置开发代理,底层基于http-proxy实现,具体选项遵循其文档:

import { defineConfig } from "@farmfe/core"; export default defineConfig({ server: { proxy: { "/api": { target: "https://music-erkelost.vercel.app/banner", changeOrigin: true, pathRewrite: (path: any) => path.replace(/^\/api/, ""), }, }, }, });

代理中间件在 middlewares/proxy.ts 中实现,挂载于 cors 之后、静态资源之前,因此代理规则优先于本地资源匹配。

三、Dev Server 中间件(middlewares)

你可以通过server.middlewares处理 DevServer 的请求。Farm v2 采用connect 风格的中间件:一个中间件是接收Server实例、返回 connect 处理器(req, res, next)的函数。

自定义中间件示例

官方文档给出的完整示例是一个"设置自定义响应头"的中间件(farm.config.ts 原文代码):

import type { IncomingMessage, ServerResponse } from 'node:http'; import { Server, defineConfig } from '@farmfe/core'; export function headers(devServerContext: Server) { const { config } = devServerContext; if (!config.headers) return; return (req: IncomingMessage, res: ServerResponse, next: () => void) => { if (config.headers) { for (const name in config.headers) { res.setHeader(name, config.headers[name] as string | string[]); } } next(); }; } export default defineConfig({ server: { middlewares: [ headers ] } })

这里的关键点:Farm 中间件的签名是(server) => (req, res, next)。外层函数让你在定义阶段拿到Server上下文(可以读取server.config、挂到server.ws上等),内层返回的才是标准 connect 处理器。

直接复用现成的 connect/express 中间件

由于内层就是标准 connect 签名,社区中间件可以"包一层"直接使用,例如挂载compression:

import { defineConfig } from "@farmfe/core"; import compression from 'compression'; export default defineConfig({ server: { middlewares: [ () => compression() ] }, });

内置中间件与用户中间件的执行顺序

从 初始化中间件的源码 可以看到完整挂载顺序:

  1. hmrPingMiddleware—— 处理Accept: text/x-farm-ping的心跳请求,直接返回 204,实现见 hmrPing.ts;
  2. corsMiddleware(若启用cors);
  3. proxyMiddleware(若配置了proxy);
  4. publicPathMiddleware(publicPath 非/时);
  5. publicMiddleware(public 目录存在时);
  6. lazyCompilationMiddleware(启用懒编译时);
  7. staticMiddleware、outputFilesMiddleware(_output_files资源树查询);
  8. resourceDiskMiddleware或resourceMiddleware(取决于writeToDisk);
  9. 插件configureServer钩子返回的清理函数;
  10. 用户的server.middlewares;
  11. htmlFallbackMiddleware+notFoundMiddleware(仅spa/mpa)。

可以推断,用户中间件在静态资源之后、HTML fallback 之前执行,因此你可以放心地拦截任意资源请求而不必担心被内置逻辑短路;同时若你的中间件消费了请求,记得不要调用next(),否则请求会继续流向 fallback 逻辑。

四、HMR 配置:server.hmr

HMR 的开关与连接参数位于server.hmr,既可以是布尔值,也可以是对象:

import { defineConfig } from '@farmfe/core'; export default defineConfig({ server: { hmr: { // Configure the port for web socket listening port: 9802, // Configure the host for web socket listening host: 'localhost', // The path for the HMR endpoint path: '/__hmr', } //... } });
  • 默认值:start命令下为true,其他命令(如build)下为false。开启后,Farm 会监听参与编译的模块变化,模块变更时自动触发重编译,并把结果推送到 Farm Runtime 进行无刷新更新。

各子项参数(类型定义见 HmrOptions,默认值见 constants.ts):

参数默认值说明
hmr.portundefined(回退到FARM_DEFAULT_HMR_PORT环境变量,再回退到 DevServer 端口)WebSocket 服务监听的端口
hmr.hostlocalhostWebSocket 服务监听的 host
hmr.clientPort9000HMR 客户端连接使用的端口,反代场景下与真实服务端口不一致时有用
hmr.path'/__hmr'HMR WebSocket 端点的 URL 路径
hmr.timeout0HMR WebSocket 连接的超时时间(毫秒)
hmr.overlaytrueHMR 出错时是否在浏览器中显示错误浮层
hmr.protocol''(自动检测)WebSocket 协议,如'ws'或'wss'

HMR 端口自动同步机制

一个容易踩坑的点是 HMR 端口与 DevServer 端口的联动。源码 resolve-server.ts 中,normalizeDevServerConfig会用server.host/server.port的默认值合并进hmr配置;而 server/index.ts 的shouldAutoSyncHmrPort与syncServerPort则保证:当实际监听端口因冲突被递增后,HMR 端口(以及注入到客户端的FARM_HMR_PORT编译期变量)会被同步更新。如果你显式指定了hmr.port且与server.port不同,Farm 会将其视为独立端口,不随服务端口漂移。

HMR 的运行时链路

从源码看,一次完整的 HMR 更新经过以下调用链:

  1. 文件监听:Server.#createWatcher(server/index.ts)订阅 watcher 的add/unlink/change事件,其中change事件调用hmrEngine.hmrUpdate(item);
  2. 增量重编译:HmrEngine 的hmrUpdate会做文件 mtime 去重,recompileAndSendResult调用compiler.update(queue)执行增量编译,并打印xxx updated in 12ms这类日志;
  3. WebSocket 广播:把{added, changed, removed, immutableModules, mutableModules, boundaries}等结果以farm-update消息广播给所有ws.wss.clients;
  4. 客户端更新:浏览器端 Runtime 与框架插件(React Refresh、@vitejs/plugin-vue等)根据更新边界决定是无刷新替换、局部刷新还是整页 reload。

此外,watcher 还处理了unlink事件的特殊逻辑:当被删除的文件仍被引用时,会额外把父级文件标记为updated并强制推送(server/index.ts,对应修复 issue #2035 的注释);配置文件的变更则会触发restartServer()自动重启整个 DevServer。

五、Vite 兼容的 HMR API(import.meta.hot)

Farm 提供了 Vite 兼容的 HMR API,通过import.meta.hot暴露,供框架作者精确地更新 Application 实例而无需整页刷新。对于应用开发者:

  • React:官方插件 @farmfe/plugin-react 自动启用React Refresh;
  • Vue、Solid 等框架:由各自插件(@vitejs/plugin-vue、vite-plugin-solid等)提供 HMR 支持;
  • 通过 create-farm 创建的项目已预置全部 HMR 能力,开箱即用。

如果你需要编写自定义更新逻辑,完整 API 面如下:

export interface ViteHotContext { readonly data: any; accept(): void; accept(cb: (mod: ModuleNamespace | undefined) => void): void; accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void; accept( deps: readonly string[], cb: (mods: Array<ModuleNamespace | undefined>) => void ): void; acceptExports( exportNames: string | readonly string[], cb?: (mod: ModuleNamespace | undefined) => void ): void; dispose(cb: (data: any) => void): void; prune(cb: (data: any) => void): void; invalidate(message?: string): void; on<T extends string>(event: T, cb: (payload: InferCustomEventPayload<T>) => void): void; off<T extends string>(event: T, cb: (payload: InferCustomEventPayload<T>) => void): void; send<T extends string>(event: T, data?: InferCustomEventPayload<T>): void; }

使用要点(完整示例见 examples/hmr 演示项目):

  • 必须条件守卫:HMR 只在开发模式生效,所有调用必须包在if (import.meta.hot) { ... }中;
  • TypeScript 提示:在env.d.ts中写入/// <reference types="@farmfe/core/client" />即可获得import.meta.hot的类型;
  • accept()/accept(cb):自接受更新;accept(deps, cb)接受依赖变更而不重载自身;
  • dispose(cb):清理上一实例的持久副作用;prune(cb):模块从页面移除时清理(Farm 当前用于 CSS 导入的移除,与 Vite 行为一致);
  • data:在同一路径模块的不同实例间持久传递数据;
  • invalidate(message?):自接受模块在运行时判断无法安全更新时,强制把更新向上传播给 importers;
  • on/off/send:自定义 HMR 事件通信,客户端send('event-name', data)后,服务端可通过server.ws.on('event-name', (data) => {})接收;
  • 兼容性说明:acceptExports目前存在于类型表面,但 Farm 运行时视为不支持(仅打印 debug 信息)。

值得一提的是,服务端还内置了与 Vite 生态的互通桥:#invalidateVite 监听vite:invalidate事件,将其转换为 Farm 内部的父文件更新并走hmrUpdate强制推送,保证 Vite 插件生态的失效通知在 Farm 中同样生效。

六、Preview Server:server.preview

farm preview用于在本地预览构建产物,相关选项嵌套在server.preview下(完整文档见 Dev Server Options):

export interface UserPreviewServerConfig { headers?: OutgoingHttpHeaders | false | undefined; host?: string | boolean; port?: number; strictPort?: boolean; https?: SecureServerOptions; distDir?: string; open?: boolean | string; cors?: boolean | any; proxy?: Record<string, ProxyOptions>; middlewares?: PreviewServerMiddleware[]; }

关键默认值:preview.port为1911,preview.host为'localhost',preview.distDir默认为'dist'(未显式指定时从compilation.output.path解析,相对路径相对root)。headers、https、proxy默认继承自server.*同名配置,设为false可显式关闭;middlewares与server.middlewares用法类似。

七、编程式创建 DevServer

如果你正在基于 Farm 构建自己的工具,可以绕过 CLI,通过 JavaScript API 以编程方式创建 DevServer。@farmfe/core导出createServer(入口见 src/index.ts),内部即调用上文分析的Server.createServer(inlineConfig),随后调用server.listen()启动监听与首次编译。这让你可以在自己的进程中完全控制配置注入、生命周期与自定义中间件的挂载。

小结

  • Farm DevServer 的全部配置收敛在server字段下,默认端口9000、strictPort: false(冲突自动换端口)、appType: 'spa'(自动 HTML fallback)、资源默认内存提供(writeToDisk: false);
  • server.middlewares采用 connect 风格两段式签名,可直接复用社区中间件,且位于内置资源中间件之后、HTML fallback 之前;
  • HMR 通过server.hmr配置端口、host、路径与 overlay 等行为,默认开启并随 DevServer 端口自动同步,更新链路为 watcher → HmrEngine 增量编译 → WebSocket 广播 → 客户端 Runtime/框架插件应用;
  • import.meta.hotAPI 与 Vite 对齐,配合 React Refresh 等官方/社区插件可实现无刷新热更新;server.preview则为farm preview提供独立的预览服务配置。

延伸阅读:Dev Server Options 完整配置、HMR API、examples/hmr 演示项目。

  • 前端构建
  • 构建工具
  • 开发工具

【免费下载链接】farm

Extremely fast Vite-compatible web build tool written in Rust

项目地址:https://gitcode.com/gh_mirrors/fa/farm
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询