- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
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() ] }, });内置中间件与用户中间件的执行顺序
从 初始化中间件的源码 可以看到完整挂载顺序:
hmrPingMiddleware—— 处理Accept: text/x-farm-ping的心跳请求,直接返回 204,实现见 hmrPing.ts;corsMiddleware(若启用cors);proxyMiddleware(若配置了proxy);publicPathMiddleware(publicPath 非/时);publicMiddleware(public 目录存在时);lazyCompilationMiddleware(启用懒编译时);staticMiddleware、outputFilesMiddleware(_output_files资源树查询);resourceDiskMiddleware或resourceMiddleware(取决于writeToDisk);- 插件
configureServer钩子返回的清理函数; - 用户的
server.middlewares; 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.port | undefined(回退到FARM_DEFAULT_HMR_PORT环境变量,再回退到 DevServer 端口) | WebSocket 服务监听的端口 |
hmr.host | localhost | WebSocket 服务监听的 host |
hmr.clientPort | 9000 | HMR 客户端连接使用的端口,反代场景下与真实服务端口不一致时有用 |
hmr.path | '/__hmr' | HMR WebSocket 端点的 URL 路径 |
hmr.timeout | 0 | HMR WebSocket 连接的超时时间(毫秒) |
hmr.overlay | true | HMR 出错时是否在浏览器中显示错误浮层 |
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 更新经过以下调用链:
- 文件监听:
Server.#createWatcher(server/index.ts)订阅 watcher 的add/unlink/change事件,其中change事件调用hmrEngine.hmrUpdate(item); - 增量重编译:HmrEngine 的
hmrUpdate会做文件 mtime 去重,recompileAndSendResult调用compiler.update(queue)执行增量编译,并打印xxx updated in 12ms这类日志; - WebSocket 广播:把
{added, changed, removed, immutableModules, mutableModules, boundaries}等结果以farm-update消息广播给所有ws.wss.clients; - 客户端更新:浏览器端 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
相关推荐
Snowpack HMR 完全指南:ESM-HMR API、Fast Refresh 与热更新实现原理
Snowpack HMR 完全指南:ESM HMR API、Fast Refresh 与热更新实现原理 Snowpack 通过 esm hmr https://
前端开发工具前端构建Farm 运行时 HMR 插件(@farmfe/runtime-plugin-hmr)源码解析:Vite 兼容热更新客户端的原理与实战
Farm 运行时 HMR 插件(@farmfe/runtime plugin hmr)源码解析:Vite 兼容热更新客户端的原理与实战 本篇技术指南以 Farm
前端构建构建工具开发工具能在 PC 上流畅运行 Switch 游戏吗?yuzu 模拟器安装、配置与调优完整指南
能在 PC 上流畅运行 Switch 游戏吗?yuzu 模拟器安装、配置与调优完整指南 yuzu 模拟器是一个用 C++ 编写的开源 Nintendo Swit
虚拟化图形学逆向工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考