5
votes

Comment configurer webpack-hot-middleware dans une application express?

J'essaie d'activer webpack HMR dans mon application express. Ce n'est PAS une application SPA. Pour le côté vue, j'utilise EJS et Vue. Je n'ai pas l'avantage de vue-cli ici, je dois donc configurer manuellement le vue-loader pour les SFC (fichiers .vue) dans le webpack. Il convient également de mentionner que mon flux de travail est très typique: j'ai mes principales ressources côté client (scss, js, vue, etc.) dans le répertoire resources . et je souhaite les regrouper dans mon répertoire public .

Mon webpack.config.js :

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

Mon fichier 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;

La section scripts de mon fichier package.json :

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()
    ]
};

J'utilise nodemon pour redémarrer le serveur afin de récupérer les modifications du code côté serveur. Dans un onglet, je garde npm run start ouvert et dans un autre onglet npm run watch .

Dans ma console, je vois que HMR est connecté:

entrez la description de l'image ici

Il ne récupère le changement que la première fois et lance un avertissement comme celui-ci:

A ignoré une mise à jour du module non accepté ./resources/css/app.scss -> 0

Et ne prend pas en compte les modifications ultérieures. Comment puis-je réparer cela?

Repo de reproduction: https://bitbucket.org/tanmayd/express-test


10 commentaires

J'ai fait des recherches sur cela tant de fois, aucun de ceux-ci n'était acceptable pour moi. Ensuite, j'ai utilisé nodemon pour cela, je ne sais pas mais j'espère que cela aide ...


Merci pour votre réponse. Nodemon est-il capable de remplacer les ressources statiques par des ressources compilées? En d'autres termes, devez-vous recharger le navigateur pour voir les nouvelles modifications? J'utilise également nodemon mais uniquement pour détecter les changements de fichiers et redémarrer le serveur uniquement.


Oui tu devrais


Désolé je ne comprends pas, je devrais quoi?


Vous devriez recharger le navigateur pour voir les changements


Mais je veux voir les changements instantanément, tout comme une application vue-cli


Je peux vous aider mais vous devriez mettre un lien GitHub ou GitLab pour me montrer une reproduction de votre projet, certainement, je peux vous aider avec votre exigence. laissez un lien pour montrer votre reproduction. J'attends.


@AmerllicA Thread mis à jour!


Avez-vous jeté un œil à medium.com/@johnstew/webpack-hmr-with-express-app-76ef42dbac‌ 17 ? Qu'en est-il de dev.to/riversun/how-to-run-webpack-dev-server-on-express-5ei‌ 9 ?


Cher frère @Tanmay, je passe toute la soirée pour vous projeter et laisser une réponse . J'espère que cela aide votre projet. pour plus de questions, laissez un commentaire sous mon message de réponse.


3 Réponses :


1
votes

J'avais rencontré un problème similaire il y a quelque temps et j'ai pu résoudre en combinant xdotool et exec dans le nœud. Cela pourrait aussi vous aider.

Voici le résumé:

  • Avoir un bash script to reload the browser . Le script utilise xdotool pour obtenir la fenêtre Chrome et recharger (le script peut également être utilisé pour Firefox et d'autres navigateurs).
    Question SO pertinente: Comment recharger l'onglet Google Chrome à partir du terminal?
  • Dans le fichier principal ( app / index.js ), à l'aide de exec , exécutez le script (dans le rappel app.listen). Lorsque des modifications sont apportées, nodemon se recharge, ce qui entraîne l'exécution du script et le rechargement du navigateur.

Script bash: reload.sh

...
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;

app / index.js

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

J'espère que cela aide. Revenez en cas de doute.


0 commentaires

2
votes

Comme ce n'est pas un SPA et que vous souhaitez utiliser EJS, il faudra un rendu côté serveur. Ce n'est pas si simple dans votre cas, vous devrez d'abord écraser la méthode de rendu et ensuite ajouter les fichiers générés par webpack.

Sur la base de votre repo de votre description, https://bitbucket.org/tanmayd/express-test , vous étiez sur la bonne voie, mais vous avez combiné les paramètres de développement et de production dans votre configuration webpack.

Comme je ne peux pas pousser sur votre repo, je vais lister ci-dessous les fichiers qui ont subi des changements ou ceux qui sont nouveaux.

1. Scripts et packages

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}`);
});

J'ai installé cross-env (parce que je suis sous Windows), cheerio (une sorte de version nodejs jquery --- ce n'est pas si mal), style-loader (qui est un must en développement lors de l'utilisation de webpack).

Les scripts:

  • start - démarrer le serveur de développement
  • build - générer des fichiers de production
  • production - démarrer le serveur en utilisant les fichiers générés à partir de "build"

2. webpack.config.js - modifié

style-loader été ajouté au mix afin que webpack fournisse votre css à partir du bundle (voir ./resources/js/app.js - ligne 1). MiniCssExtractPlugin est destiné à être utilisé lorsque vous souhaitez extraire les styles dans un fichier séparé, qui est en production.

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;

3. ./resources/js/app.js - modifié

Les styles sont maintenant ajoutés sur la première ligne import "../css/app.scss";

4. ./app/middlewares.js - nouveau

Vous y trouverez 2 intergiciels, overwriteRenderer et webpackAssets .

overwriteRenderer , doit être le premier middleware avant vos routes, il est utilisé à la fois en développement et en production, en développement, il supprimera la fin de la requête après le rendu et remplira la réponse ( res.body ) avec la chaîne rendue de votre fichier. En production, vos vues agiront comme des mises en page, par conséquent, les fichiers générés seront ajoutés en tête (lien) et corps (script).

webpackAssets ne sera utilisé qu'en développement, doit être le dernier middleware, cela ajoutera au res.body les fichiers générés en mémoire par webpack (app.css & app.js). C'est une version personnalisée de l'exemple trouvé ici 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 - modifié

Ce fichier est destiné au développement. Ici, j'ai ajouté les middlewares de 4 et ajouté l'option serverSideRender: true à devMiddleware afin que Webpack nous serve les actifs utilisés dans 4

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
};

6. ./app/server.js - nouveau

Ceci est la version de production. Il s'agit principalement d'une version de nettoyage de 5 , tous les outils de développement ont été supprimés et seul overwriteRenderer est resté.

"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"
  },


2 commentaires

Salut, je suis loin de tous les appareils pendant un certain temps. Je vérifierai le changement dès que possible. Bien que vous regardiez votre code depuis le téléphone, les choses semblent bonnes jusqu'à présent. La seule chose que je ne peux pas comprendre est le but de cheerio + overwriteRenderer. Je suppose que je dois exécuter le code pour le voir en action. Je reviendrai vers vous, merci


cheerio agit comme un dom virtuel, il peut charger votre chaîne html puis vous pouvez facilement sélectionner / modifier ces éléments html, ce qui est facile si vous connaissez un jquery de base. Dans ce cas, je ne l'utilise que pour ajouter les scripts de webpack. Cependant, sans cheerio, je devrais utiliser une sorte de remplacement pour ajouter ces scripts qui seraient un husle. overwriteRenderer est utilisé pour empêcher le res.render par défaut de terminer (et d'ajouter des en-têtes à) la requête, nous devons le faire nous-mêmes après avoir inclus les scripts cheerio avec cheerio .



1
votes

En fait, votre reproduction présente des problèmes sur la déclaration qui ne sont pas liés à votre problème actuel, mais veuillez les observer:

  1. Ne poussez pas les fichiers de construction sur le serveur git, envoyez simplement les fichiers source.
  2. Définissez un nettoyeur sur le Webpack pour nettoyer le dossier public sur la version de production.
  3. Renommez les dossiers et fichiers avec un nom exactement comme ils le font.
  4. Installez nodemon sur votre projet dans les dépendances de développement.

Et votre problème , j'ai changé beaucoup de choses sur votre structure de reproduction et si vous n'avez pas le temps de lire cette réponse, regardez simplement ce repo et obtenez ce que vous voulez.

  1. Modifiez le app/index.js comme suit:
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))
        })
    };
}
  1. Installez webpack-hot-server-middleware , nodemon et vue-server-renderer dans le projet et changez le script de start pour avoir package.json comme ci-dessous:
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. Modifiez l'intégralité du fichier de configuration de votre webpack ci-dessous:
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. Ajoutez un fichier portant le nom htmlRenderer.js dans le dossier des resources :
{
  "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. Ajoutez un nouveau fichier dont le nom est appServer.js et ses codes devraient être comme suit:
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;

Maintenant, lancez simplement yarn start et profitez du rendu côté serveur parallèlement au rechargement à chaud.


0 commentaires