Skip to content

Commit

Permalink
multi framework support, step1: extract framework specific code (#1480)
Browse files Browse the repository at this point in the history
* multi framework support, step1: extract framework specific code

* tests for subapp-web init token
  • Loading branch information
jchip authored Dec 31, 2019
1 parent 4a018e1 commit 342f3a0
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 136 deletions.
173 changes: 173 additions & 0 deletions packages/subapp-web/lib/framework-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"use strict";

/* eslint-disable max-statements */

/*
In order to support different UI framework like React/Preact,
first extract the framework specific code from subapp lib into
their own file.
Next allow a separate module to DI this into the subapp lib
in order to allow apps to use the framework they choose
*/

const assert = require("assert");
const optionalRequire = require("optional-require")(require);
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const AsyncReactDOMServer = optionalRequire("react-async-ssr");
const { default: AppContext } = require("../browser/app-context");
const ReactRedux = optionalRequire("react-redux", { default: {} });
const { Provider } = ReactRedux;
const ReactRouterDom = optionalRequire("react-router-dom");

class FrameworkLib {
constructor(ref) {
this.ref = ref;
}

async handleSSR() {
const { subApp, subAppServer, props } = 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
if (subApp.useReactRouter && !subAppServer.StartComponent) {
this.StartComponent = this.wrapReactRouter(subApp);
} else {
this.StartComponent = subAppServer.StartComponent || subApp.Component;
}

if (!this.StartComponent) {
return `<!-- serverSideRendering ${subApp.name} has no StartComponent -->`;
} else if (subApp.__redux) {
return await this.doReduxSSR();
} else if (props.serverSideRendering === true) {
return await this.doSSR();
}

return "";
}

renderTo(element, options = {}) {
if (options.streaming) {
assert(!options.suspenseSsr, "streaming and suspense SSR together are not supported");
if (options.hydrateServerData) {
return ReactDOMServer.renderToNodeStream(element);
} else {
return ReactDOMServer.renderToStaticNodeStream(element);
}
}
if (options.suspenseSsr) {
assert(AsyncReactDOMServer, "You must install react-async-ssr for suspense SSR support");
if (options.hydrateServerData) {
return AsyncReactDOMServer.renderToStringAsync(element);
} else {
return AsyncReactDOMServer.renderToStaticMarkupAsync(element);
}
}
if (options.hydrateServerData) {
return ReactDOMServer.renderToString(element);
} else {
return ReactDOMServer.renderToStaticMarkup(element);
}
}

createTopComponent(initialProps) {
const { request } = this.ref.context.user;
const { subApp } = this.ref;

let TopComponent;
if (subApp.useReactRouter) {
const rrContext = {};
const rrProps = Object.assign(
{ location: request.url.pathname, context: rrContext },
initialProps
);
// console.log("rendering", name, "for react router", rrProps);
TopComponent = React.createElement(this.StartComponent, rrProps);
} else {
// console.log("rendering without react router", name);
TopComponent = React.createElement(this.StartComponent, initialProps);
}
return React.createElement(
AppContext.Provider,
{ value: { isSsr: true, subApp, ssr: { request } } },
TopComponent
);
}

async doSSR() {
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 });
}

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

async doReduxSSR() {
const { subApp, subAppServer, context, props } = this.ref;
const { request } = context.user;
// subApp.reduxReducers || subApp.reduxCreateStore) {
// if sub app has reduxReducers or reduxCreateStore then assume it's using
// redux data model. prepare initial state and store to render it.
let reduxData;

// see if app has a prepare callback, on the server side first, and then the
// app itself, and call it. assume the object it returns would contain the
// initial redux state data.
const prepare = subAppServer.prepare || subApp.prepare;
if (prepare) {
reduxData = await prepare({ request, context });
}

if (!reduxData) {
reduxData = { initialState: {} };
}

this.initialState = reduxData.initialState || reduxData;
// if subapp didn't request to skip sending initial state, then stringify it
// and attach it to the index html.
if (subAppServer.attachInitialState !== false) {
this.initialStateStr = JSON.stringify(this.initialState);
}
// next we take the initial state and create redux store from it
this.store =
reduxData.store ||
(subApp.reduxCreateStore && (await subApp.reduxCreateStore(this.initialState)));
assert(
this.store,
`redux subapp ${subApp.name} didn't provide store, reduxCreateStore, or reducers`
);
if (props.serverSideRendering === true) {
assert(Provider, "subapp-web: react-redux Provider not available");
// finally render the element with Redux Provider and the store created
return await this.renderTo(
React.createElement(Provider, { store: this.store }, this.createTopComponent())
);
}
return "";
}

wrapReactRouter() {
assert(
ReactRouterDom && ReactRouterDom.StaticRouter,
`subapp ${this.ref.subApp.name} specified useReactRouter without a StartComponent, \
and can't generate it because module react-dom-router with StaticRouter is not found`
);
return props2 =>
React.createElement(
ReactRouterDom.StaticRouter,
props2,
React.createElement(this.ref.subApp.Component)
);
}
}

module.exports = FrameworkLib;
153 changes: 20 additions & 133 deletions packages/subapp-web/lib/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@

/* eslint-disable max-statements, no-console, complexity */

const assert = require("assert");
/*
* - Figure out all the dependencies and bundles a subapp needs and make sure
* to generate all links to load them for index.html.
* - If serverSideRendering is enabled, then load and render the subapp for SSR.
* - Prepare initial state (if redux enabled) or props for the subapp
* - run renderTo* to generate HTML output
* - include output in index.html
* - generate code to bootstrap subapp on client
*/

