Implement react server side rendering and ditch prerendering with puppeteer

This commit is contained in:
Jan Tuomi
2018-08-19 12:46:36 +03:00
parent f6d50413db
commit 05006c15e0
12 changed files with 763 additions and 302 deletions
+34 -43
View File
@@ -2,59 +2,50 @@
const {resolve} = require("path");
const {CheckerPlugin} = require("awesome-typescript-loader");
const StyleLintPlugin = require("stylelint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const Dotenv = require("dotenv-webpack");
module.exports = {
resolve: {
module.exports = function (env, argv) {
const config = {};
config.resolve = {
extensions: [".ts", ".tsx", ".js", ".jsx"]
},
context: resolve(__dirname, "../../src"),
module: {
rules: [
{
test: /\.js$/,
use: ["babel-loader", "source-map-loader"],
exclude: /node_modules/
},
{
test: /\.tsx?$/,
use: ["babel-loader", "awesome-typescript-loader"]
},
{
test: /\.css$/,
use: ["style-loader", {loader: "css-loader", options: {importLoaders: 1}}, "postcss-loader"]
},
{
test: /\.scss$/,
loaders: [
"style-loader",
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader",
"sass-loader"
]
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loaders: [
"file-loader?hash=sha512&digest=hex&name=img/[hash].[ext]",
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false"
]
}
};
config.context = resolve(__dirname, "../../src");
config.module = {
rules: [],
};
config.module.rules.push({
test: /\.js$/,
use: ["babel-loader", "source-map-loader"],
exclude: /node_modules/
});
config.module.rules.push({
test: /\.tsx?$/,
use: ["babel-loader", "awesome-typescript-loader"]
});
config.module.rules.push({
test: /\.(jpe?g|png|gif|svg)$/i,
loaders: [
"file-loader?hash=sha512&digest=hex&name=assets/img/[hash].[ext]",
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false"
]
},
plugins: [
});
config.plugins = [
new CheckerPlugin(),
new StyleLintPlugin(),
new HtmlWebpackPlugin({template: "index.html.ejs"}),
new Dotenv({
path: "./.env.sample"
}),
new Dotenv({
path: "./.env"
})
],
performance: {
}),
];
config.performance = {
hints: false
}
};
return config;
};
+30 -11
View File
@@ -2,18 +2,20 @@
const merge = require("webpack-merge");
const webpack = require("webpack");
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const commonConfig = require("./common.js");
module.exports = merge(commonConfig, {
mode: "development",
entry: [
module.exports = function (env, argv) {
const base = commonConfig(env, argv);
base.mode = "development";
base.entry = [
"react-hot-loader/patch", // Activate HMR for React
"webpack-dev-server/client?http://0.0.0.0:8080", // Bundle the client for webpack-dev-server and connect to the provided endpoint
"webpack/hot/only-dev-server", // Bundle the client for hot reloading, only- means to only hot reload for successful updates
"./index.tsx" // The entry point of our app
],
devServer: {
];
base.devServer = {
hot: true, // Enable HMR on the server
historyApiFallback: true,
host: '0.0.0.0',
@@ -41,9 +43,9 @@ module.exports = merge(commonConfig, {
// watchOptions: {
// ignored: /node_modules/
// }
},
devtool: "cheap-module-eval-source-map",
plugins: [
};
base.devtool = "cheap-module-eval-source-map";
base.plugins = base.plugins.concat([
new FaviconsWebpackPlugin({
logo: "./assets/img/favicon.png",
icons: {
@@ -60,6 +62,23 @@ module.exports = merge(commonConfig, {
}
}),
new webpack.HotModuleReplacementPlugin(), // Enable HMR globally
new webpack.NamedModulesPlugin() // Prints more readable module names in the browser console on HMR updates
]
});
new webpack.NamedModulesPlugin(), // Prints more readable module names in the browser console on HMR updates
new HtmlWebpackPlugin({template: "index.html.ejs"}),
]);
base.module.rules.push({
test: /\.css$/,
use: ["style-loader", {loader: "css-loader", options: {importLoaders: 1}}, "postcss-loader"]
});
base.module.rules.push({
test: /\.scss$/,
loaders: [
"style-loader",
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader",
"sass-loader"
]
});
return base;
};
+100 -35
View File
@@ -3,48 +3,113 @@ const path = require("path");
const merge = require("webpack-merge");
const resolve = path.resolve;
const DynamicCdnWebpackPlugin = require("dynamic-cdn-webpack-plugin");
const PrerenderSPAPlugin = require("prerender-spa-plugin");
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const nodeExternals = require('webpack-node-externals');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WebpackCdnPlugin = require('webpack-cdn-plugin');
const webpack = require('webpack');
/* NOTE: This is a list of all routes that are prerendered for production use.
Please list all routes that contain search engine accessible content, i.e.,
stuff that you would like to find with a Google Search. */
const PRERENDERED_ROUTES = ["/", "/404"];
const commonConfig = require("./common");
module.exports = merge(commonConfig, {
mode: "production",
entry: "./index.tsx",
output: {
filename: "js/bundle.[hash].min.js",
module.exports = function (env, argv) {
const base = commonConfig(env, argv);
base.mode = "production";
base.entry = "./index.tsx";
base.output = {
path: resolve(__dirname, "../../dist"),
publicPath: "/"
},
devtool: "source-map",
plugins: [
new FaviconsWebpackPlugin({
logo: "./assets/img/favicon.png",
icons: {
android: true,
appleIcon: true,
appleStartup: true,
coast: true,
favicons: true,
firefox: true,
opengraph: true,
twitter: true,
yandex: true,
windows: true
}
publicPath: "/",
};
base.devtool = "source-map";
base.plugins.push(
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: "assets/styles.css?[hash]",
}),
new DynamicCdnWebpackPlugin(),
new PrerenderSPAPlugin({
// Required - The path to the webpack-outputted app to prerender.
staticDir: resolve(__dirname, "../../dist"),
// Required - Routes to render.
routes: PRERENDERED_ROUTES
})
]
});
);
if (env.platform === 'server') {
base.entry = "./server/index.ts";
base.output.filename = 'js/server.js?[hash]';
base.target = 'node';
base.externals = [nodeExternals()];
base.module.rules.push({
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader"
],
});
base.module.rules.push({
test: /\.scss$/,
loaders: [
MiniCssExtractPlugin.loader,
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader",
"sass-loader"
]
});
}
else if (env.platform === 'client') {
base.entry = './client/index.ts';
base.output.filename = 'js/client.js?[hash]';
base.target = 'web';
base.module.rules.push({
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader"
],
});
base.module.rules.push({
test: /\.scss$/,
loaders: [
MiniCssExtractPlugin.loader,
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader",
"sass-loader"
]
});
base.plugins.push(
new HtmlWebpackPlugin({
template: "index.html.ejs",
}),
new FaviconsWebpackPlugin({
logo: "./assets/img/favicon.png",
icons: {
android: true,
appleIcon: true,
appleStartup: true,
coast: true,
favicons: true,
firefox: true,
opengraph: true,
twitter: true,
yandex: true,
windows: true
}
}),
new WebpackCdnPlugin({
modules: [
{ name: 'react', var: 'React', path: `umd/react.${process.env.NODE_ENV}.min.js` },
{ name: 'react-dom', var: 'ReactDOM', path: `umd/react-dom.${process.env.NODE_ENV}.min.js` },
],
}),
);
}
return base;
};
+532 -202
View File
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -16,15 +16,16 @@
"license": "MIT",
"homepage": "https://sik.ayy.fi",
"scripts": {
"build": "npm run clean-dist && webpack -p --config=configs/webpack/prod.js",
"clean-dist": "rm -f -r -d dist",
"build": "NODE_ENV=production npm-run-all build:client build:server",
"build:server": "webpack -p --config=configs/webpack/prod.js --env.platform=server",
"build:client": "webpack -p --config=configs/webpack/prod.js --env.platform=client",
"lint": "npm run lint:ts && npm run lint:sass",
"lint:ts": "tslint -p tsconfig.json --format stylish --exclude node_modules",
"lint:ts:fix": "tslint -p tsconfig.json --format stylish --exclude node_modules --fix",
"lint:sass": "stylelint ./src/**/**/*.scss ./src/**/*.scss ./src/*.scss",
"start": "npm run start-dev",
"start-dev": "webpack-dev-server --config=configs/webpack/dev.js",
"serve": "serve -p 3000 dist",
"serve": "node dist/js/server.js",
"start-prod": "npm run build && npm run serve",
"mock-backend": "json-server --watch db.json -p 1234",
"test": "npm-run-all lint test:e2e",
@@ -47,10 +48,11 @@
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.7.0",
"babel-preset-minify": "^0.4.3",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"dotenv-webpack": "^1.5.7",
"dynamic-cdn-webpack-plugin": "^4.0.0-rc.1",
"express": "^4.16.3",
"favicons-webpack-plugin": "0.0.9",
"file-loader": "^1.1.11",
"fs-extra": "^6.0.1",
@@ -58,13 +60,13 @@
"husky": "^1.0.0-rc.9",
"image-webpack-loader": "^4.3.0",
"json-server": "^0.14.0",
"mini-css-extract-plugin": "^0.4.1",
"module-to-cdn": "^3.1.2",
"morgan": "^1.9.0",
"node-sass": "^4.9.0",
"npm-run-all": "^4.1.3",
"plop": "^2.0.0",
"postcss-loader": "^2.1.5",
"prerender-spa-plugin": "^3.2.1",
"puppeteer": "^1.5.0",
"react": "^16.4.0",
"react-addons-test-utils": "^15.6.2",
"react-dom": "^16.4.0",
@@ -75,15 +77,17 @@
"stylelint": "^9.2.1",
"stylelint-config-standard": "^18.2.0",
"stylelint-webpack-plugin": "^0.10.5",
"testcafe": "^0.20.3",
"testcafe": "^0.20.5",
"testcafe-react-selectors": "^2.1.0",
"tslint": "^5.10.0",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "^4.11.1",
"webpack-cdn-plugin": "^2.2.0",
"webpack-cli": "^3.0.3",
"webpack-dev-middleware": "^3.1.3",
"webpack-dev-server": "^3.1.4",
"webpack-merge": "^4.1.2"
"webpack-merge": "^4.1.2",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"axios": "^0.18.0",
+9
View File
@@ -0,0 +1,9 @@
import * as React from "react";
import { BrowserRouter } from "react-router-dom";
import Routes from "../routes";
export default () => (
<BrowserRouter>
<Routes />
</BrowserRouter>
);
+7
View File
@@ -0,0 +1,7 @@
import * as React from "react";
import { hydrate } from "react-dom";
import App from "./App";
const elem = document.getElementById("root");
hydrate(React.createElement(App), elem);
+2 -1
View File
@@ -1,3 +1,4 @@
@import "./assets/scss/normalize";
@import "./assets/scss/globals";
html,
@@ -19,7 +20,7 @@ body {
body {
max-width: 1200px;
padding: 0 0.5rem;
margin: auto;
margin: auto !important;
}
h1 {
-2
View File
@@ -3,9 +3,7 @@ import {render} from "react-dom";
import { BrowserRouter } from "react-router-dom";
import {AppContainer} from "react-hot-loader";
import Routes from "./routes";
import "normalize.css";
import "./index.scss";
import "./index.d.ts";
const rootEl = document.getElementById("root");
+1
View File
@@ -3,6 +3,7 @@ import { Switch, Route } from "react-router-dom";
import FrontPage from "./pages/FrontPage";
import NotFoundPage from "./pages/NotFoundPage";
import CommonPage from "./pages/CommonPage";
import "./index.scss";
const Routes = () => (
<Switch>
+9
View File
@@ -0,0 +1,9 @@
import * as React from "react";
import { StaticRouter } from "react-router-dom";
import Routes from "../routes";
export default ({ url }) => (
<StaticRouter context={{}} location={url}>
<Routes />
</StaticRouter>
);
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react";
import * as express from "express";
import { renderToString } from "react-dom/server";
import * as morgan from "morgan";
import App from "./App";
import * as fs from "fs";
import * as path from "path";
const port = 3000;
const server = express();
const indexHtml = fs.readFileSync(path.resolve("./dist/index.html"), "utf-8");
const html = body => indexHtml.replace("<div id=\"root\"></div>", `<div id="root">${body}</div>`);
server.use(morgan("short"));
server.use("/assets", express.static("dist/assets"));
server.use("/js", express.static("dist/js"));
server.get("*", (req, res) => {
const result = renderToString(React.createElement(App, { url: req.url }));
res.send(
html(result)
);
});
server.listen(3000, () => console.log("React SSR express server listening on port 3000!"));