如何在 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 个中间件,overwriteRenderer
和 webpackAssets
。
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 中结合 xdotool
和 exec
来解决。它也可能对您有所帮助。
总结如下:
有一个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-middleware
、nodemon
和vue-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)