如何在 express 应用中设置 webpack-hot-middleware?

Tan*_*may 5 node.js express webpack vue.js webpack-hmr

我正在尝试在我的 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()
    ]
};
Run Code Online (Sandbox Code Playgroud)

我的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;
Run Code Online (Sandbox Code Playgroud)

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

"scripts": {
    "start": "nodemon app --exec babel-node -e js",
    "watch": "./node_modules/.bin/webpack --mode=development --watch",
    "build": "./node_modules/.bin/webpack --mode=production"
}
Run Code Online (Sandbox Code Playgroud)

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

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

在此处输入图片说明

它仅第一次接受更改,并抛出一些警告,如下所示:

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

并且不接受后续更改。我怎样才能解决这个问题?

复制仓库:https : //bitbucket.org/tanmayd/express-test

dar*_*ode 5

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

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

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

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"
  },
Run Code Online (Sandbox Code Playgroud)

我安装了cross-env(因为我在Windows上),cheerio(一个nodejs jquery类型的版本——它并没有那么糟糕),style-loader(这是使用webpack开发时必须的)。

脚本:

  • 启动-启动开发服务器
  • build - 生成生产文件
  • 生产 - 使用“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
};
Run Code Online (Sandbox Code Playgroud)

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

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

4../app/middlewares.js - 新

在这里你会发现 2 个中间件,overwriteRenderer以及webpackAssets.

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

webpackAssets仅在开发中使用,必须是最后一个中间件,这将添加到res.bodywebpack(app.css 和 app.js)在内存中生成的文件中。这是此处找到的示例的自定义版本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());

}
Run Code Online (Sandbox Code Playgroud)

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

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

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;
Run Code Online (Sandbox Code Playgroud)

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}`);
});
Run Code Online (Sandbox Code Playgroud)