web-dev-qa-db-ja.com

Expressアプリでwebpack-hot-middlewareを設定する方法は?

ExpressアプリでWebpack HMRを有効にしようとしています。 SPAアプリではありません。ビュー側では、EJSとVueを使用しています。ここにはvue-cliの利点がないので、webpackでSFC(.vueファイル)のvue-loaderを手動で構成する必要があります。また、言及する価値があります。私のワークフローは非常に典型的です:resourcesディレクトリにメインのクライアント側リソース(scss、js、vue etc))があり、それらをバンドルしたい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を使用してサーバーを再起動し、サーバー側コードの変更を取得しています。 1つのタブではnpm run startを開いたままにし、他のタブではnpm run watchを開いたままにします。

コンソールで、HMRが接続されていることがわかります。

enter image description here

初めて変更を取得するだけで、次のような警告がスローされます。

受け入れられないモジュール./resources/css/app.scssへの更新を無視しました-> 0

そして、その後の変更は反映されません。どうすれば修正できますか?

複製レポ: https://bitbucket.org/tanmayd/express-test

3
Tanmay

これは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"
  },

私はcross-envをインストールしました(Windowsを使用しているため)、cheerio(nodejs jqueryの種類のバージョン---それほど悪くありません)、style-loader(これは開発に不可欠です) webpackを使用している場合)。

スクリプト:

  • start-開発サーバーを起動します
  • ビルド-生産ファイルを生成する
  • 本番-「ビルド」から生成されたファイルを使用してサーバーを起動します

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は開発でのみ使用され、最後のミドルウェアである必要があります。これにより、res.bodyにwebpack(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());

}

5。 ./app/index.js-変更されました

このファイルは開発用です。ここで、4からミドルウェアを追加し、devMiddlewareserverSideRender: trueオプションを追加したので、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}`);
});
1
darklightcode

実際、あなたの複製には宣言にいくつかの問題がありますが、それらは現在の問題とは関係ありませんが、注意してください:

  1. ビルドファイルをgitサーバーにプッシュせず、ソースファイルを送信してください。
  2. Webpackにクリーナーを設定して、本番ビルドのpublicフォルダーをクリーンアップします。
  3. フォルダーとファイルの名前を、まさにそのとおりの名前に変更します。
  4. Dev依存関係のプロジェクトにnodemonをインストールします。

そしてあなたの問題、私はあなたの複製構造の多くのことを変更しました、そしてあなたがこの回答を読む時間が無い場合は を参照してくださいこのレポ とあなたが望むものを取得します。

  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, {
    watch options: {
        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 ([email protected])",
  "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ホットリロードと並行してサーバー側のレンダリングをお楽しみください。

1
AmerllicA

私は少し前に同様の問題に直面していましたが、ノードでxdotoolexecを組み合わせて解決することができました。それもあなたを助けるかもしれません。

ここに要約があります:

  • bash script to reload the browser。スクリプトは xdotool を使用してChromeウィンドウを取得し、リロードします(スクリプトは、Firefoxやその他のブラウザでも使用できます)。
    関連SO質問: Googleをリロードする方法Chromeターミナルからタブ?
  • メインファイル(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;

それが役に立てば幸い。疑いがある場合は元に戻します。

1
Sunil Chaudhary