浏览器唤起桌面端应用(终极篇)

------------基于统一协议与配置驱动的通用桌面启动方案

在进阶篇中,我们已经实现了:

  • 在多版本桌面程序共存的情况下,实现"可控、可扩展、可演进"的版本选择机制

本篇作为 终极版,核心目标不再只是「多版本」,而是:

在多个软件的一套统一唤醒规则以及实现


为什么需要"统一的规则"?

当前的专用 Launcher 方案面临几个直接问题:

❌ 每接入一个软件,都要注册新协议、编写新脚本

❌ URL 解析、路径校验和启动逻辑被重复实现

❌ exe 路径、软件版本和业务逻辑硬编码在 Launcher 中

❌ 新增应用需要同时修改注册表、脚本和 Web 页面

❌ 不同电脑的安装路径不同,统一部署困难

例如打开多个软件

因此我们需要的是 "一套规则和抽象的逻辑"


整体架构设计

text 复制代码
浏览器 / Web 系统
  |
  | launcher://open?app=<AppKey>&param1=***&param2=***
  v
Windows 自定义协议(launcher)
  |
  | 原样转发完整 URL
  v
launcher.bat
  |
  | 解析参数并读取对应 cfg
  v
config\<AppKey>.cfg
  |
  | 通用配置 + 少量应用特殊处理
  v
启动配置中的 exe

统一调用格式:

text 复制代码
launcher://open?app=<AppKey>[,param1=<值>][,param2=<值>]

浏览器只表达"启动哪个应用、携带哪些参数",本地 Launcher 决定应用路径、环境准备和启动方式。

整个方案分为四层:

  1. Web 层:构造启动意图。
  2. 协议层:转交完整 URL。
  3. 调度层:解析参数、选择配置、处理应用差异。
  4. 配置层:保存各应用在本机的启动信息。

新增普通应用只需增加一个配置文件。只有无法配置化的差异,才在 Launcher 中增加小范围分支。

最终目录结构

text 复制代码
Launcher/
├── launcher.bat
├── install.bat
├── uninstall.bat
├── launcher.reg
├── 浏览器唤起Launcher.html
└── config/
    ├── _template.cfg
    ├── wps.cfg
    └── vscode.cfg

第一步:注册统一协议

注册表只负责转发,不判断应用,也不保存软件路径。

install.bat

shell 复制代码
@echo off
setlocal

set "INSTALL_ROOT=%~dp0"
set "LAUNCHER=%INSTALL_ROOT%launcher.bat"

if not exist "%LAUNCHER%" (
    echo [install] launcher.bat not found: %LAUNCHER%
    pause
    exit /b 1
)

reg add "HKCR\launcher" /ve /d "URL:Launcher Protocol" /f
if errorlevel 1 goto reg_fail
reg add "HKCR\launcher" /v "URL Protocol" /d "" /f
if errorlevel 1 goto reg_fail
reg add "HKCR\launcher\shell\open\command" /ve /d "\"%LAUNCHER%\" \"%%1\"" /f
if errorlevel 1 goto reg_fail

if not exist "%APPDATA%\Launcher\config" mkdir "%APPDATA%\Launcher\config"

echo [install] OK
echo Protocol: launcher://
echo Launcher: %LAUNCHER%
pause
exit /b 0

:reg_fail
echo [install] Failed to write registry. Try Run as Administrator.
pause
exit /b 1

写入 HKEY_CLASSES_ROOT 失败时,以管理员身份运行安装脚本。

launcher.reg

reg 复制代码
Windows Registry Editor Version 5.00

; 导入前,将路径改为本机 launcher.bat 的实际位置

[HKEY_CLASSES_ROOT\launcher]
@="URL:Launcher Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\launcher\shell\open\command]
@="\"D:\\Tools\\Launcher\\launcher.bat\" \"%1\""

核心关系只有一条:

text 复制代码
launcher://... → "launcher.bat" "%1"

双引号不能省略,否则 URL 中的特殊字符可能被 CMD 当成命令分隔符。

第二步:设计应用配置

每个应用对应一个 .cfg,文件名就是 URL 中的 AppKey。

config_template.cfg

ini 复制代码
; app=sample 对应 sample.cfg

; 应用程序完整路径,必填
exe=C:\Path\To\Application.exe

; 可选:应用资源或扩展目录
resource=C:\Path\To\Resource

; 可选:任意值的写入,例如写入环境变量
env.resource=APP_RESOURCE_DIR
env.param1=APP_PARAM_1
env.param2=APP_PARAM_2

配置查找顺序:

  1. %APPDATA%\Launcher\config\<AppKey>.cfg
  2. <安装目录>\config\<AppKey>.cfg

用户配置优先,可以覆盖不同电脑的安装路径。

案例一:WPS

WPS 属于普通启动案例,只需配置 exe:

ini 复制代码
; config\wps.cfg
exe=C:\Program Files\Example\WPS\wps.exe
text 复制代码
launcher://open?app=wps

示例路径需要替换为本机真实路径。

案例二:VS Code

