Hot Server Middleware
In the last episode we replaced our one server config with one for dev and one for prod and named them. We also create a bunch of new files.
We're going to start where we left off in the last article.
If you need to catch up:
git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout hot-server
npm install
We've almost got SSR working in dev. Let's keep working:
In webpack.dev-client.js
and webpack.prod-client.js
let's take out the HTMLWebpackPlugin
.
Comment out these lines:
// At the top:
// const HTMLWebpackPlugin = require("html-webpack-plugin")
...
// In the plugins array near bottom:
// new HTMLWebpackPlugin({
// template: "./src/index.ejs",
// inject: true,
// title: "Link's Journal"
// })
When we npm run dev
again, we see a new error in the browser. There's no more index.html, so webpack dev server needs a little help to serve the page.
Hot Server Middleware
To the rescue comes a Deus Ex Package. The Webpack Hot Server Middleware. It's gonna give us SSR in development.
In terminal:
npm i webpack-hot-server-middleware
In express.js
:
At the top:
const webpackHotServerMiddleware = require("webpack-hot-server-middleware")
In the if (isDev) {
block:
server.use(webpackHotMiddleware) // Under this line
server.use(webpackHotServerMiddleware(compiler)) // Put this line
It takes both compilers as an argument.
Let's keep moving by running npm run dev
again. We'll go through this error by error so you can see the common ones.
Error: illegal operation on a directory at MemoryFileSystem.readFileSync
is a crazy looking error. But what I've found is it's always pointing at your webpack-dev-server.js
entry object.
entry: {
server: ["./src/server/render.js"]
},
Should become:
entry: "./src/server/render.js",
Just a simple string with one entry point.
We've graduated past naming our bundles. We're going to let webpack handle that from now on.
This one is solveable by changing the libraryTarget
option. Let's also change the final filename
.
output: {
filename: "dev-server-bundle.js",
path: path.resolve(__dirname, "../build"),
libraryTarget: "commonjs2"
},
So let's reload our server and see what we get.
Client/Server Markup Irregularities
Hey, it works!
This is a good error to have. View source in your browser. We have real markup, in development! Whitespace is the culprit here. Let's update the render.js
to get rid of this error. Remove the whitespace around the renderToString()
output.
<div id="react-root">${renderToString(<AppRoot />)}</div>
Hydrate
We have 2 new warnings. We're going to want to change what the client code does slightly. Inside app.js
, change render
to hydrate
. This will save React some time, by only attaching events and not re-rendering everything that's already done.
Note: Your React and ReactDOM packages must be at least 16.0.0
for hydrate to work.
function render(Component) {
ReactDOM.hydrate(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById("react-root")
)
}
Our last error is a weird one.
In webpack.dev-server.js
we need to add a forward slash to our name
option.
{
test: /\.jpg$/,
use: [
{
loader: "file-loader",
options: {
name: "/images/[name].[ext]",
emitFile: false
}
}
]
},
The Warning is because main.css
isn't being output when webpack.dev-client.js
runs. It still wants to use the style loader. Let's update it to be more like webpack.dev-server.js
.
In webpack.dev-client.js
:
// At the top
const ExtractTextPlugin = require("extract-text-webpack-plugin")
...
// Replace the css loader.
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: {
loader: "css-loader"
}
})
},
...
// In the plugins
new ExtractTextPlugin("[name].css"),
Production
Okay, now that the development side is done, let's move on to production.
Let's see what happens when we run npm run prod
.
So in webpack.prod-server.js
:
entry: "./src/server/render.js",
So now we need to switch the require back in express.js
to point to our compiled output.
} else {
webpack([configProdClient, configProdServer]).run((err, stats) => {
const render = require("../../build/prod-server-bundle.js").default
server.use(
"/",
expressStaticGzip("dist", {
enableBrotli: true
})
)
server.use(render())
done()
})
}
We've got the same errors for prod we had for dev. Let's fix this one.
We need to turn the functions in webpack.prod-client.js
and webpack.prod-server.js
into objects.
And the env.NODE_ENV
into the string "production".
Let's keep going. What's next?
So in webpack.prod-server.js
:
output: {
filename: "prod-server-bundle.js",
path: path.resolve(__dirname, "../build"),
libraryTarget: "commonjs2" // Add this
},
Okay. Now, When we load this up in a browser, we can see we have SSR in production as well.
In Sum
We've come quite a long way. We're running the src/server/main.js
file with both npm run dev
and npm run prod
, just using a different value for NODE_ENV
. We're using a new package, webpackHotServerMiddleware
to serve the entry point for the dev server, while using webpack.run()
to hook the final compiled file, build/prod-server-bundle.js
after the compilation as a piece of middleware. This is the real holy grail. The rest of this section is easy in comparison. We made a lot of changes and missing one is common. If you'd like to look at the final code for this article.
git checkout hot-server-final
Up Next
We're going to move ahead towards the ultimate setup by adding React Router to our client side. We'll hook into the server side and finally, we'll using dynamic imports to break our bundle into Router specific chunks.