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

Posted

技术标签:

【中文标题】如何在 express 应用中设置 webpack-hot-middleware?【英文标题】:How to set up webpack-hot-middleware in an express app? 【发布时间】: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,但仅用于检测文件更改并仅重新启动服务器。 是的,你应该 对不起,我不明白,我应该怎么做? 您应该重新加载浏览器以查看更改 【参考方案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-s-s-r

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="$baseUrlcss/app.css?$startupID" />`)
                    $("body").append(`<script type="text/javascript" src="$baseUrljs/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】:

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

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

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

    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;
    在项目中安装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"
  

    将整个 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(),
        ]
    
];
    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>`;
    添加一个名为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 并在热重载的同时享受服务器端渲染。

【讨论】:

以上是关于如何在 express 应用中设置 webpack-hot-middleware?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 express 框架在节点 js 中设置 cookie?

Webpack 4 代理(如何在 webpack 4 中设置代理 url)

如何使用 webpack 在项目中设置多个文件输入和输出?

在 Express 中设置默认响应标头

使用 Express.js 在 Node.js 中设置路由的最佳方式

CORS 与 React、Webpack 和 Axios