参与贡献

编辑此文档

翻译文档,或在您的计算机上打开完整项目。

在您选择提交工作成果之前,不会发布任何内容。

自定义 URL 协议

注册自定义 URL 方案,以便通过链接启动应用程序

自定义 URL 协议(也称为 URL 方案)允许用户点击使用自定义协议的链接(例如myapp://action或myapp://open/document)时启动您的应用程序。

概述

自定义协议支持:

  • 深层链接:使用特定数据启动应用
  • 浏览器集成:处理来自网页的链接
  • 电子邮件链接:从电子邮件客户端打开应用
  • 应用间通信:从其他应用程序启动应用

示例:myapp://open/document?id=123会启动应用并打开文档123。

配置

在应用程序选项中定义自定义协议:

自定义协议在build/config.yml中声明(各平台的打包工具会在打包时使用这些声明,包括 Windows 上的 NSIS 宏、MSIX 清单、macOS 的CFBundleURLTypes以及 Linux 的.desktop/xdg-mime)。不存在application.Protocol类型,application.Options上也不存在Protocols字段。

yaml
# build/config.yml
protocols:
  - scheme: myapp
    description: "My Application Protocol"

在 Go 代码中,通过ApplicationLaunchedWithUrl事件监听使用 URL 启动应用的操作:

go
package main

import (
    "github.com/wailsapp/wails/v3/pkg/application"
    "github.com/wailsapp/wails/v3/pkg/events"
)

func main() {
    app := application.New(application.Options{
        Name:        "My Application",
        Description: "My awesome application",
    })

    app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
        handleCustomURL(e.Context().URL())
    })

    app.Run()
}

func handleCustomURL(url string) {
    // Parse and handle the custom URL
    // Example: myapp://open/document?id=123
    println("Received URL:", url)
}

协议处理程序

监听协议事件以处理传入的 URL:

go
app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
    url := e.Context().URL()

    // Parse the URL
    parsedURL, err := parseCustomURL(url)
    if err != nil {
        app.Logger.Error("Failed to parse URL:", err)
        return
    }

    // Handle different actions
    switch parsedURL.Action {
    case "open":
        openDocument(parsedURL.DocumentID)
    case "settings":
        showSettings()
    case "user":
        showUser Profile(parsedURL.UserID)
    default:
        app.Logger.Warn("Unknown action:", parsedURL.Action)
    }
})

URL 结构

设计清晰且层次分明的 URL 结构:

text
myapp://action/resource?param=value

Examples:
myapp://open/document?id=123
myapp://settings/theme?mode=dark
myapp://user/profile?username=john

最佳实践:

  • 方案名称使用小写字母
  • 方案名称应简短易记
  • 对资源使用分层路径
  • 使用查询参数传递可选数据
  • 对特殊字符进行 URL 编码

平台注册

各平台采用不同的方式注册自定义协议。

Windows NSIS 安装程序

使用 NSIS 安装程序时,Wails v3 会自动注册自定义协议。

自动注册

使用wails3 build构建应用程序时,NSIS 安装程序会:

  1. 自动注册build/config.yml的protocols:键下声明的所有协议
  2. 将协议与应用程序可执行文件关联
  3. 设置正确的注册表项
  4. 卸载时移除协议关联

无需额外配置!

工作原理

NSIS 模板包含内置宏:

  • wails.associateCustomProtocols - 在安装期间注册协议
  • wails.unassociateCustomProtocols - 在卸载期间移除协议

系统会根据您的Protocols配置自动调用这些宏。

手动配置注册表(高级)

如果需要手动注册(不使用 NSIS):

batch
@echo off
REM Register custom protocol
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /ve /d "URL:My Application Protocol" /f
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /v "URL Protocol" /t REG_SZ /d "" /f
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp\shell\open\command" /ve /d "\"%1\"" /f

测试

测试协议注册:

PowerShell
# Open protocol URL from PowerShell
Start-Process "myapp://test/action"

# Or from command prompt
start myapp://test/action

Windows MSIX 包

使用 MSIX 打包时,也会自动注册自定义协议。

自动注册

使用 MSIX 构建应用程序时,清单会自动包含build/config.yml协议配置中的协议注册信息。

生成的清单包含:

xml
<uap:Extension Category="windows.protocol">
  <uap:Protocol Name="myapp">
    <uap:DisplayName>My Application Protocol</uap:DisplayName>
  </uap:Protocol>
</uap:Extension>

通用链接(网页到应用链接)

Windows 支持网页到应用链接,其工作方式与 macOS 上的通用链接类似。将应用程序部署为 MSIX 包时,可以启用 HTTPS 链接来直接启动应用。

要启用网页到应用链接,请参阅Microsoft 网页到应用链接指南。您需要:

  1. 手动将 App URI Handler 添加到 MSIX 清单(build/windows/msix/app_manifest.xml):
xml
<uap3:Extension Category="windows.appUriHandler">
  <uap3:AppUriHandler>
    <uap3:Host Name="myawesomeapp.com"/>
  </uap3:AppUriHandler>
</uap3:Extension>
  1. 在网站上配置windows-app-web-link:在https://myawesomeapp.com/.well-known/windows-app-web-link托管windows-app-web-link文件。此文件应包含应用的软件包信息及其处理的路径。

当网页到应用链接启动您的应用程序时,您会收到与自定义协议方案相同的ApplicationLaunchedWithUrl事件。

完整示例

下面是一个处理多种协议操作的完整示例:

go
package main

import (
    "fmt"
    "net/url"
    "strings"

    "github.com/wailsapp/wails/v3/pkg/application"
    "github.com/wailsapp/wails/v3/pkg/events"
)

type App struct {
    app    *application.App
    window *application.WebviewWindow
}

func main() {
    // Protocol registration lives in build/config.yml (the platform packagers
    // consume it); the application code just listens for the launch event.
    app := application.New(application.Options{
        Name:        "DeepLink Demo",
        Description: "Custom protocol demonstration",
    })

    myApp := &App{app: app}
    myApp.setup()

    app.Run()
}

func (a *App) setup() {
    // Create window
    a.window = a.app.Window.NewWithOptions(application.WebviewWindowOptions{
        Title:  "DeepLink Demo",
        Width:  800,
        Height: 600,
        URL:    "http://wails.localhost/",
    })

    // Handle custom protocol URLs
    a.app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
        a.handleDeepLink(e.Context().URL())
    })
}

func (a *App) handleDeepLink(rawURL string) {
    // Parse URL
    parsedURL, err := url.Parse(rawURL)
    if err != nil {
        a.app.Logger.Error("Failed to parse URL:", err)
        return
    }

    // Bring window to front
    a.window.Show()
    a.window.Focus()

    // Extract path and query
    path := strings.Trim(parsedURL.Path, "/")
    query := parsedURL.Query()

    // Handle different actions
    parts := strings.Split(path, "/")
    if len(parts) == 0 {
        return
    }

    action := parts[0]

    switch action {
    case "open":
        if len(parts) >= 2 {
            resource := parts[1]
            id := query.Get("id")
            a.openResource(resource, id)
        }

    case "settings":
        section := ""
        if len(parts) >= 2 {
            section = parts[1]
        }
        a.openSettings(section)

    case "user":
        if len(parts) >= 2 {
            username := parts[1]
            a.openUserProfile(username)
        }

    default:
        a.app.Logger.Warn("Unknown action:", action)
    }
}

func (a *App) openResource(resourceType, id string) {
    fmt.Printf("Opening %s with ID: %s\n", resourceType, id)
    // Emit event to frontend
    a.app.Event.Emit("navigate", map[string]string{
        "type": resourceType,
        "id":   id,
    })
}

func (a *App) openSettings(section string) {
    fmt.Printf("Opening settings section: %s\n", section)
    a.app.Event.Emit("navigate", map[string]string{
        "page":    "settings",
        "section": section,
    })
}

func (a *App) openUserProfile(username string) {
    fmt.Printf("Opening user profile: %s\n", username)
    a.app.Event.Emit("navigate", map[string]string{
        "page": "user",
        "user": username,
    })
}

前端集成

在前端处理导航事件:

javascript
import { Events } from '@wailsio/runtime'

// Listen for navigation events from protocol handler
Events.On('navigate', (event) => {
    const { type, id, page, section, user } = event.data

    if (type === 'document') {
        // Open document with ID
        router.push(`/document/${id}`)
    } else if (page === 'settings') {
        // Open settings
        router.push(`/settings/${section}`)
    } else if (page === 'user') {
        // Open user profile
        router.push(`/user/${user}`)
    }
})

安全注意事项

验证所有输入

始终验证并清理来自外部来源的 URL:

go
func (a *App) handleDeepLink(rawURL string) {
    // Parse URL
    parsedURL, err := url.Parse(rawURL)
    if err != nil {
        a.app.Logger.Error("Invalid URL:", err)
        return
    }

    // Validate scheme
    if parsedURL.Scheme != "myapp" {
        a.app.Logger.Warn("Invalid scheme:", parsedURL.Scheme)
        return
    }

    // Validate path
    path := strings.Trim(parsedURL.Path, "/")
    if !isValidPath(path) {
        a.app.Logger.Warn("Invalid path:", path)
        return
    }

    // Sanitize parameters
    params := sanitizeQueryParams(parsedURL.Query())

    // Process validated URL
    a.processDeepLink(path, params)
}

func isValidPath(path string) bool {
    // Only allow alphanumeric and forward slashes
    validPath := regexp.MustCompile(`^[a-zA-Z0-9/]+$`)
    return validPath.MatchString(path)
}

func sanitizeQueryParams(query url.Values) map[string]string {
    sanitized := make(map[string]string)
    for key, values := range query {
        if len(values) > 0 {
            // Take first value and sanitize
            sanitized[key] = sanitizeString(values[0])
        }
    }
    return sanitized
}

防止注入攻击

切勿直接将 URL 作为代码或 SQL 执行:

go
// ❌ DON'T: Execute URL content
func badHandler(url string) {
    exec.Command("sh", "-c", url).Run() // DANGEROUS!
}

// ✅ DO: Parse and validate
func goodHandler(url string) {
    parsed, _ := url.Parse(url)
    action := parsed.Query().Get("action")

    // Whitelist allowed actions
    allowed := map[string]bool{
        "open":     true,
        "settings": true,
        "help":     true,
    }

    if allowed[action] {
        handleAction(action)
    }
}

测试

手动测试

在开发期间测试协议处理程序:

Windows:

PowerShell
Start-Process "myapp://test/action?id=123"

macOS:

bash
open "myapp://test/action?id=123"

Linux:

bash
xdg-open "myapp://test/action?id=123"

HTML 测试

创建一个测试用 HTML 页面:

html
<!DOCTYPE html>
<html>
<head>
    <title>Protocol Test</title>
</head>
<body>
    <h1>Custom Protocol Test Links</h1>

    <ul>
        <li><a href="myapp://open/document?id=123">Open Document 123</a></li>
        <li><a href="myapp://settings/theme?mode=dark">Dark Mode Settings</a></li>
        <li><a href="myapp://user/profile?username=john">User Profile</a></li>
    </ul>
</body>
</html>

故障排除

协议未注册

Windows:

  • 检查注册表:HKEY_CURRENT_USER\SOFTWARE\Classes\<scheme>
  • 使用 NSIS 安装程序重新安装
  • 确认安装程序以适当的权限运行

macOS:

  • 使用wails3 build重新构建应用
  • 检查应用包中的Info.plist:MyApp.app/Contents/Info.plist
  • 重置 Launch Services:/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill

Linux:

  • 检查桌面文件:~/.local/share/applications/myapp.desktop
  • 更新数据库:update-desktop-database ~/.local/share/applications/
  • 验证处理程序:xdg-mime query default x-scheme-handler/myapp

应用无法启动

检查日志:

go
app := application.New(application.Options{
    LogLevel: slog.LevelDebug, // requires `import "log/slog"`
    // ...
})

常见问题:

  • 应用未安装在预期位置
  • 注册信息中的可执行文件路径与实际位置不一致
  • 权限问题

最佳实践

✅ 推荐做法

  • 使用描述性方案名称——使用mycompany-myapp,而不是mca
  • 验证所有输入——切勿信任来自外部来源的 URL
  • 妥善处理错误——记录无效 URL,不要让应用崩溃
  • 向用户提供反馈——显示触发了什么操作
  • 在所有平台上测试——协议处理方式因平台而异
  • 记录 URL 结构——为用户和集成人员提供帮助

❌ 不要这样做

  • 不要使用常见的协议方案名称——避免使用http、file、app等名称
  • 不要将 URL 作为代码执行——这会带来巨大的安全风险
  • 不要暴露敏感操作——执行破坏性操作前必须要求用户确认
  • 不要假定协议在任何环境中都能正常工作——应提供备用机制
  • 不要忘记 URL 编码——正确处理特殊字符

后续步骤


有疑问?请在Discord中提问,或查看示例。

编辑页面

最后更新: