From 0562331edc22514e2afa29aeffc768a544d130ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 12 Mar 2022 11:55:24 +0100 Subject: [PATCH 1/2] Information about current route is sent to the client --- .../Framework/Resources/Scripts/dotvvm-base.ts | 15 ++++++++++++--- .../Framework/Resources/Scripts/dotvvm-root.ts | 4 +++- .../Serialization/DefaultViewModelSerializer.cs | 6 ++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts index bd02db317c..2e695befd6 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts @@ -16,6 +16,10 @@ type DotvvmCoreState = { _viewModelCacheId?: string _virtualDirectory: string _initialUrl: string, + _routeName: string, + _routeParameters: { + [name: string]: any + }, _stateManager: StateManager } @@ -60,7 +64,8 @@ export function clearViewModelCache() { delete getCoreState()._viewModelCache; } export function getCulture(): string { return getCoreState()._culture; } - +export function getRouteName(): string { return getCoreState()._routeName; } +export function getRouteParameters(): { [name: string]: any } { return getCoreState()._routeParameters; } export function getStateManager(): StateManager { return getCoreState()._stateManager } let initialViewModelWrapper: any; @@ -85,7 +90,9 @@ export function initCore(culture: string): void { _culture: culture, _initialUrl: thisViewModel.url, _virtualDirectory: thisViewModel.virtualDirectory!, - _stateManager: manager + _stateManager: manager, + _routeName: thisViewModel.routeName, + _routeParameters: thisViewModel.routeParameters } // store cached viewmodel @@ -106,7 +113,9 @@ export function initCore(culture: string): void { _culture: currentCoreState!._culture, _initialUrl: a.serverResponseObject.url, _virtualDirectory: a.serverResponseObject.virtualDirectory!, - _stateManager: currentCoreState!._stateManager + _stateManager: currentCoreState!._stateManager, + _routeName: a.serverResponseObject.routeName, + _routeParameters: a.serverResponseObject.routeParameters } }); } diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index 1064edfdef..d58c262bb4 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -1,4 +1,4 @@ -import { initCore, getViewModel, getViewModelObservable, initBindings, getCulture, getState, getStateManager } from "./dotvvm-base" +import { initCore, getViewModel, getViewModelObservable, initBindings, getCulture, getState, getStateManager, getRouteName, getRouteParameters } from "./dotvvm-base" import * as events from './events' import * as spa from "./spa/spa" import * as validation from './validation/validation' @@ -86,6 +86,8 @@ const dotvvmExports = { get viewModel() { return getViewModel() } } }, + get routeName() { return getRouteName() }, + get routeParameters() { return getRouteParameters() }, get state() { return getState() }, patchState(a: any) { getStateManager().patchState(a) diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index ce78606f16..5e74056ea1 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -149,6 +149,12 @@ public void BuildViewModel(IDotvvmRequestContext context, object? commandResult) result["renderedResources"] = JArray.FromObject(context.ResourceManager.GetNamedResourcesInOrder().Select(r => r.Name)); } + if (context.Route != null) + { + result["routeName"] = context.Route.RouteName; + result["routeParameters"] = new JObject(context.Parameters.Select(p => new JProperty(p.Key, p.Value)).ToArray()); + } + // TODO: do not send on postbacks if (validationRules?.Count > 0) result["validationRules"] = validationRules; From 75aef5e336d3985e79070c01486d4bd63006e177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 23 Apr 2022 21:39:22 +0200 Subject: [PATCH 2/2] WebView version of the scripts added --- .../Configuration/DotvvmConfiguration.cs | 7 ++ .../Framework/DotVVM.Framework.csproj | 17 +++- .../Resources/Scripts/compileConstants.ts | 2 + .../Resources/Scripts/dotvvm-root.ts | 10 +- .../Resources/Scripts/postback/http.ts | 3 + .../Framework/Resources/Scripts/spa/spa.ts | 2 +- .../Resources/Scripts/webview/messaging.ts | 92 +++++++++++++++++++ .../Resources/Scripts/webview/webview.ts | 6 ++ src/Framework/Framework/rollup.config.js | 23 +++-- 9 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 src/Framework/Framework/Resources/Scripts/webview/messaging.ts create mode 100644 src/Framework/Framework/Resources/Scripts/webview/webview.ts diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index a3ca0cf583..abd22d82e7 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -364,6 +364,13 @@ private static void RegisterResources(DotvvmConfiguration configuration) debugName: "DotVVM.Framework.obj.javascript.root_spa_debug.dotvvm-root.js"), dependencies: new[] { ResourceConstants.KnockoutJSResourceName }, module: true); + configuration.Resources.RegisterScript(ResourceConstants.DotvvmResourceName + ".internal-webview", + new EmbeddedResourceLocation( + typeof(DotvvmConfiguration).Assembly, + "DotVVM.Framework.obj.javascript.root_webview.dotvvm-root.js", + debugName: "DotVVM.Framework.obj.javascript.root_webview_debug.dotvvm-root.js"), + dependencies: new[] { ResourceConstants.KnockoutJSResourceName }, + module: true); configuration.Resources.Register(ResourceConstants.DotvvmResourceName, new InlineScriptResource(@"", ResourceRenderPosition.Anywhere, defer: true) { Dependencies = new[] { ResourceConstants.DotvvmResourceName + ".internal" } diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 3dfdfd4085..962ff9d192 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -26,6 +26,8 @@ + + @@ -115,8 +117,21 @@ + + + + + + + + + + + + + - + NO_COLOR=1;FORCE_COLOR=0;TERM=dumb diff --git a/src/Framework/Framework/Resources/Scripts/compileConstants.ts b/src/Framework/Framework/Resources/Scripts/compileConstants.ts index dc84a64eba..e8c9ccd46f 100644 --- a/src/Framework/Framework/Resources/Scripts/compileConstants.ts +++ b/src/Framework/Framework/Resources/Scripts/compileConstants.ts @@ -1,6 +1,8 @@ declare var compileConstants : { /** If the compiled bundle is for SPA applications */ isSpa: boolean, + /** If the compiled bundle is for WebView-hosted applications */ + isWebview: boolean, /** If the compiled bundle is unminified */ debug: boolean }; diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index d58c262bb4..e070ddb3bf 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -30,6 +30,7 @@ import * as string from './utils/stringHelper' import { StateManager } from "./state-manager" import { DotvvmEvent } from "./events" import * as dateTime from './utils/dateTimeHelper' +import * as webview from './webview/webview' if (window["dotvvm"]) { throw new Error('DotVVM is already loaded!') @@ -138,13 +139,18 @@ if (compileConstants.isSpa) { (dotvvmExports as any).isSpaReady = isSpaReady; (dotvvmExports as any).handleSpaNavigation = handleSpaNavigation; } - +if (compileConstants.isWebview) { + (dotvvmExports as any).webview = webview; +} if (compileConstants.debug) { (dotvvmExports as any).debug = true } declare global { - const dotvvm: typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation}; + const dotvvm: typeof dotvvmExports & + { isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation } & + { webview?: typeof webview } & + { debug?: true }; interface Window { dotvvm: typeof dotvvmExports diff --git a/src/Framework/Framework/Resources/Scripts/postback/http.ts b/src/Framework/Framework/Resources/Scripts/postback/http.ts index 065df5d308..acda3e11bc 100644 --- a/src/Framework/Framework/Resources/Scripts/postback/http.ts +++ b/src/Framework/Framework/Resources/Scripts/postback/http.ts @@ -3,12 +3,15 @@ import { DotvvmPostbackError } from '../shared-classes'; import { logInfoVerbose, logWarning } from '../utils/logging'; import { keys } from '../utils/objects'; import { addLeadingSlash, concatUrl } from '../utils/uri'; +import { webMessageFetch } from '../webview/messaging'; export type WrappedResponse = { readonly result: T, readonly response?: Response } +const fetch = compileConstants.isWebview ? webMessageFetch : window.fetch; + export async function getJSON(url: string, spaPlaceHolderUniqueId?: string, signal?: AbortSignal, additionalHeaders?: { [key: string]: string }): Promise> { const headers = new Headers(); headers.append('Accept', 'application/json'); diff --git a/src/Framework/Framework/Resources/Scripts/spa/spa.ts b/src/Framework/Framework/Resources/Scripts/spa/spa.ts index 9b25581f71..e11154069b 100644 --- a/src/Framework/Framework/Resources/Scripts/spa/spa.ts +++ b/src/Framework/Framework/Resources/Scripts/spa/spa.ts @@ -14,7 +14,7 @@ export const isSpaReady = ko.observable(false); export function init(): void { const spaPlaceHolders = getSpaPlaceHolders(); if (spaPlaceHolders.length == 0) { - throw new Error("No SpaContentPlaceHolder control was found!"); + return; // if there are no SPA placeholder, ignore the SPA plugin } window.addEventListener("hashchange", event => handleHashChangeWithHistory(spaPlaceHolders, false)); diff --git a/src/Framework/Framework/Resources/Scripts/webview/messaging.ts b/src/Framework/Framework/Resources/Scripts/webview/messaging.ts new file mode 100644 index 0000000000..995a59e723 --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/webview/messaging.ts @@ -0,0 +1,92 @@ +type ReceivedMessage = { messageId: number } & + ( + { action: "HttpRequest", body: string, headers: [{ Key: string, Value: string }], status: number } + ); + +const pendingRequests: { resolve: (result: any) => void, reject: (result: any) => void }[] = []; + +// send messages +export function sendMessage(message: any) { + (window.external as any).sendMessage(message); +} + +export async function sendMessageAndWaitForResponse(message: any): Promise { + message.id = pendingRequests.length; + const promise = new Promise((resolve, reject) => { + pendingRequests[message.id] = { resolve, reject }; + sendMessage(message); + }); + return await promise; +} + +// handle commands from the webview +(window.external as any).receiveMessage(async (json: any) => { + + function handleCommand(message: ReceivedMessage) { + if (message.action === "HttpRequest") { + // handle incoming HTTP request responses + const promise = pendingRequests[message.messageId] + + const headers = new Headers(); + for (const h of message.headers) { + headers.append(h.Key, h.Value); + } + const response = new Response(message.body, { headers, status: message.status }); + promise.resolve(response); + return; + + } else { + // allow register custom message processors + for (const processor of messageProcessors) { + const result = processor(message); + if (typeof result !== "undefined") { + return result; + } + } + throw `Command ${message.action} not found!`; + } + } + + const message = JSON.parse(json); + try { + const result = await handleCommand(message); + if (typeof result !== "undefined") { + sendMessage({ + type: "HandlerCommand", + id: message.messageId, + result: JSON.stringify(result) + }); + } + } + catch (err) { + sendMessage({ + type: "HandlerCommand", + id: message.messageId, + errorMessage: JSON.stringify(err) + }); + } +}); + +type MessageProcessor = (processor: { action: string }) => any; +const messageProcessors: MessageProcessor[] = []; + +export function registerMessageProcessor(processor: MessageProcessor) { + messageProcessors.push(processor); +} + +export async function webMessageFetch(url: string, init: RequestInit): Promise { + if (init.method?.toUpperCase() === "GET") { + return await window.fetch(url, init); + } + + const headers: any = {}; + (init.headers)?.forEach((v, k) => headers[k] = v); + + return await sendMessageAndWaitForResponse({ + type: "HttpRequest", + url, + method: init.method || "GET", + headers: headers, + body: init.body as string + }); +} diff --git a/src/Framework/Framework/Resources/Scripts/webview/webview.ts b/src/Framework/Framework/Resources/Scripts/webview/webview.ts new file mode 100644 index 0000000000..d22e0d8285 --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/webview/webview.ts @@ -0,0 +1,6 @@ +import { sendMessage, registerMessageProcessor } from './messaging'; + +export { + sendMessage, + registerMessageProcessor +}; diff --git a/src/Framework/Framework/rollup.config.js b/src/Framework/Framework/rollup.config.js index 86c4977ee7..44cc7a6841 100644 --- a/src/Framework/Framework/rollup.config.js +++ b/src/Framework/Framework/rollup.config.js @@ -8,7 +8,7 @@ const build = process.env.BUILD || "debug"; const production = build == "production"; const suffix = production ? "" : "-debug"; -const config = ({ minify, input, output, spa }) => ({ +const config = ({ minify, input, output, spa, webview }) => ({ input, output: [ @@ -30,7 +30,8 @@ const config = ({ minify, input, output, spa }) => ({ commonjs(), replace({ "compileConstants.isSpa": spa, - "compileConstants.debug": !minify, + "compileConstants.isWebview": webview, + "compileConstants.debug": !minify }), minify && terser({ @@ -82,23 +83,25 @@ const config = ({ minify, input, output, spa }) => ({ }) export default [ - // config({ minify: production, input: ['./Resources/Scripts/dotvvm-root.ts', './Resources/Scripts/dotvvm-light.ts'], output: "default" }), config({ minify: production, input: ['./Resources/Scripts/dotvvm-root.ts'], output: "root-only" + suffix, - spa: false + spa: false, + webview: false }), config({ minify: production, input: ['./Resources/Scripts/dotvvm-root.ts'], output: "root-spa" + suffix, spa: true, + webview: false }), - //config({ - // minify: production, - // input: ['./Resources/Scripts/dotvvm-light.ts'], - // output: "root-light", - // spa: false - //}) + config({ + minify: production, + input: ['./Resources/Scripts/dotvvm-root.ts'], + output: "root-webview" + suffix, + spa: true, + webview: true + }) ]