昨天整理电脑里的 PHP 项目时,顺手刷了一眼 FrankenPHP 的发布页,发现官方挂出了 Windows 原生的 zip 包。说实话,我第一反应是去看发布日期,确认不是旧文件——因为等这个“原生支持 Windows”的版本,真的等太久了。先给不太熟悉的朋友说下背景:FrankenPHP 是一个基于 Caddy 的应用服务器,把 PHP 运行时直接“缝”进 Go 写的 Web 服务器里,一个二进制文件就能提供静态文件服务、动态 PHP 解析、HTTPS 证书自动管理,还支持 worker 模式让 PHP 脚本常驻内存。以前官方只提供 Linux 和 macOS 的构建,Windows 用户想体验得绕道 WSL 或 Docker Desktop。现在原生版本来了,这篇文章就聊聊它到底怎么装、怎么跑、有什么坑,以及划不划算把手头项目迁过去。1. FrankenPHP 到底是什么:一个 Go 写的“科学怪人”应用服务器1.1 “缝合怪”这个名字怎么来的FrankenPHP 这名字摆明了是在玩 Frankenstein 的梗——把 PHP 和 Go 生态缝到一起,造出一个“科学怪人”。它不是一个全新的编程语言,也不是什么框架,而是一个应用服务器:底层是 Caddy 2,一个老牌的 Go HTTP 服务器,默认支持 HTTPS、HTTP/2、HTTP/3;上面直接嵌入了 PHP 运行时。用一句话概括:Nginx + PHP-FPM + Supervisor + 自动 HTTPS 这一整套运维组合,在 FrankenPHP 这里被压缩成一个进程、一个可执行文件。对习惯了“装一个 Nginx,再配一个 PHP-FPM,还要用 systemd 或 supervisord 拉起来”的人来说,这东西的部署心智负担会小非常多。1.2 它到底解决了什么问题传统 PHP 架构的流程是:Nginx 收到请求,解析出 PHP 文件路径,转发给 PHP-FPM;PHP-FPM 从进程池里拉一个空闲 worker,worker 读取并执行 PHP 文件,把结果返回给 Nginx。这套架构本身很成熟,但它有几个常年被人诟病的点:每个请求都要重新加载 PHP 文件、重新初始化框架。即使有 OPcache 兜底,框架里的服务容器、路由表、配置解析还是得再来一遍。并发一上去,PHP-FPM 的进程池要调参,慢日志、超时、最大子进程数都是经验活。HTTPS 证书从申请到续期,总得配 certbot 或者手动上传,尤其是内网环境更麻烦。FrankenPHP 的解法很直接:Caddy 本身就能托管静态文件和反向代理,并且原生支持自动 HTTPS;把 PHP 编译进去之后,它又在 Caddy 里加了一层 PHP 执行引擎。更狠的是 worker 模式——PHP 脚本可以启动后常驻内存,请求来的时候由 Caddy 通过内部通道把请求交给常驻进程处理,处理完不退出,等着下一个请求。这相当于把传统 FPM 的“进程池”变成了“常驻内存队列”。1.3 什么时候值得用它从实际使用经验来看,它最适合这么几类场景:本地开发环境:不用再装一堆软件联动,一个 exe 就够,支持自动生成 localhost 证书。中小型 Web 服务:并发量在几百到几千的 API 或后台系统,worker 模式能明显压住 CPU 开销。内网工具、微服务切片:项目很多但每个都很小,用 Caddyfile 一段配置起一个站点,比维护 N 套 Nginx 配置省心。当然它也有不擅长的地方:如果你的生产环境是共享虚拟主机,只能用别人装好的 Apache + mod_php,那 FrankenPHP 对你没什么意义;如果项目重度依赖 Swoole、RoadRunner 这一层已经解决了常驻内存问题的方案,也没必要非要迁移。不过在 Windows 原生支持落地之后,它在“开发体验”这一项上的竞争力明显上来了。2. Windows 想跑 FrankenPHP,之前的路径有多绕2.1 WSL 方案的几个核心痛点以前想在 Windows 上跑 FrankenPHP,首选方案是 WSL。WSL 里跑 Linux 版二进制确实能跑通,但真实开发体验一言难尽。最折磨人的是跨文件系统 IO。项目放在/mnt/c/下面,Composer 安装依赖的时候,那个速度慢到你可以泡完一杯茶再回来;把项目放在 WSL 内部文件系统里,速度是快了,但你用 Windows 侧编辑器直接编辑文件又会出现各种权限和路径转换问题,必须装 WSL 插件,提交 Git 时还得注意换行符。一套组合拳下来,光环境折腾就能消耗半天。2.2 Docker Desktop 的别扭之处Docker Desktop 是另一条常见路线。它本质上是在 Windows 里起一个虚拟机,跑 Linux 容器,然后把容器里的端口映射到宿主机。问题是这个虚拟机的资源占用很现实,8G 内存的电脑开个 Docker Desktop 再加 IDE,风扇就开始狂转。更麻烦的是每次端口映射、目录挂载、证书导入都要额外配置,调试 PHP 断点还得处理容器网络。对一个只想“本地跑起来看看效果”的场景来说,太重了。2.3 官方原生支持到底解决了什么这次官方 Windows 构建的意义不是“多了一个平台能跑”,而是把体验抹平了:提供 zip 包直接解压运行,不需要额外安装 PHP,包里已经带了一套编译好的 PHP 运行时。Caddyfile 的写法跟 Linux 完全一致,不再因为环境差异产生配置漂移。Caddy 自动生成的本地 HTTPS 证书在 Windows 上也会自动写入证书存储,访问 https://localhost 的阻力和 Linux 一样小。以前开发者社区里有人用 msys2 或自己编译折腾 Windows 构建,但版本跟不上上游,踩坑了也没处问。现在官方包滚动更新,issue 里 Windows 相关的问题被维护者认真对待,这个信号本身就很重要。我把几种方案的体验差异整理成了表格,方便直观对比:维度WSL 方案Docker Desktop 方案官方 Windows 原生版启动耗时打开 WSL 实例较慢软件整体启动很慢毫秒级直接起进程内存占用中,取决于发行版偏高,虚拟机固定开销低,仅一个进程跨盘 IO/mnt/c极慢,项目放 WSL 内又别扭挂载卷有额外开销原生 NTFS,无中间层配置复杂度中,路径、换行符让人头疼高,端口、卷、网络都要管低,zip 解压就可用HTTPS 便利度证书生成没问题,但要在 Windows 侧信任证书要手动挂载导入自动写入本机信任区适合场景熟悉 Linux 且愿意折腾需要完整 Linux 环境做联调日常本地开发、轻量部署3. 在 Windows 上装好并跑起第一个 PHP 页面3.1 下载发布包解压去 FrankenPHP 的 GitHub Releases 页面找带 windows 字样的 zip 包。注意区分 x86_64 和 arm64,现在基本默认选 x86_64。下载完不要放在太深的带空格目录,我的习惯是解压到 D 盘根目录下单独建一个文件夹,比如D:\tools\frankenphp。解压后你会看到主程序 exe 和一些依赖 DLL,还有一个带有 PHP 二进制和扩展库的目录。这个包是“开箱即用”的,不需要你再去 php.net 下载 PHP,也不用手动配置php.ini——重要的事说三遍:不要手动配 php.ini,它自己带了一套。如果你想确认自带的 PHP 版本信息,可以在命令行切到解压目录,先看看是否有 php 命令的可执行文件,或者直接拿 Caddyfile 跑一个空站点输出 phpinfo。3.2 写一个最小可用的 Caddyfile在解压目录(或你自己的项目目录)下新建一个文本文件,命名为Caddyfile,内容如下:{ frankenphp } http://localhost:8080 { root * C:/code/franken-demo php_server }解释一下每行作用。第一段全局配置里声明了frankenphp,告诉 Caddy 要启用这个模块;第二段是一个站点块,监听http://localhost:8080,避免默认占用 80/443 触发管理员权限问题;root指向你的项目根目录,注意路径里的斜杠必须用正斜杠,这在 Windows 上是个很容易踩的坑;php_server是 FrankenPHP 提供的指令,会自动完成静态文件服务和 PHP 解析的兜底路由。如果你的项目入口不是index.php,或者你要显式隐藏 PHP 后缀,应该改成:{ frankenphp } http://localhost:8080 { root * C:/code/franken-demo php_server { root C:/code/franken-demo/public } }这样请求进来会优先匹配public目录下的文件,找不到静态资源时再走 PHP 入口。3.3 启动并验证打开 PowerShell 或 CMD,切到解压目录,直接执行:frankenphp.exe run看到控制台输出 Caddy 的启动日志,并且没有报错退出,就说明服务已经起来了。浏览器访问http://localhost:8080,如果你的franken-demo目录下放了一个index.php文件,内容可以是最简单的:<?php phpinfo();如果页面出现了 PHP 配置信息表格,说明 FrankenPHP 已经从“能启动”到了“能跑 PHP 代码”的阶段。这一步走通,后续所有玩法都建立在这个基础上。3.4 worker 模式怎么开启普通php_server模式每个请求走完就结束生命周期,跟传统 PHP 行为一致。worker 模式的核心区别是进程不退出,处理完当前请求继续待命,从而省掉重复初始化的开销。配置写法如下:{ frankenphp } http://localhost:8080 { root * C:/code/franken-demo php_server { worker C:/code/franken-demo/public/index.php 2 } }worker后面第一个参数是入口脚本的绝对路径,第二个参数是启动几个 worker 进程。强烈建议第一次接触的时候先别把项目直接切到 worker 模式,而是先跑普通模式,确认逻辑没问题,再改配置加 worker。因为常驻内存会放大很多隐性问题,后面专门有一节讲这些坑。3.5 检查 PHP 扩展是否与 Windows 版兼容Windows 版和 Linux 版 PHP 在扩展层有一个显著差异:有些扩展在 Windows 上没有预编译版本,典型的就是pcntl、posix这类 Unix 系统调用相关扩展。worker 模式虽然常驻内存,但它不是基于pcntl_fork实现的,所以不会因为缺pcntl而跑不起来;但如果你的项目用到了这一类扩展,Windows 原生版确实没有对应能力。判断方法很简单:在项目里建一个临时文件,内容<?php print_r(get_loaded_extensions()); ?>,访问后肉眼扫一遍,对比官方文档里的 Windows 扩展支持列表,心里就有数了。4. Windows 上跑通之后的五个真实坑4.1 端口被 IIS 或其他服务占用,启动直接崩FrankenPHP 默认监听 80 和 443,而 Windows 上 80 端口经常被系统自带的 IIS、其他开发服务器或各种“网络加速软件”占用。启动时如果看到类似address already in use的日志,先用命令行查端口占用:netstat -ano | findstr :80 netstat -ano | findstr :443最后一列是进程 PID,再用tasklist | findstr <PID>查出是谁占的。开发环境最省事的解法是把站点监听改成高阶端口,比如我上面的配置就用了http://localhost:8080。注意 Caddyfile 里站点地址一旦写了明确的端口,就不会再试图去占 80/443。还有一个隐藏点:非管理员权限下绑定 80/443 经常直接失败。如果你确实需要在标准端口跑,用管理员身份打开 PowerShell 再执行frankenphp.exe run,否则你会看到一个连不上、说不出道理的报错。4.2 SmartScreen 和杀毒软件把 exe 报毒刚发布的 Windows 二进制没有签名,Windows Defender 的 SmartScreen 很可能会在第一次运行时弹出警告,有些杀毒软件甚至会直接隔离。这不是 FrankenPHP 特有的,任何新出品的开源工具在 Windows 上都会经历这个阶段。处理办法是先在发布页核对一下 zip 包的 SHA256 校验值,官方提供了 checksum,核对无误后,右键 exe → 属性 → 勾选“解除锁定”,或者把解压目录加入 Defender 排除项。不要一看到弹窗就 running,也别盲目关掉整个系统的安全防护。核对 checksum 这个动作本身,比什么第三方“安全检测”都靠谱。4.3 worker 模式下 Ctrl+C 结束服务,PHP 子进程可能残留普通模式下 Ctrl+C 结束 frankenphp.exe,子进程会跟着退出。但 worker 模式有时会有子进程没退干净,表现为再次启动时端口被占,或者旧进程还在运行。因为 Windows 对进程组的信号处理跟 Linux 不一样,Ctrl+C 不一定能完整传导给所有子进程。我的排查习惯是:tasklist | findstr php tasklist | findstr frankenphp发现残留进程后,用taskkill /F /PID <PID>杀干净。为了避免每次都要手敲,我在项目目录放了一个restart.bat:@echo off taskkill /F /IM php.exe 2>nul taskkill /F /IM frankenphp.exe 2>nul timeout /t 1 >nul frankenphp.exe run开发机用这种暴力重启脚本没什么心理负担,反正本地数据都是可重建的。但生产环境万万不能这样搞,生产环境请交给正规的 Windows 服务托管方案。4.4 Caddyfile 里路径分隔符和中文目录问题Caddyfile 在 Windows 上解析路径时,反斜杠很容易出问题。比如:root * C:\code\franken-demo # 不推荐 root * C:/code/franken-demo # 推荐原因很简单:Caddy 配置解析器对反斜杠有转义处理,某些版本里\c会被当成转义字符解析,造成路径完全对不上。此外,中文目录名、带空格的目录名在访问时也可能触发编码问题,不是绝对不能碰,但为了少踩一个没必要的坑,建议项目目录统一用纯英文路径。4.5 本地 HTTPS 证书的首次信任问题Caddy 访问https://localhost时,会自动生成一个本地 CA,并把这个 CA 写入系统证书存储。大多数情况下浏览器直接就能通过验证。但如果你用的浏览器是启动较早的进程,或者安全策略比较严格,第一次访问 https 页面仍会弹警告。处理方式是在浏览器地址栏点“高级” → “继续访问”。如果你在命令行工具里拉取 HTTPS 接口报证书错误,检查一下 localhost 证书有没有被当前用户信任。这个坑不深,但第一次遇到时容易误判成 FrankenPHP 启动失败。5. 跟 WSL、Docker 和 XAMPP 实测对比,结论很实在5.1 一组直观的开发体验对比我在同一台 Windows 机器上先后试过四种方案:官方原生版、WSL 版、Docker 版、传统 XAMPP。测试项目是一个结构比较标准的 PHP 项目,包含 Composer 依赖、若干路由、一个 MySQL 连接。结论如下:方案启动时间静态资源响应PHP 响应重启代码生效时间日常开发综合体验XAMPP启动 Apache 较慢快一般,单线程模型刷新即生效老牌但调优老旧Docker Desktop软件启动很慢一般有中间层开销改代码基本即时,但日志在容器里配置烦恼最多WSL 原生版WSL 启动慢/mnt/c下明显慢在 WSL 内部放项目则快跨盘编辑有延迟适合 Linux 重度用户FrankenPHP Windows 原生启动瞬时快普通模式与 FPM 相当,worker 模式更快刷新即生效,worker 模式需重启最顺,基本无感知这里有一个细节值得展开:worker 模式下因为进程常驻,改完代码必须重启 FrankenPHP 才能让改动生效。普通模式没有这个问题,刷新页面就是新代码。所以你要是不是为了压测性能,开发调试阶段用普通模式就够了,等要验证 worker 模式下的并发能力再切换。5.2 为什么本地开发场景“原生版”完胜本地开发最看重的不是极限并发,而是三个字:不折腾。原生版启动快、内存占用低、没有虚拟机网络层、不需要额外挂载目录,而且 Caddyfile 配置和 Linux 生产环境完全一致。你可以在本地跑通的环境,原样丢到一台 Linux 服务器上,几乎不会因为环境差异翻车。这一点对我来说价值最大,因为很多坑就是“本地能跑,上服务器就挂”,两边配置不一致导致的。5.3 哪些人仍然应该选 WSL 或 Docker做一个项目要依赖 Redis、MySQL 多个中间件,并且团队其他人的环境是 Linux 的时候,Docker Compose 仍然是更稳的选择,毕竟一个编排文件拉起整套依赖比一个个装方便太多。如果你正在写 Composer 扩展或者给 Linux 应用做底层调试,WSL 依然有不可替代的价值。FrankenPHP 原生版解决的是“Web 服务 + PHP 运行时”这一层,它不代表你不需要数据库、队列和缓存。6. 把现有项目迁过来:多站点配置与几个关键原则6.1 一个 Caddyfile 跑多个项目开发机通常不止一个项目要起。用 Caddyfile 组织多个站点很简单,直接在同一个文件里写多个站点块:{ frankenphp } http://localhost:8081 { root * C:/code/project-a php_server } http://localhost:8082 { root * C:/code/project-b php_server }每个站点块就是一个独立项目。这样做的优势是只用一个进程,配置都在一个文件里,启动时一次性都拉起来,配合自动 HTTPS,不用再访问什么http://localhost:8080还得手动切来切去。如果你项目里用到了环境变量,可以在全局块里加:{ frankenphp # 设置整个服务进程的全局环境变量 env APP_ENV dev }不过注意全局 env 对所有站点生效,不同项目需要不同环境变量时,最好在站点块里用单独配置,或者在 PHP 代码里基于请求 Host 判断环境。6.2 worker 模式的全局状态坑,一定要心里有数worker 模式最吸引人,也最容易埋雷。普通模式下,每次请求结束 PHP 进程的相关资源都会被回收,你的全局变量、静态变量、单例对象,下个请求来的时候都是全新一份。worker 模式下,进程不退出,这些状态会被下一个请求继续复用。听起来省事,但对老代码是灾难:某段代码里static $cache = []想用来做请求内缓存,结果它跨请求保留了,数据一多内存膨胀。某个全局变量在脚本里被随意赋值,下一个请求拿到的可能是上一个请求残留的脏数据。数据库连接、文件句柄没有及时释放的话,长时间运行后连接数会异常堆积。所以我的建议是:迁移现有项目时,先默认普通模式跑一周,观察日志和内存曲线。确认代码里没有明显的全局状态依赖闭环,再切换 worker 模式。新项目从第一行代码就按 worker 模式约束来写,反而没那么多坑。6.3 什么场景暂时别迁不是所有项目都适合立刻迁到 FrankenPHP。如果你现有项目用了大量.htaccess重写规则,而这些规则依赖 Apache 的mod_rewrite特定行为,迁移时需要逐条翻译成 Caddyfile 的try_files或handle_errors,工作量和风险都不小。如果团队运维完全不熟悉 Caddyfile,宁可先在开发环境用起来,生产环境继续维持现有架构。还有一个容易被忽略的点:你的项目是否依赖 Windows 上的 IIS 扩展或 Windows 特有的 PHP 扩展。如果目标部署机是 Windows Server,又用到了某个只有官方 Windows 包才有的扩展,那迁移前先确认这个扩展在新版 PHP 运行时里依然可用,否则开工到一半发现缺扩展,收不了场。说回我自己的使用习惯。现在这台 Windows 开发机上,已经默认用 FrankenPHP 替代了 php -S 和 XAMPP,日常开发小项目基本是:Caddyfile 写一段、启动、刷新页面,没了。最明显的感觉是重启一次服务不再是“开一整套环境”的心理负担。如果你也经常在 Windows 上给 PHP 项目写代码,建议下一次重建本地环境时试试这个组合——先用普通模式跑通,再慢慢摸 worker 模式,整个过程可能比你想的还要短。
昨天整理电脑里的 PHP 项目时,顺手刷了一眼 FrankenPHP 的发布页,发现官方挂出了 Windows 原生的 zip 包。说实话,我第一反应是去看发布日期,确认不是旧文件——因为等这个“原生支持 Windows”的版本,真的等太久了。先给不太熟悉的朋友说下背景:FrankenPHP 是一个基于 Caddy 的应用服务器,把 PHP 运行时直接“缝”进 Go 写的 Web 服务器里,一个二进制文件就能提供静态文件服务、动态 PHP 解析、HTTPS 证书自动管理,还支持 worker 模式让 PHP 脚本常驻内存。以前官方只提供 Linux 和 macOS 的构建,Windows 用户想体验得绕道 WSL 或 Docker Desktop。现在原生版本来了,这篇文章就聊聊它到底怎么装、怎么跑、有什么坑,以及划不划算把手头项目迁过去。1. FrankenPHP 到底是什么:一个 Go 写的“科学怪人”应用服务器1.1 “缝合怪”这个名字怎么来的FrankenPHP 这名字摆明了是在玩 Frankenstein 的梗——把 PHP 和 Go 生态缝到一起,造出一个“科学怪人”。它不是一个全新的编程语言,也不是什么框架,而是一个应用服务器:底层是 Caddy 2,一个老牌的 Go HTTP 服务器,默认支持 HTTPS、HTTP/2、HTTP/3;上面直接嵌入了 PHP 运行时。用一句话概括:Nginx + PHP-FPM + Supervisor + 自动 HTTPS 这一整套运维组合,在 FrankenPHP 这里被压缩成一个进程、一个可执行文件。对习惯了“装一个 Nginx,再配一个 PHP-FPM,还要用 systemd 或 supervisord 拉起来”的人来说,这东西的部署心智负担会小非常多。1.2 它到底解决了什么问题传统 PHP 架构的流程是:Nginx 收到请求,解析出 PHP 文件路径,转发给 PHP-FPM;PHP-FPM 从进程池里拉一个空闲 worker,worker 读取并执行 PHP 文件,把结果返回给 Nginx。这套架构本身很成熟,但它有几个常年被人诟病的点:每个请求都要重新加载 PHP 文件、重新初始化框架。即使有 OPcache 兜底,框架里的服务容器、路由表、配置解析还是得再来一遍。并发一上去,PHP-FPM 的进程池要调参,慢日志、超时、最大子进程数都是经验活。HTTPS 证书从申请到续期,总得配 certbot 或者手动上传,尤其是内网环境更麻烦。FrankenPHP 的解法很直接:Caddy 本身就能托管静态文件和反向代理,并且原生支持自动 HTTPS;把 PHP 编译进去之后,它又在 Caddy 里加了一层 PHP 执行引擎。更狠的是 worker 模式——PHP 脚本可以启动后常驻内存,请求来的时候由 Caddy 通过内部通道把请求交给常驻进程处理,处理完不退出,等着下一个请求。这相当于把传统 FPM 的“进程池”变成了“常驻内存队列”。1.3 什么时候值得用它从实际使用经验来看,它最适合这么几类场景:本地开发环境:不用再装一堆软件联动,一个 exe 就够,支持自动生成 localhost 证书。中小型 Web 服务:并发量在几百到几千的 API 或后台系统,worker 模式能明显压住 CPU 开销。内网工具、微服务切片:项目很多但每个都很小,用 Caddyfile 一段配置起一个站点,比维护 N 套 Nginx 配置省心。当然它也有不擅长的地方:如果你的生产环境是共享虚拟主机,只能用别人装好的 Apache + mod_php,那 FrankenPHP 对你没什么意义;如果项目重度依赖 Swoole、RoadRunner 这一层已经解决了常驻内存问题的方案,也没必要非要迁移。不过在 Windows 原生支持落地之后,它在“开发体验”这一项上的竞争力明显上来了。2. Windows 想跑 FrankenPHP,之前的路径有多绕2.1 WSL 方案的几个核心痛点以前想在 Windows 上跑 FrankenPHP,首选方案是 WSL。WSL 里跑 Linux 版二进制确实能跑通,但真实开发体验一言难尽。最折磨人的是跨文件系统 IO。项目放在/mnt/c/下面,Composer 安装依赖的时候,那个速度慢到你可以泡完一杯茶再回来;把项目放在 WSL 内部文件系统里,速度是快了,但你用 Windows 侧编辑器直接编辑文件又会出现各种权限和路径转换问题,必须装 WSL 插件,提交 Git 时还得注意换行符。一套组合拳下来,光环境折腾就能消耗半天。2.2 Docker Desktop 的别扭之处Docker Desktop 是另一条常见路线。它本质上是在 Windows 里起一个虚拟机,跑 Linux 容器,然后把容器里的端口映射到宿主机。问题是这个虚拟机的资源占用很现实,8G 内存的电脑开个 Docker Desktop 再加 IDE,风扇就开始狂转。更麻烦的是每次端口映射、目录挂载、证书导入都要额外配置,调试 PHP 断点还得处理容器网络。对一个只想“本地跑起来看看效果”的场景来说,太重了。2.3 官方原生支持到底解决了什么这次官方 Windows 构建的意义不是“多了一个平台能跑”,而是把体验抹平了:提供 zip 包直接解压运行,不需要额外安装 PHP,包里已经带了一套编译好的 PHP 运行时。Caddyfile 的写法跟 Linux 完全一致,不再因为环境差异产生配置漂移。Caddy 自动生成的本地 HTTPS 证书在 Windows 上也会自动写入证书存储,访问 https://localhost 的阻力和 Linux 一样小。以前开发者社区里有人用 msys2 或自己编译折腾 Windows 构建,但版本跟不上上游,踩坑了也没处问。现在官方包滚动更新,issue 里 Windows 相关的问题被维护者认真对待,这个信号本身就很重要。我把几种方案的体验差异整理成了表格,方便直观对比:维度WSL 方案Docker Desktop 方案官方 Windows 原生版启动耗时打开 WSL 实例较慢软件整体启动很慢毫秒级直接起进程内存占用中,取决于发行版偏高,虚拟机固定开销低,仅一个进程跨盘 IO/mnt/c极慢,项目放 WSL 内又别扭挂载卷有额外开销原生 NTFS,无中间层配置复杂度中,路径、换行符让人头疼高,端口、卷、网络都要管低,zip 解压就可用HTTPS 便利度证书生成没问题,但要在 Windows 侧信任证书要手动挂载导入自动写入本机信任区适合场景熟悉 Linux 且愿意折腾需要完整 Linux 环境做联调日常本地开发、轻量部署3. 在 Windows 上装好并跑起第一个 PHP 页面3.1 下载发布包解压去 FrankenPHP 的 GitHub Releases 页面找带 windows 字样的 zip 包。注意区分 x86_64 和 arm64,现在基本默认选 x86_64。下载完不要放在太深的带空格目录,我的习惯是解压到 D 盘根目录下单独建一个文件夹,比如D:\tools\frankenphp。解压后你会看到主程序 exe 和一些依赖 DLL,还有一个带有 PHP 二进制和扩展库的目录。这个包是“开箱即用”的,不需要你再去 php.net 下载 PHP,也不用手动配置php.ini——重要的事说三遍:不要手动配 php.ini,它自己带了一套。如果你想确认自带的 PHP 版本信息,可以在命令行切到解压目录,先看看是否有 php 命令的可执行文件,或者直接拿 Caddyfile 跑一个空站点输出 phpinfo。3.2 写一个最小可用的 Caddyfile在解压目录(或你自己的项目目录)下新建一个文本文件,命名为Caddyfile,内容如下:{ frankenphp } http://localhost:8080 { root * C:/code/franken-demo php_server }解释一下每行作用。第一段全局配置里声明了frankenphp,告诉 Caddy 要启用这个模块;第二段是一个站点块,监听http://localhost:8080,避免默认占用 80/443 触发管理员权限问题;root指向你的项目根目录,注意路径里的斜杠必须用正斜杠,这在 Windows 上是个很容易踩的坑;php_server是 FrankenPHP 提供的指令,会自动完成静态文件服务和 PHP 解析的兜底路由。如果你的项目入口不是index.php,或者你要显式隐藏 PHP 后缀,应该改成:{ frankenphp } http://localhost:8080 { root * C:/code/franken-demo php_server { root C:/code/franken-demo/public } }这样请求进来会优先匹配public目录下的文件,找不到静态资源时再走 PHP 入口。3.3 启动并验证打开 PowerShell 或 CMD,切到解压目录,直接执行:frankenphp.exe run看到控制台输出 Caddy 的启动日志,并且没有报错退出,就说明服务已经起来了。浏览器访问http://localhost:8080,如果你的franken-demo目录下放了一个index.php文件,内容可以是最简单的:<?php phpinfo();如果页面出现了 PHP 配置信息表格,说明 FrankenPHP 已经从“能启动”到了“能跑 PHP 代码”的阶段。这一步走通,后续所有玩法都建立在这个基础上。3.4 worker 模式怎么开启普通php_server模式每个请求走完就结束生命周期,跟传统 PHP 行为一致。worker 模式的核心区别是进程不退出,处理完当前请求继续待命,从而省掉重复初始化的开销。配置写法如下:{ frankenphp } http://localhost:8080 { root * C:/code/franken-demo php_server { worker C:/code/franken-demo/public/index.php 2 } }worker后面第一个参数是入口脚本的绝对路径,第二个参数是启动几个 worker 进程。强烈建议第一次接触的时候先别把项目直接切到 worker 模式,而是先跑普通模式,确认逻辑没问题,再改配置加 worker。因为常驻内存会放大很多隐性问题,后面专门有一节讲这些坑。3.5 检查 PHP 扩展是否与 Windows 版兼容Windows 版和 Linux 版 PHP 在扩展层有一个显著差异:有些扩展在 Windows 上没有预编译版本,典型的就是pcntl、posix这类 Unix 系统调用相关扩展。worker 模式虽然常驻内存,但它不是基于pcntl_fork实现的,所以不会因为缺pcntl而跑不起来;但如果你的项目用到了这一类扩展,Windows 原生版确实没有对应能力。判断方法很简单:在项目里建一个临时文件,内容<?php print_r(get_loaded_extensions()); ?>,访问后肉眼扫一遍,对比官方文档里的 Windows 扩展支持列表,心里就有数了。4. Windows 上跑通之后的五个真实坑4.1 端口被 IIS 或其他服务占用,启动直接崩FrankenPHP 默认监听 80 和 443,而 Windows 上 80 端口经常被系统自带的 IIS、其他开发服务器或各种“网络加速软件”占用。启动时如果看到类似address already in use的日志,先用命令行查端口占用:netstat -ano | findstr :80 netstat -ano | findstr :443最后一列是进程 PID,再用tasklist | findstr <PID>查出是谁占的。开发环境最省事的解法是把站点监听改成高阶端口,比如我上面的配置就用了http://localhost:8080。注意 Caddyfile 里站点地址一旦写了明确的端口,就不会再试图去占 80/443。还有一个隐藏点:非管理员权限下绑定 80/443 经常直接失败。如果你确实需要在标准端口跑,用管理员身份打开 PowerShell 再执行frankenphp.exe run,否则你会看到一个连不上、说不出道理的报错。4.2 SmartScreen 和杀毒软件把 exe 报毒刚发布的 Windows 二进制没有签名,Windows Defender 的 SmartScreen 很可能会在第一次运行时弹出警告,有些杀毒软件甚至会直接隔离。这不是 FrankenPHP 特有的,任何新出品的开源工具在 Windows 上都会经历这个阶段。处理办法是先在发布页核对一下 zip 包的 SHA256 校验值,官方提供了 checksum,核对无误后,右键 exe → 属性 → 勾选“解除锁定”,或者把解压目录加入 Defender 排除项。不要一看到弹窗就 running,也别盲目关掉整个系统的安全防护。核对 checksum 这个动作本身,比什么第三方“安全检测”都靠谱。4.3 worker 模式下 Ctrl+C 结束服务,PHP 子进程可能残留普通模式下 Ctrl+C 结束 frankenphp.exe,子进程会跟着退出。但 worker 模式有时会有子进程没退干净,表现为再次启动时端口被占,或者旧进程还在运行。因为 Windows 对进程组的信号处理跟 Linux 不一样,Ctrl+C 不一定能完整传导给所有子进程。我的排查习惯是:tasklist | findstr php tasklist | findstr frankenphp发现残留进程后,用taskkill /F /PID <PID>杀干净。为了避免每次都要手敲,我在项目目录放了一个restart.bat:@echo off taskkill /F /IM php.exe 2>nul taskkill /F /IM frankenphp.exe 2>nul timeout /t 1 >nul frankenphp.exe run开发机用这种暴力重启脚本没什么心理负担,反正本地数据都是可重建的。但生产环境万万不能这样搞,生产环境请交给正规的 Windows 服务托管方案。4.4 Caddyfile 里路径分隔符和中文目录问题Caddyfile 在 Windows 上解析路径时,反斜杠很容易出问题。比如:root * C:\code\franken-demo # 不推荐 root * C:/code/franken-demo # 推荐原因很简单:Caddy 配置解析器对反斜杠有转义处理,某些版本里\c会被当成转义字符解析,造成路径完全对不上。此外,中文目录名、带空格的目录名在访问时也可能触发编码问题,不是绝对不能碰,但为了少踩一个没必要的坑,建议项目目录统一用纯英文路径。4.5 本地 HTTPS 证书的首次信任问题Caddy 访问https://localhost时,会自动生成一个本地 CA,并把这个 CA 写入系统证书存储。大多数情况下浏览器直接就能通过验证。但如果你用的浏览器是启动较早的进程,或者安全策略比较严格,第一次访问 https 页面仍会弹警告。处理方式是在浏览器地址栏点“高级” → “继续访问”。如果你在命令行工具里拉取 HTTPS 接口报证书错误,检查一下 localhost 证书有没有被当前用户信任。这个坑不深,但第一次遇到时容易误判成 FrankenPHP 启动失败。5. 跟 WSL、Docker 和 XAMPP 实测对比,结论很实在5.1 一组直观的开发体验对比我在同一台 Windows 机器上先后试过四种方案:官方原生版、WSL 版、Docker 版、传统 XAMPP。测试项目是一个结构比较标准的 PHP 项目,包含 Composer 依赖、若干路由、一个 MySQL 连接。结论如下:方案启动时间静态资源响应PHP 响应重启代码生效时间日常开发综合体验XAMPP启动 Apache 较慢快一般,单线程模型刷新即生效老牌但调优老旧Docker Desktop软件启动很慢一般有中间层开销改代码基本即时,但日志在容器里配置烦恼最多WSL 原生版WSL 启动慢/mnt/c下明显慢在 WSL 内部放项目则快跨盘编辑有延迟适合 Linux 重度用户FrankenPHP Windows 原生启动瞬时快普通模式与 FPM 相当,worker 模式更快刷新即生效,worker 模式需重启最顺,基本无感知这里有一个细节值得展开:worker 模式下因为进程常驻,改完代码必须重启 FrankenPHP 才能让改动生效。普通模式没有这个问题,刷新页面就是新代码。所以你要是不是为了压测性能,开发调试阶段用普通模式就够了,等要验证 worker 模式下的并发能力再切换。5.2 为什么本地开发场景“原生版”完胜本地开发最看重的不是极限并发,而是三个字:不折腾。原生版启动快、内存占用低、没有虚拟机网络层、不需要额外挂载目录,而且 Caddyfile 配置和 Linux 生产环境完全一致。你可以在本地跑通的环境,原样丢到一台 Linux 服务器上,几乎不会因为环境差异翻车。这一点对我来说价值最大,因为很多坑就是“本地能跑,上服务器就挂”,两边配置不一致导致的。5.3 哪些人仍然应该选 WSL 或 Docker做一个项目要依赖 Redis、MySQL 多个中间件,并且团队其他人的环境是 Linux 的时候,Docker Compose 仍然是更稳的选择,毕竟一个编排文件拉起整套依赖比一个个装方便太多。如果你正在写 Composer 扩展或者给 Linux 应用做底层调试,WSL 依然有不可替代的价值。FrankenPHP 原生版解决的是“Web 服务 + PHP 运行时”这一层,它不代表你不需要数据库、队列和缓存。6. 把现有项目迁过来:多站点配置与几个关键原则6.1 一个 Caddyfile 跑多个项目开发机通常不止一个项目要起。用 Caddyfile 组织多个站点很简单,直接在同一个文件里写多个站点块:{ frankenphp } http://localhost:8081 { root * C:/code/project-a php_server } http://localhost:8082 { root * C:/code/project-b php_server }每个站点块就是一个独立项目。这样做的优势是只用一个进程,配置都在一个文件里,启动时一次性都拉起来,配合自动 HTTPS,不用再访问什么http://localhost:8080还得手动切来切去。如果你项目里用到了环境变量,可以在全局块里加:{ frankenphp # 设置整个服务进程的全局环境变量 env APP_ENV dev }不过注意全局 env 对所有站点生效,不同项目需要不同环境变量时,最好在站点块里用单独配置,或者在 PHP 代码里基于请求 Host 判断环境。6.2 worker 模式的全局状态坑,一定要心里有数worker 模式最吸引人,也最容易埋雷。普通模式下,每次请求结束 PHP 进程的相关资源都会被回收,你的全局变量、静态变量、单例对象,下个请求来的时候都是全新一份。worker 模式下,进程不退出,这些状态会被下一个请求继续复用。听起来省事,但对老代码是灾难:某段代码里static $cache = []想用来做请求内缓存,结果它跨请求保留了,数据一多内存膨胀。某个全局变量在脚本里被随意赋值,下一个请求拿到的可能是上一个请求残留的脏数据。数据库连接、文件句柄没有及时释放的话,长时间运行后连接数会异常堆积。所以我的建议是:迁移现有项目时,先默认普通模式跑一周,观察日志和内存曲线。确认代码里没有明显的全局状态依赖闭环,再切换 worker 模式。新项目从第一行代码就按 worker 模式约束来写,反而没那么多坑。6.3 什么场景暂时别迁不是所有项目都适合立刻迁到 FrankenPHP。如果你现有项目用了大量.htaccess重写规则,而这些规则依赖 Apache 的mod_rewrite特定行为,迁移时需要逐条翻译成 Caddyfile 的try_files或handle_errors,工作量和风险都不小。如果团队运维完全不熟悉 Caddyfile,宁可先在开发环境用起来,生产环境继续维持现有架构。还有一个容易被忽略的点:你的项目是否依赖 Windows 上的 IIS 扩展或 Windows 特有的 PHP 扩展。如果目标部署机是 Windows Server,又用到了某个只有官方 Windows 包才有的扩展,那迁移前先确认这个扩展在新版 PHP 运行时里依然可用,否则开工到一半发现缺扩展,收不了场。说回我自己的使用习惯。现在这台 Windows 开发机上,已经默认用 FrankenPHP 替代了 php -S 和 XAMPP,日常开发小项目基本是:Caddyfile 写一段、启动、刷新页面,没了。最明显的感觉是重启一次服务不再是“开一整套环境”的心理负担。如果你也经常在 Windows 上给 PHP 项目写代码,建议下一次重建本地环境时试试这个组合——先用普通模式跑通,再慢慢摸 worker 模式,整个过程可能比你想的还要短。