ホットモジュール置換 (Hot Module Repalcement -React)
以前のコンセプトページで詳細に説明されているように、ホットモジュール置換 (HMR) は、アプリケーションの実行中に、ページをリロードすることなくモジュールを動的に置き換え、追加、または削除します。HMRは、アプリケーションが単一の状態ツリー (single state tree) を持つ場合に非常に役立ちます。
以下で説明する方法ではBabelとReactを使用していますが、これらはHMRを使用するために必須のツールではありません。
プロジェクト設定
ここでは、Babel、React、およびPostCSSをHMRと組み合わせてプロジェクトをデモンストレーションする方法を説明します。以下の手順を進めるために、これらの依存関係をpackage.json
に追加する必要があります。
HMRを使用するには、以下の依存関係が必要です。
npm install --save-dev [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected]
同時に、デモンストレーションの目的のために、以下も必要です。
npm install --save [email protected] [email protected]
Babel設定
.babelrc
ファイルは以下のようになります。
{
"presets": [
["es2015", { "modules": false }],
// webpack understands the native import syntax, and uses it for tree shaking
"stage-2",
// Specifies what level of language features to activate.
// Stage 2 is "draft", 4 is finished, 0 is strawman.
// See https://tc39.github.io/process-document/
"react"
// Transpile React components to JavaScript
],
"plugins": [
"react-hot-loader/babel"
// Enables React code to work with HMR.
]
}
Webpack設定
const { resolve } = require("path");
const webpack = require("webpack");
module.exports = {
entry: [
"react-hot-loader/patch",
// activate HMR for React
"webpack-dev-server/client?http://localhost:8080",
// bundle the client for webpack-dev-server
// and connect to the provided endpoint
"webpack/hot/only-dev-server",
// bundle the client for hot reloading
// only- means to only hot reload for successful updates
"./index.js",
// the entry point of our app
],
output: {
filename: "bundle.js",
// the output bundle
path: resolve(__dirname, "dist"),
publicPath: "/",
// necessary for HMR to know where to load the hot update chunks
},
context: resolve(__dirname, "src"),
devtool: "inline-source-map",
devServer: {
hot: true,
// enable HMR on the server
contentBase: resolve(__dirname, "dist"),
// match the output path
publicPath: "/",
// match the output `publicPath`
},
module: {
rules: [
{
test: /\.js$/,
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader?modules", "postcss-loader"],
},
],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// enable HMR globally
new webpack.NamedModulesPlugin(),
// prints more readable module names in the browser console on HMR updates
],
};
上記には多くの設定がありますが、すべてがHMRに関連しているわけではありません。webpack-dev-server
のオプションとコンセプトページを参照することで、理解を深めることができます。
私たちの基本的な前提は、JavaScriptのエントリファイルが./src/index.js
にあり、CSS Modulesを使用してスタイルファイルを記述しているということです。
設定ファイルで特に注目すべきは、devServer
とentry
キーです。HotModueReplacementPlugin
もplugins
キーに含める必要があります。
目的を達成するために、2つのモジュールを導入しました。
react-hot-loader
は、ReactがHMRをサポートできるようにエントリに追加されています。- HMRが更新されるたびに何が行われるかをよりよく理解するために、
NamedModulePlugin
を追加しました。
コード
// ./src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";
// AppContainer is a necessary wrapper component for HMR
import App from "./components/App";
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById("root"),
);
};
render(App);
// Hot Module Replacement API
if (module.hot) {
module.hot.accept("./components/App", () => {
const NewApp = require("./components/App").default;
render(NewApp);
});
}
// ./src/components/App.js
import React from "react";
import styles from "./App.css";
const App = () => (
<div className={styles.app}>
<h2>Hello, </h2>
</div>
);
export default App;
.app {
text-size-adjust: none;
font-family: helvetica, arial, sans-serif;
line-height: 200%;
padding: 6px 20px 30px;
}
特に注意すべきはmodule
の参照です。
devServer: { hot: true }
を設定すると、Webpackはmodule.hot
を私たちのコードに公開します。- これにより、
module.hot
を使用して特定のソース(ここではApp.js
)のHMRを無効にできます。ここでは、これらの特定の依存関係をどのように処理するかを決定するために使用される非常に重要なAPImodule.hot.accept
があります。 - 注意すべきは、webpack2がES2015モジュール構文を組み込みでサポートしているため、
module.hot.accept
でルートコンポーネントを再参照する必要がないことです。この目的を達成するには、.babelrc
でBabel ES2015のプリセットを設定する必要があります。
[“es2015”, { modules: false }];
以前のBabel設定で設定したようにです。Babelのモジュール機能を無効にすることは、HMRを有効にするためだけではないことに注意してください。この設定をオフにしないと、多くの問題に遭遇するでしょう。
4. webpack2の設定ファイルでES6モジュールを使用しており、#3に従って`.babelrc`を変更した場合、`require`構文を使用して2つの`.babelrc`ファイルを作成する必要があります。
1. 1つはルートディレクトリに置き、`"presets: ["es2015"]"`と設定します。
1. もう1つはwebpackがコンパイルするフォルダ、この例では`src/`に置きます。
したがって、このケースでは、`src/compoents/App.js`またはその他の依存ファイルが変更されると、`module.hot.accept`は`render`メソッドを実行します。これは、`App.css`が`App.js`にインポートされた後、`App.css`が変更された場合でも、`render`メソッドが実行されることを意味します。
### Index.html
エントリページは`dist`ディレクトリに配置する必要があります。`webpack-dev-server`の実行にはこのファイルが必要です。
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Example Index</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
Package.json
最後に、webpack-dev-server
を起動してコードをバンドルし、HMRがどのように機能するかを確認する必要があります。以下のpackage.json
エントリを使用できます。
{
"scripts": {
"start": "webpack-dev-server"
}
}
npm start
を実行し、ブラウザでhttp://localhost:8080
を開くと、以下の項目がconsole.log
に表示されるはずです。
dev-server.js:49[HMR] Waiting for update signal from WDS…
only-dev-server.js:74[HMR] Waiting for update signal from WDS…
client?c7c8:24 [WDS] Hot Module Replacement enabled.
次にApp.js
ファイルを編集して変更すると、console.log
に以下のようなログが表示されます。
[WDS] App updated. Recompiling…
client?c7c8:91 [WDS] App hot update…
dev-server.js:45 [HMR] Checking for updates on the server…
log-apply-result.js:20 [HMR] Updated modules:
log-apply-result.js:22 [HMR] - ./components/App.js
dev-server.js:27 [HMR] App is up to date.
HMRが更新されたモジュールのパスを示していることに注意してください。これはNamedModulesPlugin
を使用しているためです。
開発環境 (Development)
この章では、開発プロセスで使用できるツールについて説明します。
注意:本番環境では使用できません
ソースマップ
JSで例外が発生した場合、どのファイルのどの行でエラーが発生したかを知る必要があります。しかし、ファイルがwebpackによってバンドルされると、問題を見つけるのが非常に困難になります。ソースマップは、この問題を解決するために存在します。それには多くの異なるオプションがあり、それぞれに長所と短所があります。まず、以下を使用します。
devtool: "cheap-eval-source-map"
ツールの選択 (Choosing a Tool)
Webpackは監視モード (watch mode) で使用できます。このモードでは、webpackはファイルを監視し、変更があった場合に再コンパイルします。webpack-dev-server
は、使いやすい開発環境サービスを提供し、自動リロード機能をサポートしています。既に開発環境サービスがあり、より高い適応性を望む場合は、webpack-dev-middleware
をミドルウェアとして使用してこの目的を達成できます。
webpack-dev-server
とwebpack-dev-middleware
はメモリ内でコンパイルを実行します。これは、バンドルされたコードがローカルディスクに保存されないことを意味します。これにより、バンドルが非常に高速になり、一時ファイルが多数生成されてローカルファイルシステムを汚染することもありません。
ほとんどの場合、webpack-dev-server
を使用したいと思うでしょう。なぜなら、それは非常に使いやすく、多くのすぐに使える機能を提供しているからです。
Webpack監視モード (wtach mode)
Webpackの監視モードはファイルの変更を検出します。変更が検出されると、すぐに再コンパイルを実行します。コンパイルプロセスが良い進捗表示を持つことを望みます。そのためには、以下のコマンドを実行します。
webpack --progress --watch
任意のファイルを変更して保存すると、再コンパイルのプロセスが表示されます。
監視モードはサービスに関する問題を考慮していないため、自分でサービスを提供する必要があります。簡単なサービスは[server](https://github.com/tj/serve)
です。インストール後(npm i server -g
)、バンドルされたファイルディレクトリで以下を実行します。
server
再コンパイルのたびに、手動でブラウザをリロードする必要があります。
webpack-dev-server
webpack-dev-server
は、自動リロードをサポートするサービスを提供します。
まず、index.html
ページでコードバンドルが参照されていることを確認してください。ここでは、output.filename
がbundle.js
に設定されていると仮定します。
<script src="/bundle.js"></srcipt>
npmからwebpack-dev-server
をインストールします。
npm install webpack-dev-server --save-dev
その後、webpack-dev-server
コマンドを実行できます。
webpack-dev-server --open
上記のコマンドは、ブラウザを自動的に開き、http://localhost:8080
に指定します。
ファイルを変更して保存します。コードが再バンドルされ、バンドルが完了するとページが自動的にリロードされることがわかります。期待通りに動作しない場合は、watchOptions(https://webpack.js.org/configuration/dev-server#devserver-watchoptions-)
を調整する必要があります。
自動リロード可能なサービスができたので、次にホットモジュール置換 (Hot Module Replacement) を有効にする方法を見ていきましょう。これは、ページをリロードせずにモジュールを置き換えるインターフェースを提供します。詳細については、こちらを参照してください。
webpack-dev-server
は、バックエンドサービスへのリクエストのプロキシなど、多くのことができます。より多くの設定オプションについては、devServerのドキュメントを参照してください。
webpack-dev-middleware
webpack-dev-middleware
は、ミドルウェアベースの接続スタックに適しています。既にNode.jsサービスがある場合や、サービスを完全に制御したい場合に非常に役立ちます。
このミドルウェアは、ファイルのコンパイルをメモリ内で実行します。コンパイルが進行中の場合、コンパイルが完了するまでファイルリクエストを遅延させます。
まず、npmからインストールします。
npm install express webpack-dev-server --save-dev
例として、ミドルウェアを次のように使用できます。
var express = require("express");
var webpackDevMiddleware = require("webpack-dev-middleware");
var webpack = require("webpack");
var webpackConfig = require("./webpack.config");
var app = express();
var compiler = webpack(webpackConfig);
app.use(
webpackDevMiddleware(compiler, {
publicPath: "/", // Same as `output.publicPath` in most cases.
}),
);
app.listen(3000, function () {
console.log("Listening on port 3000!");
});
output.publicPath
とoutput.filename
の設定に応じて、バンドルされたコードはhttp://localhost:3000/bundle.js
からアクセスできるはずです。
デフォルトでは監視モードが使用されます。また、リクエストがあったときにのみ再コンパイルする遅延モード (lazy mode) もサポートしています。
設定は以下の通りです。
app.use(
webpackDevMiddleware(compiler, {
lazy: true,
filename: "bundle.js", // Same as `output.filename` in most cases.
}),
);
他にも多くの便利なオプションがあります。詳細については、ドキュメントを参照してください。
本番環境向けビルド (Building for Production)
この章では、webpackを使用して本番環境向けビルドを行う方法について説明します。
自動化された方法
webpack -p
を実行します(これはwebpack --optimize--minimize --define process.env.NODE_ENV="production"
と同等です)。これにより、以下の手順が実行されます。
UglifyJsPlugin
を使用してファイルを圧縮します。LoaderOptionsPlugin
を実行します。ドキュメントを参照してください。- Nodeの環境変数を設定します。
ソースコードの圧縮
webpackはUglifyJsPlugin
を使用してソースコードを圧縮し、UglifyJsを実行することで出力コードの圧縮を実現します。このプラグインはUgilfyJsのすべての機能をサポートしています。コマンドラインで--optimize-minimize
と入力すると、設定ファイルに以下の設定を追加したことと同等になります。
// webpack.config.js
const webpack = require("webpack");
module.exports = {
/*...*/
plugins: [
new webpack.optimize.UglifyJsPlugin({
sourceMap:
options.devtool &&
(options.devtool.indexOf("sourcemap") >= 0 ||
options.devtool.indexOf("source-map") >= 0),
}),
],
};
これにより、devtoolsオプションに基づいて、バンドル時にソースマップが生成されます。
ソースマップ (Source Map)
開発環境ではソースマップを有効にすることをお勧めします。デバッグやテスト時に非常に役立つからです。Webpackは、コードバンドルに含まれるインラインソースマップ、または分離されたファイル内のソースマップを生成できます。
設定ファイルでは、devtools
設定を変更してソースマップのタイプを設定します。現在、7種類の異なるソースマップタイプをサポートしています。詳細については、具体的なドキュメントで確認できます。
良い選択肢の1つは、ソースマップを1行ごとのマッピングに簡素化する(simplifies the Source Maps to a single mapping per line
)cheap-module-source-map
を使用することです。
Node環境変数
webpack -p
(--define process.env.NODE_EMV="production"
) を実行すると、以下の設定でDefindPlugin
が呼び出されます。
// webpack.config.js
const webpack = require("webpack");
module.exports = {
/*...*/
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
}),
],
};
DefindPlugin
はソースコード内で検索と置換を行います。見つかったすべてのprocess.env.NODE_ENV
はproduction
に置き換えられます。これにより、if(process.env.NODE_ENV !=='procution') console.log(…)
のようなコードは、UnglifyJs
によってif(false) console.log(…)
と同等と見なされます。
手動の方法:異なる環境変数に応じたwebpack設定ファイルの構成
webpackに異なる環境変数に応じた設定ファイルを構成する最も簡単な方法は、複数の設定ファイルを作成することです。例えば:
dev.js
// 此处官网文档有语法错误,我改了一下
module.exports = function (env) {
return {
devtool: "cheap-module-source-map",
output: {
path: path.join(__dirname, "/../dist/assets"),
filename: "[name].bundle.js",
publicPath: publicPath,
sourceMapFilename: "[name].map",
},
devServer: {
port: 7777,
host: "localhost",
historyApiFallback: true,
noInfo: false,
stats: "minimal",
publicPath: publicPath,
},
};
};
prod.js
module.exports = function (env) {
return {
output: {
path: path.join(__dirname, "/../dist/assets"),
filename: "[name].bundle.js",
publicPath: publicPath,
sourceMapFilename: "[name].map",
},
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false,
}),
new UglifyJsPlugin({
beautify: false,
mangle: {
screw_ie8: true,
keep_fnames: true,
},
compress: {
screw_ie8: true,
},
comments: false,
}),
],
};
};
その後、webpack.config.js
の内容を以下のように変更します。
function buildConfig(env) {
return require("./config/" + env + ".js")({ env: env });
}
module.exports = buildConfig(env);
最後に、package.json
に以下のコマンドを追加します。
"build:dev": "webpack --env=dev --progress --profile --colors",
"build:dist": "webpack --env=prod --progress --profile --colors",
ご覧の通り、環境変数をwebpack.config.js
ファイルに渡しています。ここから、環境変数を渡すことで正しい設定ファイルを使用するかどうかを決定する簡単な方法を使用します。
より高度なアプローチは、すべての共通機能を含む基本設定ファイルを用意し、異なる環境変数下の異なる機能を特定のファイルで指定し、webpack-merge
を使用して完全な設定に結合することです。これにより、多くの重複コードを書くことを避けることができます。例えば、js
、ts
、png
、jpeg
などの解析はすべて共通機能であり、基本設定ファイルに含める必要があります。
base.js
module.exports = function () {
return {
entry: {
polyfills: "./src/polyfills.ts",
vendor: "./src/vendor.ts",
main: "./src/main.ts",
},
output: {
path: path.join(__dirname, "/../dist/assets"),
filename: "[name].bundle.js",
publicPath: publicPath,
sourceMapFilename: "[name].map",
},
resolve: {
extensions: ["", ".ts", ".js", ".json"],
modules: [path.join(__dirname, "src"), "node_modules"],
},
module: {
loaders: [
{
test: /\.ts$/,
loaders: ["awesome-typescript-loader", "angular2-template-loader"],
exclude: [/\.(spec|e2e)\.ts$/],
},
{
test: /\.css$/,
loaders: ["to-string-loader", "css-loader"],
},
{
test: /\.(jpg|png|gif)$/,
loader: "file",
},
{
test: /\.(woff|woff2|eot|ttf|svg)$/,
loader: "url-loader?limit=100000",
},
],
},
plugins: [
new ForkCheckerPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: ["polyfills", "vendor"].reverse(),
}),
new HtmlWebpackPlugin({
template: "src/index.html",
chunksSortMode: "dependency",
}),
],
};
};
その後、webpack-merge
を使用して、特定の環境変数で指定された設定ファイルをマージします。本番環境固有の設定をマージする例を見てみましょう(上記のprod.js
と比較してください)。
prod.js(更新済み)
const webpackMerge = require("webpack-merge");
const commonConfig = require("./base.js");
module.exports = function (env) {
return webpackMerge(commonConfig(), {
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false,
}),
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("prod"),
},
}),
new webpack.optimize.UglifyJsPlugin({
beautify: false,
mangle: {
screw_ie8: true,
keep_fnames: true,
},
compress: {
screw_ie8: true,
},
comments: false,
}),
],
});
};
ご覧の通り、prod.js
には主に3つの更新があります。
webpack-meger
を介してbase.js
をマージしました。output
プロパティをbase.js
に移動しました。base.js
以外の異なる設定のみを気にすればよいです。DefinePlugin
を介してprocess.env.NODE_ENV
をprod
に設定しました。これにより、アプリケーションコード全体のprocess.env.NODE_ENV
がprod
の値を持つようになります。
どの設定を異なる環境変数間で一貫させるかは、あなたが決定します。ここでは、異なる環境変数下で一部の設定を統一する方法を典型的に示すためのデモに過ぎません。
ご覧の通り、webpack-merge
は非常に強力で、多くの重複コードを書くことを避けることができます。
React遅延ロード (Lazy Loading - React)
高階関数を使用することで、コンポーネントがその依存関係を消費者に知られることなく遅延ロードしたり、関数またはモジュールを受け取るコンポーネントを使用することで、消費者がその子コンポーネントに知られることなく遅延ロードしたりできます。
コンポーネントの遅延ロード
まず、消費者がコンポーネントを遅延ロードすることを選択するケースを見てみましょう。importLazy
はdefualt
プロパティを返す関数であり、これはBabel/ES2015との相互運用性を目的としています。必要ない場合は、importLazy
メソッドを無視できます。importLazy
は単にexport default
によって公開されたモジュールを返します。
<LazilyLoad
modules={{
TodoHandler: () => importLazy(import("./components/TodoHandler")),
TodoMenuHandler: () => importLazy(import("./components/TodoMenuHandler")),
TodoMenu: () => importLazy(import("./components/TodoMenu")),
}}
>
{({ TodoHandler, TodoMenuHandler, TodoMenu }) => (
<TodoHandler>
<TodoMenuHandler>
<TodoMenu />
</TodoMenuHandler>
</TodoHandler>
)}
</LazilyLoad>
高階コンポーネント (Higher Order Component)
コンポーネントとして、コンポーネント自体の依存関係が遅延ロードされることを保証できます。これは、コンポーネントが非常に大きなライブラリファイルに依存している場合に非常に役立ちます。コードハイライトをサポートするTodoコンポーネントを作成すると仮定します。
class Todo extends React.Component {
render() {
return (
<div>
{this.props.isCode ? <Highlight>{content}</Highlight> : content}
</div>
);
}
}
コードハイライト機能が必要な場合にのみ、このコストのかかるライブラリファイルをロードするようにできます。
// Highlight.js
class Highlight extends React.Component {
render() {
const {Highlight} = this.props.highlight;
// highlight js is now on our props for use
}
}
export LazilyLoadFactory(Highlight, {
highlight: () => import('highlight'),
});
このHighlight
コンポーネントの消費者が、知らされることなく遅延ロードされていることに注目してください。
完全なコード
LazilyLoad
コンポーネントのソースコードは、コンポーネントインターフェースと高階コンポーネントインターフェースを公開しています。
import React from "react";
class LazilyLoad extends React.Component {
constructor() {
super(...arguments);
this.state = {
isLoaded: false,
};
}
componentWillMount() {
this.load(this.props);
}
componentDidMount() {
this._isMounted = true;
}
componentWillReceiveProps(next) {
if (next.modules === this.props.modules) return null;
this.load(next);
}
componentWillUnmount() {
this._isMounted = false;
}
load(props) {
this.setState({
isLoaded: false,
});
const { modules } = props;
const keys = Object.keys(modules);
Promise.all(keys.map((key) => modules[key]()))
.then((values) =>
keys.reduce((agg, key, index) => {
agg[key] = values[index];
return agg;
}, {}),
)
.then((result) => {
if (!this._isMounted) return null;
this.setState({ modules: result, isLoaded: true });
});
}
render() {
if (!this.state.isLoaded) return null;
return React.Children.only(this.props.children(this.state.modules));
}
}
LazilyLoad.propTypes = {
children: React.PropTypes.func.isRequired,
};
export const LazilyLoadFactory = (Component, modules) => {
return (props) => (
<LazilyLoad modules={modules}>
{(mods) => <Component {...mods} {...props} />}
</LazilyLoad>
);
};
export const importLazy = (promise) => promise.then((result) => result.default);
export default LazilyLoad;
ヒント
bundle-loader
を使用することで、コードブロックに意味のある名前を付け、一度にコードのグループをインテリジェントにロードできます。babel-preset-2015
を使用し、modules
をfalse
に設定していることを確認してください。これにより、webpackがモジュールを処理できるようになります。
パブリックパス (Public Path)
Webpackは、アプリケーション内のすべてのリソース参照のベースパスを設定できる、非常に便利な機能を提供します。それはpublicPath
と呼ばれます。
ユースケース (Use case)
ここでは、この機能を使用して目的を達成する実際のアプリケーションのいくつかのシナリオを紹介します。
ビルド時に値を設定する
開発モードでは、通常assets/
ディレクトリをエントリページと同じ階層に配置します。これで問題はありませんが、本番環境で静的リソースがCDNに保存されている場合はどうでしょうか?
この問題は、環境変数を使用して簡単に解決できます。ASSET_PATH
という変数があると仮定します。
// 这里看起来好像有问题
import webpack from "webpack";
// Whatever comes as an environment variable, otherwise use root
const ASSET_PATH = process.env.ASSET_PATH || "/";
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// This makes it possible for us to safely use env vars on our code
new webpack.DefinePlugin({
"process.env.ASSET_PATH": JSON.stringify(ASSET_PATH),
}),
],
};
開発中に値を設定する (Set Value on the fly)
もう1つの方法は、開発プロセス中にパブリックパスを設定することです。Webpackは、この目的を達成するためにグローバル変数__webpack_public_path__
を公開しています。したがって、エントリファイルで次のようにすることができます。
__webpack_publick_path__ = process.en.ASSET_PATH;
どのように行うかはあなた次第です。DefinePlugin
を通じて設定を行った後、process.env.ASSET_PATH
はどこでも直接使用できます。
この記事は 2017年1月18日 に公開され、2017年1月18日 に最終更新されました。3182 日が経過しており、内容が古くなっている可能性があります。