The Server-side JavaScript runtime
I’m back, after a short break and much swearing at third party libraries. I was going to use React.NET as that bundles up all the things required, but it has too many opinions about how you call your React components. Since I’m using Redux an react-router this makes most of the code redundant, so I went back to using the raw components.
Additionally, I switched from V8 to using Microsoft’s Chakra runtime because the existing V8 interop libraries do not support .NET Core yet. I’m using JavaScriptEngineSwitcher so it’s not hard to change out the engine later.
The libraries I’m using are:
"Newtonsoft.Json": "7.0.1",
"JavaScriptEngineSwitcher.Core": "1.2.4",
"JSPool": "0.3.1",
"JavaScriptEngineSwitcher.Msie": "1.4.0"
Setting up the JS runtime
The documentation for everything except JSON.NET is lacking. This is common in open source software - people are donating their time, I’m just glad someone’s done most of the work already. Fortunately this is a hobby project so I could take the time to read the source of the unit tests to figure out what to do.
The engine pool
First thing is to create the JSPool configuration. JSPool manages a pool of the JavaScript engines much like a SQL connection pool, this is because much like a SQL connection the engines are all single-threaded and there’s a noticeable startup time. You can tune the number of engines and how often they recycle. If there isn’t an engine to service a request the pool will block until one’s available.
Right now I’m hard coding the settings, but these can easily be loaded from a configuration file.
var poolConfig = new JSPool.JsPoolConfig();
poolConfig.MaxUsagesPerEngine = 20;
poolConfig.StartEngines = 2;
Engine Configuration
JavaScriptEngineSwitcher can load the configuration from the standard ASP.NET web.config and will set this up when you install the nuget packages. ASP.NET 5 doesn’t have a web.config, so that isn’t done automatically. However, I want certainty of our runtime as I’ll be testing against it, so I’ll just configure it in source.
The only configuration I need is to set the engine mode to Chakra Edge. This does limit the app to running on machines with Edge installed, but the beauty of JavaScriptEngineSwitcher is you can change the engine without changing your code.
var ieConfig = new JavaScriptEngineSwitcher.Msie.Configuration.MsieConfiguration
{
EngineMode = JavaScriptEngineSwitcher.Msie.JsEngineMode.ChakraEdgeJsRt
};
Once this has been done I create a small factory that simply creates the new IE JS engine.
poolConfig.EngineFactory = () => new JavaScriptEngineSwitcher.Msie.MsieJsEngine(ieConfig);
Loading the JavaScript
First the file has to be found. I’ve decided I’m keeping this in wwwroot/js/server.js
,
so I’ll have to use a PhysicalFileProvider
to
obtain it. Creating this needs the application base path, the easiest way to
obtain his is from the IApplicationEnvironment
provided by ASP.NET.
var appEnv = provider.GetRequiredService<IApplicationEnvironment>();
var fileProvider = new PhysicalFileProvider(appEnv.ApplicationBasePath);
var jsPath = fileProvider.GetFileInfo("wwwroot/js/server.js").PhysicalPath;
next up a quick function to load the file in to the engine.
public static void InitialiseJSRuntime(string jsPath, IJsEngine engine)
{
engine.ExecuteFile(jsPath);
}
Finally load that all in to the JSPool config. Additionally I set WatchFiles to the JS path, so when it changes the engines will be automatically restarted.
poolConfig.Initializer = engine => InitialiseJSRuntime(jsPath, engine);
poolConfig.WatchFiles = new[] { jsPath };
Binding the service
Wrap all of the above in to a static method
private static JSPool.IJsPool CreateJSEngine(IServiceProvider provider)
{ ... }
And finally AddSingleton
from ConfigureServices
call this method, then it’s available in the .NET runtime.
services.AddSingleton(CreateJSEngine);
The render method
I’m rendering from Razor templates, so I’m creating a Razor helper method for this. One of my goals is to have all pages be proper URLs, so every page can be rendered server-side properly. Combining this with react-router means the pages are pretty small. To start with I import these namespaces
using JSPool;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
The method is a static extension method on HtmlHelper
, like many other
ASP.NET mvc helpers. The parameters are the JavaScript method to call, the
model to pass in and the bundle name - I’m using browserify to separate the
common libraries and views from the page-specific ones, so the bundle contains
the JavaScript needed to run the page browser-side.
public static object RenderJS(this IHtmlHelper helper, string entryPoint,
object model, string bundle)
{
Grab some services. This is a helper extension method so we can’t user proper DI.
var appServices = helper.ViewContext.HttpContext.ApplicationServices;
var pool = appServices.GetRequiredService<IJsPool>();
Due to the limitations of the JS engine calls we marshal everything through JSON. It’d be nice if the engine wrappers did this automatically, but it’d just happen the same way.
var jsModel = JsonConvert.SerializeObject(model);
Then call the appropriate function on an engine from the pool
var result = pool.GetEngine().Evaluate($"pages.{entryPoint}({jsModel})") as string;
if (result == null)
{
var logger = appServices.GetRequiredService<ILoggerFactory>().CreateLogger("JSRender");
logger.LogError($"JavaScript failed to return a string when calling {entryPoint}");
throw new System.Exception();
}
Evaluate returns an object so I’ve added check that the return type is correct, just in case. Once the result has returned we deserialize it. I’m lazy so I’ll just use dynamic as the target type.
var resultObject = JsonConvert.DeserializeObject<dynamic>(result);
Last bit of boilerplate, I store an id in the viewbag so I can support multiple react components per page, though I doubt I ever will.
int reactId = helper.ViewContext.ViewBag.REACT__ViewId ?? 1;
helper.ViewContext.ViewBag.REACT__ViewId = reactId + 1;
and with all that done I wire up the HTML needed.
The render methods return a JSON object that looks like {html: htmlString}
.
This is the HTML output my setup needs - two JS tags for the common
libraries and the page-specific bundle, then a call to the render entry point.
I use the page prefix when creating the Javascript bundle. Then I render in
the content in a named div so React can find it later.
var resultScript = $"<script src=\"/js/libs.js\"></script><script src=\"/js/{bundle}.js\" type=\"text/javascript\"></script>";
resultScript += $"<script type=\"text/javascript\" defer>page.{entryPoint}('component-{reactId}')</script>";
var resultHtml = $"{resultScript}<div id=\"component-{reactId}\">{resultObject.html}</div>";
finally return the result in an HtmlString
so it gets rendered without
escaping
return new HtmlString(resultHtml);
and we’re done!
Next up I’ll go through my gulp pipeline for taking TypeScript and building the bundles needed to run this.
As always the code can be found in my GitHub repository