5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 04:42:00 +08:00

[windows-x] Support calling bound methods

This commit is contained in:
Lea Anthony 2021-08-13 23:44:24 +10:00
parent 316e4de8e2
commit 8e84bdfa8d
10 changed files with 324 additions and 19 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package frontend
type Calls interface {
Callback(string)
}

View File

@ -2,4 +2,5 @@ package frontend
type Dispatcher interface {
ProcessMessage(message string) error
SetCallbackHandler(func(string))
}

View 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"`
}

View File

@ -8,19 +8,26 @@ import (
)
type Dispatcher struct {
log *logger.Logger
bindings *binding.Bindings
events frontend.Events
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 {
return &Dispatcher{
log: log,
bindings: bindings,
events: events,
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)
}

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

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

View File

@ -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'];

View File

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