- Published:
- By:
- Colin Tester
Build backwards compatible apps with Go and Wails.
Wails was created to make it easier for developers to create desktop apps using Go and web technologies.
A main feature of Wails is that it uses the OS native rendering engine, e.g. WebKit WebView on Mac OS, in order to present HTML views and run client-side JavaScript. An approach different from, say, Electron, which embeds Chromium and Node.js directly into the final application binary. Wails' approach, therefore, results in a much smaller file size for the app binary.
Wails uses Vite – a fast build tool for web technologies – to bundle and watch client-side JavaScript as it is developed. Vite's default operating mode is to parse client code to run on browsers that support ECMAScript 2015+.
However, I needed to be sure that the apps I intend to share would run on both current and older desktop platforms. So, my approach in this article, was to configure Vite to produce client code that will be backwards compatible.
Ultimately, I wished to explore how the experience of developing with Wails could be tailored, restructuring the folder and files Wails creates automatically; in order to accommodate my operational preferences.
I primarily use an Apple Mac and the following article outlines the steps I took to provide a coherent working environment for developing desktop apps with Go and Wails.
An example project to support this article is available on Github
Wails Installation
Wails requires two dependencies, Go 1.18+ and NPM (Node15+), so make sure they are installed before continuing.
One should also view the Wails installation document for platform specific guidance.
To install Wails, type the following into the CLI:
$ go install github.com/wailsapp/wails/v2/cmd/wails@v2.3.1Latest version at the time of writing is: v2.3.1
Once installation has completed, it is recommended to run the Wails system check, making sure the correct Wails dependencies are also installed. At the CLI, type the following:
$ wails doctor
Wails app project initialisation
This section is about allowing Wails to initialise a project workspace.
Create the project folder in the workspace – substitute your own project name for ‘project-name’.
$ mkdir project-name \ && cd project-nameInitialise the new project workspace using Wails.
$ wails init -n project-name -d .Flags used with the init command:
-nSet the name of the project – this is mandatory.-dfollowed by the project directory; I specified ‘.’ meaning the current directory.
Wails will create the following folder and files structure.
. ├── README.md ├── app.go ├── build ├── frontend ├── go.mod ├── go.sum ├── main.go └── wails.json
Set up JavaScript dev environment
For this project, I wish to develop views using sass files, bundled into a single css asset. I also wish to structure the project's JavaScript, CSS and image files under a sub-directory; maintaining an organisation and working convention.
- Within the
frontendfolder, remove thesrcfolder and its files. - Rename the
frontendfolder tosrc. - Within the renamed ‘src’ folder create folders for:
js,sassandimage, these will hold the JavaScript, CSS and view images respectively. - Within
src/jscreate a new file labelled:index.js. Within
src/sasscreate a new file labelled:index.sass.Our
srcfolder should be similar to the following:src ├── dist ├── image ├── index.html ├── js │ └── index.js ├── package.json ├── sass │ └── index.sass └── wailsjs
The src/index.html created by Wails is used to present the view and content within the desktop app window.
Edit the
src/index.htmlfile, adding to the HTML<head>element a link to our newsrc/sass/index.sassfile.<link rel="stylesheet" href="sass/index.sass">Lower down in the same
src/index.htmlfile, modify the script tag to source thejs/index.jsfile.<script src="js/index.js" type="module">Now back in the CLI, change working directory to
src.$ cd src
Vite uses RollUp to bundle JavaScript code. In order to get the desired backwards compatibility, we need to utilise a Babel plugin.
At the CLI, install the following npm modules as dev dependencies:
- vite@latest
- sass
- @rollup/plugin-babel
- @babel/plugin-proposal-class-properties
- @babel/plugin-proposal-nullish-coalescing-operator
- @babel/plugin-proposal-optional-chaining
- @babel/plugin-syntax-dynamic-import
e.g.
$ npm i -D vite@latestRepeat for each listed npm module.Open the
src/package.jsonfile, which should indicate the installed devDependencies:"devDependencies": { "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.21.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@rollup/plugin-babel": "^6.0.3", "sass": "^1.58.3", "vite": "^4.1.4" }Note: that installed version numbers indicated in your package.json file may differ from those above.
We now need to let Vite know that the RollUp and Babel dependencies are to be used when bundling JavaScript files.
Create a Babel configuration file:
src/babel.config.jsonand place within it the following JSON:{ "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-optional-chaining", "@babel/plugin-syntax-dynamic-import" ] }Now create a Vite configuration file:
src/vite.config.jsand place within it the following JavaScript:import { babel } from '@rollup/plugin-babel'; export default { plugins: [ babel({ babelHelpers: 'bundled' }) ], }
We have now configured Vite to utilise a plugin for the Babel compiler to convert modern ECMAScript code into a backwards compatible version of JavaScript, specifically using the @babel plugins defined in the src/babel.config.json file.
Adding a custom icon for our Desktop app
Our app will need an icon, giving it a unique visual identity wherever it is viewable on a target platform, e.g. the Mac OS Dock and Windows Taskbar.
Create a PNG image file (1024 × 1024 pixels) containing your custom icon artwork and place it within the
buildfolder, replacing the existing file:appicon.png.Wails will use
appicon.pngas the application icon when building desktop app binaries.Whilst in the
buildfolder, remove both thedarwinandwindowsfolders. These will be rebuilt by Wails later, at which point our custom icon file will be utilised.
Set up Go dev environment
Wails generates bindings for Inter-process Communication (IPC) between Go application code and JavaScript code.
- Within the root of the project folder, edit the
go.modfile changing themodulename, ‘changeme’ declared at the top, to something relevant to you and the project, e.g.project-domain/project-name– substituting your own project name. - Remove the
app.gofile. - Create a folder labelled: ‘go’. This folder will hold all the app Go packages and files.
- Within the new
gofolder, create a new subfolder labelled:IPC(Inter-process Communication), and within that a file labelled:service.go. Open the
go/IPC/service.gofile and add to the top:package IPC.The Service will act as the main pipe through which all communications will be handled between the Go and JavaScript code.
Within
service.goappend the following:package IPC import ( "context" ) // Service struct type Service struct { ctx context.Context } // NewService creates a new IPC Service struct. func NewService() *Service { return &Service{} } // Represents IPC shared data. type data map[string]interface{} // Expected form of Service request parameters. type Request struct { Module string `json:"module"` Method string `json:"method"` Data data `json:"data"` } // Expected form of Service response data. type response struct { Module string `json:"module"` Method string `json:"method"` Data data `json:"data"` } // Request – method through which all IPC service requests are received. func (s *Service) Request(req Request) response { res := response{} res.Module = req.Module res.Method = req.Method // Set content to be returned to the request caller (JavaScript code): res.Data = data{"content": "I am thinking, therefore I exist"} return res }From the project's root folder, open to edit the
main.gofile.To keeps things simple at this level, the
main.gofile will contain only the definition of options for Wails and its instantiation, it will also import our IPC service API.We are going to modify the code generated by the Wails initialisation process.
Edit the line embedding the assets for the client view to make sure it now points to our renamed
srcfolder: substitutesrcforfrontendmaking the embed command read as//go:embed all:src/dist.The
distfolder is created by Vite and used by Wails to pull our client-side assets into the desktop app.We wish also to embed our custom icon into our app binary, so that it can be used within desktop views, e.g. within an ‘About’ view. Add to the
main.gofile the lines://go:embed build/appicon.png var appIcon []byteWithin the
func main()remove all the contained code and then insert the following line:// assign an instance of our IPC Service API: service := IPC.NewService()We also need to make sure we add to the upper part of
main.goan import statement for:import ( … "project-domain/project-name/go/IPC" )Further within the
func main()we will configure the Wails app. Add the following tofunc main():// Specific macOS desktop app options. macOptions := &mac.Options{ Appearance: mac.NSAppearanceNameDarkAqua, About: &mac.AboutInfo{ Title: "ProjectName", // substitute your own App's name. Message: "© 2023 Project Owner", // subtitute your own ‘Project Owner’. Icon: appIcon, // here we reference our embedded custom icon. }, } // Define Wails operating options: wailsOptions := &options.App{ Title: "ProjectName", // substitute your own App's name. Width: 1024, Height: 768, AssetServer: &assetserver.Options{ Assets: assets, // passing in our resources embedded from src/dist. }, // Bind our Service instance for IPC. Bind: []interface{}{ service, }, Mac: macOptions, } // Create application with defined options: if err := wails.Run(wailsOptions); err != nil { fmt.Println("Wails setup error:", err.Error()) }We also need to make sure we import the required packages:
import ( "embed" "fmt" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/mac" // substitute your own domain and project name as specified in the go.mod file. "project-domain/project-name/go/IPC" )
Connecting up the IPC pipe
Wails, when generating bindings for IPC, builds JavaScript files within which the dependencies are defined. These dependencies are determined from exported (public) properties and methods defined within the Go app. They can be imported to the client code as needed.
However, on the client-side, the IPC is also represented by a global object assigned to the window object; this is how dependencies are referenced directly by the Wails generated JavaScript file.
We have, therefore, two ways to reference our IPC service, either by importing it from the Wails generated JavaScript file or directly off the window object. I favour using the latter.
We can now, within our src/js/index.js file, establish a connection to the Go app IPC service.
Within the
src/jsfolder open to editindex.jsand add the following:// assign a reference to the IPC servive. const service = window.go && window.go.IPC.Service;In the example code above, the
servicereference is a global but could be assigned any way deemed appropriate.Any methods exported via the Go app service can be accessed via the
servicereference, e.g. to send a request to the Go runtime – using the Service API Request method – we would use:service.Request().
Let's add some more to our src/index.js file to make a simple request to the Service IPC to get content for our app main view.
Append to
src/index.jsthe following:// assign a reference to the <div id="app"> element: const app = document.querySelector('#app'); // set up service request parameters: const params = { module: 'ergo', method: 'get', }; // call the IPC service Request method, which returns a Promise: service.Request(params).then(res => { // display the content returned by the service Request method: app.innerText = res.data.content; });When the Service request is made, we would expect the Promise to resolve with a response object returned from the Service IPC written in Go. The response object carries data with a
contentproperty value, which is set as the text inside our referencedappHTML element.
Let's also add some simple styling for our app view.
Open to edit
src/sass/index.sassand add the following:body margin: 0 height: 100vh font-family: verdana, helvetica, sans-serif font-size: 2rem display: flex justify-content: center align-items: center
Configure Wails for building our app
From the project root folder, open to edit the Wails configuration file:
wails.json.Change the fields:
nameandoutputfilenameto a value you wish to use.Add fields for:
frontend:dirandreloaddirssetting each with the value of"src". These tell Wails the name of our client code folder, in which it observes the Vitedistdirectory and that files within thesrcfolder should be watched for changes.The
wails.jsonfile should now include the following properties and values:{ ... "frontend:dir": "src", "reloaddirs": "src", ... }
Let's now take the opportunity to test that all is working Ok.
On the CLI in the root of the project folder, i.e. change directory up one level from
src, type:$ wails devWails will go through steps to prepare the client-side code, run Vite, then build and run a desktop app.
As a result you should see a desktop window open for your app, similar to figure 1 below.
Wails will now operate in watching mode; watching for changes to the app files and assets. You can now make changes to the JavaScript and Go code and Wails will either push those changes to the app or rebuild the app, as needed.
You can exit out of watching mode by either quitting the open app, or pressing Ctrl+C in the CLI.
Build a final binary
With the development phase working, let's now build a final binary file, one each to run on Mac and Windows.
On the CLI in the root of the project folder, type the build command:
$ wails build -clean -platform "darwin,windows/amd64" -o MyApp.exeFlags used with the build command:
-cleanremoves previous builds before compiling.-platformfollowed by the platform types, separated by a comma, informs Wails which OS platforms to build binaries for. See more information on the supported platforms.-ofollowed by a filename, specifies the output filename and extension to use, although this appears to apply only to Windows binaries.
The folder build/bin should contain two versions of your app: one for Mac and another .exe file for Windows.
Summary
The article outlined:
- Installing and configuring Wails.
- Configuring Vite and its JavaScript bundling options.
- Establishing an IPC (Inter-process Communcation) service between Go and JavaScript.
- Adding an application custom icon.
- Building final binaries for Mac and Windows.
It would be worth exploring further the Wails configuration options, particularly how to configure application menus, and the Wails runtime library.
I hope this article has been helpful for you to begin developing desktop apps with Go and Wails.
Article relevant links:
- Example project files on Github to support this article.
- Go All releases downloads page: https://go.dev/dl/
- Node Downloads page: https://nodejs.org/en/download/
- Wails website: https://wails.io/
- Wails Introduction: https://wails.io/docs/introduction
- Wails Installation: https://wails.io/docs/gettingstarted/installation
- Configuring Vite: https://vitejs.dev/config/
- Rollup Plugins: https://vite-rollup-plugins.patak.dev/
- What is Babel? https://babeljs.io/docs/