二维码服务
创建二维码服务,了解 Wails 服务
Wails 中的服务是一个 Go 结构体,其中包含你希望提供给前端的业务逻辑。服务将相关功能归为一组,使代码井然有序。
可以将服务视为一组可供 JavaScript 代码调用的方法。生成绑定后,前端便可调用服务中的每个公共方法。
在本教程中,我们将创建一个二维码生成器服务来演示这些概念。完成后,你将了解如何创建服务、管理依赖项,以及将 Go 代码连接到前端。
创建二维码服务文件
在应用程序目录中创建一个名为qrservice.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包来实际生成二维码
注册服务
仅创建服务还不够——我们需要将其注册到 Wails 应用程序中,让应用程序知道该服务存在并能为其生成绑定。
创建应用程序时,需要在main.go中进行注册。将服务实例传给Services选项:
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 现在会扫描此服务中的公共方法,并将其提供给前端
安装依赖项
我们已在代码中引用github.com/skip2/go-qrcode包,但尚未实际下载它。Go 需要获知这个依赖项并将其下载到你的项目中。
在终端中从项目目录运行以下命令:
go mod tidy
这里做了什么:
go mod tidy扫描 Go 文件中的 import 语句- 它会下载所有缺失的包(例如
go-qrcode),并将其添加到go.mod中 - 它还会移除所有不再使用的依赖项
- 这可以确保项目拥有成功编译所需的全部代码
你应该会看到输出,表明二维码包已下载并添加到项目中。
生成绑定
要从前端调用这些方法,我们需要生成绑定。 在项目根目录中运行wails generate bindings即可完成此操作。
运行后,你应该会在终端中看到类似以下内容:
% 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的目录:
frontend/
└── bindings
└── changeme
├── index.js
└── qrservice.js
了解绑定
让我们查看bindings/changeme/qrservice.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文件重新导出:
// @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来访问它, 无需指定任何文件名。
在前端中使用绑定
现在可以从 JavaScript 调用 Go 服务了!生成的绑定让调用变得简单且类型安全。
更新frontend/src/main.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函数中使用新绑定:
<!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以启动开发服务器。几秒钟后,应用程序应该会打开。
输入一些文本,然后单击“生成二维码”按钮。页面中央应该会显示一个二维码:

替代方法:HTTP 处理程序
到目前为止,我们已经介绍了以下内容:
- 创建新服务
- 生成绑定
- 在前端代码中使用绑定
为什么使用 HTTP 处理程序?
方法绑定非常适合数据操作,但在提供文件、图像或其他媒体时,还有另一种方式。你可以让服务充当一个迷你 Web 服务器,而不必将所有内容转换为 base64 后再通过绑定发送。
这种方式适用于以下情况:
- 需要提供图像、视频或大文件
- 希望使用带有
src属性的标准 HTML<img>或<video>标签 - 需要通过 URL 直接访问资源
如果服务实现了 Go 的标准ServeHTTP(w http.ResponseWriter, r *http.Request)方法,Wails 就能让该服务通过 HTTP 端点访问。下面扩展二维码服务以支持此功能:
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,指定用于访问二维码服务的路由:
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 编码:
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 转换!
再次运行应用程序,应当会得到相同的二维码:

支持动态配置
硬编码路由的问题:
在上面的示例中,我们在 JavaScript 代码中使用了硬编码路由/qrservice。 这会导致 Go 配置与前端代码紧密耦合。
如果编辑main.go并更改Route选项,却没有更新main.js, 应用程序将无法正常工作:
// ...
application.NewServiceWithOptions(NewQRService(), application.ServiceOptions{
Route: "/services/qr",
}),
// ...硬编码路由适用于简单的应用程序,但会使代码变得脆弱且更难维护。
解决方案:动态配置
方法绑定和 HTTP 处理程序可以协同工作!我们可以使用绑定告知前端应使用哪个路由,使配置动态化并消除硬编码路径。
其工作方式如下:
- 应用程序启动时会运行
ServiceStartup生命周期方法 - 我们保存选项中配置的路由
- 我们添加一个
URL()方法,供前端调用以获取正确的路由 - 现在,前端会向 Go 服务查询其路由,而不再自行猜测
首先,实现ServiceStartup接口并添加新的URL方法:
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方法替代硬编码路径:
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中的服务路由 不会再导致前端无法正常工作。