参与贡献

编辑此文档

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

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

二维码服务

创建二维码服务,了解 Wails 服务

Wails 中的服务是一个 Go 结构体,其中包含你希望提供给前端的业务逻辑。服务将相关功能归为一组,使代码井然有序。

可以将服务视为一组可供 JavaScript 代码调用的方法。生成绑定后,前端便可调用服务中的每个公共方法。

在本教程中,我们将创建一个二维码生成器服务来演示这些概念。完成后,你将了解如何创建服务、管理依赖项,以及将 Go 代码连接到前端。


1

创建二维码服务文件

在应用程序目录中创建一个名为qrservice.go的新文件:

qrservice.go
go
package main

import (
    "github.com/skip2/go-qrcode"
)

// QRService handles QR code generation
type QRService struct {
    // We can add state here if needed
}

// NewQRService creates a new QR service
func NewQRService() *QRService {
    return &QRService{}
}

// Generate creates a QR code from the given text
func (s *QRService) Generate(text string, size int) ([]byte, error) {
    // Generate the QR code
    qr, err := qrcode.New(text, qrcode.Medium)
    if err != nil {
        return nil, err
    }

    // Convert to PNG
    png, err := qr.PNG(size)
    if err != nil {
        return nil, err
    }

    return png, nil
}

这里做了什么:

  • QRService是一个空结构体,用于存放二维码生成方法
  • NewQRService()是一个构造函数,用于创建服务的新实例
  • Generate()是一个接收文本和尺寸,并以 PNG 字节数组形式返回二维码的方法
  • 该方法返回([]byte, error),遵循 Go 将错误作为最后一个值返回的惯例
  • 我们使用github.com/skip2/go-qrcode包来实际生成二维码

2

注册服务

仅创建服务还不够——我们需要将其注册到 Wails 应用程序中,让应用程序知道该服务存在并能为其生成绑定。

创建应用程序时,需要在main.go中进行注册。将服务实例传给Services选项:

main.go
go
 func main() {

     app := application.New(application.Options{
         Name:        "myproject",
         Description: "A demo of using raw HTML & CSS",
         LogLevel:    slog.LevelDebug,
         Services: []application.Service{
             application.NewService(NewQRService()),
         },
         Assets: application.AssetOptions{
             Handler: application.AssetFileServerFS(assets),
         },
         Mac: application.MacOptions{
             ApplicationShouldTerminateAfterLastWindowClosed: true,
         },
     })

     app.Window.NewWithOptions(application.WebviewWindowOptions{
         Title:  "myproject",
         Width:  600,
         Height: 400,
     })

     // Run the application. This blocks until the application has been exited.
     err := app.Run()

     // If an error occurred while running the application, log it and exit.
     if err != nil {
         log.Fatal(err)
     }
 }

这里做了什么:

  • application.NewService()封装服务,以便 Wails 对其进行管理
  • 我们调用NewQRService()来创建服务实例
  • 该服务被添加到应用程序选项的Services切片中
  • Wails 现在会扫描此服务中的公共方法,并将其提供给前端

3

安装依赖项

我们已在代码中引用github.com/skip2/go-qrcode包,但尚未实际下载它。Go 需要获知这个依赖项并将其下载到你的项目中。

在终端中从项目目录运行以下命令:

bash
go mod tidy

这里做了什么:

  • go mod tidy扫描 Go 文件中的 import 语句
  • 它会下载所有缺失的包(例如go-qrcode),并将其添加到go.mod中
  • 它还会移除所有不再使用的依赖项
  • 这可以确保项目拥有成功编译所需的全部代码

你应该会看到输出,表明二维码包已下载并添加到项目中。


4

生成绑定

要从前端调用这些方法,我们需要生成绑定。 在项目根目录中运行wails generate bindings即可完成此操作。

运行后,你应该会在终端中看到类似以下内容:

bash
 % wails3 generate bindings
 INFO  Processed: 337 Packages, 1 Service, 1 Method, 0 Enums, 0 Models in 740.196125ms.
 INFO  Output directory: /Users/leaanthony/myproject/frontend/bindings

你应该会注意到,前端目录中新增了一个名为bindings的目录:

bash
frontend/
└── bindings
    └── changeme
        ├── index.js
        └── qrservice.js

5

了解绑定

让我们查看bindings/changeme/qrservice.js中生成的绑定:

bindings/changeme/qrservice.js
js
 // @ts-check
 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 // This file is automatically generated. DO NOT EDIT

 /**
  * QRService handles QR code generation
  * @module
  */

 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore: Unused imports
 import {Call as $Call, Create as $Create} from "@wailsio/runtime";

 /**
  * Generate creates a QR code from the given text
  * @param {string} text
  * @param {number} size
  * @returns {Promise<string> & { cancel(): void }}
  */
 export function Generate(text, size) {
     let $resultPromise = /** @type {any} */($Call.ByID(3576998831, text, size));
     let $typingPromise = /** @type {any} */($resultPromise.then(($result) => {
         return $Create.ByteSlice($result);
     }));
     $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
     return $typingPromise;
 }

可以看到,已为Generate方法生成绑定。参数名称和注释均已保留。 此外,还为该方法生成了 JSDoc,以便向 IDE 提供类型信息。

这些绑定提供:

  • 与 Go 方法等效的函数
  • Go 类型与 JavaScript 类型之间的自动转换
  • 基于 Promise 的异步操作
  • 以 JSDoc 注释形式提供的类型信息

生成的服务由一个index.js文件重新导出:

bindings/changeme/index.js
js
 // @ts-check
 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 // This file is automatically generated. DO NOT EDIT

 import * as QRService from "./qrservice.js";
 export {
     QRService
 };

然后,你可以通过仅由 Go 包路径组成的简化导入路径 ./bindings/changeme来访问它, 无需指定任何文件名。


6

在前端中使用绑定

现在可以从 JavaScript 调用 Go 服务了!生成的绑定让调用变得简单且类型安全。

更新frontend/src/main.js以使用新绑定:

frontend/src/main.js
js
 import { QRService } from './bindings/changeme';

 async function generateQR() {
     const text = document.getElementById('text').value;
     if (!text) {
         alert('Please enter some text');
         return;
     }

     try {
         // Generate QR code as base64
         const qrCodeBase64 = await QRService.Generate(text, 256);

         // Display the QR code
         const qrDiv = document.getElementById('qrcode');
         qrDiv.src = `data:image/png;base64,${qrCodeBase64}`;

     } catch (err) {
         console.error('Failed to generate QR code:', err);
         alert('Failed to generate QR code: ' + err);
     }
 }

 export function initializeQRGenerator() {
     const button = document.getElementById('generateButton');
     button.addEventListener('click', generateQR);
 }

这里做了什么:

  • 我们从生成的绑定中导入QRService
  • QRService.Generate()调用我们的 Go 方法——它返回一个 Promise,因此我们使用await
  • Go 方法返回[]byte,Wails 会自动将其转换为供 JavaScript 使用的 base64 字符串
  • 我们使用 base64 字符串创建数据 URL,以显示 PNG 图像
  • try/catch代码块处理来自 Go 端的所有错误(例如无效输入)
  • 如果 Go 代码返回错误,Promise 会被拒绝,我们会在此处捕获该错误

现在更新index.html,以便在initializeQRGenerator函数中使用新绑定:

frontend/src/index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>QR Code Generator</title>
            <style>
                body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                height: 100vh;
                margin: 0;
            }
                #qrcode {
                margin-bottom: 20px;
                width: 256px;
                height: 256px;
                display: flex;
                align-items: center;
                justify-content: center;
            }
                #controls {
                display: flex;
                gap: 10px;
            }
                #text {
                padding: 5px;
            }
                #generateButton {
                padding: 5px 10px;
                cursor: pointer;
            }
            </style>
</head>
<body>
<img id="qrcode"/>
<div id="controls">
    <input type="text" id="text" placeholder="Enter text">
        <button id="generateButton">Generate QR Code</button>
</div>

<script type="module">
    import { initializeQRGenerator } from './main.js';
    document.addEventListener('DOMContentLoaded', initializeQRGenerator);
</script>
</body>
</html>

运行wails3 dev以启动开发服务器。几秒钟后,应用程序应该会打开。

输入一些文本,然后单击“生成二维码”按钮。页面中央应该会显示一个二维码:

二维码



7

替代方法:HTTP 处理程序

到目前为止,我们已经介绍了以下内容:

  • 创建新服务
  • 生成绑定
  • 在前端代码中使用绑定

为什么使用 HTTP 处理程序?

方法绑定非常适合数据操作,但在提供文件、图像或其他媒体时,还有另一种方式。你可以让服务充当一个迷你 Web 服务器,而不必将所有内容转换为 base64 后再通过绑定发送。

这种方式适用于以下情况:

  • 需要提供图像、视频或大文件
  • 希望使用带有src属性的标准 HTML <img>或<video>标签
  • 需要通过 URL 直接访问资源

