Lewati ke konten

Layanan QR Code

Service di Wails adalah struct Go yang berisi logika bisnis yang ingin Anda sediakan ke frontend. Service menjaga kode tetap terorganisir dengan mengelompokkan fungsionalitas terkait.

Anggap service sebagai kumpulan metode yang dapat dipanggil oleh kode JavaScript Anda. Setiap metode publik pada service menjadi dapat dipanggil dari frontend setelah binding dibuat.

Dalam tutorial ini, kita akan membuat layanan generator QR code untuk mendemonstrasikan konsep-konsep ini. Di akhir tutorial, Anda akan memahami cara membuat service, mengelola dependensi, dan menghubungkan kode Go ke frontend.


  1. Buat file baru bernama qrservice.go di direktori aplikasi Anda:

    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
    }

    Apa yang terjadi di sini:

    • QRService adalah struct kosong yang akan menampung metode pembuatan QR code kita
    • NewQRService() adalah fungsi konstruktor yang membuat instance baru dari service kita
    • Generate() adalah metode yang menerima teks dan ukuran, lalu mengembalikan QR code sebagai array byte PNG
    • Metode mengembalikan ([]byte, error) mengikuti konvensi Go yang mengembalikan error sebagai nilai terakhir
    • Kita menggunakan paket github.com/skip2/go-qrcode untuk menangani pembuatan QR code

  2. Membuat service saja tidak cukup — kita perlu mendaftarkannya ke aplikasi Wails agar aplikasi mengetahui service tersebut ada dan dapat membuat binding untuknya.

    Pendaftaran dilakukan di main.go saat Anda membuat aplikasi. Anda meneruskan instance service ke opsi Services:

    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.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)
    }
    }

    Apa yang terjadi di sini:

    • application.NewService() membungkus service Anda agar Wails dapat mengelolanya
    • Kita memanggil NewQRService() untuk membuat instance service kita
    • Service ditambahkan ke slice Services dalam opsi aplikasi
    • Wails akan memindai service ini untuk metode publik yang tersedia di frontend

  3. Kita mereferensikan paket github.com/skip2/go-qrcode di kode kita, tetapi belum mengunduhnya. Go perlu mengetahui dependensi ini dan mengunduhnya ke proyek Anda.

    Jalankan perintah ini di terminal dari direktori proyek Anda:

    Terminal window
    go mod tidy

    Apa yang terjadi di sini:

    • go mod tidy memindai file Go Anda untuk pernyataan import
    • Perintah ini mengunduh paket yang hilang (seperti go-qrcode) dan menambahkannya ke go.mod
    • Juga menghapus dependensi yang tidak lagi digunakan
    • Ini memastikan proyek Anda memiliki semua kode yang diperlukan untuk kompilasi berhasil

    Anda seharusnya melihat output yang menunjukkan paket QR code telah diunduh dan ditambahkan ke proyek.


  4. Untuk memanggil metode ini dari frontend, kita perlu membuat binding. Anda dapat melakukannya dengan menjalankan wails generate bindings di direktori root proyek.

    Setelah dijalankan, Anda seharusnya melihat output serupa berikut di terminal:

    Terminal window
    % 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

    Perhatikan bahwa di direktori frontend, ada direktori baru bernama bindings:

    Terminal window
    frontend/
    └── bindings
    └── changeme
    ├── index.js
    └── qrservice.js

  5. Mari lihat binding yang dihasilkan di bindings/changeme/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;
    }

    Kita dapat melihat binding dibuat untuk metode Generate. Nama parameter dipertahankan, begitu pula komentar. JSDoc juga dihasilkan untuk metode guna memberikan informasi tipe ke IDE Anda.

    Binding menyediakan:

    • Fungsi yang setara dengan metode Go Anda
    • Konversi otomatis antara tipe Go dan JavaScript
    • Operasi async berbasis Promise
    • Informasi tipe sebagai komentar JSDoc

    Service yang dihasilkan diekspor ulang oleh file index.js:

    bindings/changeme/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
    };

    Anda kemudian dapat mengaksesnya melalui path import yang disederhanakan ./bindings/changeme yang hanya terdiri dari path paket Go Anda, tanpa perlu menentukan nama file.


  6. Sekarang kita dapat memanggil service Go dari JavaScript! Binding yang dihasilkan membuat ini mudah dan type-safe.

    Perbarui frontend/src/main.js untuk menggunakan binding baru:

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

    Apa yang terjadi di sini:

    • Kita mengimpor QRService dari binding yang dihasilkan
    • QRService.Generate() memanggil metode Go kita — mengembalikan Promise, jadi kita gunakan await
    • Metode Go mengembalikan []byte, yang Wails otomatis konversi ke string base64 untuk JavaScript
    • Kita membuat data URL dengan string base64 untuk menampilkan gambar PNG
    • Blok try/catch menangani error dari sisi Go (seperti input tidak valid)
    • Jika kode Go kita mengembalikan error, Promise ditolak dan kita tangkap di sini

    Sekarang perbarui index.html untuk menggunakan binding baru di fungsi initializeQRGenerator:

    frontend/src/index.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>

    Jalankan wails3 dev untuk memulai dev server. Setelah beberapa detik, aplikasi seharusnya terbuka.

    Ketik teks dan klik tombol “Generate QR Code”. Anda seharusnya melihat QR code di tengah halaman:

    QR Code

  7. Sejauh ini, kita telah membahas area berikut:

    • Membuat Service baru
    • Membuat Binding
    • Menggunakan Binding di kode Frontend kita

    Mengapa menggunakan HTTP handler?

    Method binding bekerja dengan baik untuk operasi data, tetapi ada pendekatan alternatif untuk menyajikan file, gambar, atau media lainnya. Alih-alih mengonversi semuanya ke base64 dan mengirimkannya melalui binding, Anda dapat membuat service bertindak seperti mini web server.

    Ini berguna ketika:

    • Anda menyajikan gambar, video, atau file besar
    • Anda ingin menggunakan tag HTML standar <img> atau <video> dengan atribut src
    • Anda memerlukan akses URL langsung ke resource

    Jika service Anda mengimplementasikan metode standar Go ServeHTTP(w http.ResponseWriter, r *http.Request), Wails dapat menjadikannya dapat diakses sebagai endpoint HTTP. Mari perluas layanan QR code kita untuk mendukung ini:

    qrservice.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)
    }

    Apa yang terjadi di sini:

    • ServeHTTP adalah interface standar Go untuk menangani permintaan HTTP
    • Kita mem-parsing parameter query dari URL (?text=hello&size=256)
    • Kita memanggil metode Generate() yang sudah ada untuk membuat QR code
    • Kita set content type ke image/png agar browser tahu ini gambar
    • Kita menulis byte PNG mentah langsung ke response — tidak perlu base64!

    Sekarang perbarui main.go untuk menentukan route tempat layanan QR code dapat diakses:

    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.NewService(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)
    }
    }

    Apa yang terjadi di sini:

    • Kita menambahkan application.ServiceOptions untuk mengonfigurasi cara service diekspos
    • Route: "/qrservice" membuat HTTP handler dapat diakses di /qrservice
    • Sekarang permintaan ke /qrservice?text=hello akan memanggil metode ServeHTTP kita
    • Tanpa mengatur Route, fungsionalitas HTTP handler dinonaktifkan

    Terakhir, perbarui main.js untuk menggunakan src gambar sederhana alih-alih encoding base64:

    frontend/src/main.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');
    }
    }

    Apa yang terjadi di sini:

    • Kita menghapus import dan pemanggilan await QRService.Generate()
    • Sebagai gantinya, kita cukup set img.src ke endpoint HTTP kita
    • encodeURIComponent() meng-escape karakter khusus dengan aman di URL
    • Browser otomatis membuat permintaan HTTP GET saat kita set src
    • Ini lebih sederhana dan efisien untuk gambar — tidak perlu konversi base64!

    Menjalankan aplikasi lagi seharusnya menghasilkan QR code yang sama:

    QR Code
  8. Masalah dengan route hardcoded:

    Pada contoh di atas kita menggunakan route hardcoded /qrservice di kode JavaScript. Ini menciptakan coupling ketat antara konfigurasi Go dan kode frontend Anda.

    Jika Anda mengedit main.go dan mengubah opsi Route tanpa memperbarui main.js, aplikasi akan rusak:

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

    Route hardcoded bekerja untuk aplikasi sederhana, tetapi membuat kode rapuh dan sulit dirawat.

    Solusinya: Konfigurasi dinamis

    Method binding dan HTTP handler dapat bekerja bersama! Kita dapat menggunakan binding untuk memberi tahu frontend route mana yang digunakan, sehingga konfigurasi menjadi dinamis dan menghilangkan path hardcoded.

    Cara kerjanya:

    1. Metode lifecycle ServiceStartup berjalan saat aplikasi dimulai
    2. Kita menyimpan route yang dikonfigurasi dari opsi
    3. Kita menambahkan metode URL() yang dapat dipanggil frontend untuk mendapatkan route yang benar
    4. Sekarang frontend meminta route ke service Go alih-alih menebak

    Pertama, implementasikan interface ServiceStartup dan tambahkan metode URL baru:

    qrservice.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)
    }

    Apa yang terjadi di sini:

    • Kita menambahkan field route untuk menyimpan route yang dikonfigurasi dari ServiceStartup
    • ServiceStartup(ctx, options) dipanggil saat aplikasi dimulai — kita simpan route di sini
    • Metode URL() membangun URL lengkap dengan parameter query
    • Jika tidak ada route yang dikonfigurasi (route kosong), kita kembalikan error
    • url.QueryEscape() meng-encode teks dengan aman untuk digunakan di URL
    • Metode ini akan tersedia di frontend melalui binding

    Sekarang perbarui main.js untuk menggunakan metode URL alih-alih path hardcoded:

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

    Apa yang terjadi di sini:

    • Kita mengimpor QRService untuk menggunakan binding lagi
    • Alih-alih hardcode /qrservice, kita panggil await QRService.URL(text, 256)
    • Service Go membangun URL dengan route dan parameter yang benar
    • Sekarang jika Anda mengubah route di main.go, frontend otomatis menggunakan route baru
    • Tidak perlu sinkronisasi manual antara konfigurasi Go dan kode frontend!

    Seharusnya bekerja seperti contoh sebelumnya, tetapi mengubah route service di main.go tidak akan merusak frontend lagi.