Skip to content

[v3] Built-in service standardisation and enhancement #4050

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions docs/src/content/docs/learn/services.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,16 @@ application.NewServiceWithOptions(fileserver.New(&fileserver.Config{
Let's look at a simplified version of the `fileserver` service as an example:

```go
type Config struct {
RootPath string
}

type Service struct {
config *Config
fs http.Handler
fs http.Handler
}

func New(config *Config) *Service {
func NewWithConfig(config *Config) *Service {
return &Service{
config: config,
fs: http.FileServer(http.Dir(config.RootPath)),
}
}
Expand All @@ -149,11 +151,6 @@ func (s *Service) ServiceName() string {
return "github.com/wailsapp/wails/v3/services/fileserver"
}

func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
// Any initialization code here
return nil
}

func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.fs.ServeHTTP(w, r)
}
Expand Down
223 changes: 199 additions & 24 deletions docs/src/content/docs/tutorials/01-creating-a-service.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ This will show you how to organize your code into reusable services and handle e
return &QRService{}
}

// GenerateQRCode creates a QR code from the given text
func (s *QRService) GenerateQRCode(text string, size int) ([]byte, error) {
// 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 {
Expand Down Expand Up @@ -149,21 +149,21 @@ This will show you how to organize your code into reusable services and handle e
// This file is automatically generated. DO NOT EDIT

/**
* QRService handles QR code generation
* @module
*/
* 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";

/**
* GenerateQRCode creates a QR code from the given text
* @param {string} text
* @param {number} size
* @returns {Promise<string> & { cancel(): void }}
*/
export function GenerateQRCode(text, size) {
* 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);
Expand All @@ -173,7 +173,7 @@ This will show you how to organize your code into reusable services and handle e
}
```

We can see that the bindings are generated for the `GenerateQRCode` method. The parameter names have been preserved,
We can see that the bindings are generated for the `Generate` method. The parameter names have been preserved,
as well as the comments. JSDoc has also been generated for the method to provide type information to your IDE.

:::note
Expand All @@ -190,14 +190,36 @@ This will show you how to organize your code into reusable services and handle e
The bindings generator also supports generating Typescript bindings. You can do this by running `wails3 generate bindings -ts`.
:::

<br/>
The generated service is re-exported by an `index.js` file:

```js title="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
};
```

You may then access it through the simplified import path
`./bindings/changeme` consisting just of your Go package path,
without specifying any file name.

:::note
Simplified import paths are only available when using frontend bundlers.
If have a vanilla frontend that does not employ a bundler,
you will have to import either `index.js` or `qrservice.js` manually.
:::
<br/>

6. ## Use Bindings in Frontend

Firstly, update `frontend/src/main.js` to use the new bindings:

```js title="frontend/src/main.js"
import { GenerateQRCode } from './bindings/changeme/qrservice.js';
import { QRService } from './bindings/changeme';

async function generateQR() {
const text = document.getElementById('text').value;
Expand All @@ -208,7 +230,7 @@ This will show you how to organize your code into reusable services and handle e

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

// Display the QR code
const qrDiv = document.getElementById('qrcode');
Expand Down Expand Up @@ -285,7 +307,7 @@ This will show you how to organize your code into reusable services and handle e

Type in some text and click the "Generate QR Code" button. You should see a QR code in the center of the page:

<Image src={qr1} alt="QR Code"/>
<Image src={qr1} alt="QR Code"/>

<br/>
<br/>
Expand All @@ -303,13 +325,14 @@ This will show you how to organize your code into reusable services and handle e
If your service defines Go's standard http handler function `ServeHTTP(w http.ResponseWriter, r *http.Request)`,
then it can be made accessible on the frontend. Let's extend our QR code service to do this:

```go title="qrservice.go" ins={5-6,36-63}
```go title="qrservice.go" ins={4-5,37-65}
package main

import (
"github.com/skip2/go-qrcode"
"net/http"
"strconv"

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

// QRService handles QR code generation
Expand All @@ -322,8 +345,8 @@ This will show you how to organize your code into reusable services and handle e
return &QRService{}
}

// GenerateQRCode creates a QR code from the given text
func (s *QRService) GenerateQRCode(text string, size int) ([]byte, error) {
// 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 {
Expand Down Expand Up @@ -358,7 +381,7 @@ This will show you how to organize your code into reusable services and handle e
}

// Generate the QR code
qrCodeData, err := s.GenerateQRCode(text, size)
qrCodeData, err := s.Generate(text, size)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -408,11 +431,14 @@ This will show you how to organize your code into reusable services and handle e
}
```

:::note
If you do not set the `Route` option explicitly,
the HTTP handler won't be accessible from the frontend.
:::

Finally, update `main.js` to make the image source the path to the QR code service, passing the text as a query parameter:

```js title="frontend/src/main.js"
import { GenerateQRCode } from './bindings/changeme/qrservice.js';

async function generateQR() {
const text = document.getElementById('text').value;
if (!text) {
Expand All @@ -422,7 +448,7 @@ This will show you how to organize your code into reusable services and handle e

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

export function initializeQRGenerator() {
Expand All @@ -440,4 +466,153 @@ This will show you how to organize your code into reusable services and handle e
<Image src={qr1} alt="QR Code"/>
<br/>

8. ## Supporting dynamic configurations

In the example above we used an hardcoded route `/qrservice`.
If you edit `main.go` and change the `Route` option without updating `main.js`,
the application will break:

```go title="main.go" ins={3}
// ...
application.NewService(NewQRService(), application.ServiceOptions{
Route: "/services/qr",
}),
// ...
```

Hardcoded routes can be good for many applications,
but if you need more flexibility, method bindings and HTTP handlers
can work together to improve the development experience.

The `ServiceStartup` Lifecycle method provides access to service options at startup,
and a custom method can be used to announce the configured route to the frontend.

First, implement the `ServiceStartup` interface and add a new `URL` method:

```go title="qrservice.go" ins={4,6,10,15,23-27,46-55}
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)
}
```

Now update `main.js` to use the `URL` method in place of an hardcoded path:

```js title="frontend/src/main.js" ins={1,11-12}
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');
}
}
```

It should work just like the previous example,
but changing the service route in `main.go`
will not break the frontend anymore.

:::note
If a Go method returns a non-nil error,
the promise on the JS side will reject
and await statements will throw an exception.
:::
<br/>

</Steps>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

import * as KeyValueStore from "./keyvaluestore.js";
import * as Service from "./service.js";
export {
KeyValueStore
Service
};
Loading
Loading