Fix SSR, upgrade react router and loadable components.

This commit is contained in:
Will O'Beirne 2019-01-23 15:48:22 -05:00
parent 5b3e5522b0
commit 737ec0e59c
No known key found for this signature in database
GPG Key ID: 44C190DB5DEAF9F6
11 changed files with 136 additions and 105 deletions

View File

@ -286,9 +286,15 @@ class Routes extends React.PureComponent<Props> {
const routeComponents = routeConfigs.map(config => {
const { route, onlyLoggedIn, onlyLoggedOut } = config;
if (onlyLoggedIn || onlyLoggedOut) {
return <AuthRoute key={route.path} onlyLoggedOut={onlyLoggedOut} {...route} />;
return (
<AuthRoute
key={route.path as string}
onlyLoggedOut={onlyLoggedOut}
{...route}
/>
);
} else {
return <Route key={route.path} {...route} />;
return <Route key={route.path as string} {...route} />;
}
});

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect, RouteProps } from 'react-router-dom';
import { Route, Redirect, RouteProps } from 'react-router';
import { message } from 'antd';
import { AppState } from 'store/reducers';
import { authActions } from 'modules/auth';

View File

@ -2,12 +2,12 @@ import '@babel/polyfill';
import React from 'react';
import { hot } from 'react-hot-loader';
import { hydrate } from 'react-dom';
import { loadComponents } from 'loadable-components';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import * as Sentry from '@sentry/browser';
import { I18nextProvider } from 'react-i18next';
import { loadableReady } from '@loadable/component';
import { configureStore } from 'store/configure';
import history from 'store/history';
import { massageSerializedState } from 'utils/api';
@ -38,6 +38,6 @@ const App = hot(module)(() => (
</I18nextProvider>
));
loadComponents().then(() => {
loadableReady(() => {
hydrate(<App />, document.getElementById('app'));
});

View File

@ -26,7 +26,7 @@ interface Props {
code: '403' | '404' | '500';
}
const ExceptionComponent: React.SFC<Props> = ({ code }) => (
const ExceptionComponent = ({ code }: Props) => (
<Exception type={code} {...content[code]} />
);

View File

@ -1,22 +0,0 @@
declare module 'loadable-components/server' {
import * as React from 'react';
export function getLoadableState(
rootElement: React.ReactElement<{}>,
rootContext?: any,
fetchRoot?: boolean,
tree?: any,
): Promise<DeferredState>;
export interface DeferredStateTree {
id: string;
children: DeferredStateTree[];
}
export interface DeferredState {
tree: DeferredStateTree;
getScriptContent(): string;
getScriptTag(): string;
getScriptElement(): React.ReactHTMLElement<HTMLScriptElement>;
}
}

View File

@ -23,7 +23,7 @@ const tsBabelLoaderClient = {
options: {
plugins: [
'dynamic-import-webpack', // for client
'loadable-components/babel',
'@loadable/babel-plugin',
'react-hot-loader/babel',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
@ -47,7 +47,7 @@ const tsBabelLoaderServer = {
options: {
plugins: [
'dynamic-import-node', // for server
'loadable-components/babel',
'@loadable/babel-plugin',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
['import', { libraryName: 'antd', style: false }],

View File

@ -6,6 +6,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ModuleDependencyWarning = require('./module-dependency-warning');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const LoadablePlugin = require('@loadable/webpack-plugin');
const env = require('../env')();
const paths = require('../paths');
@ -75,6 +76,7 @@ const client = [
return JSON.stringify(trans, null, 2);
},
}),
new LoadablePlugin(),
];
const server = [

View File

@ -60,7 +60,7 @@
"@types/react-dom": "16.0.9",
"@types/react-helmet": "^5.0.7",
"@types/react-redux": "^6.0.2",
"@types/react-router": "^4.0.31",
"@types/react-router": "4.4.3",
"@types/react-router-dom": "^4.3.1",
"@types/recompose": "^0.26.1",
"@types/redux-actions": "^2.3.0",
@ -131,8 +131,8 @@
"react-i18next": "^8.3.5",
"react-mde": "^5.8.0",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-router": "4.4.0-beta.6",
"react-router-dom": "4.4.0-beta.6",
"recompose": "^0.27.1",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.2",
@ -167,9 +167,13 @@
"xss": "1.0.3"
},
"devDependencies": {
"@loadable/babel-plugin": "5.5.0",
"@loadable/server": "5.5.0",
"@loadable/webpack-plugin": "5.5.0",
"@storybook/react": "4.0.0-alpha.22",
"@types/bn.js": "4.11.1",
"@types/loadable__component": "5.2.0",
"@types/loadable__server": "5.2.0",
"@types/qrcode.react": "^0.8.1",
"@types/query-string": "6.1.0",
"@types/react-copy-to-clipboard": "^4.2.6",

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { ChunkExtractor } from '@loadable/server';
export interface Props {
children: any;
@ -9,7 +10,7 @@ export interface Props {
metaTags: Array<React.MetaHTMLAttributes<HTMLMetaElement>>;
state: string;
i18n: string;
loadableStateScript: string;
extractor: ChunkExtractor;
}
const HTML: React.SFC<Props> = ({
@ -20,7 +21,7 @@ const HTML: React.SFC<Props> = ({
i18n,
linkTags,
metaTags,
loadableStateScript,
extractor,
}) => {
const head = Helmet.renderStatic();
return (
@ -43,6 +44,7 @@ const HTML: React.SFC<Props> = ({
crossOrigin="anonymous"
/> */}
{/* Custom link & meta tags from webpack */}
{extractor.getLinkElements()}
{linkTags.map((l, idx) => (
<link key={idx} {...l as any} />
))}
@ -57,9 +59,12 @@ const HTML: React.SFC<Props> = ({
{head.link.toComponent()}
{head.script.toComponent()}
{extractor.getStyleElements()}
{css.map(href => {
return <link key={href} rel="stylesheet" href={href} />;
})}
{extractor.getScriptElements()}
<script
dangerouslySetInnerHTML={{
__html: `window.__PRELOADED_STATE__ = ${state}`,
@ -73,7 +78,6 @@ const HTML: React.SFC<Props> = ({
</head>
<body>
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />
<script dangerouslySetInnerHTML={{ __html: loadableStateScript }} />
{scripts.map(src => {
return <script key={src} src={src} />;
})}

View File

@ -1,9 +1,8 @@
import fs from 'fs';
import path from 'path';
import React from 'react';
import { Request, Response } from 'express';
import { renderToString } from 'react-dom/server';
import { getLoadableState } from 'loadable-components/server';
import { ChunkExtractor } from '@loadable/server';
import { StaticRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
@ -19,63 +18,6 @@ import i18n from './i18n';
// @ts-ignore
import * as paths from '../config/paths';
import { storeActionsForPath } from './ssrAsync';
const isDev = process.env.NODE_ENV === 'development';
let cachedStats: any;
const getStats = () =>
new Promise((res, rej) => {
if (!isDev && cachedStats) {
res(cachedStats);
return;
}
const statsPath = path.join(paths.clientBuild, paths.publicPath, 'stats.json');
fs.readFile(statsPath, (e, d) => {
if (e) {
rej(e);
return;
}
cachedStats = JSON.parse(d.toString());
res(cachedStats);
});
});
const extractLoadableIds = (tree: any): string[] => {
const ids = (tree.id && [tree.id]) || [];
if (tree.children) {
return tree.children
.reduce((a: string[], c: any) => a.concat(extractLoadableIds(c)), [])
.concat(ids);
}
return ids;
};
// TODO: write tests for this
const chunkExtractFromLoadables = (loadableState: any) =>
getStats().then((stats: any) => {
const loadableIds = extractLoadableIds(loadableState.tree);
const mods = stats.modules.filter(
(m: any) =>
m.reasons.filter((r: any) => loadableIds.indexOf(r.userRequest) > -1).length > 0,
);
const chunks = mods.reduce((a: string[], m: any) => a.concat(m.chunks), []);
const origins = stats.chunks
.filter((c: any) => chunks.indexOf(c.id) > -1)
.map((c: any) => ({ loc: c.origins[0].loc, moduleId: c.origins[0].moduleId }));
const origin = origins[0];
const files = stats.chunks
.filter(
(c: any) =>
c.origins.filter(
(o: any) => origin && o.loc === origin.loc && o.moduleId === origin.moduleId,
).length > 0,
)
.reduce((a: string[], c: any) => a.concat(c.files), []);
return {
css: files.filter((f: string) => /.css$/.test(f)),
js: files.filter((f: string) => /.js$/.test(f)),
};
});
const serverRenderer = () => async (req: Request, res: Response) => {
const { store } = configureStore();
@ -98,18 +40,22 @@ const serverRenderer = () => async (req: Request, res: Response) => {
</I18nextProvider>
);
let loadableState;
let loadableFiles;
let extractor;
// 1. loadable state will render dynamic imports
try {
loadableState = await getLoadableState(reactApp);
loadableFiles = await chunkExtractFromLoadables(loadableState);
const statsFile = path.join(
paths.clientBuild,
paths.publicPath,
'loadable-stats.json',
);
extractor = new ChunkExtractor({ statsFile, entrypoints: ['bundle'] });
} catch (e) {
const disp = `Error getting loadable state for SSR`;
e.message = disp + ': ' + e.message;
log.error(e);
return res.status(500).send(disp + ' (more info in server logs)');
}
// 2. render and collect state
const content = renderToString(reactApp);
const state = JSON.stringify(store.getState());
@ -124,10 +70,19 @@ const serverRenderer = () => async (req: Request, res: Response) => {
return res.status(500).send(disp);
}
const cssFiles = ['bundle.css', 'vendor.css', ...loadableFiles.css]
console.log('About to ask extractor for shit');
try {
console.log('style', extractor.getStyleTags());
console.log('script', extractor.getScriptTags());
console.log('link', extractor.getLinkTags());
} catch (err) {
console.error(err);
}
console.log('Donezo');
const cssFiles = ['bundle.css', 'vendor.css']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const jsFiles = [...loadableFiles.js, 'vendor.js', 'bundle.js']
const jsFiles = ['vendor.js', 'bundle.js']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const mappedLinkTags = linkTags
@ -137,6 +92,7 @@ const serverRenderer = () => async (req: Request, res: Response) => {
.map(m => ({ ...m, content: res.locals.assetPath(m.content) }))
.filter(m => !!m.content);
console.log('Sending!');
return res.send(
'<!doctype html>' +
renderToString(
@ -147,7 +103,7 @@ const serverRenderer = () => async (req: Request, res: Response) => {
metaTags={mappedMetaTags}
state={state}
i18n={i18nClient}
loadableStateScript={loadableState.getScriptContent()}
extractor={extractor}
>
{content}
</Html>,

View File

@ -613,6 +613,12 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-dynamic-import@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-export-default-from@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.0.0.tgz#084b639bce3d42f3c5bf3f68ccb42220bb2d729d"
@ -1564,12 +1570,28 @@
dependencies:
core-js "^2.5.7"
"@loadable/babel-plugin@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@loadable/babel-plugin/-/babel-plugin-5.5.0.tgz#72387f03c6c85e75c0055ab64e2b4b8bb7c028b6"
dependencies:
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
"@loadable/component@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@loadable/component/-/component-5.5.0.tgz#d26ae31bb4da3cac6ae34074eda7e70ffcc2617b"
dependencies:
hoist-non-react-statics "^3.2.1"
"@loadable/server@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@loadable/server/-/server-5.5.0.tgz#b5fb14826db5d4f4e603c47c1232c80f2081251b"
dependencies:
lodash "^4.17.11"
"@loadable/webpack-plugin@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@loadable/webpack-plugin/-/webpack-plugin-5.5.0.tgz#4d44812d3cc106b6278570fa4c8ebdeb07cc3549"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -1975,6 +1997,12 @@
dependencies:
"@types/react" "*"
"@types/loadable__server@5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@types/loadable__server/-/loadable__server-5.2.0.tgz#32a236fc012053d071fd201fcfc9f87a6c6c4fc5"
dependencies:
"@types/react" "*"
"@types/lodash@^4.14.112":
version "4.14.116"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
@ -2052,13 +2080,20 @@
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*", "@types/react-router@^4.0.31":
"@types/react-router@*":
version "4.0.31"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.31.tgz#416bac49d746800810886c7b8582a622ed9604fc"
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router@4.4.3":
version "4.4.3"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.4.3.tgz#ea68b4021cb576866f83365b2201411537423d50"
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react@*":
version "16.4.11"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.11.tgz#330f3d864300f71150dc2d125e48644c098f8770"
@ -4717,7 +4752,7 @@ create-react-context@0.2.2:
fbjs "^0.8.0"
gud "^1.0.0"
create-react-context@0.2.3:
create-react-context@0.2.3, create-react-context@^0.2.2:
version "0.2.3"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
dependencies:
@ -7236,6 +7271,17 @@ history@4.7.2, history@^4.7.2:
value-equal "^0.4.0"
warning "^3.0.0"
history@^4.8.0-beta.0:
version "4.8.0-beta.0"
resolved "https://registry.yarnpkg.com/history/-/history-4.8.0-beta.0.tgz#8b48c1354ac290341c0d73dd33c763bd140531f4"
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^2.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^0.4.0"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -9273,7 +9319,7 @@ lodash.without@~4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.3.0:
"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.3.0:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
@ -12401,6 +12447,18 @@ react-router-dom@4.3.1, react-router-dom@^4.3.1:
react-router "^4.3.1"
warning "^4.0.1"
react-router-dom@4.4.0-beta.6:
version "4.4.0-beta.6"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.4.0-beta.6.tgz#0bec50c8a4276a555b5f1159bb94e7b6fbb73699"
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.8.0-beta.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "^4.4.0-beta.6"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@4.3.1, react-router@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e"
@ -12413,6 +12471,21 @@ react-router@4.3.1, react-router@^4.3.1:
prop-types "^15.6.1"
warning "^4.0.1"
react-router@4.4.0-beta.6, react-router@^4.4.0-beta.6:
version "4.4.0-beta.6"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.4.0-beta.6.tgz#37e1d9ce2be93df397cc1feb1dcd6460ea0b236b"
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "^0.2.2"
history "^4.8.0-beta.0"
hoist-non-react-statics "^2.5.0"
loose-envify "^1.3.1"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.5.2"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-side-effect@^1.0.2, react-side-effect@^1.1.0:
version "1.1.5"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.1.5.tgz#f26059e50ed9c626d91d661b9f3c8bb38cd0ff2d"
@ -14339,10 +14412,18 @@ timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
tiny-invariant@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.3.tgz#91efaaa0269ccb6271f0296aeedb05fc3e067b7a"
tiny-relative-date@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
tiny-warning@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28"
tinycolor2@^1.1.2, tinycolor2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"