【问题标题】:How to set up webpack-hot-middleware in an express app?如何在 express 应用中设置 webpack-hot-middleware?
【发布时间】:2020-05-15 23:19:36
【问题描述】:

我正在尝试在我的 express 应用中启用 webpack HMR。它不是一个 SPA 应用程序。对于视图方面,我使用的是 EJS 和 Vue。我这里没有 vue-cli 的优势,所以我必须手动为 webpack 中的 SFC(.vue 文件)配置 vue-loader。另外值得一提的是,我的工作流程非常典型:我的主要客户端资源(scss、js、vue 等)位于resources 目录中。我希望将它们捆绑在我的public 目录中。

我的webpack.config.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpack = require('webpack');

module.exports = {
    mode: 'development',
    entry: [
        './resources/css/app.scss',
        './resources/js/app.js',
        'webpack-hot-middleware/client'
    ],
    output: {
        path: path.resolve(__dirname, 'public/js'),
        publicPath: '/',
        filename: 'app.js',
        hotUpdateChunkFilename: "../.hot/[id].[hash].hot-update.js",
        hotUpdateMainFilename: "../.hot/[hash].hot-update.json"
    },
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            hmr: process.env.NODE_ENV === 'development'
                        }
                    },
                    'css-loader',
                    'sass-loader'
                ],
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({
            filename: '../css/app.css'
        }),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
    ]
};

我的app/index.js 文件:

import express from 'express';
import routes from './routes';
import path from 'path';
import webpack from 'webpack';
import devMiddleware from 'webpack-dev-middleware';
import hotMiddleware from 'webpack-hot-middleware';
const config = require('../webpack.config');
const compiler = webpack(config);

const app = express();

app.use(express.static('public'));
app.use(devMiddleware(compiler, {
    noInfo: true,
    publicPath: config.output.publicPath
}));
app.use(hotMiddleware(compiler));

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../resources/views'))

routes(app);

app.listen(4000);

export default app;

我的package.json 文件的scripts 部分:

"scripts": {
    "start": "nodemon app --exec babel-node -e js",
    "watch": "./node_modules/.bin/webpack --mode=development --watch",
    "build": "./node_modules/.bin/webpack --mode=production"
}

我正在使用 nodemon 重新启动服务器以获取服务器端代码的更改。在一个标签中,我保持npm run start 处于打开状态,而在另一个标签中npm run watch

在我的控制台中,我看到 HMR 已连接:

它只在第一次获取更改,并抛出如下警告:

忽略对未接受模块 ./resources/css/app.scss 的更新 -> 0

并且不接受后续的更改。我该如何解决这个问题?

复制回购: https://bitbucket.org/tanmayd/express-test

【问题讨论】:

  • 我研究了很多次,这些都不适合我。然后我为此使用了nodemon,我不知道但希望这会有所帮助......
  • 感谢您的回复。 nodemon 是否能够用已编译的资产替换静态资产?换句话说,您是否必须重新加载浏览器才能看到新的变化?我也在使用 nodemon,但仅用于检测文件更改并仅重新启动服务器。
  • 是的,你应该
  • 对不起,我不明白,我应该怎么做?
  • 您应该重新加载浏览器以查看更改

标签: node.js express vue.js webpack webpack-hmr


【解决方案1】:

由于它不是 SPA,并且您想使用需要服务器端渲染的 EJS。在你的情况下这并不容易,首先你需要覆盖渲染方法,然后你需要添加那些由 webpack 生成的文件。

根据您的描述中的 repo,https://bitbucket.org/tanmayd/express-test,您在正确的轨道上,但是您在 webpack 配置中结合了开发和生产设置。

由于我无法推送您的 repo,我将在下面列出发生更改或新的文件。

1.脚本和包

"scripts": {
    "start": "cross-env NODE_ENV=development nodemon app --exec babel-node -e js",
    "watch": "./node_modules/.bin/webpack --mode=development --watch",
    "build": "cross-env NODE_ENV=production ./node_modules/.bin/webpack --mode=production",
    "dev": "concurrently --kill-others \"npm run watch\" \"npm run start\"",
    "production": "cross-env NODE_ENV=production babel-node ./app/server.js"
  },

我安装了cross-env(因为我在windows上),cheerio(一个nodejs jquery类型的版本——还不错),style-loader(这是使用webpack开发时必须的) .

脚本:

  • start - 启动开发服务器
  • 构建 - 生成生产文件
  • 生产 - 使用“build”生成的文件启动服务器

2。 webpack.config.js - 改变了

style-loader 已添加到组合中,因此 webpack 将从包中交付您的 css(请参阅 ./resources/js/app.js - 第 1 行)。 MiniCssExtractPlugin 用于将样式提取到单独的文件中,即生产环境中。

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpack = require('webpack');

// Plugins
let webpackPlugins = [
    new VueLoaderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
];
// Entry points
let webpackEntryPoints = [
    './resources/js/app.js',
];

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

    webpackPlugins = [
        new VueLoaderPlugin()
    ];
    // MiniCssExtractPlugin should be used in production
    webpackPlugins.push(
        new MiniCssExtractPlugin({
            filename: '../css/app.css',
            allChunks: true
        })
    )

}else{

    // Development
    webpackEntryPoints.push('./resources/css/app.scss');
    webpackEntryPoints.push('webpack-hot-middleware/client');
}


module.exports = {
    mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
    entry: webpackEntryPoints,
    devServer: {
        hot: true
    },
    output: {
        path: path.resolve(__dirname, 'public/js'),
        filename: 'app.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    // use style-loader in development
                    (process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader),
                    'css-loader',
                    'sass-loader',
                ],
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            }
        ]
    },
    plugins: webpackPlugins
};

3. ./resources/js/app.js - 已更改

样式现在添加到第一行import "../css/app.scss";

4. ./app/middlewares.js - 新

在这里您会找到 2 个中间件,overwriteRendererwebpackAssets

overwriteRenderer,必须是你的路由之前的第一个中间件,它用于开发和生产,在开发中它会在渲染后抑制请求的结束,并用渲染的字符串填充响应(res.body)你的文件。在生产中,您的视图将充当布局,因此生成的文件将添加到 head(link) 和 body(script) 中。

webpackAssets只会在开发中使用,必须是最后一个中间件,这会将webpack(app.css & app.js)在内存中生成的文件添加到res.body。这是此处的示例的自定义版本webpack-dev-server-ssr

const cheerio = require('cheerio');
let startupID = new Date().getTime();

exports.overwriteRenderer = function (req, res, next) {
    var originalRender = res.render;
    res.render = function (view, options, fn) {
        originalRender.call(this, view, options, function (err, str) {
            if (err) return fn(err, null); // Return the original callback passed on error

            if (process.env.NODE_ENV === 'development') {

                // Force webpack in insert scripts/styles only on text/html
                // Prevent webpack injection on XHR requests
                // You can tweak this as you see fit
                if (!req.xhr) {
                    // We need to set this header now because we don't use the original "fn" from above which was setting the headers for us.
                    res.setHeader('Content-Type', 'text/html');
                }

                res.body = str; // save the rendered string into res.body, this will be used later to inject the scripts/styles from webpack
                next();

            } else {

                const $ = cheerio.load(str.toString());
                if (!req.xhr) {

                    const baseUrl = req.protocol + '://' + req.headers['host'] + "/";
                    // We need to set this header now because we don't use the original "fn" from above which was setting the headers for us.
                    res.setHeader('Content-Type', 'text/html');

                    $("head").append(`<link rel="stylesheet" href="${baseUrl}css/app.css?${startupID}" />`)
                    $("body").append(`<script type="text/javascript" src="${baseUrl}js/app.js?${startupID}"></script>`)

                }

                res.send($.html());

            }

        });
    };
    next();
};
exports.webpackAssets = function (req, res) {

    let body = (res.body || '').toString();

    let h = res.getHeaders();

    /**
     * Inject scripts only when Content-Type is text/html
     */
    if (
        body.trim().length &&
        h['content-type'] === 'text/html'
    ) {

        const webpackJson = typeof res.locals.webpackStats.toJson().assetsByChunkName === "undefined" ?
            res.locals.webpackStats.toJson().children :
            [res.locals.webpackStats.toJson()];

        webpackJson.forEach(item => {

            const assetsByChunkName = item.assetsByChunkName;
            const baseUrl = req.protocol + '://' + req.headers['host'] + "/";
            const $ = require('cheerio').load(body.toString());

            Object.values(assetsByChunkName).forEach(chunk => {

                if (typeof chunk === 'string') {
                    chunk = [chunk];
                }
                if (typeof chunk === 'object' && chunk.length) {

                    chunk.forEach(item => {

                        console.log('File generated by webpack ->', item);

                        if (item.endsWith('js')) {

                            $("body").append(`<script type="text/javascript" src="${baseUrl}${item}"></script>`)

                        }

                    });

                }

                body = $.html();

            });

        });

    }

    res.end(body.toString());

}

5. ./app/index.js - 已更改

此文件用于开发。这里我添加了来自 4 的中间件,并将 serverSideRender: true 选项添加到 devMiddleware 以便 webpack 将为我们提供那些在 4

中使用的资产
import express from 'express';
import routes from './routes';
import path from 'path';
import devMiddleware from 'webpack-dev-middleware';
import hotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';
const {webpackAssets, overwriteRenderer} = require('./middlewares');
const config = require('../webpack.config');
const compiler = webpack(config);
const app = express();

app.use(express.static('public'));
app.use(devMiddleware(compiler, {
    publicPath: config.output.publicPath,
    serverSideRender: true // enable serverSideRender, https://github.com/webpack/webpack-dev-middleware
}));
app.use(hotMiddleware(compiler));

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../resources/views'));

// This new renderer must be loaded before your routes.
app.use(overwriteRenderer); // Local render

routes(app);

// This is a custom version for server-side rendering from here https://github.com/webpack/webpack-dev-middleware
app.use(webpackAssets);

app.listen(4000, '0.0.0.0', function () {
    console.log(`Server up on port ${this.address().port}`)
    console.log(`Environment: ${process.env.NODE_ENV}`);
});

export default app;

6. ./app/server.js - 新

这是生产版本。主要是5的清理版,去掉了所有的开发工具,只剩下overwriteRenderer了。

import express from 'express';
import routes from './routes';
import path from 'path';

const {overwriteRenderer} = require('./middlewares');
const app = express();

app.use(express.static('public'));
app.use(overwriteRenderer); // Live render

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../resources/views'));

routes(app);

app.listen(5000, '0.0.0.0', function() {
    if( process.env.NODE_ENV === 'development'){
        console.error(`Incorrect environment, "production" expected`);
    }
    console.log(`Server up on port ${this.address().port}`);
    console.log(`Environment: ${process.env.NODE_ENV}`);
});

【讨论】:

  • 嗨,我已经有一段时间没有使用所有设备了。我会尽快验证更改。尽管从电话中查看您的代码,但到目前为止情况看起来还不错。我唯一无法理解的是cheerio + overwriteRenderer的目的。我想我必须运行代码才能看到它的实际效果。我会回复你的,谢谢
  • cheerio 充当虚拟 dom,它可以加载您的 html 字符串,然后您可以轻松选择/更改这些 html 元素,如果您了解一些基本的 jquery,这很容易。在这种情况下,我只使用它来添加来自 webpack 的脚本。但是,如果没有cheerio,我将不得不使用某种替换来添加那些很麻烦的脚本。 overwriteRenderer 用于防止默认的 res.render 结束(并向其中添加标头)请求,我们需要自己执行此操作我们已将 webpack 脚本包含在 cheerio 中。跨度>
【解决方案2】:

我前段时间遇到过类似的问题,并且能够通过在 node.js 中结合 xdotoolexec 来解决。它也可能对您有所帮助。

总结如下:

  • 有一个bash script to reload the browser。该脚本使用xdotool 获取Chrome 窗口并重新加载(脚本也可用于firefox 和其他浏览器)。
    相关的SO问题: How to reload Google Chrome tab from terminal?
  • 在主文件 (app/index.js) 中,使用 exec,运行脚本(在 app.listen 回调中)。进行任何更改后,nodemon 将重新加载,从而导致脚本执行并重新加载浏览器。