如果服务实现了 Go 的标准ServeHTTP(w http.ResponseWriter, r *http.Request)方法,Wails 就能让该服务通过 HTTP 端点访问。下面扩展二维码服务以支持此功能:

qrservice.go
go
package main

import (
    "net/http"
    "strconv"

    "github.com/skip2/go-qrcode"
)

// QRService handles QR code generation
type QRService struct {
    // We can add state here if needed
}

// NewQRService creates a new QR service
func NewQRService() *QRService {
    return &QRService{}
}

// Generate creates a QR code from the given text
func (s *QRService) Generate(text string, size int) ([]byte, error) {
    // Generate the QR code
    qr, err := qrcode.New(text, qrcode.Medium)
    if err != nil {
        return nil, err
    }

    // Convert to PNG
    png, err := qr.PNG(size)
    if err != nil {
        return nil, err
    }

    return png, nil
}

func (s *QRService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Extract the text parameter from the request
    text := r.URL.Query().Get("text")
    if text == "" {
        http.Error(w, "Missing 'text' parameter", http.StatusBadRequest)
        return
    }
    // Extract Size parameter from the request
    sizeText := r.URL.Query().Get("size")
    if sizeText == "" {
        sizeText = "256"
    }
    size, err := strconv.Atoi(sizeText)
    if err != nil {
        http.Error(w, "Invalid 'size' parameter", http.StatusBadRequest)
        return
    }

    // Generate the QR code
    qrCodeData, err := s.Generate(text, size)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Write the QR code data to the response
    w.Header().Set("Content-Type", "image/png")
    w.Write(qrCodeData)
}

这里的工作原理:

  • ServeHTTP是 Go 用于处理 HTTP 请求的标准接口
  • 我们从 URL 中解析查询参数(?text=hello&size=256)
  • 我们调用现有的Generate()方法来创建二维码
  • 我们将内容类型设置为image/png,以便浏览器知道它是图像
  • 我们将原始 PNG 字节直接写入响应,无需 base64!

现在更新main.go,指定用于访问二维码服务的路由:

main.go
go
 func main() {

     app := application.New(application.Options{
         Name:        "myproject",
         Description: "A demo of using raw HTML & CSS",
         LogLevel:    slog.LevelDebug,
         Services: []application.Service{
             application.NewServiceWithOptions(NewQRService(), application.ServiceOptions{
                 Route: "/qrservice",
             }),
         },
         Assets: application.AssetOptions{
             Handler: application.AssetFileServerFS(assets),
         },
         Mac: application.MacOptions{
             ApplicationShouldTerminateAfterLastWindowClosed: true,
         },
     })

     app.Window.NewWithOptions(application.WebviewWindowOptions{
         Title:  "myproject",
         Width:  600,
         Height: 400,
     })

     // Run the application. This blocks until the application has been exited.
     err := app.Run()

     // If an error occurred while running the application, log it and exit.
     if err != nil {
         log.Fatal(err)
     }
 }

这里的工作原理:

  • 我们添加application.ServiceOptions,以配置服务的公开方式
  • Route: "/qrservice"使 HTTP 处理程序可通过/qrservice访问
  • 现在,任何发送到/qrservice?text=hello的请求都会调用我们的ServeHTTP方法
  • 如果不设置Route,HTTP 处理程序功能将被禁用

最后更新main.js,使用简单的图像src,不再进行 base64 编码:

frontend/src/main.js
js
async function generateQR() {
    const text = document.getElementById('text').value;
    if (!text) {
        alert('Please enter some text');
        return;
    }

    const img = document.getElementById('qrcode');
    // Make the image source the path to the QR code service, passing the text
    img.src = `/qrservice?text=${encodeURIComponent(text)}`
}

export function initializeQRGenerator() {
    const button = document.getElementById('generateButton');
    if (button) {
        button.addEventListener('click', generateQR);
    } else {
        console.error('Generate button not found');
    }
}

这里的工作原理:

  • 我们移除了导入和await QRService.Generate()调用
  • 我们改为直接将img.src设置为指向 HTTP 端点
  • encodeURIComponent()会安全地转义 URL 中的特殊字符
  • 设置src后,浏览器会自动发出 HTTP GET 请求
  • 对于图像而言,这种方式更简单、更高效,无需进行 base64 转换!

再次运行应用程序,应当会得到相同的二维码:

二维码



8

支持动态配置

硬编码路由的问题:

在上面的示例中,我们在 JavaScript 代码中使用了硬编码路由/qrservice。 这会导致 Go 配置与前端代码紧密耦合。

如果编辑main.go并更改Route选项,却没有更新main.js, 应用程序将无法正常工作:

main.go
go
        // ...
            application.NewServiceWithOptions(NewQRService(), application.ServiceOptions{
                Route: "/services/qr",
            }),
        // ...

硬编码路由适用于简单的应用程序,但会使代码变得脆弱且更难维护。

解决方案:动态配置

方法绑定和 HTTP 处理程序可以协同工作!我们可以使用绑定告知前端应使用哪个路由,使配置动态化并消除硬编码路径。

其工作方式如下:

  1. 应用程序启动时会运行ServiceStartup生命周期方法
  2. 我们保存选项中配置的路由
  3. 我们添加一个URL()方法,供前端调用以获取正确的路由
  4. 现在,前端会向 Go 服务查询其路由,而不再自行猜测

首先,实现ServiceStartup接口并添加新的URL方法:

qrservice.go
go
package main

import (
    "context"
    "net/http"
    "net/url"
    "strconv"

    "github.com/skip2/go-qrcode"
    "github.com/wailsapp/wails/v3/pkg/application"
)

// QRService handles QR code generation
type QRService struct {
    route string
}

// NewQRService creates a new QR service
func NewQRService() *QRService {
    return &QRService{}
}

// ServiceStartup runs at application startup.
func (s *QRService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
    s.route = options.Route
    return nil
}

// Generate creates a QR code from the given text
func (s *QRService) Generate(text string, size int) ([]byte, error) {
    // Generate the QR code
    qr, err := qrcode.New(text, qrcode.Medium)
    if err != nil {
        return nil, err
    }

    // Convert to PNG
    png, err := qr.PNG(size)
    if err != nil {
        return nil, err
    }

    return png, nil
}

// URL returns an URL that may be used to fetch
// a QR code with the given text and size.
// It returns an error if the HTTP handler is not available.
func (s *QRService) URL(text string, size int) (string, error) {
    if s.route == "" {
        return "", errors.New("http handler unavailable")
    }

    return fmt.Sprintf("%s?text=%s&size=%d", s.route, url.QueryEscape(text), size), nil
}

func (s *QRService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Extract the text parameter from the request
    text := r.URL.Query().Get("text")
    if text == "" {
        http.Error(w, "Missing 'text' parameter", http.StatusBadRequest)
        return
    }
    // Extract Size parameter from the request
    sizeText := r.URL.Query().Get("size")
    if sizeText == "" {
        sizeText = "256"
    }
    size, err := strconv.Atoi(sizeText)
    if err != nil {
        http.Error(w, "Invalid 'size' parameter", http.StatusBadRequest)
        return
    }

    // Generate the QR code
    qrCodeData, err := s.Generate(text, size)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Write the QR code data to the response
    w.Header().Set("Content-Type", "image/png")
    w.Write(qrCodeData)
}

这里的工作原理:

  • 我们添加了route字段,用于存储来自ServiceStartup的已配置路由
  • 应用程序启动时会调用ServiceStartup(ctx, options),我们在此处保存路由
  • URL()方法会构建包含查询参数的完整 URL
  • 如果未配置路由(路由为空),我们将返回错误
  • url.QueryEscape()会安全地编码文本,以便在 URL 中使用
  • 前端将能够通过绑定调用此方法

现在更新main.js,使用URL方法替代硬编码路径:

frontend/src/main.js
js
import { QRService } from "./bindings/changeme";

async function generateQR() {
    const text = document.getElementById('text').value;
    if (!text) {
        alert('Please enter some text');
        return;
    }

    const img = document.getElementById('qrcode');
    // Invoke the URL method to obtain an URL for the given text.
    img.src = await QRService.URL(text, 256);
}

export function initializeQRGenerator() {
    const button = document.getElementById('generateButton');
    if (button) {
        button.addEventListener('click', generateQR);
    } else {
        console.error('Generate button not found');
    }
}

这里的工作原理:

  • 我们导入QRService,以再次使用绑定
  • 我们不再对/qrservice进行硬编码,而是调用await QRService.URL(text, 256)
  • Go 服务会使用正确的路由和参数构建 URL
  • 现在,如果更改main.go中的路由,前端会自动使用新路由
  • 不再需要手动同步 Go 配置与前端代码!

其效果应当与上一个示例完全相同, 但更改main.go中的服务路由 不会再导致前端无法正常工作。


编辑页面

最后更新: