mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 17:41:24 +08:00
[windows-x] Support calling bound methods
This commit is contained in:
parent
316e4de8e2
commit
8e84bdfa8d
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/leaanthony/debme"
|
"github.com/leaanthony/debme"
|
||||||
"github.com/leaanthony/slicer"
|
"github.com/leaanthony/slicer"
|
||||||
|
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -14,18 +15,21 @@ import (
|
|||||||
type AssetServer struct {
|
type AssetServer struct {
|
||||||
assets debme.Debme
|
assets debme.Debme
|
||||||
indexFile []byte
|
indexFile []byte
|
||||||
|
runtimeJS string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAssetServer(assets embed.FS, bindingsJSON string) (*AssetServer, error) {
|
||||||
|
result := &AssetServer{
|
||||||
|
runtimeJS: `window.wailsbindings='` + bindingsJSON + `';` + runtime.RuntimeJS,
|
||||||
|
}
|
||||||
|
err := result.init(assets)
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AssetServer) IndexHTML() string {
|
func (a *AssetServer) IndexHTML() string {
|
||||||
return string(a.indexFile)
|
return string(a.indexFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAssetServer(assets embed.FS) (*AssetServer, error) {
|
|
||||||
result := &AssetServer{}
|
|
||||||
err := result.init(assets)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func injectScript(input string, script string) ([]byte, error) {
|
func injectScript(input string, script string) ([]byte, error) {
|
||||||
splits := strings.Split(input, "<head>")
|
splits := strings.Split(input, "<head>")
|
||||||
if len(splits) != 2 {
|
if len(splits) != 2 {
|
||||||
|
@ -4,7 +4,6 @@ package assetserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,7 +18,7 @@ func (a *AssetServer) init(assets embed.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
a.indexFile, err = injectScript(string(indexHTML), "<script>"+runtime.RuntimeJS+"</script>")
|
a.indexFile, err = injectScript(string(indexHTML), "<script>"+a.runtimeJS+"</script>")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
5
v2/internal/frontend/calls.go
Normal file
5
v2/internal/frontend/calls.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package frontend
|
||||||
|
|
||||||
|
type Calls interface {
|
||||||
|
Callback(string)
|
||||||
|
}
|
@ -2,4 +2,5 @@ package frontend
|
|||||||
|
|
||||||
type Dispatcher interface {
|
type Dispatcher interface {
|
||||||
ProcessMessage(message string) error
|
ProcessMessage(message string) error
|
||||||
|
SetCallbackHandler(func(string))
|
||||||
}
|
}
|
||||||
|
59
v2/internal/frontend/dispatcher/calls.go
Normal file
59
v2/internal/frontend/dispatcher/calls.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callMessage struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args []json.RawMessage `json:"args"`
|
||||||
|
CallbackID string `json:"callbackID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dispatcher) processCallMessage(message string) error {
|
||||||
|
|
||||||
|
var payload callMessage
|
||||||
|
err := json.Unmarshal([]byte(message[1:]), &payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Lookup method
|
||||||
|
registeredMethod := d.bindingsDB.GetMethod(payload.Name)
|
||||||
|
|
||||||
|
// Check we have it
|
||||||
|
if registeredMethod == nil {
|
||||||
|
return fmt.Errorf("method '%s' not registered", payload.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := registeredMethod.ParseArgs(payload.Args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing arguments: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := registeredMethod.Call(args)
|
||||||
|
callbackMessage := &CallbackMessage{
|
||||||
|
CallbackID: payload.CallbackID,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
callbackMessage.Err = err.Error()
|
||||||
|
} else {
|
||||||
|
callbackMessage.Result = result
|
||||||
|
}
|
||||||
|
messageData, err := json.Marshal(callbackMessage)
|
||||||
|
d.log.Trace("json call result data: %+v\n", string(messageData))
|
||||||
|
if err != nil {
|
||||||
|
// what now?
|
||||||
|
d.log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
d.resultCallback(string(messageData))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallbackMessage defines a message that contains the result of a call
|
||||||
|
type CallbackMessage struct {
|
||||||
|
Result interface{} `json:"result"`
|
||||||
|
Err string `json:"error"`
|
||||||
|
CallbackID string `json:"callbackid"`
|
||||||
|
}
|
@ -8,19 +8,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Dispatcher struct {
|
type Dispatcher struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
bindings *binding.Bindings
|
bindings *binding.Bindings
|
||||||
events frontend.Events
|
events frontend.Events
|
||||||
|
bindingsDB *binding.DB
|
||||||
|
resultCallback func(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDispatcher(log *logger.Logger, bindings *binding.Bindings, events frontend.Events) *Dispatcher {
|
func NewDispatcher(log *logger.Logger, bindings *binding.Bindings, events frontend.Events) *Dispatcher {
|
||||||
return &Dispatcher{
|
return &Dispatcher{
|
||||||
log: log,
|
log: log,
|
||||||
bindings: bindings,
|
bindings: bindings,
|
||||||
events: events,
|
events: events,
|
||||||
|
bindingsDB: bindings.DB(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dispatcher) SetCallbackHandler(handler func(string)) {
|
||||||
|
d.resultCallback = handler
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dispatcher) ProcessMessage(message string) error {
|
func (d *Dispatcher) ProcessMessage(message string) error {
|
||||||
if message == "" {
|
if message == "" {
|
||||||
return errors.New("No message to process")
|
return errors.New("No message to process")
|
||||||
@ -30,6 +37,8 @@ func (d *Dispatcher) ProcessMessage(message string) error {
|
|||||||
return d.processLogMessage(message)
|
return d.processLogMessage(message)
|
||||||
case 'E':
|
case 'E':
|
||||||
return d.processEventMessage(message)
|
return d.processEventMessage(message)
|
||||||
|
case 'C':
|
||||||
|
return d.processCallMessage(message)
|
||||||
default:
|
default:
|
||||||
return errors.New("Unknown message from front end: " + message)
|
return errors.New("Unknown message from front end: " + message)
|
||||||
}
|
}
|
||||||
|
66
v2/internal/frontend/runtime/desktop/bindings.js
Normal file
66
v2/internal/frontend/runtime/desktop/bindings.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The lightweight framework for web-like apps
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
|
import { Call } from './calls';
|
||||||
|
|
||||||
|
window.backend = {};
|
||||||
|
|
||||||
|
export function SetBindings(bindingsMap) {
|
||||||
|
try {
|
||||||
|
bindingsMap = JSON.parse(bindingsMap);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise the backend map
|
||||||
|
window.backend = window.backend || {};
|
||||||
|
|
||||||
|
// Iterate package names
|
||||||
|
Object.keys(bindingsMap).forEach((packageName) => {
|
||||||
|
|
||||||
|
// Create inner map if it doesn't exist
|
||||||
|
window.backend[packageName] = window.backend[packageName] || {};
|
||||||
|
|
||||||
|
// Iterate struct names
|
||||||
|
Object.keys(bindingsMap[packageName]).forEach((structName) => {
|
||||||
|
|
||||||
|
// Create inner map if it doesn't exist
|
||||||
|
window.backend[packageName][structName] = window.backend[packageName][structName] || {};
|
||||||
|
|
||||||
|
Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => {
|
||||||
|
|
||||||
|
window.backend[packageName][structName][methodName] = function () {
|
||||||
|
|
||||||
|
// No timeout by default
|
||||||
|
let timeout = 0;
|
||||||
|
|
||||||
|
// Actual function
|
||||||
|
function dynamic() {
|
||||||
|
const args = [].slice.call(arguments);
|
||||||
|
return Call([packageName, structName, methodName].join('.'), args, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow setting timeout to function
|
||||||
|
dynamic.setTimeout = function (newTimeout) {
|
||||||
|
timeout = newTimeout;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow getting timeout to function
|
||||||
|
dynamic.getTimeout = function () {
|
||||||
|
return timeout;
|
||||||
|
};
|
||||||
|
|
||||||
|
return dynamic;
|
||||||
|
}();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
143
v2/internal/frontend/runtime/desktop/calls.js
Normal file
143
v2/internal/frontend/runtime/desktop/calls.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The lightweight framework for web-like apps
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
|
import { SendMessage } from './ipc';
|
||||||
|
|
||||||
|
var callbacks = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a number from the native browser random function
|
||||||
|
*
|
||||||
|
* @returns number
|
||||||
|
*/
|
||||||
|
function cryptoRandom() {
|
||||||
|
var array = new Uint32Array(1);
|
||||||
|
return window.crypto.getRandomValues(array)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a number using da old-skool Math.Random
|
||||||
|
* I likes to call it LOLRandom
|
||||||
|
*
|
||||||
|
* @returns number
|
||||||
|
*/
|
||||||
|
function basicRandom() {
|
||||||
|
return Math.random() * 9007199254740991;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random number function based on browser capability
|
||||||
|
var randomFunc;
|
||||||
|
if (window.crypto) {
|
||||||
|
randomFunc = cryptoRandom;
|
||||||
|
} else {
|
||||||
|
randomFunc = basicRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call sends a message to the backend to call the binding with the
|
||||||
|
* given data. A promise is returned and will be completed when the
|
||||||
|
* backend responds. This will be resolved when the call was successful
|
||||||
|
* or rejected if an error is passed back.
|
||||||
|
* There is a timeout mechanism. If the call doesn't respond in the given
|
||||||
|
* time (in milliseconds) then the promise is rejected.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} args
|
||||||
|
* @param {number=} timeout
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function Call(name, args, timeout) {
|
||||||
|
|
||||||
|
// Timeout infinite by default
|
||||||
|
if (timeout == null) {
|
||||||
|
timeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a promise
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
||||||
|
// Create a unique callbackID
|
||||||
|
var callbackID;
|
||||||
|
do {
|
||||||
|
callbackID = name + '-' + randomFunc();
|
||||||
|
} while (callbacks[callbackID]);
|
||||||
|
|
||||||
|
// Set timeout
|
||||||
|
if (timeout > 0) {
|
||||||
|
var timeoutHandle = setTimeout(function () {
|
||||||
|
reject(Error('Call to ' + name + ' timed out. Request ID: ' + callbackID));
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store callback
|
||||||
|
callbacks[callbackID] = {
|
||||||
|
timeoutHandle: timeoutHandle,
|
||||||
|
reject: reject,
|
||||||
|
resolve: resolve
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
callbackID,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the call
|
||||||
|
SendMessage('C' + JSON.stringify(payload));
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the backend to return data to a previously called
|
||||||
|
* binding invocation
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {string} incomingMessage
|
||||||
|
*/
|
||||||
|
export function Callback(incomingMessage) {
|
||||||
|
// Decode the message - Credit: https://stackoverflow.com/a/13865680
|
||||||
|
//incomingMessage = decodeURIComponent(incomingMessage.replace(/\s+/g, '').replace(/[0-9a-f]{2}/g, '%$&'));
|
||||||
|
|
||||||
|
// Parse the message
|
||||||
|
var message;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(incomingMessage);
|
||||||
|
} catch (e) {
|
||||||
|
const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`;
|
||||||
|
wails.LogDebug(error);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
var callbackID = message.callbackid;
|
||||||
|
var callbackData = callbacks[callbackID];
|
||||||
|
if (!callbackData) {
|
||||||
|
const error = `Callback '${callbackID}' not registered!!!`;
|
||||||
|
console.error(error); // eslint-disable-line
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
clearTimeout(callbackData.timeoutHandle);
|
||||||
|
|
||||||
|
delete callbacks[callbackID];
|
||||||
|
|
||||||
|
if (message.error) {
|
||||||
|
callbackData.reject(message.error);
|
||||||
|
} else {
|
||||||
|
callbackData.resolve(message.result);
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,8 @@ The lightweight framework for web-like apps
|
|||||||
/* jshint esversion: 9 */
|
/* jshint esversion: 9 */
|
||||||
import * as Log from './log';
|
import * as Log from './log';
|
||||||
import {EventsEmit, EventsNotify, EventsOff, EventsOn, EventsOnce, EventsOnMultiple} from './events';
|
import {EventsEmit, EventsNotify, EventsOff, EventsOn, EventsOnce, EventsOnMultiple} from './events';
|
||||||
// import {Callback, SystemCall} from './calls';
|
import {Callback} from './calls';
|
||||||
|
import {SetBindings} from "./bindings";
|
||||||
// import {AddScript, DisableDefaultContextMenu, InjectCSS} from './utils';
|
// import {AddScript, DisableDefaultContextMenu, InjectCSS} from './utils';
|
||||||
// import {AddIPCListener, SendMessage} from 'ipc';
|
// import {AddIPCListener, SendMessage} from 'ipc';
|
||||||
|
|
||||||
@ -28,8 +29,9 @@ window.runtime = {
|
|||||||
|
|
||||||
// Initialise global if not already
|
// Initialise global if not already
|
||||||
window.wails = {
|
window.wails = {
|
||||||
// Callback,
|
Callback,
|
||||||
EventsNotify,
|
EventsNotify,
|
||||||
|
SetBindings,
|
||||||
// AddScript,
|
// AddScript,
|
||||||
// InjectCSS,
|
// InjectCSS,
|
||||||
// DisableDefaultContextMenu,
|
// DisableDefaultContextMenu,
|
||||||
@ -39,3 +41,6 @@ window.wails = {
|
|||||||
// SendMessage,
|
// SendMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.wails.SetBindings(window.wailsbindings);
|
||||||
|
delete window.wails['SetBindings'];
|
||||||
|
delete window['wailsbindings'];
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"log"
|
"log"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -213,13 +214,19 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Frontend) processMessage(message string) {
|
func (f *Frontend) processMessage(message string) {
|
||||||
|
println("msg:", message)
|
||||||
err := f.dispatcher.ProcessMessage(message)
|
err := f.dispatcher.ProcessMessage(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Work out what this means
|
f.logger.Error(err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Frontend) Callback(message string) {
|
||||||
|
f.mainWindow.Dispatch(func() {
|
||||||
|
f.chromium.Eval(`window.wails.Callback(` + strconv.Quote(message) + `);`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func NewFrontend(appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
|
func NewFrontend(appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
|
||||||
|
|
||||||
result := &Frontend{
|
result := &Frontend{
|
||||||
@ -229,8 +236,15 @@ func NewFrontend(appoptions *options.App, myLogger *logger.Logger, appBindings *
|
|||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup the callback handler (Go -> JS)
|
||||||
|
dispatcher.SetCallbackHandler(result.Callback)
|
||||||
|
|
||||||
if appoptions.Windows.Assets != nil {
|
if appoptions.Windows.Assets != nil {
|
||||||
assets, err := assetserver.NewAssetServer(*appoptions.Windows.Assets)
|
bindingsJSON, err := appBindings.ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
assets, err := assetserver.NewAssetServer(*appoptions.Windows.Assets, bindingsJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user