Skip to content

Commit 1e6856d

Browse files
committed
WIP: SSR
1 parent f80afa4 commit 1e6856d

File tree

7 files changed

+237
-24
lines changed

7 files changed

+237
-24
lines changed

config/webpack.config.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
'use strict';
1+
22

33
const fs = require('fs');
44
const path = require('path');
@@ -24,6 +24,7 @@ const getClientEnvironment = require('./env');
2424
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
2525
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
2626
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
27+
const LoadablePlugin = require('@loadable/webpack-plugin');
2728

2829
const postcssNormalize = require('postcss-normalize');
2930

@@ -36,7 +37,7 @@ const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
3637
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
3738

3839
const imageInlineSizeLimit = parseInt(
39-
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
40+
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000',
4041
);
4142

4243
// Check if TypeScript is setup
@@ -129,7 +130,7 @@ module.exports = function(webpackEnv) {
129130
options: {
130131
sourceMap: true,
131132
},
132-
}
133+
},
133134
);
134135
}
135136
return loaders;
@@ -280,7 +281,7 @@ module.exports = function(webpackEnv) {
280281
// if there are any conflicts. This matches Node resolution mechanism.
281282
// https://github.com/facebook/create-react-app/issues/253
282283
modules: ['node_modules', paths.appNodeModules].concat(
283-
modules.additionalModulePaths || []
284+
modules.additionalModulePaths || [],
284285
),
285286
// These are the reasonable defaults supported by the Node ecosystem.
286287
// We also include JSX as a common component filename extension to support
@@ -339,7 +340,6 @@ module.exports = function(webpackEnv) {
339340
formatter: require.resolve('react-dev-utils/eslintFormatter'),
340341
eslintPath: require.resolve('eslint'),
341342
resolvePluginsRelativeTo: __dirname,
342-
343343
},
344344
loader: require.resolve('eslint-loader'),
345345
},
@@ -370,9 +370,9 @@ module.exports = function(webpackEnv) {
370370
loader: require.resolve('babel-loader'),
371371
options: {
372372
customize: require.resolve(
373-
'babel-preset-react-app/webpack-overrides'
373+
'babel-preset-react-app/webpack-overrides',
374374
),
375-
375+
376376
plugins: [
377377
[
378378
require.resolve('babel-plugin-named-asset-import'),
@@ -414,7 +414,7 @@ module.exports = function(webpackEnv) {
414414
cacheDirectory: true,
415415
// See #6846 for context on why cacheCompression is disabled
416416
cacheCompression: false,
417-
417+
418418
// Babel sourcemaps are needed for debugging into node_modules
419419
// code. Without the options below, debuggers like VSCode
420420
// show incorrect code and set breakpoints on the wrong lines.
@@ -465,7 +465,7 @@ module.exports = function(webpackEnv) {
465465
importLoaders: 2,
466466
sourceMap: isEnvProduction && shouldUseSourceMap,
467467
},
468-
'sass-loader'
468+
'sass-loader',
469469
),
470470
// Don't consider CSS imports dead code even if the
471471
// containing package claims to have no side effects.
@@ -485,7 +485,7 @@ module.exports = function(webpackEnv) {
485485
getLocalIdent: getCSSModuleLocalIdent,
486486
},
487487
},
488-
'sass-loader'
488+
'sass-loader',
489489
),
490490
},
491491
// "file" loader makes sure those assets get served by WebpackDevServer.
@@ -534,8 +534,8 @@ module.exports = function(webpackEnv) {
534534
minifyURLs: true,
535535
},
536536
}
537-
: undefined
538-
)
537+
: undefined,
538+
),
539539
),
540540
// Inlines the webpack runtime script. This script is too small to warrant
541541
// a network request.
@@ -593,7 +593,7 @@ module.exports = function(webpackEnv) {
593593
return manifest;
594594
}, seed);
595595
const entrypointFiles = entrypoints.main.filter(
596-
fileName => !fileName.endsWith('.map')
596+
fileName => !fileName.endsWith('.map'),
597597
);
598598

599599
return {
@@ -626,6 +626,7 @@ module.exports = function(webpackEnv) {
626626
new RegExp('/[^/?]+\\.[^/]+$'),
627627
],
628628
}),
629+
isEnvProduction && new LoadablePlugin(),
629630
// TypeScript type checking
630631
useTypeScript &&
631632
new ForkTsCheckerWebpackPlugin({

config/webpack.config.server.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const nodeExternals = require('webpack-node-externals');
12
const paths = require('./paths');
23
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
34
const webpack = require('webpack');
@@ -128,6 +129,13 @@ module.exports = {
128129
},
129130
resolve: {
130131
modules: ['node_modules'],
132+
extensions: paths.moduleFileExtensions
133+
.map(ext => `.${ext}`)
134+
.filter(ext => true || !ext.includes('ts')),
131135
},
132136
plugins: [new webpack.DefinePlugin(env.stringified)],
137+
optimization: {
138+
minimize: false,
139+
},
140+
externals: [nodeExternals()],
133141
};

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@types/date-fns": "^2.6.0",
2020
"@types/jest": "^24.0.0",
2121
"@types/koa": "^2.11.0",
22+
"@types/koa-static": "^4.0.1",
2223
"@types/loadable__component": "^5.10.0",
2324
"@types/loadable__server": "^5.9.1",
2425
"@types/node": "^12.0.0",
@@ -69,6 +70,7 @@
6970
"jest-resolve": "24.9.0",
7071
"jest-watch-typeahead": "0.4.2",
7172
"koa": "^2.11.0",
73+
"koa-static": "^5.0.0",
7274
"mini-css-extract-plugin": "0.8.0",
7375
"optimize-css-assets-webpack-plugin": "5.0.3",
7476
"pnp-webpack-plugin": "1.5.0",
@@ -123,6 +125,7 @@
123125
"scripts": {
124126
"start": "node scripts/start.js",
125127
"build": "node scripts/build.js",
128+
"build:server": "node scripts/build.server.js",
126129
"test": "node scripts/test.js"
127130
},
128131
"eslintConfig": {
@@ -196,7 +199,11 @@
196199
"babel": {
197200
"presets": [
198201
"react-app"
199-
]
202+
],
203+
"plugins": ["@loadable/babel-plugin"]
200204
},
201-
"proxy": "http://localhost:5000"
205+
"proxy": "http://localhost:5000",
206+
"devDependencies": {
207+
"@loadable/webpack-plugin": "^5.7.1"
208+
}
202209
}

src/index.server.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import Koa from 'koa';
2-
import React from 'react';
3-
import ReactDOMServer from 'react-dom/server';
2+
import path from 'path';
3+
import serve from 'koa-static';
4+
import serverRender from './server/serverRender';
45

56
const app = new Koa();
67

7-
app.use(ctx => {
8-
const html = ReactDOMServer.renderToString(<div>Hello Koa!</div>);
9-
ctx.body = html;
10-
});
8+
app.use(serve(path.resolve('./build')));
9+
10+
app.use(serverRender);
1111

1212
app.listen(3001, () => {
1313
console.log('SSR server is listening to http://localhost:3001');

src/server/Html.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import { ChunkExtractor } from '@loadable/server';
3+
4+
export type HtmlProps = {
5+
content: string;
6+
styledElement: React.ReactNode; // styled-components
7+
extractor: ChunkExtractor;
8+
apolloState: any;
9+
reduxState: any;
10+
};
11+
12+
function Html({
13+
content,
14+
styledElement,
15+
extractor,
16+
apolloState,
17+
reduxState,
18+
}: HtmlProps) {
19+
return (
20+
<html>
21+
{/* <head dangerouslySetInnerHTML={{ __html: head }}></head> */}
22+
<head>
23+
{styledElement}
24+
{extractor.getLinkElements}
25+
{extractor.getStyleElements}
26+
</head>
27+
<body>
28+
<div id="root" dangerouslySetInnerHTML={{ __html: content }}></div>
29+
<script
30+
dangerouslySetInnerHTML={{
31+
__html: `window.__APOLLO_STATE__=${JSON.stringify(
32+
apolloState,
33+
).replace(/</g, '\\u003c')};`,
34+
}}
35+
/>
36+
<script
37+
dangerouslySetInnerHTML={{
38+
__html: `window.__REDUX_STATE__=${JSON.stringify(
39+
reduxState,
40+
).replace(/</g, '\\u003c')};`,
41+
}}
42+
/>
43+
{extractor.getScriptElements}
44+
</body>
45+
</html>
46+
);
47+
}
48+
49+
export default Html;

src/server/serverRender.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import path from 'path';
2+
import React from 'react';
3+
import ReactDOMServer from 'react-dom/server';
4+
import { ApolloProvider } from '@apollo/react-common';
5+
import { ApolloClient } from 'apollo-client';
6+
import { createHttpLink } from 'apollo-link-http';
7+
import { InMemoryCache } from 'apollo-boost';
8+
import { getDataFromTree } from '@apollo/react-ssr';
9+
import { Middleware } from 'koa';
10+
import { createStore } from 'redux';
11+
import { Provider } from 'react-redux';
12+
import { StaticRouter } from 'react-router';
13+
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
14+
import rootReducer from '../modules';
15+
import App from '../App';
16+
import Html from './Html';
17+
import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server';
18+
19+
const statsFile = path.resolve('../build/loadable-stats.json');
20+
21+
const serverRender: Middleware = async ctx => {
22+
// const html = ReactDOMServer.renderToString(<div>Hello Koa!</div>);
23+
24+
// prepare redux store
25+
const store = createStore(rootReducer);
26+
// prepare apollo client
27+
const client = new ApolloClient({
28+
ssrMode: true,
29+
link: createHttpLink({
30+
uri: 'http://localhost:5000/graphql',
31+
}),
32+
cache: new InMemoryCache(),
33+
});
34+
35+
const context = {};
36+
const sheet = new ServerStyleSheet();
37+
const extractor = new ChunkExtractor({ statsFile });
38+
39+
const Root = (
40+
<ChunkExtractorManager extractor={extractor}>
41+
<StyleSheetManager sheet={sheet}>
42+
<Provider store={store}>
43+
<ApolloProvider client={client}>
44+
<StaticRouter location={ctx.url} context={context}>
45+
<App />
46+
</StaticRouter>
47+
</ApolloProvider>
48+
</Provider>
49+
</StyleSheetManager>
50+
</ChunkExtractorManager>
51+
);
52+
53+
try {
54+
await getDataFromTree(Root);
55+
} catch (e) {
56+
ctx.throw(500);
57+
return;
58+
}
59+
60+
const content = ReactDOMServer.renderToString(Root);
61+
const initialState = client.extract();
62+
const styledElement = sheet.getStyleElement();
63+
64+
const html = (
65+
<Html
66+
content={content}
67+
apolloState={initialState}
68+
reduxState={store.getState()}
69+
styledElement={styledElement}
70+
extractor={extractor}
71+
/>
72+
);
73+
74+
ctx.body = `<!doctype html>\n${ReactDOMServer.renderToStaticMarkup(html)}`;
75+
};
76+
77+
export default serverRender;

0 commit comments

Comments
 (0)