From 5bb01bd951379eb4f8d2c7fdb5b788cf1a929795 Mon Sep 17 00:00:00 2001 From: Wojciech Szmidt Date: Mon, 24 Apr 2023 15:38:07 +0200 Subject: [PATCH] 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 */);