VS Code 用来展示"启动时还要传入目录"的情况:

ini 复制代码
; config\vscode.cfg
exe=C:\Program Files\Microsoft VS Code\Code.exe
text 复制代码
launcher://open?app=vscode,param1=D:\Workspace\Example

它的启动参数形式与普通软件不同,稍后在 launcher.bat 中保留一个小范围特殊分支。

第三步:实现 launcher.bat

最终方案不再调用 BAT/PS1 插件,也不经过额外 EXE。解析、配置读取、特殊处理和启动都在同一个批处理上下文中完成。

bat 复制代码
@echo off
chcp 65001 >nul
setlocal EnableExtensions EnableDelayedExpansion

set "INSTALL_DIR=%~dp0"
set "CONFIG_DIR=%INSTALL_DIR%config"
set "USER_CONFIG_DIR=%APPDATA%\Launcher\config"

set "RAW_URL=%~1"
if not defined RAW_URL (
    echo [launcher] Missing URL argument.
    exit /b 1
)

:: 清理协议前缀
set "PARAMS=!RAW_URL!"
set "PARAMS=!PARAMS:launcher://open/=!"
set "PARAMS=!PARAMS:launcher://open?=!"
set "PARAMS=!PARAMS:launcher://open=!"
if defined PARAMS if "!PARAMS:~0,1!"=="?" set "PARAMS=!PARAMS:~1!"

:: 同时兼容英文逗号和 &
set "APP="
set "PARAM1="
set "PARAM2="
set "P=!PARAMS:&=,!"

:parse_loop
if not defined P goto parse_done
for /f "tokens=1* delims=," %%A in ("!P!") do (
    set "PAIR=%%A"
    set "P=%%B"
    call :trim PAIR "!PAIR!"
    if /i "!PAIR:~0,4!"=="app=" set "APP=!PAIR:~4!"
    if /i "!PAIR:~0,7!"=="param1=" set "PARAM1=!PAIR:~7!"
    if /i "!PAIR:~0,7!"=="param2=" set "PARAM2=!PAIR:~7!"
)
goto parse_loop

:parse_done
call :trim APP "!APP!"
if not defined APP (
    echo [launcher] Missing app= in URL.
    exit /b 1
)

:: 禁止路径字符和空格
set "APP_SAFE=!APP!"
set "APP_SAFE=!APP_SAFE:\=!"
set "APP_SAFE=!APP_SAFE:/=!"
set "APP_SAFE=!APP_SAFE: =!"
if not "!APP_SAFE!"=="!APP!" (
    echo [launcher] Invalid app name: !APP!
    exit /b 1
)

:: 查找配置,用户配置优先
set "CFG_FILE="
if exist "%USER_CONFIG_DIR%\!APP!.cfg" (
    set "CFG_FILE=%USER_CONFIG_DIR%\!APP!.cfg"
) else if exist "%CONFIG_DIR%\!APP!.cfg" (
    set "CFG_FILE=%CONFIG_DIR%\!APP!.cfg"
) else (
    echo [launcher] App config not found: !APP!.cfg
    exit /b 1
)

:: 读取配置
set "EXE="
set "RESOURCE="
set "ENV_RESOURCE="
set "ENV_PARAM1="
set "ENV_PARAM2="

for /f "usebackq eol=; tokens=1,* delims==" %%K in ("!CFG_FILE!") do (
    set "KEY=%%K"
    set "VAL=%%L"
    call :trim KEY "!KEY!"
    call :trim VAL "!VAL!"
    if /i "!KEY!"=="exe" set "EXE=!VAL!"
    if /i "!KEY!"=="resource" set "RESOURCE=!VAL!"
    if /i "!KEY!"=="env.resource" set "ENV_RESOURCE=!VAL!"
    if /i "!KEY!"=="env.param1" set "ENV_PARAM1=!VAL!"
    if /i "!KEY!"=="env.param2" set "ENV_PARAM2=!VAL!"
)

if not defined EXE (
    echo [launcher] exe= not set in !CFG_FILE!
    exit /b 1
)
if not exist "!EXE!" (
    echo [launcher] Invalid exe path in config.
    echo Config: !CFG_FILE!
    echo EXE: !EXE!
    exit /b 1
)

:: 当前进程立即生效,并按需持久化
if defined ENV_RESOURCE if defined RESOURCE (
    set "!ENV_RESOURCE!=!RESOURCE!"
    setx !ENV_RESOURCE! "!RESOURCE!" >nul
)
if defined ENV_PARAM1 if defined PARAM1 (
    set "!ENV_PARAM1!=!PARAM1!"
    setx !ENV_PARAM1! "!PARAM1!" >nul
)
if defined ENV_PARAM2 if defined PARAM2 (
    set "!ENV_PARAM2!=!PARAM2!"
    setx !ENV_PARAM2! "!PARAM2!" >nul
)

:: 少量无法配置化的应用差异
if /i "!APP!"=="vscode" (
    if defined PARAM1 (
        start "" "!EXE!" "!PARAM1!"
    ) else (
        start "" "!EXE!"
    )
    exit /b 0
)

:: 某类应用需要把参数写入临时文件
if /i "!APP!"=="special_app" (
    if defined PARAM1 >"%TEMP%\launcher-task.txt" echo(!PARAM1!
)

:: 普通应用统一启动
start "" "!EXE!"
exit /b 0

:trim
setlocal EnableDelayedExpansion
set "STR=%~2"
:trim_left
if defined STR if "!STR:~0,1!"==" " set "STR=!STR:~1!" & goto trim_left
:trim_right
if defined STR if "!STR:~-1!"==" " set "STR=!STR:~0,-1!" & goto trim_right
endlocal & set "%~1=%STR%"
goto :eof

主流程始终一致:

text 复制代码
完整 URL → 解析参数 → 查找 cfg → 准备环境 → 特殊处理 → 启动应用

路径、变量映射等静态差异放入 cfg;普通应用走统一流程;只有启动参数形式、临时文件等行为才增加应用分支。

第四步:浏览器构造唤起链接

html 复制代码
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>浏览器唤起桌面应用</title>
  </head>
  <body>
    <h2>launcher:// 统一唤起</h2>
    <select id="app">
      <option value="wps">WPS</option>
      <option value="vscode">VS Code</option>
    </select>
    <input id="param1" placeholder="例如 VS Code 工作目录" />
    <button id="openBtn">唤起</button>

    <script>
      document.getElementById("openBtn").addEventListener("click", function () {
        var app = document.getElementById("app").value;
        var param1 = document.getElementById("param1").value;
        var params = ["app=" + encodeURIComponent(app)];
        if (param1) params.push("param1=" + encodeURIComponent(param1));
        window.location.href = "launcher://open?" + params.join(",");
      });
    </script>
  </body>
</html>

浏览器不保存 exe 路径,也不判断软件是否安装,只传递 AppKey 和可选参数。

第五步:卸载

uninstall.bat

bat 复制代码
@echo off
setlocal

set "USER_CONFIG=%APPDATA%\Launcher\config"
reg delete "HKCR\launcher" /f >nul 2>&1
echo [uninstall] launcher:// protocol removed.

if exist "%USER_CONFIG%" (
    rmdir /s /q "%USER_CONFIG%"
    echo [uninstall] User config removed.
)
if exist "%APPDATA%\Launcher" rmdir "%APPDATA%\Launcher" 2>nul

echo [uninstall] Done.
pause

卸载脚本删除协议和用户覆盖配置,不删除安装目录,避免误删用户文件。

安装与测试

  1. 将文件放在同一目录,以管理员身份运行 install.bat
  2. wps.cfgvscode.cfg 中的 exe= 改为本机路径。
  3. 先测试 launcher://open?app=wps
  4. 再测试 launcher://open?app=vscode,param1=D:\Workspace\Example

失败时按顺序检查:协议注册、注册表脚本路径、AppKey 与 cfg 文件名、exe 路径、特殊字符以及应用是否需要特殊启动处理。

成果展示

更进一步:插件化的终极目标

当前方案最终落地为:

text 复制代码
统一协议 + 配置驱动 + 单一 Launcher + 少量明确的特殊处理

它缩短了调用链,解决了现阶段的启动问题,但少量应用差异仍留在 launcher.bat 中。

最初设想的插件化架构、BAT/PowerShell 插件代码、EXE 中转尝试、参数踩坑与失败结论,已拆分为独立文章:

浏览器唤起桌面端应用(终极目标)

✅ 总结

进阶篇解决"同一个软件有多个版本时启动哪一个",终极篇解决"软件类型、机器环境和启动参数不断增加时如何统一管理"。

当前真正解决问题的关键,是缩短调用链,让参数在同一个批处理上下文中完成解析、准备和启动。

用 Web 表达意图,用配置隔离机器差异,用 Launcher 收敛桌面启动能力。

相关推荐
柯克七七1 小时前
我把 bug 修得太干净了,测试组以为这个模块本来就没问题
前端·javascript·typescript
Quz1 小时前
QML Label 完整用法与 Text 组件选型
前端·qt
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(三十四):后台初始化请求实现
前端·javascript·go
天若有情6732 小时前
前端性能优化:从“按下预加载”到“AI级智能预判系统”
前端·性能优化·js·active·hover·首屏加载
JackieZhengChina2 小时前
零依赖轻量级JS悬浮提示插件title2tip-js的使用教程
开发语言·javascript·ecmascript
Csvn2 小时前
📦 CSS Container Queries 实战踩坑:5 个让你怀疑人生的隐藏陷阱
前端
大家的林语冰2 小时前
👍 Rust 为 Node 插上翅膀,反超 Bun 和 pnpm,一体化工具我也有!
前端·javascript·node.js
坐而论道_2 小时前
兼容低版本 Android:让现代 Web 应用在旧 WebView 上稳定运行
前端·面试
ssshooter3 小时前
Promise 和 async/await 被 try-catch 包裹时的行为差异
前端·javascript·面试