简体   繁体   中英

Webpack bundle Node Express with hot reloading = hell

I hate to admit it, but I've been spending three long evenings trying to do - what I thought would be straightforward thing to do. I finally reached the stage where I'm fed up with it, and frankly rather frustrated because "it just won't work".

Here is what I try to achieve:

  1. Bundle my Express server with Webpack (although my current code just renders a string in the browser, it is supposed to compile server rendered React components compiled with Babel)
  2. Save the bundle in memory (or on disk if there is no other way)
  3. Run webpack / dev / hot middleware to serve my Node Express app in a way that changes to the server rendered pages (which will be React components) will auto-update in the browser.

I've tried numerous combinations, tutorials that have been deprecated, npm packages that are no longer maintained and downloaded examples that just don't work.

Here is my current setup:

webpack.server.config.js:

const path = require('path');
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');

module.exports = {
    name: 'server',
    mode: 'development',
    target: 'node',
    externals: nodeExternals(),
    entry: [ './src/server/index' ],
    output: {
        path: path.resolve(__dirname, 'dist'),
        // path: "/",
        filename: '[name].js',
        publicPath: '/assets/',
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    module: {
        rules: [
            {
                test: /.js$/,
                loader: 'babel-loader',
                include: path.resolve(__dirname, 'src/'),
                exclude: /node_modules/,
                options: {
                    presets:
                        [['@babel/preset-env', { modules: 'false' }], '@babel/preset-react'],
                    plugins: [
                        ['@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
                        '@babel/plugin-proposal-class-properties'
                    ]
                }
            },
            {
                test: /\.scss$/,
                loader: 'ignore-loader'
            },
            {
                test: /\.css$/,
                loader: 'ignore-loader'
            },
            {
                test: /\.(jpg|png|svg|gif|pdf)$/,
                loader: 'file-loader',
                options: {
                    name: '[path][name].[ext]'
                }
            }
        ]
    }
};

index.js:

import http from 'http';
import fs from "fs";
import express from "express";
import favicon from 'serve-favicon';
// import renderer from "./renderer";
import renderApp from './welcome';


const app = express();

app.use(favicon('./public/favicon.ico'));
app.use(express.static("public"));


if (process.env.NODE_ENV !== 'production') {

    const webpack = require('webpack');
    const webpackDevMiddleware = require('webpack-dev-middleware');
    const webpackHotMiddleware = require('webpack-hot-middleware');
    const serverConfig = require('../../webpack.server.config');

    const compiler = webpack(serverConfig);

    app.use(webpackDevMiddleware(compiler, {
        stats: {colors: true},
        headers: { "Access-Control-Allow-Origin": "http://localhost"},
        publicPath: serverConfig.output.publicPath
    }));

    app.use(require("webpack-hot-middleware")(compiler));

}

app.get("*", function(req, res) {
    fs.readFile("./src/server/html/index.html", "utf8", function(err, data) {
        const context = {};
        const html = renderApp();
        //const html = renderer(data, req.path, context);

        res.set('content-type', 'text/html');
        res.send(html);
        res.end();
    });
});

const PORT = process.env.PORT || 8080;

app.listen(3000);

Frankly I'm also rather confused about how this is supposed to work.
Are the following steps supposed to be executed?:

  • webpack webpack.server.config.js --watch
  • node dist/server.js // webpack output folder

Would this magically hot reload my server?

All help is welcome, or if you happened to have a working demo.
I just couldn't manage to make this work.

In the end I will also hot reload (re-render) my client bundle but I guess that will be the easy part as I've seen many, many resources about that.

Night sleep was probably needed.
I got this working (incl with React server rendered components) using StartServerPlugin.
Following setup hot reloads the Node Express server:

const path = require('path');
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');
const StartServerPlugin = require('start-server-webpack-plugin');

module.exports = {
    name: 'server',
    mode: 'development',
    target: 'node',
    externals: nodeExternals({
        whitelist: ['webpack/hot/poll?1000']
    }),
    entry: [ 'webpack/hot/poll?1000', './src/server/index' ],
    output: {
        path: path.resolve(__dirname, 'dist'),
        // path: "/",
        filename: 'server.js',
        publicPath: '/assets/',
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new StartServerPlugin({'name': 'server.js', nodeArgs: ['--inspect']}),
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.DefinePlugin({
            "process.env": {
                "BUILD_TARGET": JSON.stringify('server')
            }
        })
    ],
    module: {
        rules: [
            {
                test: /.js$/,
                loader: 'babel-loader',
                include: path.resolve(__dirname, 'src/'),
                exclude: /node_modules/,
                options: {
                    presets:
                        [['@babel/preset-env', { modules: 'false' }], '@babel/preset-react'],
                    plugins: [
                        ['@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
                        '@babel/plugin-proposal-class-properties'
                    ]
                }
            },
            {
                test: /\.scss$/,
                loader: 'ignore-loader'
            },
            {
                test: /\.css$/,
                loader: 'ignore-loader'
            },
            {
                test: /\.(jpg|png|svg|gif|pdf)$/,
                loader: 'file-loader',
                options: {
                    name: '[path][name].[ext]'
                }
            }
        ]
    }
};

index.js:

import http from 'http'
import app from './server'

const server = http.createServer(app)
let currentApp = app;

const PORT = process.env.PORT || 8080;

server.listen(PORT);

if (module.hot) {
    module.hot.accept('./server', () => {
        server.removeListener('request', currentApp);
        server.on('request', app);
        currentApp = app;
    })
}

server.js:

import http from 'http';
import fs from "fs";
import express from "express";
import favicon from 'serve-favicon';
import renderer from "./renderer";
import renderApp from './welcome';


const app = express();

app.use(favicon('./public/favicon.ico'));
app.use(express.static("public"));


app.get("*", function(req, res) {
    fs.readFile("./src/server/html/index.html", "utf8", function(err, data) {
        const context = {};
        //const html = renderApp();
        console.log('test');
        const html = renderer(data, req.path, context);
        res.set('content-type', 'text/html');
        res.send(html);
        res.end();
    });
});

export default app;

Run with:

rm -rf ./dist && webpack --config webpack.server.config.js --watch

I think anwsers here are bit too complicated. It seems that Webpack does not make this easy.

Rather than trying to cook up complicated configurations, I took the issue in my own hand and created a small devserver, that does both the webpack build and server reload upon file changes. The client also has a reload logic, so the page in both cases is auto reloaded.

hot module reload for express server and webpack client

Synopsis

devserver

const fetch = require('node-fetch')

let process

function spawnserver(){
    process = require('child_process').spawn("node", ["server/server.js", "dev"])

    process.stdout.on('data', (data) => {    
        console.error(`stdout: ${data}`)
    })

    process.stderr.on('data', (data) => {
        console.error(`stderr: ${data}`)
    })
}

function rebuildsrc(){
    process = require('child_process').spawn("npm", ["run", "build"])

    process.stdout.on('data', (data) => {    
        console.error(`stdout: ${data}`)
    })

    process.stderr.on('data', (data) => {
        console.error(`stderr: ${data}`)
    })

    process.on("close", code => {
        console.log("build exited with code", code)
        fetch("http://localhost:3000/reloadsrc").then(response=>response.text().then(content=>console.log(content)))
    })
}

spawnserver()

const watcher = require("chokidar").watch("./server")

watcher.on("ready", _=>{    
    watcher.on("all", _=>{      
        console.log("server reload")
        process.kill()
        spawnserver()
    })
})

const srcWatcher = require("chokidar").watch("./src")

srcWatcher.on("ready", _=>{    
    srcWatcher.on("all", _=>{      
        console.log("rebuild src")        
        rebuildsrc()
    })
})

client reload

let stamp = new Date().getTime()
let lastStamp = null

app.get('/stamp', (req, res) => {
    lastStamp = new Date().getTime()
    res.send(`${stamp}`)
})

app.get('/reloadsrc', (req, res) => {
    stamp = new Date().getTime()
    res.send(`updated stamp to ${stamp}`)
})

let welcomeMessage = "Welcome !!"

let reloadScript = IS_PROD ? ``:`
let stamp = null
let reloadStarted = false
setInterval(_=>{
    fetch('/stamp').then(response=>response.text().then(content=>{                        
        if(stamp){
            if(content != stamp) setTimeout(_=>{                                
                if(!reloadStarted){
                    console.log("commence reload")
                    setInterval(_=>{
                        fetch(document.location.href).then(response=>response.text().then(content=>{
                            if(content.match(/Welcome/)){
                                console.log("reloading")
                                document.location.reload()
                            }                                        
                        }))
                    }, 1000)                                
                    reloadStarted = true
                }                                
            }, 500)
        }else{
            if(!reloadStarted){
                stamp = content
                console.log("stamp set to", stamp)
            }
        }
    }))
}, 200)    
`

app.use('/dist', express.static('dist'))

app.get('/', (req, res) => {
    res.send(`
    <!doctype html>
        <head>
            <title>Reload Express Sample App</title>
        </head>
        <body>
            <h1>${welcomeMessage}</h1>            
            <script>
                ${reloadScript}
            </script>
            <script src="dist/bundle.js"></script>
        </body>
    </html>
    `)  
})

Node.js, babel and webpack have been the bane of my month. There's several components that should be included. You should have a start file called "package.json"

The contents should look like:

{
  "name": "react-complete-guide",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server",
    "build": "rimraf dist && webpack --config webpack.prod.config.js --progress --profile --color"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^7.1.5",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "babel-preset-env": "^1.6.0",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "css-loader": "^0.28.7",
    "file-loader": "^1.1.5",
    "html-webpack-plugin": "^2.30.1",
    "postcss-loader": "^2.0.7",
    "style-loader": "^0.19.0",
    "url-loader": "^0.6.2",
    "webpack": "^3.6.0",
    "webpack-dev-server": "^2.9.1"
  },
  "dependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "react-router-dom": "^4.2.2"
  }
}

If you type "npm start", then the bit of code "start": "webpack-dev-server" is run. This will load a preview of the code. To package the contents into a build you type "npm run build" and that will run the code "build": "rimraf dist && webpack --config webpack.prod.config.js --progress --profile --color". That code will run the "rimraf", which deletes the "dist" folder if it exists, the rest runs the webpack config file.

You should have two webpack files. One for hot reloads and one for packaging for the production environment. The files should be called:

"webpack.config.js" and "webpack.prod.config.js".

The contents of "webpack.config.js" look like this:

const path = require('path');
const autoprefixer = require('autoprefixer');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    devtool: 'cheap-module-eval-source-map',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
        chunkFilename: '[id].js',
        publicPath: ''
    },
    resolve: {
        extensions: ['.js', '.jsx']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                exclude: /node_modules/,
                use: [
                    { loader: 'style-loader' },
                    { 
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: true,
                            localIdentName: '[name]__[local]__[hash:base64:5]'
                        }
                     },
                     { 
                         loader: 'postcss-loader',
                         options: {
                             ident: 'postcss',
                             plugins: () => [
                                 autoprefixer({
                                     browsers: [
                                        "> 1%",
                                        "last 2 versions"
                                     ]
                                 })
                             ]
                         }
                      }
                ]
            },
            {
                test: /\.(png|jpe?g|gif)$/,
                loader: 'url-loader?limit=8000&name=images/[name].[ext]'
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: __dirname + '/src/index.html',
            filename: 'index.html',
            inject: 'body'
        })
    ]
};

