简体   繁体   English

带有热重载的 Webpack 捆绑 Node Express = 地狱

[英]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)用 Webpack 捆绑我的 Express 服务器(虽然我当前的代码只是在浏览器中呈现一个字符串,但它应该编译用 Babel 编译的服务器呈现的 React 组件)
  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.运行 webpack / dev / hot 中间件来为我的 Node Express 应用程序提供服务,这种方式对服务器呈现的页面(将是 React 组件)的更改将在浏览器中自动更新。

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.我尝试了多种组合、已弃用的教程、不再维护的 npm 包和下载的不起作用的示例。

Here is my current setup:这是我目前的设置:

webpack.server.config.js: 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:索引.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 webpack webpack.server.config.js --watch
  • node dist/server.js // webpack output folder node dist/server.js // webpack 输出文件夹

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.我使用 StartServerPlugin 完成了这项工作(包括 React 服务器呈现的组件)。
Following setup hot reloads the Node Express server:以下设置热重载 Node Express 服务器:

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:索引.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:服务器.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.似乎 Webpack 并没有让这变得容易。

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.我没有尝试编写复杂的配置,而是自己解决了这个问题并创建了一个小型开发服务器,它会在文件更改时进行 webpack 构建和服务器重新加载。 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快速服务器和 webpack 客户端的热模块重新加载

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. Node.js、babel 和 webpack 是我这个月的祸根。 There's several components that should be included.应该包括几个组件。 You should have a start file called "package.json"您应该有一个名为“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.如果您键入“npm start”,则运行代码“start”:“webpack-dev-server”。 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".要将内容打包到构建中,您键入“npm run build”,这将运行代码“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.该代码将运行“rimraf”,它会删除“dist”文件夹(如果存在),其余的将运行 webpack 配置文件。

You should have two webpack files.你应该有两个 webpack 文件。 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". “webpack.config.js”和“webpack.prod.config.js”。

The contents of "webpack.config.js" look like this: “webpack.config.js”的内容如下所示:

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: “webpack.prod.config.js”的内容如下所示:

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.您还需要一个文件来告诉 babel 如何操作。 The file is called ".babelrc" if you are using babel.如果您使用 babel,该文件名为“.babelrc”。 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.我强烈建议您观看一些有关此的教程视频。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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