Skip to content

Commit

Permalink
Merge pull request #14 from fortedigital/feature/streaming
Browse files Browse the repository at this point in the history
Added function to render to pipeable stream
  • Loading branch information
wredzio authored Apr 26, 2023
2 parents 46d7358 + cbbe62e commit d2f3693
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 27 deletions.
1 change: 1 addition & 0 deletions Forte.React.AspNetCore/Configuration/ReactConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ internal class ReactConfiguration
public List<string> ScriptUrls { get; set; } = new();
public bool IsServerSideDisabled { get; set; } = false;
public Version ReactVersion { get; set; } = null!;
public string NameOfObjectToSaveProps { get; set; } = "__reactProps";
}
7 changes: 4 additions & 3 deletions Forte.React.AspNetCore/Forte.React.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Packable>true</Packable>
<VersionPrefix>0.1.1.0</VersionPrefix>
<VersionPrefix>0.1.2.0</VersionPrefix>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -21,14 +21,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Jering.Javascript.NodeJS" Version="6.3.1" />
<PackageReference Include="Jering.Javascript.NodeJS" Version="7.0.0-beta.4" />
</ItemGroup>

<ItemGroup>
<None Include="./LICENSE" Pack="true" PackagePath=""/>
<None Include="./LICENSE" Pack="true" PackagePath="" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="renderToPipeableStream.js" />
<EmbeddedResource Include="renderToString.js" />
</ItemGroup>

Expand Down
6 changes: 4 additions & 2 deletions Forte.React.AspNetCore/ForteReactAspNetCoreExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public static void AddReact(this IServiceCollection services,
{
configureOutOfProcessNodeJs?.Invoke(options);
});
services.Configure<ReactJsonSerializerOptions>(options => configureJsonSerializerOptions?.Invoke(options.Options));
services.Configure<ReactJsonSerializerOptions>(options =>
configureJsonSerializerOptions?.Invoke(options.Options));

services.AddSingleton<ReactConfiguration>();
if (reactServiceFactory == null)
Expand All @@ -43,7 +44,7 @@ public static void AddReact(this IServiceCollection services,
}

public static void UseReact(this IApplicationBuilder app, IEnumerable<string> scriptUrls, Version reactVersion,
bool disableServerSideRendering = false)
bool disableServerSideRendering = false, string? nameOfObjectToSaveProps = null)
{
var config = app.ApplicationServices.GetService<ReactConfiguration>();

Expand All @@ -55,5 +56,6 @@ public static void UseReact(this IApplicationBuilder app, IEnumerable<string> sc
config.IsServerSideDisabled = disableServerSideRendering;
config.ScriptUrls = scriptUrls.ToList();
config.ReactVersion = reactVersion;
config.NameOfObjectToSaveProps = nameOfObjectToSaveProps ?? config.NameOfObjectToSaveProps;
}
}
92 changes: 73 additions & 19 deletions Forte.React.AspNetCore/React/ReactService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> RenderToStringAsync(string componentName, object props);

Task WriteOutputHtmlToAsync(TextWriter writer, string componentName, object props,
WriteOutputHtmlToOptions? writeOutputHtmlToOptions = null);

string GetInitJavascript();
}

Expand All @@ -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<Type, string> MethodToNodeJsScriptName = new()
{
{ typeof(HttpResponseMessage), "renderToPipeableStream.js" },
{ typeof(string), "renderToString.js" }
};

public static IReactService Create(IServiceProvider serviceProvider)
{
Expand All @@ -38,28 +49,25 @@ private ReactService(INodeJSService nodeJsService, ReactConfiguration config)
_config = config;
}

public async Task<string> RenderToStringAsync(string componentName, object props)
private async Task<T> InvokeRenderTo<T>(Component component, object props, params object[] args)
{
var component = new Component(componentName, props);
Components.Add(component);

if (_config.IsServerSideDisabled)
{
return WrapRenderedStringComponent(string.Empty, component);
}
var allArgs = new List<object>() { component.Name, component.JsonContainerId, props, _config.ScriptUrls, _config.NameOfObjectToSaveProps };
allArgs.AddRange(args);

var args = new[] { componentName, component.JsonContainerId, props, _config.ScriptUrls };
var type = typeof(T);
var nodeJsScriptName = MethodToNodeJsScriptName[type];

var (success, cachedResult) =
await _nodeJsService.TryInvokeFromCacheAsync<string>(RenderToStringCacheIdentifier, args: args);
await _nodeJsService.TryInvokeFromCacheAsync<T>(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()
{
Expand All @@ -69,13 +77,57 @@ Stream ModuleFactory()
}

await using var stream = ModuleFactory();
var result = await _nodeJsService.InvokeFromStreamAsync<string>(stream,
RenderToStringCacheIdentifier,
args: args);
var result = await _nodeJsService.InvokeFromStreamAsync<T>(stream,
nodeJsScriptName,
args: allArgs.ToArray());

return result!;
}

public async Task<string> 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 InvokeRenderTo<string>(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($"<div id=\"{component.ContainerId}\">");

if (_config.IsServerSideDisabled)
{
return;
}

var result = await InvokeRenderTo<HttpResponseMessage>(component, props,
writeOutputHtmlToOptions ?? new WriteOutputHtmlToOptions());

using var reader = new StreamReader(await result.Content.ReadAsStreamAsync());

char[] buffer = new char[1024];
int numChars;

while ((numChars = await reader.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
await writer.WriteAsync(buffer, 0, numChars);
}

await writer.WriteAsync("</div>");
}

private static string WrapRenderedStringComponent(string? renderedStringComponent, Component component)
{
if (renderedStringComponent is null)
Expand All @@ -100,7 +152,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.NameOfObjectToSaveProps}[\"{component.JsonContainerId}\"])";


private string Render(Component component)
Expand All @@ -117,3 +169,5 @@ private string Hydrate(Component component)
: $"ReactDOMClient.hydrateRoot({GetElementById(component.ContainerId)}, {CreateElement(component)});";
}
}

public record WriteOutputHtmlToOptions(bool ServerOnly = false, bool EnableStreaming = true);
75 changes: 75 additions & 0 deletions Forte.React.AspNetCore/renderToPipeableStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

const callbackPipe = (callback, pipe, error) => {
callback(error, null, (res) => {
pipe(res);
return true;
});
};

module.exports = (
callback,
componentName,
jsonContainerId,
props = {},
scriptFiles,
nameOfObjectToSaveProps,
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.${nameOfObjectToSaveProps} = window.${nameOfObjectToSaveProps} || {})['${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);
}
};
8 changes: 5 additions & 3 deletions Forte.React.AspNetCore/renderToString.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
componentName,
jsonContainerId,
props = {},
scriptFiles
scriptFiles,
nameOfObjectToSaveProps
) => {
scriptFiles.forEach((scriptFile) => {
require(scriptFile);
Expand All @@ -15,9 +16,10 @@
const element = React.createElement(component, props);

const componentHtml = `${ReactDOMServer.renderToString(element)}`;
const jsonHtml = `<script id="${jsonContainerId}" type="json">${JSON.stringify(
const bootstrapScriptContent = `(window.${nameOfObjectToSaveProps} = window.${nameOfObjectToSaveProps} || {})['${jsonContainerId}'] = ${JSON.stringify(
props
)}</script>`;
)};`;
const jsonHtml = `<script>${bootstrapScriptContent}</script>`;
const result = componentHtml + jsonHtml;

callback(null /* error */, result /* result */);
Expand Down

0 comments on commit d2f3693

Please sign in to comment.