參與貢獻

編輯此文件

翻譯文件,或在您的電腦上開啟完整專案。

在您選擇提交成果之前,不會發布任何內容。

QR Code 服務

建立 QR Code 服務,瞭解 Wails 服務

Wails 中的服務是一個 Go 結構,其中包含您想提供給前端使用的業務邏輯。服務會將相關功能集中在一起,讓程式碼井然有序。

您可以將服務視為一組可由 JavaScript 程式碼呼叫的方法。產生繫結後,前端就能呼叫服務中的每個公開方法。

在本教學中,我們將建立 QR Code 產生器服務來示範這些概念。完成後,您將瞭解如何建立服務、管理相依套件,以及將 Go 程式碼連接至前端。


1

建立 QR 服務檔案

在應用程式目錄中建立名為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是一個空結構,將用來存放 QR Code 產生方法
  • NewQRService()是一個建構函式,用來建立服務的新執行個體
  • Generate()是一個接受文字與尺寸,並以 PNG 位元組陣列傳回 QR Code 的方法
  • 此方法會傳回([]byte, error),遵循 Go 將錯誤作為最後一個值傳回的慣例
  • 我們使用github.com/skip2/go-qrcode套件實際產生 QR Code

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
  • 它也會移除任何不再使用的相依套件
  • 這可確保專案具備成功編譯所需的所有程式碼

您應該會看到輸出,指出 QR Code 套件已下載並新增至專案。


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
 };

接著,您可以透過簡化的匯入路徑存取它。 ./bindings/changeme只包含 Go 套件路徑, 無須指定任何檔案名稱。


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以啟動開發伺服器。幾秒後,應用程式應會開啟。

輸入一些文字,然後按一下「產生 QR 碼」按鈕。您應該會在頁面中央看到 QR 碼:

QR 碼



7

替代方法:HTTP 處理常式

到目前為止,我們已涵蓋以下內容:

  • 建立新服務
  • 產生繫結
  • 在前端程式碼中使用繫結

為什麼要使用 HTTP 處理常式?

方法繫結很適合用於資料操作,但若要提供檔案、圖片或其他媒體,還有另一種做法。您不必將所有內容轉換成 base64 並透過繫結傳送,而是可以讓服務像小型網頁伺服器一樣運作。

這適用於下列情況:

  • 您要提供圖片、影片或大型檔案
  • 您想使用具有src屬性的標準 HTML <img>或<video>標籤
  • 您需要透過 URL 直接存取資源

如果您的服務實作 Go 的標準ServeHTTP(w http.ResponseWriter, r *http.Request)方法,Wails 就能將它公開為 HTTP 端點。讓我們擴充 QR Code 服務以支援此功能:

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()方法來建立 QR Code
  • 我們將內容類型設為image/png,讓瀏覽器知道這是圖片
  • 我們將原始 PNG 位元組直接寫入回應,不需要 base64!

現在更新main.go,指定可存取 QR Code 服務的路由:

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"讓您可在/qrservice存取 HTTP 處理常式
  • 現在,任何傳送至/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!

再次執行應用程式後,應該會產生相同的 QR Code:

QR 碼



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中的服務路由時, 前端不會再因此出錯。


編輯頁面

上次更新: