JOC.Update 客户端集成指南:WinForms、WPF 与 UniApp APK 在线升级

JOC.Update 客户端集成指南:WinForms、WPF 与 UniApp APK 在线升级
本文给出三套可直接落地的集成方式:Windows 客户端使用 GZUpdate 的 ZIP 增量更新能力;WPF 增加独立发布配置和统一进度窗口;UniApp Android 使用完整 APK 与系统安装器完成覆盖安装。
1. 后台产品与升级包约定
每种客户端建立独立产品,并固定产品编号、发布通道和升级模式:
| 客户端 | 版本类型 | 更新模式 | 机器版本 |
|---|---|---|---|
| WinForms / WPF | release 或 beta | incremental | GZUpdate 的 __version__ 数字版本 |
| Android APK | android | full | APK versionCode |
后台版本号必须严格递增。versionTxt 仅用于显示,不参与比较。客户端只能看到已发布且通道、模式均匹配的版本。
Windows 更新包是 ZIP,包内路径相对应用根目录:
update-20401.zip
├── DesktopApp.exe
├── DesktopApp.dll
└── plugins/
└── Reporting.Plugin.dll
不要把用户数据、数据库、环境配置或 __version__ 放进升级 ZIP。定制更新执行器(例如 __update__.dll)能运行本机代码,必须作为受信任发布物审核。
2. WinForms:启动前检查、进度窗体与手动检查
2.1 引入并注册客户端库
主项目引用:
<ItemGroup>
<PackageReference Include="GZUpdate.Client" Version="*" />
<PackageReference Include="GZUpdate.Client.Win" Version="*" />
</ItemGroup>
在 Program.cs 中集中注册升级服务,清理上一轮备份,并在创建主窗体前完成检查:
using GZUpdate.Client;
using GZUpdate.Client.Win;
internal static class Program
{
const string UpdateServer = "https://updates.example.com/";
const string ProductId = "ERP_DESKTOP";
const string Executor = "__update__.dll";
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
UpdateServices.Regsiter(
UpdateServer, ProductId,
EnumVersionType.release, Executor);
UpdateServices.deleteBak();
WinUpdateClient.Configure(
server: UpdateServer,
productId: ProductId,
versionType: EnumVersionType.release,
upgradeExecutorDllPath: Executor);
var result = WinUpdateClient.CheckAndUpdate();
if (result.ShouldExitApplication)
return;
Application.Run(new MainForm());
}
}
检查应早于任何主窗体、插件或子进程启动,否则被占用的 EXE/DLL 会导致替换失败。
2.2 自定义 WinForms 进度界面
不要手写“下载后直接覆盖”的逻辑。使用 UpdateProcess 执行更新,并把事件绑定到进度窗体:
var process = new UpdateProcess();
process.OnUpdateMessage += dialog.SetMessage;
process.OnUpdateProcess += dialog.SetPackageProgress;
process.OnUpdateDownProgress += dialog.SetDownloadProgress;
process.OnFail += dialog.SetFailure;
process.OnComplate += dialog.SetComplete;
var updated = await Task.Run(() => process.DoUpdate());
进度窗体至少显示当前消息、下载百分比/速度、更新包数量、“关闭”和“重启”。更新进行时禁止关闭;失败或完成后才启用关闭。所有回调都可能来自后台线程,因此在 SetMessage、SetPackageProgress 等方法中通过 BeginInvoke 回到 UI 线程。
2.3 增加手动检查按钮
主窗体按钮与启动检查共用同一套更新流程。检查期间禁用按钮,先找出数字版本最大的候选项,再让用户确认:
private async void CheckUpdateButton_Click(object? sender, EventArgs e)
{
CheckUpdateButton.Enabled = false;
try
{
var versions = await Task.Run(() =>
UpdateServices.doCheckVersion() ?? []);
if (versions.Count == 0)
{
MessageBox.Show("当前已经是最新版本。", "在线升级");
return;
}
var latest = versions.OrderByDescending(item => item.version).First();
if (MessageBox.Show(
$"发现新版本 {latest.versionTxt},是否立即升级?",
"在线升级",
MessageBoxButtons.YesNo) == DialogResult.Yes)
{
await RunUpdateWithDialogAsync(this);
}
}
finally
{
CheckUpdateButton.Enabled = true;
}
}
3. WPF:可开关配置、升级协调器和统一主题进度窗
WPF 项目引用 GZUpdate.Client 和 GZUpdate.Client.WPF,并将升级配置放进发布目录的 update.json。默认关闭:
{
"enabled": false,
"serverBaseUrl": "https://updates.example.com/",
"productCode": "ERP_WPF",
"versionType": "release",
"upgradeExecutorDllPath": "__update__.dll"
}
文件不存在或 enabled=false 时,不检查更新。启用后若缺少地址、产品编号、通道或执行器文件名,必须报告配置错误,不能将其伪装成“无更新”。
3.1 建立协调器
将配置转换为 WpfUpdateOptions,并把底层结果归一为 Disabled、NoUpdate、Updated、UpdatedAndRestarted、Failed,供启动与手动检查复用:
var options = new WpfUpdateOptions
{
Server = config.ServerBaseUrl,
ProductId = config.ProductCode,
VersionType = config.VersionType,
UpgradeExecutorDllPath = config.UpgradeExecutorDllPath,
PromptBeforeManualUpdate = false,
CloseDialogOnStartupComplete = true,
RestartAfterStartupUpdate = true,
RestartAfterManualUpdate = true,
DialogTitle = "程序升级",
CheckFailedTitle = "检查更新失败",
ProgressDialogFactory = new ApplicationUpdateProgressDialogFactory()
};
WpfUpdateClient.Configure(options);
var result = WpfUpdateClient.CheckAndUpdate(owner, WpfUpdateMode.Startup);
3.2 在显示主窗口前处理结果
强制升级策略应在创建 MainWindow 前执行:
var result = updates.CheckForUpdate(owner: null);
if (result.Status == ApplicationUpdateStatus.Failed)
{
MessageBox.Show("启动更新检查失败,请检查网络或升级配置。",
"检查更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown(-1);
return;
}
if (result.Status is ApplicationUpdateStatus.Updated
or ApplicationUpdateStatus.UpdatedAndRestarted)
{
Shutdown();
return;
}
MainWindow = new MainWindow(viewModel);
MainWindow.Show();
只有 Disabled 或 NoUpdate 才能进入主窗口。若产品要求离线可用,可以将 Failed 改为风险提示后继续,但这是产品策略,不能静默处理。
3.3 进度窗口接口
实现 IWpfUpdateProgressDialogFactory,每次升级创建一个 IWpfUpdateProgressDialog。该窗口应实现 Shown、SetMessage、SetDownloadProgress、SetPackageProgress、SetComplete、SetFailure 和 EnableClose。
升级中在 Closing 事件取消关闭;失败或结束后才允许关闭。事件更新 UI 时使用 Dispatcher.CheckAccess() 与 Dispatcher.BeginInvoke()。主界面“检查更新”菜单只调用协调器,并按五种状态显示相应结果。
4. UniApp Android:完整 APK、原生进度层和系统安装
Android 不能以 ZIP 覆盖正在运行的应用,后台必须发布 full APK,后台版本号必须等于 manifest.json 中递增的 versionCode。
4.1 配置和启动检查
将配置独立为模块,并只使用 HTTPS:
export const appUpdateConfig = Object.freeze({
serverUrl: 'https://updates.example.com',
productId: 'ERP_ANDROID',
versionType: 'android',
updateMode: 'full',
})
import { checkAppUpdate } from './utils/app-updater'
export default {
onLaunch() {
checkAppUpdate()
},
}
使用 plus.runtime.getProperty 获取已安装 APK 的真实 versionCode;请求 /checkversion 后,只接受 HTTP 200、status=0、更高的整数版本和 HTTPS 下载地址。
4.2 下载与安装
下载到应用私有目录,先比对服务端 fileSizeBytes,通过才交给系统安装器:
const task = plus.downloader.createDownload(
downloadUrl,
{ filename: `_doc/updates/app-${versionCode}.apk` },
(download, status) => {
if (status !== 200 || !download.filename) {
showUpdateError('APK 下载失败,请稍后重试。')
return
}
if (expectedFileSize > 0 &&
download.downloadedSize !== expectedFileSize) {
showUpdateError('APK 文件大小校验失败,已取消安装。')
return
}
plus.runtime.install(download.filename, {}, onInstalled, onInstallFailed)
},
)
task.start()
通过 statechanged 读取 downloadedSize、totalSize 和时间差,计算百分比和速度。原生 WebView 会遮挡普通 Vue 弹层时,使用透明原生 Webview 显示全屏下载进度,并在成功、失败或取消后关闭。
服务端返回的显示版本字段是 versionTxt,目标版本文本应优先读取它:
function getTargetVersionText(version, versionCode) {
const text = version.versionTxt ?? version.VersionTxt
const value = String(text ?? versionCode).trim()
return /^v/i.test(value) ? `目标版本:${value}` : `目标版本:v${value}`
}
Android 最终会校验包名、签名证书和 versionCode。清单应声明网络、网络状态和安装包请求权限,用户仍需在系统中允许未知来源安装,应用不能绕过系统确认。
该链路已覆盖 HTTPS 与文件大小。若要使用服务端 fileSha256,应接入可审计的原生哈希能力,并在散列匹配前禁止调用 plus.runtime.install。
4.3 APK 发布步骤
- 递增
manifest.json的versionCode,填写versionName。 - 使用相同包名和相同签名证书构建 APK。
- 在后台上传 APK:版本号填
versionCode,显示版本填versionName,类型填android,模式填full。 - 保存草稿,在真机验证检查、下载、进度、系统安装和重启。
- 验证后发布。回滚也必须发布更高
versionCode的 APK,Android 不允许低版本覆盖安装。
5. 发布前检查
- Windows ZIP 保持相对目录,不携带用户数据、环境配置或
__version__。 - 检查前关闭占用安装目录的插件和子进程。
- 后台产品编号、通道、版本号和模式与客户端一致。
- WPF 发布包包含
update.json,启用状态可审计。 - Android 使用 HTTPS、完整 APK、同一签名证书和递增
versionCode。 - 坚持“草稿 → 真机验证 → 发布”;异常版本应下架。
