From 5bb01bd951379eb4f8d2c7fdb5b788cf1a929795 Mon Sep 17 00:00:00 2001 From: Wojciech Szmidt Date: Mon, 24 Apr 2023 15:38:07 +0200 Subject: [PATCH 1/5] Added function to render to pipeable stream --- .../Configuration/ReactConfiguration.cs | 1 + .../Forte.React.AspNetCore.csproj | 9 +- .../ForteReactAspNetCoreExtensions.cs | 6 +- .../HtmlHelperExtensions.cs | 5 +- Forte.React.AspNetCore/React/ReactService.cs | 90 +++++++++++++++---- .../renderToPipeableStream.js | 80 +++++++++++++++++ Forte.React.AspNetCore/renderToString.js | 8 +- 7 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 Forte.React.AspNetCore/renderToPipeableStream.js diff --git a/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs b/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs index f0373be..215802c 100644 --- a/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs +++ b/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs @@ -8,4 +8,5 @@ internal class ReactConfiguration public List ScriptUrls { get; set; } = new(); public bool IsServerSideDisabled { get; set; } = false; public Version ReactVersion { get; set; } = null!; + public string ObjectToSavePropsName { get; set; } = "__reactProps"; } diff --git a/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj b/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj index 23d6432..1244024 100644 --- a/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj +++ b/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj @@ -21,14 +21,19 @@ - + - + + + + + + diff --git a/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs b/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs index 106ccce..529743c 100644 --- a/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs +++ b/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs @@ -29,7 +29,8 @@ public static void AddReact(this IServiceCollection services, { configureOutOfProcessNodeJs?.Invoke(options); }); - services.Configure(options => configureJsonSerializerOptions?.Invoke(options.Options)); + services.Configure(options => + configureJsonSerializerOptions?.Invoke(options.Options)); services.AddSingleton(); if (reactServiceFactory == null) @@ -43,7 +44,7 @@ public static void AddReact(this IServiceCollection services, } public static void UseReact(this IApplicationBuilder app, IEnumerable scriptUrls, Version reactVersion, - bool disableServerSideRendering = false) + bool disableServerSideRendering = false, string? objectToSavePropsName = null) { var config = app.ApplicationServices.GetService(); @@ -55,5 +56,6 @@ public static void UseReact(this IApplicationBuilder app, IEnumerable sc config.IsServerSideDisabled = disableServerSideRendering; config.ScriptUrls = scriptUrls.ToList(); config.ReactVersion = reactVersion; + config.ObjectToSavePropsName = objectToSavePropsName ?? config.ObjectToSavePropsName; } } diff --git a/Forte.React.AspNetCore/HtmlHelperExtensions.cs b/Forte.React.AspNetCore/HtmlHelperExtensions.cs index 44e9707..6704928 100644 --- a/Forte.React.AspNetCore/HtmlHelperExtensions.cs +++ b/Forte.React.AspNetCore/HtmlHelperExtensions.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System.IO; +using System.Text.Encodings.Web; +using System; +using System.Threading.Tasks; using Forte.React.AspNetCore.React; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; diff --git a/Forte.React.AspNetCore/React/ReactService.cs b/Forte.React.AspNetCore/React/ReactService.cs index a55e189..50920fd 100644 --- a/Forte.React.AspNetCore/React/ReactService.cs +++ b/Forte.React.AspNetCore/React/ReactService.cs @@ -2,18 +2,24 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.Json; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; +using System.Xml; using Forte.React.AspNetCore.Configuration; using Jering.Javascript.NodeJS; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; namespace Forte.React.AspNetCore.React; public interface IReactService { Task RenderToStringAsync(string componentName, object props); + + Task WriteOutputHtmlToAsync(TextWriter writer, string componentName, object props, + WriteOutputHtmlToOptions? writeOutputHtmlToOptions = null); + string GetInitJavascript(); } @@ -23,7 +29,12 @@ public class ReactService : IReactService private readonly INodeJSService _nodeJsService; private readonly ReactConfiguration _config; - private const string RenderToStringCacheIdentifier = nameof(RenderToStringAsync); + + private static readonly Dictionary MethodToNodeJsScriptName = new() + { + { typeof(HttpResponseMessage), "renderToPipeableStream.js" }, + { typeof(string), "renderToString.js" } + }; public static IReactService Create(IServiceProvider serviceProvider) { @@ -38,28 +49,25 @@ private ReactService(INodeJSService nodeJsService, ReactConfiguration config) _config = config; } - public async Task RenderToStringAsync(string componentName, object props) + private async Task ASD(Component component, object props, params object[] args) { - var component = new Component(componentName, props); - Components.Add(component); + var allArgs = new List() { component.Name, component.JsonContainerId, props, _config.ScriptUrls, _config.ObjectToSavePropsName }; + allArgs.AddRange(args); - if (_config.IsServerSideDisabled) - { - return WrapRenderedStringComponent(string.Empty, component); - } - - var args = new[] { componentName, component.JsonContainerId, props, _config.ScriptUrls }; + var type = typeof(T); + var nodeJsScriptName = MethodToNodeJsScriptName[type]; var (success, cachedResult) = - await _nodeJsService.TryInvokeFromCacheAsync(RenderToStringCacheIdentifier, args: args); + await _nodeJsService.TryInvokeFromCacheAsync(nodeJsScriptName, args: allArgs.ToArray()); if (success) { - return WrapRenderedStringComponent(cachedResult, component); + return cachedResult!; } var currentAssembly = typeof(ReactService).Assembly; - var renderToStringScriptManifestName = currentAssembly.GetManifestResourceNames().Single(); + var renderToStringScriptManifestName = currentAssembly.GetManifestResourceNames() + .Single(s => s == $"Forte.React.AspNetCore.{nodeJsScriptName}"); Stream ModuleFactory() { @@ -69,13 +77,55 @@ Stream ModuleFactory() } await using var stream = ModuleFactory(); - var result = await _nodeJsService.InvokeFromStreamAsync(stream, - RenderToStringCacheIdentifier, - args: args); + var result = await _nodeJsService.InvokeFromStreamAsync(stream, + nodeJsScriptName, + args: allArgs.ToArray()); + + return result!; + } + + public async Task RenderToStringAsync(string componentName, object props) + { + var component = new Component(componentName, props); + Components.Add(component); + + if (_config.IsServerSideDisabled) + { + return WrapRenderedStringComponent(string.Empty, component); + } + + var result = await ASD(component, props); return WrapRenderedStringComponent(result, component); } + public async Task WriteOutputHtmlToAsync(TextWriter writer, string componentName, object props, + WriteOutputHtmlToOptions? writeOutputHtmlToOptions) + { + var component = new Component(componentName, props); + Components.Add(component); + + await writer.WriteAsync($"
"); + + if (_config.IsServerSideDisabled) + { + return; + } + + var result = await ASD(component, props, + writeOutputHtmlToOptions ?? new WriteOutputHtmlToOptions()); + + using var reader = new StreamReader(await result.Content.ReadAsStreamAsync()); + int character; + + while ((character = reader.Read()) != -1) + { + await writer.WriteAsync((char)character); + } + + await writer.WriteAsync("
"); + } + private static string WrapRenderedStringComponent(string? renderedStringComponent, Component component) { if (renderedStringComponent is null) @@ -100,7 +150,7 @@ private static string GetElementById(string containerId) private string CreateElement(Component component) => - $"React.createElement({component.Name}, JSON.parse(document.getElementById(\"{component.JsonContainerId}\").textContent))"; + $"React.createElement({component.Name}, window.{_config.ObjectToSavePropsName}[\"{component.JsonContainerId}\"])"; private string Render(Component component) @@ -117,3 +167,5 @@ private string Hydrate(Component component) : $"ReactDOMClient.hydrateRoot({GetElementById(component.ContainerId)}, {CreateElement(component)});"; } } + +public record WriteOutputHtmlToOptions(bool ServerOnly = false, bool EnableStreaming = true); diff --git a/Forte.React.AspNetCore/renderToPipeableStream.js b/Forte.React.AspNetCore/renderToPipeableStream.js new file mode 100644 index 0000000..7c2139a --- /dev/null +++ b/Forte.React.AspNetCore/renderToPipeableStream.js @@ -0,0 +1,80 @@ + +const callbackPipe = (callback, pipe, error) => { + callback(error, null, (res) => { + pipe(res); + return true; + }); +}; + +module.exports = ( + callback, + componentName, + jsonContainerId, + props = {}, + scriptFiles, + objectToSavePropsName, + options, +) => { + try { + scriptFiles.forEach((scriptFile) => { + require(scriptFile); + }); + + const ReactDOMServer = global["ReactDOMServer"]; + const React = global["React"]; + + const component = global[componentName]; + + if (options.serverOnly) { + const res = ReactDOMServer.renderToStaticNodeStream( + React.createElement( + component, + props + ) + ); + + callback(null, res); + return; + } + + let error; + const bootstrapScriptContent = `(window.${objectToSavePropsName} = window.${objectToSavePropsName} || {})['${jsonContainerId}'] = ${JSON.stringify( + props + )};`; + + const { pipe } = ReactDOMServer.renderToPipeableStream( + React.createElement( + component, + props + ), + { + bootstrapScriptContent: bootstrapScriptContent, + onShellReady() { + if (options.enableStreaming) { + callbackPipe(callback, pipe, error); + } + }, + onShellError(error) { + callback(error, null); + }, + onAllReady() { + if (!options.enableStreaming) { + callbackPipe(callback, pipe, error); + } + }, + onError(err) { + error = err; + console.error(err); + }, + } + + ); + } catch (err) { + callback(err, null); + } + // const componentHtml = `${ReactDOMServer.renderToString(element)}`; + // const jsonHtml = ``; + // const result = componentHtml + jsonHtml; +}; diff --git a/Forte.React.AspNetCore/renderToString.js b/Forte.React.AspNetCore/renderToString.js index 325ff99..8596841 100644 --- a/Forte.React.AspNetCore/renderToString.js +++ b/Forte.React.AspNetCore/renderToString.js @@ -3,7 +3,8 @@ componentName, jsonContainerId, props = {}, - scriptFiles + scriptFiles, + objectToSavePropsName ) => { scriptFiles.forEach((scriptFile) => { require(scriptFile); @@ -15,9 +16,10 @@ const element = React.createElement(component, props); const componentHtml = `${ReactDOMServer.renderToString(element)}`; - const jsonHtml = ``; + )};`; + const jsonHtml = ``; const result = componentHtml + jsonHtml; callback(null /* error */, result /* result */); From 65ccac5ac85b8938e177fd4c8c454c2b8f616813 Mon Sep 17 00:00:00 2001 From: Wojciech Szmidt Date: Mon, 24 Apr 2023 15:48:04 +0200 Subject: [PATCH 2/5] Fixes before review --- .../Configuration/ReactConfiguration.cs | 2 +- Forte.React.AspNetCore/Forte.React.AspNetCore.csproj | 4 ---- Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs | 4 ++-- Forte.React.AspNetCore/HtmlHelperExtensions.cs | 5 +---- Forte.React.AspNetCore/React/ReactService.cs | 4 ++-- Forte.React.AspNetCore/renderToPipeableStream.js | 9 ++------- Forte.React.AspNetCore/renderToString.js | 4 ++-- 7 files changed, 10 insertions(+), 22 deletions(-) diff --git a/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs b/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs index 215802c..5787cd7 100644 --- a/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs +++ b/Forte.React.AspNetCore/Configuration/ReactConfiguration.cs @@ -8,5 +8,5 @@ internal class ReactConfiguration public List ScriptUrls { get; set; } = new(); public bool IsServerSideDisabled { get; set; } = false; public Version ReactVersion { get; set; } = null!; - public string ObjectToSavePropsName { get; set; } = "__reactProps"; + public string NameOfObjectToSaveProps { get; set; } = "__reactProps"; } diff --git a/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj b/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj index 1244024..95a4776 100644 --- a/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj +++ b/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj @@ -20,10 +20,6 @@ false - - - - diff --git a/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs b/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs index 529743c..cef80a8 100644 --- a/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs +++ b/Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs @@ -44,7 +44,7 @@ public static void AddReact(this IServiceCollection services, } public static void UseReact(this IApplicationBuilder app, IEnumerable scriptUrls, Version reactVersion, - bool disableServerSideRendering = false, string? objectToSavePropsName = null) + bool disableServerSideRendering = false, string? nameOfObjectToSaveProps = null) { var config = app.ApplicationServices.GetService(); @@ -56,6 +56,6 @@ public static void UseReact(this IApplicationBuilder app, IEnumerable sc config.IsServerSideDisabled = disableServerSideRendering; config.ScriptUrls = scriptUrls.ToList(); config.ReactVersion = reactVersion; - config.ObjectToSavePropsName = objectToSavePropsName ?? config.ObjectToSavePropsName; + config.NameOfObjectToSaveProps = nameOfObjectToSaveProps ?? config.NameOfObjectToSaveProps; } } diff --git a/Forte.React.AspNetCore/HtmlHelperExtensions.cs b/Forte.React.AspNetCore/HtmlHelperExtensions.cs index 6704928..44e9707 100644 --- a/Forte.React.AspNetCore/HtmlHelperExtensions.cs +++ b/Forte.React.AspNetCore/HtmlHelperExtensions.cs @@ -1,7 +1,4 @@ -using System.IO; -using System.Text.Encodings.Web; -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Forte.React.AspNetCore.React; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; diff --git a/Forte.React.AspNetCore/React/ReactService.cs b/Forte.React.AspNetCore/React/ReactService.cs index 50920fd..2b5c741 100644 --- a/Forte.React.AspNetCore/React/ReactService.cs +++ b/Forte.React.AspNetCore/React/ReactService.cs @@ -51,7 +51,7 @@ private ReactService(INodeJSService nodeJsService, ReactConfiguration config) private async Task ASD(Component component, object props, params object[] args) { - var allArgs = new List() { component.Name, component.JsonContainerId, props, _config.ScriptUrls, _config.ObjectToSavePropsName }; + var allArgs = new List() { component.Name, component.JsonContainerId, props, _config.ScriptUrls, _config.NameOfObjectToSaveProps }; allArgs.AddRange(args); var type = typeof(T); @@ -150,7 +150,7 @@ private static string GetElementById(string containerId) private string CreateElement(Component component) => - $"React.createElement({component.Name}, window.{_config.ObjectToSavePropsName}[\"{component.JsonContainerId}\"])"; + $"React.createElement({component.Name}, window.{_config.NameOfObjectToSaveProps}[\"{component.JsonContainerId}\"])"; private string Render(Component component) diff --git a/Forte.React.AspNetCore/renderToPipeableStream.js b/Forte.React.AspNetCore/renderToPipeableStream.js index 7c2139a..af46590 100644 --- a/Forte.React.AspNetCore/renderToPipeableStream.js +++ b/Forte.React.AspNetCore/renderToPipeableStream.js @@ -12,7 +12,7 @@ module.exports = ( jsonContainerId, props = {}, scriptFiles, - objectToSavePropsName, + nameOfObjectToSaveProps, options, ) => { try { @@ -38,7 +38,7 @@ module.exports = ( } let error; - const bootstrapScriptContent = `(window.${objectToSavePropsName} = window.${objectToSavePropsName} || {})['${jsonContainerId}'] = ${JSON.stringify( + const bootstrapScriptContent = `(window.${objectToSavePropsName} = window.${nameOfObjectToSaveProps} || {})['${jsonContainerId}'] = ${JSON.stringify( props )};`; @@ -72,9 +72,4 @@ module.exports = ( } catch (err) { callback(err, null); } - // const componentHtml = `${ReactDOMServer.renderToString(element)}`; - // const jsonHtml = ``; - // const result = componentHtml + jsonHtml; }; diff --git a/Forte.React.AspNetCore/renderToString.js b/Forte.React.AspNetCore/renderToString.js index 8596841..7e8739d 100644 --- a/Forte.React.AspNetCore/renderToString.js +++ b/Forte.React.AspNetCore/renderToString.js @@ -4,7 +4,7 @@ jsonContainerId, props = {}, scriptFiles, - objectToSavePropsName + nameOfObjectToSaveProps ) => { scriptFiles.forEach((scriptFile) => { require(scriptFile); @@ -16,7 +16,7 @@ const element = React.createElement(component, props); const componentHtml = `${ReactDOMServer.renderToString(element)}`; - const bootstrapScriptContent = `(window.${objectToSavePropsName} = window.${objectToSavePropsName} || {})['${jsonContainerId}'] = ${JSON.stringify( + const bootstrapScriptContent = `(window.${nameOfObjectToSaveProps} = window.${nameOfObjectToSaveProps} || {})['${jsonContainerId}'] = ${JSON.stringify( props )};`; const jsonHtml = ``; From 17870b61c250e36e2cbbacdcbb64700dab2ed111 Mon Sep 17 00:00:00 2001 From: Wojciech Szmidt Date: Mon, 24 Apr 2023 15:48:58 +0200 Subject: [PATCH 3/5] Updated version --- Forte.React.AspNetCore/Forte.React.AspNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj b/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj index 95a4776..b1b8e16 100644 --- a/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj +++ b/Forte.React.AspNetCore/Forte.React.AspNetCore.csproj @@ -6,7 +6,7 @@ enable latest true - 0.1.1.0 + 0.1.2.0 From db23960781a6bd174919eb867130d0ecb2849c40 Mon Sep 17 00:00:00 2001 From: Wojciech Szmidt Date: Tue, 25 Apr 2023 08:24:12 +0200 Subject: [PATCH 4/5] Fixes before review --- Forte.React.AspNetCore/React/ReactService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Forte.React.AspNetCore/React/ReactService.cs b/Forte.React.AspNetCore/React/ReactService.cs index 2b5c741..4a8c592 100644 --- a/Forte.React.AspNetCore/React/ReactService.cs +++ b/Forte.React.AspNetCore/React/ReactService.cs @@ -49,7 +49,7 @@ private ReactService(INodeJSService nodeJsService, ReactConfiguration config) _config = config; } - private async Task ASD(Component component, object props, params object[] args) + private async Task InvokeRenderTo(Component component, object props, params object[] args) { var allArgs = new List() { component.Name, component.JsonContainerId, props, _config.ScriptUrls, _config.NameOfObjectToSaveProps }; allArgs.AddRange(args); @@ -94,7 +94,7 @@ public async Task RenderToStringAsync(string componentName, object props return WrapRenderedStringComponent(string.Empty, component); } - var result = await ASD(component, props); + var result = await InvokeRenderTo(component, props); return WrapRenderedStringComponent(result, component); } @@ -112,7 +112,7 @@ public async Task WriteOutputHtmlToAsync(TextWriter writer, string componentName return; } - var result = await ASD(component, props, + var result = await InvokeRenderTo(component, props, writeOutputHtmlToOptions ?? new WriteOutputHtmlToOptions()); using var reader = new StreamReader(await result.Content.ReadAsStreamAsync()); From cbbe62e49db65d2d644007d859bc155b0279a492 Mon Sep 17 00:00:00 2001 From: Wojciech Szmidt Date: Tue, 25 Apr 2023 10:29:08 +0200 Subject: [PATCH 5/5] Fixes to pr --- Forte.React.AspNetCore/React/ReactService.cs | 8 +++++--- Forte.React.AspNetCore/renderToPipeableStream.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Forte.React.AspNetCore/React/ReactService.cs b/Forte.React.AspNetCore/React/ReactService.cs index 4a8c592..c012a33 100644 --- a/Forte.React.AspNetCore/React/ReactService.cs +++ b/Forte.React.AspNetCore/React/ReactService.cs @@ -116,11 +116,13 @@ public async Task WriteOutputHtmlToAsync(TextWriter writer, string componentName writeOutputHtmlToOptions ?? new WriteOutputHtmlToOptions()); using var reader = new StreamReader(await result.Content.ReadAsStreamAsync()); - int character; - while ((character = reader.Read()) != -1) + char[] buffer = new char[1024]; + int numChars; + + while ((numChars = await reader.ReadAsync(buffer, 0, buffer.Length)) != 0) { - await writer.WriteAsync((char)character); + await writer.WriteAsync(buffer, 0, numChars); } await writer.WriteAsync(""); diff --git a/Forte.React.AspNetCore/renderToPipeableStream.js b/Forte.React.AspNetCore/renderToPipeableStream.js index af46590..1f672a5 100644 --- a/Forte.React.AspNetCore/renderToPipeableStream.js +++ b/Forte.React.AspNetCore/renderToPipeableStream.js @@ -38,7 +38,7 @@ module.exports = ( } let error; - const bootstrapScriptContent = `(window.${objectToSavePropsName} = window.${nameOfObjectToSaveProps} || {})['${jsonContainerId}'] = ${JSON.stringify( + const bootstrapScriptContent = `(window.${nameOfObjectToSaveProps} = window.${nameOfObjectToSaveProps} || {})['${jsonContainerId}'] = ${JSON.stringify( props )};`;