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é:
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
3 Réponses :
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é:
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).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.... 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;
BID=$(xdotool search --onlyvisible --class Chrome) xdotool windowfocus $BID key ctrl+r
J'espère que cela aide. Revenez en cas de doute.
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:
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" },
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
.
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:
public
sur la version de production.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.
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)) }) }; }
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>`;
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(), ] } ];
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" } }
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.
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.