├── .gitignore
├── .babelrc
├── src
├── App.js
├── client.js
├── index.js
└── server.js
├── package.json
├── webpack.config.js
├── webpack.server.config.js
├── dev.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | public/main.js
3 | webpack-assets.json
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: [
3 | 'env',
4 | 'react'
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default () =>
4 |
hello
5 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { hydrate } from 'react-dom'
3 | import App from './App'
4 |
5 | hydrate(
6 | ,
7 | document.getElementById('root')
8 | )
9 |
10 | if (module.hot) module.hot.accept()
11 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import http from 'http'
2 | import app from './server'
3 |
4 | const server = http.createServer(app)
5 |
6 | let current = app
7 |
8 | server.listen(3000, err => {
9 | if (err) console.log(err)
10 | console.log('server listening on port 3000')
11 | })
12 |
13 | if (module.hot) {
14 | module.hot.accept('./server.js', () => {
15 | console.log('server reload')
16 | server.removeListener('request', current)
17 | const next = require('./server').default
18 | server.on('request', next)
19 | current = next
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { renderToString } from 'react-dom/server'
3 | import express from 'express'
4 | import App from './App'
5 |
6 | const assets = require('../webpack-assets.json')
7 |
8 | const app = (req, res) => {
9 | const body = renderToString(
10 |
11 | )
12 |
13 | const html = `
14 | hello
15 | ${body}
16 |
17 | `
18 |
19 | res.send(html)
20 | }
21 |
22 | const server = express()
23 |
24 | server.use(express.static('dist/public'))
25 | server.use(app)
26 |
27 | export default server
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "react-server",
4 | "scripts": {
5 | "start": "node dist/server.js",
6 | "build": "webpack && webpack --config webpack.server.config.js",
7 | "dev": "node dev.js"
8 | },
9 | "author": "Brent Jackson",
10 | "dependencies": {
11 | "@koa/cors": "^2.2.2",
12 | "assets-webpack-plugin": "^3.9.5",
13 | "babel-core": "^6.26.3",
14 | "babel-loader": "^7.1.5",
15 | "babel-preset-env": "^1.7.0",
16 | "babel-preset-react": "^6.24.1",
17 | "chalk": "^2.4.1",
18 | "express": "^4.16.3",
19 | "fs-extra": "^7.0.0",
20 | "koa": "^2.5.2",
21 | "koa-webpack": "^5.1.0",
22 | "progress-bar-webpack-plugin": "^1.11.0",
23 | "react": "^16.4.2",
24 | "react-dom": "^16.4.2",
25 | "start-server-webpack-plugin": "^2.2.5",
26 | "webpack": "^4.16.5",
27 | "webpack-command": "^0.4.1",
28 | "webpack-node-externals": "^1.7.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin')
3 | const AssetsPlugin = require('assets-webpack-plugin')
4 | const chalk = require('chalk')
5 |
6 | const isDev = process.env.NODE_ENV === 'development'
7 |
8 | const rules = [
9 | {
10 | test: /\.js$/,
11 | exclude: /node_modules/,
12 | use: 'babel-loader'
13 | }
14 | ]
15 |
16 | const progress = new ProgressBarPlugin({
17 | width: '24',
18 | complete: '█',
19 | incomplete: chalk.gray('░'),
20 | format: [
21 | chalk.blue('[client] :bar'),
22 | chalk.blue(':percent'),
23 | chalk.gray(':elapseds :msg'),
24 | ].join(' '),
25 | summary: false,
26 | customSummary: () => {},
27 | })
28 |
29 | module.exports = {
30 | stats: 'errors-only',
31 | mode: isDev ? 'development' : 'production',
32 | entry: [
33 | path.resolve('src/client.js')
34 | ],
35 | output: {
36 | path: path.resolve('dist/public'),
37 | filename: 'main.js',
38 | publicPath: isDev ? 'http://localhost:3001/' : '/'
39 | },
40 | module: {
41 | rules
42 | },
43 | plugins: [
44 | progress,
45 | new AssetsPlugin()
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const nodeExternals = require('webpack-node-externals')
3 | const ProgressBarPlugin = require('progress-bar-webpack-plugin')
4 | const chalk = require('chalk')
5 | const base = require('./webpack.config')
6 |
7 | const isDev = process.env.NODE_ENV === 'development'
8 |
9 | module.exports = Object.assign({}, base, {
10 | target: 'node',
11 | externals: [
12 | nodeExternals({
13 | whitelist: [
14 | 'webpack/hot/poll?300'
15 | ]
16 | })
17 | ],
18 | entry: [
19 | isDev ? 'webpack/hot/poll?300' : null,
20 | path.resolve('src/index.js')
21 | ].filter(Boolean),
22 | output: {
23 | libraryTarget: 'umd',
24 | path: path.resolve('dist'),
25 | filename: 'server.js',
26 | publicPath: '/'
27 | },
28 | plugins: [
29 | new ProgressBarPlugin({
30 | width: '24',
31 | complete: '█',
32 | incomplete: chalk.gray('░'),
33 | format: [
34 | chalk.magenta('[server] :bar'),
35 | chalk.magenta(':percent'),
36 | chalk.gray(':elapseds :msg'),
37 | ].join(' '),
38 | summary: false,
39 | customSummary: () => {},
40 | })
41 | ]
42 | })
43 |
--------------------------------------------------------------------------------
/dev.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'development'
2 | const path = require('path')
3 | const fs = require('fs-extra')
4 | const Koa = require('koa')
5 | const koaWebpack = require('koa-webpack')
6 | const cors = require('@koa/cors')
7 | const webpack = require('webpack')
8 | const StartServerPlugin = require('start-server-webpack-plugin')
9 |
10 | const app = new Koa()
11 |
12 | const rules = [
13 | {
14 | test: /\.js$/,
15 | exclude: /node_modules/,
16 | use: 'babel-loader'
17 | }
18 | ]
19 |
20 | const config = {
21 | client: require('./webpack.config'),
22 | server: require('./webpack.server.config')
23 | }
24 |
25 | config.server.plugins.push(
26 | new webpack.HotModuleReplacementPlugin(),
27 | new webpack.WatchIgnorePlugin([
28 | path.resolve('webpack-assets.json')
29 | ]),
30 | new StartServerPlugin({
31 | name: 'server.js'
32 | })
33 | )
34 |
35 | const compiler = webpack(config.client)
36 | const serverCompiler = webpack(config.server)
37 |
38 | compiler.hooks.done.tap('dev-server', () => {
39 | serverCompiler.watch({}, stats => {})
40 | })
41 |
42 | const start = async () => {
43 | fs.removeSync('./webpack-assets.json')
44 |
45 | const middleware = await koaWebpack({
46 | compiler,
47 | hotClient: {
48 | logLevel: 'error'
49 | },
50 | devMiddleware: {
51 | logLevel: 'error',
52 | stats: 'errors-only',
53 | }
54 | })
55 | app.use(cors())
56 | app.use(middleware)
57 | const server = app.listen(3001)
58 | }
59 |
60 | start()
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # React Server
3 |
4 | Univeral (isomorphic) React express app with webpack and hot module replacement
5 |
6 | **Note:** This is intended for demonstration and educational purposes.
7 | If you're building a server-rendered React application, I'd recommend looking at [Next.js][] or [Razzle][], which was the inspiration for this.
8 |
9 | - Universal app with SSR and client side rehydration
10 | - Hot module replacement on both server and client
11 | - Support for ES6 & JSX with [Babel][]
12 | - Minimal boilerplate with no other webpack loaders or configuration
13 |
14 |
15 | ## Development
16 |
17 | ```sh
18 | npm i
19 | npm run dev
20 | ```
21 |
22 | ## Production
23 |
24 | ```sh
25 | npm run build && npm start
26 | ```
27 |
28 | ## How development mode works
29 |
30 | In much the same way [Razzle][] works, development mode runs two webpack compilers.
31 | The client compiler is passed to [koa-webpack][] to start a development server on port `3001`.
32 | Once the client compiler has finished, the server compiler is put into watch mode.
33 | Using the [start-server-webpack-plugin][], the server starts running on port `3000`.
34 | Opening `http://localhost:3000` in a browser will make a request from the server and get `http://localhost:3001/main.js` for the client side app, which will rehydrate the server side HTML.
35 | Both the server and the client use `App.js` for rendering the body.
36 |
37 | When any of the source files change, webpack's hot module replacement will update both the server and client bundles.
38 |
39 | ### Credits
40 |
41 | Lots of ideas stolen directly from [Razzle][]
42 |
43 | [Razzle]: https://github.com/jaredpalmer/razzle
44 | [Next.js]: https://github.com/zeit/next.js/
45 | [Babel]: https://github.com/babel/babel
46 | [koa-webpack]: https://github.com/shellscape/koa-webpack
47 | [start-server-webpack-plugin]: https://github.com/ericclemmons/start-server-webpack-plugin
48 |
49 | MIT License
50 |
--------------------------------------------------------------------------------