The contents of "webpack.prod.config.js" look like this:

const path = require('path');
const autoprefixer = require('autoprefixer');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
    devtool: 'cheap-module-source-map',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist/new'),
        filename: 'bundle.js',
        chunkFilename: '[id].js',
        publicPath: ''
    },
    resolve: {
        extensions: ['.js', '.jsx']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                exclude: /node_modules/,
                use: [
                    { loader: 'style-loader' },
                    { 
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: true,
                            localIdentName: '[name]__[local]__[hash:base64:5]'
                        }
                     },
                     { 
                         loader: 'postcss-loader',
                         options: {
                             ident: 'postcss',
                             plugins: () => [
                                 autoprefixer({
                                     browsers: [
                                        "> 1%",
                                        "last 2 versions"
                                     ]
                                 })
                             ]
                         }
                      }
                ]
            },
            {
                test: /\.(png|jpe?g|gif)$/,
                loader: 'url-loader?limit=8000&name=images/[name].[ext]'
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: __dirname + '/src/index.html',
            filename: 'index.html',
            inject: 'body'
        }),
        new webpack.optimize.UglifyJsPlugin()
    ]
};

You also need a file to tell babel how to act. The file is called ".babelrc" if you are using babel. The contents look like this

{
    "presets": [
        ["env", {
            "targets": {
                "browsers": [
                    "> 1%",
                    "last 2 versions"
                ]
            }
        }],
        "stage-2",
        "react"
    ],
    "plugins": [
        "syntax-dynamic-import"
    ]
}

There's a lot going on in this code. I strongly recomment watching some tutorial videos on this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM