如何在 Express 应用中设置 webpack-hot-middleware?
How to set up webpack-hot-middleware in an express app?
我正在尝试在我的 express 应用中启用 webpack HMR。这不是 SPA 应用程序。对于视图方面,我使用 EJS 和 Vue。我在这里没有 vue-cli 的优势,所以我必须在 webpack 中为 SFC(.vue 文件)手动配置 vue-loader。另外值得一提的是,我的工作流程非常典型:我的主要 client-side 资源(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 重新启动服务器以获取 server-side 代码的更改。在一个选项卡中,我保持 npm run start
打开,在另一个选项卡中 npm run watch
.
在我的控制台中,我看到 HMR 已连接:
它只在第一次获取更改,并抛出一些警告,如下所示:
Ignored an update to unaccepted module ./resources/css/app.scss -> 0
并且不接受后续更改。我该如何解决这个问题?
我前段时间遇到过类似的问题,并且能够通过在节点中组合 xdotool
和 exec
来解决。它也可能对您有所帮助。
摘要如下:
- 有一个
bash script to reload the browser
。该脚本使用 xdotool
获取 Chrome window 并重新加载(脚本也可用于 firefox 和其他浏览器)。
相关的问题:
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;
希望对您有所帮助。如有任何疑问,请回复。
因为它不是 SPA,并且您想使用需要服务器端呈现的 EJS。在你的情况下这并不容易,首先你需要覆盖渲染方法,然后你需要添加由 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 - 启动开发服务器
- build - 生成生产文件
- production - 使用从 "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在内存中生成的文件添加到res.body
(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 的中间件,并向 devMiddleware
添加了 serverSideRender: true
选项,因此 webpack 将为我们提供那些在 4[ 中使用的资产=91=]
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}`);
});
实际上,您的转载在声明上存在一些问题,与您当前的问题无关,但请注意:
- 不要将构建文件推送到 git 服务器,只发送源文件。
- 在 webpack 上设置清理器以清理生产构建中的
public
文件夹。
- 将文件夹和文件重命名为与它们完全相同的名称。
- 在您的项目的开发依赖项中安装
nodemon
。
还有你的问题,我在你的复制结构上改变了很多东西,如果你没有时间阅读这个答案post,请看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 HMR。这不是 SPA 应用程序。对于视图方面,我使用 EJS 和 Vue。我在这里没有 vue-cli 的优势,所以我必须在 webpack 中为 SFC(.vue 文件)手动配置 vue-loader。另外值得一提的是,我的工作流程非常典型:我的主要 client-side 资源(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 重新启动服务器以获取 server-side 代码的更改。在一个选项卡中,我保持 npm run start
打开,在另一个选项卡中 npm run watch
.
在我的控制台中,我看到 HMR 已连接:
它只在第一次获取更改,并抛出一些警告,如下所示:
Ignored an update to unaccepted module ./resources/css/app.scss -> 0
并且不接受后续更改。我该如何解决这个问题?
我前段时间遇到过类似的问题,并且能够通过在节点中组合 xdotool
和 exec
来解决。它也可能对您有所帮助。
摘要如下:
- 有一个
bash script to reload the browser
。该脚本使用xdotool
获取 Chrome window 并重新加载(脚本也可用于 firefox 和其他浏览器)。
相关的问题: 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;
希望对您有所帮助。如有任何疑问,请回复。
因为它不是 SPA,并且您想使用需要服务器端呈现的 EJS。在你的情况下这并不容易,首先你需要覆盖渲染方法,然后你需要添加由 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 - 启动开发服务器
- build - 生成生产文件
- production - 使用从 "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在内存中生成的文件添加到res.body
(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 的中间件,并向 devMiddleware
添加了 serverSideRender: true
选项,因此 webpack 将为我们提供那些在 4[ 中使用的资产=91=]
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}`);
});
实际上,您的转载在声明上存在一些问题,与您当前的问题无关,但请注意:
- 不要将构建文件推送到 git 服务器,只发送源文件。
- 在 webpack 上设置清理器以清理生产构建中的
public
文件夹。 - 将文件夹和文件重命名为与它们完全相同的名称。
- 在您的项目的开发依赖项中安装
nodemon
。
还有你的问题,我在你的复制结构上改变了很多东西,如果你没有时间阅读这个答案post,请看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
享受服务器端渲染和热重载。