Skip to content

Commit e3b957f

Browse files
committed
add modern ssr method
1 parent 5ab4b2f commit e3b957f

23 files changed

+516
-107
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/node_modules
2-
/dist
2+
/dist
3+
*.env

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,21 @@ npm run dev
5656
- **fonts**
5757
- **styles**
5858
- **types**
59+
- **views**
5960

6061
## How it works
6162

6263
### Server-Side Rendering
6364

6465
`./src` contains `App.tsx`, which is the entry point of the React app. It imports components from `./src/shared` and global styles from `./assets/styles/`. `App.tsx` is in turn imported by server (`server.tsx`) and client (`index.tsx`) files in `./src/server/` and `./src/client/`, respectively.
6566

66-
The `app.get()` method in server code (`./src/server/server.tsx`) specifies a callback function that will render to string React component from `App.tsx` and past it in a HTML template (`./src/server/indexTemplate.ts`), whenever there is an HTTP GET request with a path (`/`) relative to the site root.
67+
The `app.get()` method in server code (`./src/server/server.tsx`) specifies a callback function that will render to string React component from `App.tsx` and past it in a template (`./src/views/index.tsx`), whenever there is an HTTP GET request with a path (`/`) relative to the site root.
6768

68-
After the `load` event is fired, the client code `hydrate` obtained React root. The `hydrate` method is used to render React components on the client side. It is similar to `render`, but it will attach event listeners to the existing markup instead of replacing it.
69+
The server code use modern React method `renderToPipeableStream` to render React components to a stream. It is similar to `renderToString`, but it returns a Node.js Readable stream instead of a string. This is useful for sending the rendered HTML to the client in chunks.
70+
71+
The client code `hydrate` obtained React root. The `hydrate` method is used to render React components on the client side. It is similar to `render`, but it will attach event listeners to the existing markup instead of replacing it.
72+
73+
In the `App.tsx` component, `React.Suspense` component and `React.lazy` method can be used to load components asynchronously. The `React.Suspense` component is used to display a fallback component while waiting for the asynchronous component to load. The `React.lazy` function is used to load components asynchronously.
6974

7075
### Hot Module Replacement
7176

@@ -93,12 +98,17 @@ It is possible to write snapshot tests (using `jest` and `react-test-renderer`)
9398

9499
The template contains a simple tests for the `Header` component. The test is located in the `./src/__tests__/Header.test.tsx` file.
95100

101+
### Dotenv
102+
103+
The `dotenv` package is used to load environment variables from a `.env` file into `process.env`. The `.env` files is located in the root folder of the project.
104+
96105
## What is used in the template?
97106

98107
### Server-Side Rendering
99108

100109
- Express server
101110
- Nodemon
111+
- Dotenv
102112

103113
### Hot Module Replacement
104114

@@ -107,19 +117,22 @@ The template contains a simple tests for the `Header` component. The test is loc
107117
- React Fast Refresh
108118
- Webpack modules and plugins (webpack-dev-middleware, webpack-hot-middleware)
109119
- Webpack plugins (webpack.HotModuleReplacementPlugin)
120+
- Dotenv
110121

111122
### Code compilation
112123

113124
- Webpack loader (babel-loader)
114125
- Webpack plugin (fork-ts-checker-webpack-plugin)
115126
- browserslist
127+
- Dotenv
116128

117129
### Styles
118130

119131
- Stylus
120132
- postcss
121-
- Webpack loaders (stylus-loader, css-loader, postcss-loader, style-loader)
133+
- Webpack loaders (mini-css-extract-plugin, css-loader, postcss-loader, style-loader)
122134
- browserslist
135+
- Dotenv
123136

124137
### Image Optimization
125138

@@ -136,3 +149,4 @@ The template contains a simple tests for the `Header` component. The test is loc
136149
- Jest
137150
- React Test Renderer
138151
- React Testing Library
152+
- Dotenv

