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が接続されていることがわかります。
初めて変更を取得するだけで、次のような警告がスローされます。
受け入れられないモジュール./resources/css/app.scssへの更新を無視しました-> 0
そして、その後の変更は反映されません。どうすれば修正できますか?
これは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を使用している場合)。
スクリプト:
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
は開発でのみ使用され、最後のミドルウェアである必要があります。これにより、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からミドルウェアを追加し、devMiddleware
にserverSideRender: 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}`);
});
実際、あなたの複製には宣言にいくつかの問題がありますが、それらは現在の問題とは関係ありませんが、注意してください:
public
フォルダーをクリーンアップします。nodemon
をインストールします。そしてあなたの問題、私はあなたの複製構造の多くのことを変更しました、そしてあなたがこの回答を読む時間が無い場合は を参照してくださいこのレポ とあなたが望むものを取得します。
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;
webpack-hot-server-middleware
、nodemon
、vue-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"
}
}
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
ホットリロードと並行してサーバー側のレンダリングをお楽しみください。
私は少し前に同様の問題に直面していましたが、ノードでxdotool
とexec
を組み合わせて解決することができました。それもあなたを助けるかもしれません。
ここに要約があります:
bash script to reload the browser
。スクリプトは xdotool
を使用してChromeウィンドウを取得し、リロードします(スクリプトは、Firefoxやその他のブラウザでも使用できます)。exec
を使用して、スクリプトを実行します(app.listenコールバック内)。変更を加えると、nodemonがリロードされ、スクリプトが実行されてブラウザーがリロードされます。BID=$(xdotool search --onlyvisible --class Chrome)
xdotool windowfocus $BID key ctrl+r
...
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;
それが役に立てば幸い。疑いがある場合は元に戻します。