mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 00:09:56 +08:00
[windows-x] Support calling bound methods
This commit is contained in:
parent
316e4de8e2
commit
8e84bdfa8d
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/leaanthony/debme"
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -14,18 +15,21 @@ import (
|
||||
type AssetServer struct {
|
||||
assets debme.Debme
|
||||
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 {
|
||||
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) {
|
||||
splits := strings.Split(input, "<head>")
|
||||
if len(splits) != 2 {
|
||||
|
@ -4,7 +4,6 @@ package assetserver
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -19,7 +18,7 @@ func (a *AssetServer) init(assets embed.FS) error {
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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"`
|
||||
}
|
@ -11,6 +11,8 @@ type Dispatcher struct {
|
||||
log *logger.Logger
|
||||
bindings *binding.Bindings
|
||||
events frontend.Events
|
||||
bindingsDB *binding.DB
|
||||
resultCallback func(string)
|
||||
}
|
||||
|
||||
func NewDispatcher(log *logger.Logger, bindings *binding.Bindings, events frontend.Events) *Dispatcher {
|
||||
@ -18,9 +20,14 @@ func NewDispatcher(log *logger.Logger, bindings *binding.Bindings, events fronte
|
||||
log: log,
|
||||
bindings: bindings,
|
||||
events: events,
|
||||
bindingsDB: bindings.DB(),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) SetCallbackHandler(handler func(string)) {
|
||||
d.resultCallback = handler
|
||||
}
|
||||
|
||||
func (d *Dispatcher) ProcessMessage(message string) error {
|
||||
if message == "" {
|
||||
return errors.New("No message to process")
|
||||
@ -30,6 +37,8 @@ func (d *Dispatcher) ProcessMessage(message string) error {
|
||||
return d.processLogMessage(message)
|
||||
case 'E':
|
||||
return d.processEventMessage(message)
|
||||
case 'C':
|
||||
return d.processCallMessage(message)
|
||||
default:
|
||||
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 */
|
||||
import * as Log from './log';
|
||||
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 {AddIPCListener, SendMessage} from 'ipc';
|
||||
|
||||
@ -28,8 +29,9 @@ window.runtime = {
|
||||
|
||||
// Initialise global if not already
|
||||
window.wails = {
|
||||
// Callback,
|
||||
Callback,
|
||||
EventsNotify,
|
||||
SetBindings,
|
||||
// AddScript,
|
||||
// InjectCSS,
|
||||
// DisableDefaultContextMenu,
|
||||
@ -39,3 +41,6 @@ window.wails = {
|
||||
// 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"
|
||||
"log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -213,13 +214,19 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg
|
||||
}
|
||||
|
||||
func (f *Frontend) processMessage(message string) {
|
||||
println("msg:", message)
|
||||
err := f.dispatcher.ProcessMessage(message)
|
||||
if err != nil {
|
||||
// TODO: Work out what this means
|
||||
return
|
||||
f.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
result := &Frontend{
|
||||
@ -229,8 +236,15 @@ func NewFrontend(appoptions *options.App, myLogger *logger.Logger, appBindings *
|
||||
dispatcher: dispatcher,
|
||||
}
|
||||
|
||||
// Setup the callback handler (Go -> JS)
|
||||
dispatcher.SetCallbackHandler(result.Callback)
|
||||
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user