项目打包与部署(完结篇)
这是「从零搭建灌装监控系统」系列第21篇,也是最后一篇。功能写完了,测试过了,但把整个目录拷到现场机器上双击,它不一定跑得起来。这篇把系统打成真正能交付的包:发布配置、自包含运行时、单文件的取舍,以及首次启动必须自动完成的初始化。
拷过去打不开
第一次交付的时候,我把开发机上bin\Debug整个目录拷到 U 盘,拿到现场,双击 exe。
弹了一个框:“需要安装 .NET Desktop Runtime”。
现场的工控机装的是 Win7 升级上来的 Win10,没有 .NET 8 桌面运行时。现场也没网,装不了。我只好回办公室,重新发布一次。
第二次我发了自包含版本,能跑了。但第三天操作员说软件打开要等十几秒,比在我这儿慢得多——单文件解压的开销。
第三次交付前,我把该做的都做了一遍。这篇就是那份清单。
发布命令
先明确一件事:bin\Debug不是部署包。它包含调试符号、没有优化、依赖开发机上的运行时。部署要用publish:
dotnet publish FillTrack.csproj ^-cRelease ^-rwin-x64 ^ --self-containedtrue^-p:PublishSingleFile=true ^-o.\publish参数逐个说:
| 参数 | 作用 | 能不能省 |
|---|---|---|
-c Release | 关掉调试符号、打开优化 | 不能省 |
-r win-x64 | 指定目标运行时(RID) | 不能省 |
--self-contained true | 把 .NET 运行时打进包里 | 看现场 |
-p:PublishSingleFile=true | 合成单个 exe | 可省,有取舍 |
-o .\publish | 输出目录 | 可省 |
自包含:现场机器不一定有运行时
这是第一个必须做的决定。
框架依赖发布(--self-contained false)产出的包很小,几十 KB 到几 MB,但要求目标机器装了对应版本的 .NET 桌面运行时。工业现场的机器通常:
- 没网,装不了运行时
- 有网也不敢随便装——很多厂区禁止私自安装软件
- 系统版本杂,Win10 各种小版本都有
自包含发布把运行时一起打包,产物大概 150-200 MB。换来的是:双击就能跑,不依赖机器上装了什么。
对现场交付来说,这个交换非常值。U 盘拷贝本来就无所谓体积,但“打不开”是现场事故。
单文件:方便,但有代价
PublishSingleFile=true把几十个 dll 合成一个 exe,看起来干净很多。
代价是首次启动变慢。单文件 exe 运行时需要把内容解压到临时目录,冷启动可能多出好几秒。第二次启动会快一些,因为解压结果被缓存了——但缓存在哪个目录、会不会被清理工具删掉,都不完全可控。
所以在项目里加了这么一个设置:
<PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>net8.0-windows</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings><UseWPF>true</UseWPF><IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract><Platforms>AnyCPU;x64</Platforms></PropertyGroup>IncludeAllContentForSelfExtract让内容文件也参与自解压,避免运行时要访问的配置/资源文件散落在 exe 外面找不到。
取舍建议:如果现场对启动速度敏感(比如操作员上岗就开机、开机就要用),用非单文件的自包含发布——一个文件夹,多几十个文件,但启动快、路径行为跟开发时一致。单文件的收益主要是“看起来整洁”和“不容易误删某个 dll”。
不要开裁剪
看到 200 MB 的包,第一反应是开PublishTrimmed=true瘦身。
WPF 项目不要开裁剪。
WPF 大量依赖反射:XAML 里的{Binding}、DataTemplate按类型名匹配、依赖属性的注册,都发生在运行时。裁剪器只做静态分析,看不到这些隐式引用,会把“看起来没用到”的类型删掉。结果是编译通过、发布成功、运行时报TypeLoadException或绑定静默失效。
更隐蔽的是绑定失效不报错。界面能打开,只是某些字段永远是空的——这种 bug 在现场极难定位。
同理,ReadyToRun之类的优化可以按需开,但裁剪对 WPF 是明确的风险项。
运行时的目录约定
打包只是第一步,程序跑起来之后往哪儿写数据同样要想清楚。
不要把数据库和日志写在 exe 所在目录。如果部署到C:\Program Files\下,普通用户没有写权限,程序一启动就崩。
用用户数据目录:
vardataRoot=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"FillTrack");Directory.CreateDirectory(Path.Combine(dataRoot,"Data"));Directory.CreateDirectory(Path.Combine(dataRoot,"Logs"));vardbPath=Path.Combine(dataRoot,"Data","filltrack.db");DbProvider.Initialize($"Data Source={dbPath}");三个目录各司其职:
| 目录 | 内容 | 是否随程序升级保留 |
|---|---|---|
Data\ | SQLite 数据库 | ✅ 必须保留 |
Logs\ | 滚动日志文件 | 按保留策略清理 |
| 配置 | 设备参数 JSON | ✅ 必须保留 |
现场升级软件的时候,通常只替换程序文件,数据目录不动。所以数据目录必须在程序目录之外——否则升级时整个覆盖,历史生产记录全没了。
首次启动要自动做完的事
一个能交付的程序,第一次运行应该自己把环境准备好,而不是让人看一份安装文档。启动流程按这个顺序:
顺序不能乱,尤其两点:
日志初始化必须在数据库之前。否则数据库初始化失败时,错误没地方记,现场只能看到程序闪退。
建表和建默认账户在登录之前。登录要查用户表,表不存在直接崩。
部署清单
每次交付前对照一遍:
dotnet publish -c Release发布通过- 自包含发布,目标机器无需装 .NET 运行时
- 未开启
PublishTrimmed - 数据库/日志/配置写在用户数据目录,不在程序目录
- 首次启动能自动建表、建默认账户、建目录
- 默认账户密码已在交付前修改或强制首登修改
- 数据目录不在升级覆盖范围内
- 现场机器上实际双击验证过一次
- 断网状态下验证过启动和离线功能
最后两条最容易被跳过,也最容易出问题。在开发机上跑通不等于在现场机器上跑通。
踩坑记录
拷bin\Debug当部署包
带调试符号、没优化、依赖开发机环境。必须走publish。
框架依赖发布到没装运行时的机器
现场弹“需要安装 .NET Desktop Runtime”。工业现场大概率装不了,用自包含。
对 WPF 开裁剪
编译通过、发布成功、运行时绑定静默失效。这种 bug 在现场几乎没法查。不要开。
数据和程序放同一个目录
部署到Program Files下没写权限;升级覆盖时历史数据一起丢。数据目录必须在程序目录之外。
日志初始化放在数据库之后
数据库初始化失败时没有任何记录,现场只能看到闪退。
本篇小结
| 环节 | 要点 |
|---|---|
| 发布命令 | dotnet publish -c Release -r win-x64 --self-contained |
| 运行时 | 自包含,现场不用装 .NET |
| 单文件 | 方便但冷启动慢,按现场需求取舍 |
| 裁剪 | WPF 不要开 |
| 数据目录 | 用户数据目录,与程序目录分离 |
| 首次启动 | 建目录 → 初始化日志 → 建库建表 → 建默认账户 → 登录 |
| 验证 | 必须在现场机器上双击跑一次 |
系列完结
二十一篇,从一行代码都没有,到一个能拷到现场、双击就跑的灌装监控系统。回头看这条路径:
打地基— MVVM 骨架、依赖注入、导航、主题
接通设备— Modbus RTU 协议、串口/TCP 双模式、轮询循环、断线重连
留住数据— SQLite + FreeSql、条码追溯、CSV 导出、配置原子写入
让它说话— 报警生命周期、四路日志、日志查看器
给人看— Dashboard、实时曲线、自定义控件
能交付— 单元测试、权限管理、打包部署
如果只能留三条经验给准备做类似项目的人:
第一,先想清楚"出错时怎么办"。断线要重连、配置要能恢复、导出要能取消、密码要能挡住。这些逻辑不写,软件在办公室永远看着正常,一到现场就出问题。这个系列里我花在异常路径上的篇幅,比花在正常路径上多。
第二,别让界面直接碰数据。从第一层的 MVVM 到最后一层的打包,所有设计都指向同一个目标:让"谁负责什么"有明确边界。ViewModel 不碰 Modbus,控件不碰 Service,测试不碰现场数据。边界一清楚,改一处就不会崩一片。
第三,测试不是可选项。手动验证二十次和自动验证二十次,看着结果一样,区别在第二十一次——那时候你还能不能确定没改坏。
这个系列记录的是一个单机灌装监控上位机,通信协议是 Modbus RTU,规模不大。如果你对更复杂的工业控制场景感兴趣——多设备并行、工艺配方编排、安全联锁、权限分级更细、部署到产线集群——可以看我的另一个系列「从零搭建工业控制系统」,那边把这些问题都展开写过。
写到这里就结束了。感谢一路看到这里的读者,如果这个系列帮你在自己的项目里少踩一个坑,那它就值了。