bin/dev.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/* eslint-disable no-undef */
22
const path = require('path');
3+
const dotenv = require('dotenv');
4+
5+
dotenv.config({
6+
path: path.resolve(__dirname, `../.${process.env.DOTENV}.env`),
7+
});
8+
9+
const { DEV_SERVER_PORT = 3000, HMR_SERVER_PORT = 3001 } = process.env;
10+
311
const express = require('express');
412
const webpack = require('webpack');
513
const [
@@ -15,19 +23,20 @@ const hmrServer = express();
1523

1624
const clientCompiler = webpack(webpackClientConfig);
1725

18-
const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001'];
26+
const allowedOrigins = [
27+
`http://localhost:${DEV_SERVER_PORT}`,
28+
`http://localhost:${HMR_SERVER_PORT}`,
29+
];
1930

2031
hmrServer.use(
2132
cors({
2233
origin: function (origin, callback) {
23-
// allow requests with no origin
24-
// (like mobile apps or curl requests)
2534
if (!origin) return callback(null, true);
2635
if (allowedOrigins.indexOf(origin) === -1) {
27-
const msg =
36+
const message =
2837
'The CORS policy for this site does not ' +
2938
'allow access from the specified Origin.';
30-
return callback(new Error(msg), false);
39+
return callback(new Error(message), false);
3140
}
3241
return callback(null, true);
3342
},
@@ -48,8 +57,10 @@ hmrServer.use(
4857
})
4958
);
5059

51-
hmrServer.listen(3001, () => {
52-
console.log('\nHMR Server successfully started on http://localhost:3001\n');
60+
hmrServer.listen(HMR_SERVER_PORT, () => {
61+
console.log(
62+
`\nHMR Server successfully started on http://localhost:${HMR_SERVER_PORT}\n`
63+
);
5364
});
5465

5566
const serverCompiler = webpack(webpackServerConfig);
@@ -75,6 +86,6 @@ serverCompiler.run((err) => {
7586
});
7687

7788
console.log(
78-
'\n!!!Server!!!\nServer started on port http://localhost:3000\n!!!Server!!!'
89+
`\n!!!Server!!!\nServer started on port http://localhost:${DEV_SERVER_PORT}\n!!!Server!!!`
7990
);
8091
});

config/webpack.client.config.js

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
11
/* eslint-disable no-undef */
22
const path = require('path');
3+
const dotenv = require('dotenv');
4+
5+
dotenv.config({
6+
path: path.resolve(__dirname, `../.${process.env.DOTENV}.env`),
7+
});
8+
9+
const { NODE_ENV = 'production' } = process.env;
10+
311
const webpack = require('webpack');
12+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
413
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
514
const CopyPlugin = require('copy-webpack-plugin');
615
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
716
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
817
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
18+
const AssetsPlugin = require('assets-webpack-plugin');
919

10-
const { NODE_ENV } = process.env;
1120
const MODULE_CODE_REGEXP = /\.[tj]sx?$/;
1221
const MODULE_STYLES_REGEXP = /\.module\.styl$/;
1322
const GLOBAL_STYLES_REGEXP = /\.global\.styl$/;
1423
const ALL_IMAGES_REGEXP = /\.(png|svg|jpe?g|gif|ico)$/i;
1524
const TOWEBP_IMAGES_REGEXP = /\.(jpe?g|png)$/i;
1625

1726
const basePlugins = [
18-
new CopyPlugin({
19-
patterns: [{ from: path.resolve(__dirname, '../src/static') }],
27+
new AssetsPlugin({
28+
path: path.resolve(__dirname, '../dist'),
29+
fullPath: false,
30+
filename: 'assetsMap.json',
31+
prettyPrint: NODE_ENV === 'development',
32+
includeAllFileTypes: false,
33+
fileTypes: ['css', 'js'],
34+
processOutput(assets) {
35+
const output = JSON.stringify(assets);
36+
37+
process.env.ASSETS_MAP_GENERAL = output;
38+
39+
return output;
40+
},
41+
}),
42+
new MiniCssExtractPlugin({
43+
filename:
44+
NODE_ENV === 'development' ? 'main.css' : 'main.[contenthash].css',
45+
chunkFilename:
46+
NODE_ENV === 'development' ? '[id].css' : '[id].[contenthash].css',
2047
}),
2148
new ImageMinimizerPlugin({
2249
deleteOriginalAssets: false,
@@ -64,6 +91,9 @@ const basePlugins = [
6491
},
6592
],
6693
}),
94+
new CopyPlugin({
95+
patterns: [{ from: path.resolve(__dirname, '../src/static') }],
96+
}),
6797
];
6898

6999
module.exports = {
@@ -73,7 +103,8 @@ module.exports = {
73103
],
74104
output: {
75105
path: path.resolve(__dirname, '../dist/client'),
76-
filename: 'client.js',
106+
filename:
107+
NODE_ENV === 'development' ? 'client.js' : 'client.[contenthash].js',
77108
publicPath: 'http://localhost:3001/static',
78109
},
79110

@@ -92,7 +123,10 @@ module.exports = {
92123
exclude: GLOBAL_STYLES_REGEXP,
93124
use: [
94125
{
95-
loader: 'style-loader',
126+
loader: MiniCssExtractPlugin.loader,
127+
options: {
128+
emit: true,
129+
},
96130
},
97131
{
98132
loader: 'string-replace-loader',
@@ -122,7 +156,10 @@ module.exports = {
122156
test: GLOBAL_STYLES_REGEXP,
123157
use: [
124158
{
125-
loader: 'style-loader',
159+
loader: MiniCssExtractPlugin.loader,
160+
options: {
161+
emit: true,
162+
},
126163
},
127164
{
128165
loader: 'string-replace-loader',
@@ -157,20 +194,20 @@ module.exports = {
157194
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
158195
},
159196

160-
devtool: NODE_ENV === 'development' ? 'eval' : false,
197+
devtool: NODE_ENV === 'development' ? 'eval-source-map' : false,
161198

162199
plugins:
163200
NODE_ENV === 'development'
164201
? [
165-
new CleanWebpackPlugin(),
166202
...basePlugins,
167-
new webpack.HotModuleReplacementPlugin(),
168203
new ReactRefreshPlugin({
169204
overlay: {
170205
sockIntegration: 'whm',
171206
},
172207
}),
208+
new webpack.HotModuleReplacementPlugin(),
173209
new ForkTsCheckerWebpackPlugin({ typescript: { mode: 'write-dts' } }),
210+
new CleanWebpackPlugin(),
174211
]
175212
: basePlugins,
176213

config/webpack.server.config.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
/* eslint-disable no-undef */
22
const path = require('path');
3+
const dotenv = require('dotenv');
4+
5+
dotenv.config({
6+
path: path.resolve(__dirname, `../.${process.env.DOTENV}.env`),
7+
});
8+
9+
const { NODE_ENV = 'production' } = process.env;
10+
311
const webpackNodeExternals = require('webpack-node-externals');
412
const webpack = require('webpack');
513
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
614

7-
const { NODE_ENV } = process.env;
8-
915
const MODULE_CODE_REGEXP = /\.[tj]sx?$/;
1016
const STYLES_REGEXP = /.styl$/;
1117

18+
const basePlugins = [];
19+
1220
module.exports = {
1321
entry: path.resolve(__dirname, '../src/server/server.tsx'),
1422

@@ -55,14 +63,15 @@ module.exports = {
5563
plugins:
5664
NODE_ENV === 'development'
5765
? [
58-
new webpack.HotModuleReplacementPlugin(),
66+
...basePlugins,
5967
new ReactRefreshPlugin({
6068
overlay: {
6169
sockIntegration: 'whm',
6270
},
6371
}),
72+
new webpack.HotModuleReplacementPlugin(),
6473
]
65-
: [],
74+
: [...basePlugins],
6675

6776
externals: [webpackNodeExternals()],
6877

0 commit comments

Comments
 (0)