Skip to content

Commit

Permalink
refactor SSR flow to support await data prepare
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip committed Jan 30, 2020
1 parent 0506454 commit ba296ae
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 51 deletions.
47 changes: 39 additions & 8 deletions packages/subapp-react/lib/framework-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ const ReactRouterDom = optionalRequire("react-router-dom");
class FrameworkLib {
constructor(ref) {
this.ref = ref;
this._prepared = false;
}

async handleSSR() {
async handlePrepare() {
this._prepared = true;
const { subApp, subAppServer, options } = this.ref;
// If subapp wants to use react router and server didn't specify a StartComponent,
// then create a wrap StartComponent that uses react router's StaticRouter
Expand All @@ -27,6 +29,22 @@ class FrameworkLib {
this.StartComponent = subAppServer.StartComponent || subApp.Component;
}

if (!this.StartComponent) {
return false;
} else if (subApp.__redux) {
return await this.prepareReduxData();
} else if (options.serverSideRendering === true) {
return await this.prepareData();
}
}

async handleSSR() {
const { subApp, options } = this.ref;

if (!this._prepared) {
await this.handlePrepare();
}

if (!this.StartComponent) {
return `<!-- serverSideRendering ${subApp.name} has no StartComponent -->`;
} else if (subApp.__redux) {
Expand Down Expand Up @@ -86,25 +104,30 @@ class FrameworkLib {
);
}

async doSSR() {
async prepareData() {
const { subApp, subAppServer, context } = this.ref;
const { request } = context.user;

let initialProps;

// even though we don't know what data model the component is using, but if it
// has a prepare callback, we will just call it to get initial props to pass
// to the component when rendering it
const prepare = subAppServer.prepare || subApp.prepare;
if (prepare) {
initialProps = await prepare({ request, context });
this._initialProps = await prepare({ request, context });
}

return await this.renderTo(this.createTopComponent(initialProps), this.ref.options);
return this._initialProps;
}

async doReduxSSR() {
const { subApp, subAppServer, context, options } = this.ref;
async doSSR() {
if (!this._prepared) {
await this.prepareData();
}
return await this.renderTo(this.createTopComponent(this._initialProps), this.ref.options);
}

async prepareReduxData() {
const { subApp, subAppServer, context } = this.ref;
const { request } = context.user;
// subApp.reduxReducers || subApp.reduxCreateStore) {
// if sub app has reduxReducers or reduxCreateStore then assume it's using
Expand Down Expand Up @@ -137,6 +160,14 @@ class FrameworkLib {
this.store,
`redux subapp ${subApp.name} didn't provide store, reduxCreateStore, or reducers`
);
}

async doReduxSSR() {
if (!this._prepared) {
await this.prepareData();
}
const { options } = this.ref;

if (options.serverSideRendering === true) {
assert(Provider, "subapp-web: react-redux Provider not available");
// finally render the element with Redux Provider and the store created
Expand Down
126 changes: 86 additions & 40 deletions packages/subapp-web/lib/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Path = require("path");
const _ = require("lodash");
const retrieveUrl = require("request");
const util = require("./util");
const xaa = require("xaa");
const { loadSubAppByName, loadSubAppServerByName } = require("subapp-util");

// global name to store client subapp runtime, ie: window.xarcV1
Expand Down Expand Up @@ -160,25 +161,90 @@ module.exports = function setup(setupContext, token) {
const clientProps = JSON.stringify(_.pick(options, ["useReactRouter"]));

return {
process: context => {
process: (context, token) => {
const { request } = context.user;

if (request.app.webpackDev && subAppLoadTime < request.app.webpackDev.compileTime) {
subAppLoadTime = request.app.webpackDev.compileTime;
loadSubApp();
}

const props = { token };
let { group = "_" } = props;
group = [].concat(group);
const ssrGroups = group.map(grp =>
util.getOrSet(context, ["user", "xarcSubappSSR", grp], { queue: [] })
);

//
// push {awaitData, ready, renderSSR, props} into queue
//
// awaitData - promise
// ready - defer promise to signal SSR info is ready for processing
// props - token.props
// renderSSR - callback to start rendering SSR for the group
//

const ssrInfo = { props, group, ready: xaa.defer() };
ssrGroups.forEach(grp => grp.queue.push(ssrInfo));

const outputSpot = context.output.reserve();
// console.log("subapp load", name, "useReactRouter", subApp.useReactRouter);

const outputSSRContent = (ssrContent, initialStateStr) => {
// If user specified an element ID for a DOM Node to host the SSR content then
// add the div for the Node and the SSR content to it, and add JS to start the
// sub app on load.
let elementId = "";
if (options.elementId) {
elementId = `elementId:"${options.elementId}",\n `;
outputSpot.add(`<div id="${options.elementId}">`);
outputSpot.add(ssrContent); // must add by itself since this could be a stream
outputSpot.add(`</div>`);
} else {
outputSpot.add("<!-- no elementId for starting subApp on load -->\n");
outputSpot.add(ssrContent);
}

outputSpot.add(`
<script>${xarc}.startSubAppOnLoad({
name:"${name}",
${elementId}serverSideRendering:${Boolean(options.serverSideRendering)},
clientProps:${clientProps},
initialState:${initialStateStr || "{}"}
});</script>
`);
};

const handleError = err => {
if (process.env.NODE_ENV !== "production") {
const stack = util.removeCwd(err.stack);
console.error(`SSR subapp ${name} failed <error>${stack}</error>`); // eslint-disable-line
outputSpot.add(`<!-- SSR subapp ${name} failed
${stack}
-->`);
} else if (request && request.log) {
request.log(["error"], { msg: `SSR subapp ${name} failed`, err });
}
};

const closeOutput = () => {
if (options.timestamp) {
outputSpot.add(`<!-- time: ${Date.now()} -->`);
}

outputSpot.close();
};

const processSubapp = async () => {
let ssrContent = "";
let initialStateStr = "";
const ref = {
context,
subApp,
subAppServer,
options
options,
ssrGroups
};

const { bundles, scripts } = await prepareSubAppSplitBundles(context);
Expand All @@ -191,29 +257,19 @@ module.exports = function setup(setupContext, token) {

if (options.serverSideRendering) {
const lib = util.getFramework(ref);
ssrContent = await lib.handleSSR(ref);
initialStateStr = lib.initialStateStr;
} else {
ssrContent = `<!-- serverSideRendering flag is ${options.serverSideRendering} -->`;
}

// If user specified an element ID for a DOM Node to host the SSR content then
// add the div for the Node and the SSR content to it, and add JS to start the
// sub app on load.
if (options.elementId) {
outputSpot.add(`<div id="${options.elementId}">`);
outputSpot.add(ssrContent); // must add by itself since this could be a stream
outputSpot.add(`</div>
<script>${xarc}.startSubAppOnLoad({
name:"${name}",
elementId:"${options.elementId}",
serverSideRendering:${Boolean(options.serverSideRendering)},
clientProps:${clientProps},
initialState:${initialStateStr || "{}"}
});</script>
`);
ssrInfo.awaitData = lib.handlePrepare();

ssrInfo.renderSSR = async () => {
try {
outputSSRContent(await lib.handleSSR(ref), lib.initialStateStr);
} catch (err) {
handleError(err);
} finally {
closeOutput();
}
};
} else {
outputSpot.add("<!-- no elementId for starting subApp on load -->\n");
outputSSRContent("");
}
};

Expand All @@ -224,23 +280,13 @@ module.exports = function setup(setupContext, token) {

try {
await processSubapp();
ssrInfo.ready.resolve();
} catch (err) {
if (process.env.NODE_ENV !== "production") {
console.error(`SSR subapp ${name} failed <error>${err.stack}</error>`); // eslint-disable-line
outputSpot.add(`<!-- SSR subapp ${name} failed
${err.stack}
-->`);
} else if (request && request.log) {
request.log(["error"], { msg: `SSR subapp ${name} failed`, err });
}
handleError(err);
} finally {
if (options.timestamp) {
outputSpot.add(`<!-- time: ${Date.now()} -->`);
if (!ssrInfo.renderSSR) {
closeOutput();
}

outputSpot.close();
}
};

Expand Down
38 changes: 36 additions & 2 deletions packages/subapp-web/lib/start.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
"use strict";

const xaa = require("xaa");
/*
* subapp start for SSR
* Nothing needs to be done to start subapp for SSR
*/
module.exports = function setup() {
return {
process: () => {
return "\n<!-- subapp start -->\n";
process: (context, token) => {
const { xarcSubappSSR } = context.user;
const startMsg = "\n<!-- subapp start -->\n";

if (!xarcSubappSSR) return startMsg;

const concurrency = token.props.concurrency > 0 ? token.props.concurrency : 15;
xaa.map(
Object.entries(xarcSubappSSR),
async ([, { queue }]) => {
await xaa.map(
queue,
async info => {
// make sure subapp is ready with SSR
if (info.ready) await info.ready.promise;
// and then wait for it to complete data prepare
// awaitData should be available once ready is awaited
await info.awaitData;
},
{ concurrency }
);

// finally kick off rendering for every subapp in the group
await xaa.map(
queue,
async ({ renderSSR }) => {
if (renderSSR) await renderSSR();
},
{ concurrency }
);
},
{ concurrency }
);

return startMsg;
}
};
};
19 changes: 19 additions & 0 deletions packages/subapp-web/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ let CDN_JS_BUNDLES;
let FrameworkLib;

const utils = {
getOrSet(obj, path, initial) {
const x = _.get(obj, path);
if (x === undefined) {
_.set(obj, path, initial);
return initial;
}
return x;
},

removeCwd(msg) {
if (!msg) return msg;
const cwd = process.cwd();
if (cwd.length > 1) {
const regex = new RegExp(cwd + Path.sep, "g");
return msg.replace(regex, `.${Path.sep}`);
}
return msg;
},

getFramework(ref) {
return new FrameworkLib(ref);
},
Expand Down
3 changes: 2 additions & 1 deletion packages/subapp-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"lodash": "^4.17.15",
"optional-require": "^1.0.0",
"request": "^2.88.0",
"subapp-util": "^1.0.3"
"subapp-util": "^1.0.3",
"xaa": "^1.1.4"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
Expand Down

0 comments on commit ba296ae

Please sign in to comment.