Bash 脚本:reload.sh

BID=$(xdotool search --onlyvisible --class Chrome)
xdotool windowfocus $BID key ctrl+r


app/index.js

...
const exec = require('child_process').exec;

app.listen(4000, () => {
    exec('sh script/reload.sh',
        (error, stdout, stderr) => {
            console.log(stdout);
            console.log(stderr);
            if (error !== null) {
                console.log(`exec error: ${error}`);
            }
        }
    );
});

export default app;

希望对您有所帮助。如有任何疑问,请回复。

【讨论】:

    【解决方案3】:

    实际上,您的复制品在声明中存在一些问题,与您当前的问题无关,但请注意:

    1. 不要将构建文件推送到 git 服务器,只发送源文件。
    2. 在 webpack 上设置一个清理器来清理生产版本上的 public 文件夹。
    3. 将文件夹和文件重命名为与它们完全相同的名称。
    4. 在您的项目的开发依赖项中安装nodemon

    还有你的问题,我在你的复制结构上改变了很多东西,如果你没有时间阅读这篇回答帖子,请查看this repo 并得到你想要的。

    1. app/index.js 更改为以下内容:
    import express from 'express';
    import routes from './routes';
    import hotServerMiddleware from 'webpack-hot-server-middleware';
    import devMiddleware from 'webpack-dev-middleware';
    import hotMiddleware from 'webpack-hot-middleware';
    import webpack from 'webpack';
    const config = require('../webpack.config');
    const compiler = webpack(config);
    
    const app = express();
    
    app.use(devMiddleware(compiler, {
        watchOptions: {
            poll: 100,
            ignored: /node_modules/,
        },
        headers: { 'Access-Control-Allow-Origin': '*' },
        hot: true,
        quiet: true,
        noInfo: true,
        writeToDisk: true,
        stats: 'minimal',
        serverSideRender: true,
        publicPath: '/public/'
    }));
    app.use(hotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client')));
    app.use(hotServerMiddleware(compiler));
    
    const PORT = process.env.PORT || 4000;
    
    routes(app);
    
    app.listen(PORT, error => {
        if (error) {
            return console.error(error);
        } else {
            console.log(`Development Express server running at http://localhost:${PORT}`);
        }
    });
    
    export default app;
    
    1. 在项目中安装webpack-hot-server-middlewarenodemonvue-server-renderer并将start脚本更改为package.json,如下所示:
    {
      "name": "express-test",
      "version": "1.0.0",
      "main": "index.js",
      "author": "Tanmay Mishu (tanmaymishu@gmail.com)",
      "license": "MIT",
      "scripts": {
        "start": "NODE_ENV=development nodemon app --exec babel-node -e ./app/index.js",
        "watch": "./node_modules/.bin/webpack --mode=development --watch",
        "build": "./node_modules/.bin/webpack --mode=production",
        "dev": "concurrently --kill-others \"npm run watch\" \"npm run start\""
      },
      "dependencies": {
        "body-parser": "^1.19.0",
        "csurf": "^1.11.0",
        "dotenv": "^8.2.0",
        "ejs": "^3.0.1",
        "errorhandler": "^1.5.1",
        "express": "^4.17.1",
        "express-validator": "^6.3.1",
        "global": "^4.4.0",
        "mongodb": "^3.5.2",
        "mongoose": "^5.8.10",
        "multer": "^1.4.2",
        "node-sass-middleware": "^0.11.0",
        "nodemon": "^2.0.2",
        "vue": "^2.6.11",
        "vue-server-renderer": "^2.6.11"
      },
      "devDependencies": {
        "babel-cli": "^6.26.0",
        "babel-preset-env": "^1.7.0",
        "babel-preset-stage-0": "^6.24.1",
        "concurrently": "^5.1.0",
        "css-loader": "^3.4.2",
        "mini-css-extract-plugin": "^0.9.0",
        "node-sass": "^4.13.1",
        "nodemon": "^2.0.2",
        "sass-loader": "^8.0.2",
        "vue-loader": "^15.8.3",
        "vue-style-loader": "^4.1.2",
        "vue-template-compiler": "^2.6.11",
        "webpack": "^4.41.5",
        "webpack-cli": "^3.3.10",
        "webpack-dev-middleware": "^3.7.2",
        "webpack-hot-middleware": "^2.25.0",
        "webpack-hot-server-middleware": "^0.6.0"
      }
    }
    
    1. 将整个 webpack 配置文件更改为以下内容:
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    const webpack = require('webpack');
    
    module.exports = [
        {
            name: 'client',
            target: 'web',
            mode: 'development',
            entry: [
                'webpack-hot-middleware/client?reload=true',
                './resources/js/app.js',
            ],
            devServer: {
                hot: true
            },
            output: {
                path: path.resolve(__dirname, 'public'),
                filename: 'client.js',
                publicPath: '/',
            },
            module: {
                rules: [
                    {
                        test: /\.(sa|sc|c)ss$/,
                        use: [
                            {
                                loader: MiniCssExtractPlugin.loader,
                                options: {
                                    hmr: process.env.NODE_ENV === 'development'
                                }
                            },
                            'css-loader',
                            'sass-loader'
                        ],
                    },
                    {
                        test: /\.vue$/,
                        loader: 'vue-loader'
                    }
                ]
            },
            plugins: [
                new VueLoaderPlugin(),
                new MiniCssExtractPlugin({
                    filename: 'app.css'
                }),
                new webpack.HotModuleReplacementPlugin(),
                new webpack.NoEmitOnErrorsPlugin(),
            ]
        },
        {
            name: 'server',
            target: 'node',
            mode: 'development',
            entry: [
                './resources/js/appServer.js',
            ],
            devServer: {
                hot: true
            },
            output: {
                path: path.resolve(__dirname, 'public'),
                filename: 'server.js',
                publicPath: '/',
                libraryTarget: 'commonjs2',
            },
            module: {
                rules: [
                    {
                        test: /\.(sa|sc|c)ss$/,
                        use: [
                            {
                                loader: MiniCssExtractPlugin.loader,
                                options: {
                                    hmr: process.env.NODE_ENV === 'development'
                                }
                            },
                            'css-loader',
                            'sass-loader'
                        ],
                    },
                    {
                        test: /\.vue$/,
                        loader: 'vue-loader'
                    }
                ]
            },
            plugins: [
                new VueLoaderPlugin(),
                new MiniCssExtractPlugin({
                    filename: 'app.css'
                }),
                new webpack.HotModuleReplacementPlugin(),
                new webpack.NoEmitOnErrorsPlugin(),
            ]
        }
    ];
    
    1. resources 文件夹中添加一个名为htmlRenderer.js 的文件:
    export default html => `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Tanmay Mishu</title>
        <link rel="stylesheet" href="/app.css">
    </head>
    <body>
        <div id="app">${html}</div>
        <script src="/client.js"></script>
    </body>
    </html>`;
    
    1. 添加一个名为appServer.js的新文件,其代码应如下所示:
    import Vue from 'vue';
    import App from './components/App.vue';
    import htmlRenderer from "../htmlRenderer";
    
    const renderer = require('vue-server-renderer').createRenderer()
    
    export default function serverRenderer({clientStats, serverStats}) {
        Vue.config.devtools = true;
    
        return (req, res, next) => {
            const app = new Vue({
                render: h => h(App),
            });
    
            renderer.renderToString(app, (err, html) => {
                if (err) {
                    res.status(500).end('Internal Server Error')
                    return
                }
                res.end(htmlRenderer(html))
            })
        };
    }
    

    现在,只需运行 yarn start 并在热重载的同时享受服务器端渲染。

    【讨论】:

      猜你喜欢
      • 2016-05-25
      • 2018-11-26
      • 2018-06-23
      • 2020-06-07
      • 2017-11-28
      • 2017-07-06
      • 2021-05-25
      • 2018-02-26
      • 1970-01-01
      相关资源
      最近更新 更多