const Fs = require("fs");
const Path = require("path");
const _ = require("lodash");
const optionalRequire = require("optional-require")(require);
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const AsyncReactDOMServer = optionalRequire("react-async-ssr");
const retrieveUrl = require("request");
const util = require("./util");
const { loadSubAppByName, loadSubAppServerByName } = require("subapp-util");
const { default: AppContext } = require("../browser/app-context");

const ReactRedux = optionalRequire("react-redux", { default: {} });
const { Provider } = ReactRedux;

const ReactRouterDom = optionalRequire("react-router-dom");
const FrameworkLib = require("./framework-lib");

module.exports = function setup(setupContext, token) {
const props = token.props;
Expand Down Expand Up @@ -87,38 +88,8 @@ module.exports = function setup(setupContext, token) {
}
};

//
// do server side rendering for the subapp instance
//
const renderElement = element => {
if (props.streaming) {
assert(!props.suspenseSsr, "streaming and suspense SSR together are not supported");
if (props.hydrateServerData) {
return ReactDOMServer.renderToNodeStream(element);
} else {
return ReactDOMServer.renderToStaticNodeStream(element);
}
}

if (props.suspenseSsr) {
assert(AsyncReactDOMServer, "You must install react-async-ssr for suspense SSR support");
if (props.hydrateServerData) {
return AsyncReactDOMServer.renderToStringAsync(element);
} else {
return AsyncReactDOMServer.renderToStaticMarkupAsync(element);
}
}

if (props.hydrateServerData) {
return ReactDOMServer.renderToString(element);
} else {
return ReactDOMServer.renderToStaticMarkup(element);
}
};

let subApp;
let subAppServer;
let StartComponent;
let subAppLoadTime = 0;

//
Expand Down Expand Up @@ -163,7 +134,6 @@ module.exports = function setup(setupContext, token) {
const loadSubApp = () => {
subApp = loadSubAppByName(name);
subAppServer = loadSubAppServerByName(name);
StartComponent = subAppServer.StartComponent || subApp.Component;
};

loadSubApp();
Expand All @@ -182,103 +152,20 @@ module.exports = function setup(setupContext, token) {

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

const createElement = (Component, initialProps) => {
let TopComponent;
if (subApp.useReactRouter) {
rrContext = {};
const rrProps = Object.assign(
{ location: request.url.pathname, context: rrContext },
initialProps
);
// console.log("rendering", name, "for react router", rrProps);
TopComponent = React.createElement(Component, rrProps);
} else {
// console.log("rendering without react router", name);
TopComponent = React.createElement(Component, initialProps);
}
return React.createElement(
AppContext.Provider,
{ value: { isSsr: true, subApp, ssr: { request } } },
TopComponent
);
};

const processSubapp = async () => {
let ssrContent = "";
let initialStateStr = "";
const ref = {
context,
subApp,
subAppServer,
props
};
if (props.serverSideRendering) {
// 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
if (subApp.useReactRouter && !subAppServer.StartComponent) {
assert(
ReactRouterDom && ReactRouterDom.StaticRouter,
`subapp ${subApp.name} specified useReactRouter without a StartComponent, \
and can't generate it because module react-dom-router with StaticRouter is not found`
);
StartComponent = props2 =>
React.createElement(
ReactRouterDom.StaticRouter,
props2,
React.createElement(subApp.Component)
);
}

if (!StartComponent) {
ssrContent = `<!-- serverSideRendering ${name} has no StartComponent -->`;
} else if (subApp.__redux) {
// subApp.reduxReducers || subApp.reduxCreateStore) {
// if sub app has reduxReducers or reduxCreateStore then assume it's using
// redux data model. prepare initial state and store to render it.
let reduxData;

// see if app has a prepare callback, on the server side first, and then the
// app itself, and call it. assume the object it returns would contain the
// initial redux state data.
const prepare = subAppServer.prepare || subApp.prepare;
if (prepare) {
reduxData = await prepare({ request, context });
}

if (!reduxData) {
reduxData = { initialState: {} };
}

const initialState = reduxData.initialState || reduxData;
// if subapp didn't request to skip sending initial state, then stringify it
// and attach it to the index html.
if (subAppServer.attachInitialState !== false) {
initialStateStr = JSON.stringify(initialState);
}
// next we take the initial state and create redux store from it
const store =
reduxData.store ||
(subApp.reduxCreateStore && (await subApp.reduxCreateStore(initialState)));
assert(
store,
`redux subapp ${subApp.name} didn't provide store, reduxCreateStore, or reducers`
);
if (props.serverSideRendering === true) {
assert(Provider, "subapp-web: react-redux Provider not available");
// finally render the element with Redux Provider and the store created
ssrContent = await renderElement(
React.createElement(Provider, { store }, createElement(StartComponent))
);
}
} else if (props.serverSideRendering === true) {
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 });
}

ssrContent = await renderElement(createElement(StartComponent, initialProps));
}
const lib = new FrameworkLib(ref);
ssrContent = await lib.handleSSR(ref);
initialStateStr = lib.initialStateStr;
} else {
ssrContent = `<!-- serverSideRendering flag is ${props.serverSideRendering} -->`;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/subapp-web/lib/start.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"use strict";

/*
* subapp start for SSR
* Nothing needs to be done to start subapp for SSR
*/
module.exports = function setup() {
return {
process: () => {
Expand Down
Loading

0 comments on commit 342f3a0

Please sign in to comment.