动态模块替代(Hot Module Repalcement -React)
就像之前 理念页面 中剖析的细节那样,动态模块替代(HMR)会在运用运转时动态的替代、增加或许删除模块而不必从新革新页面。 HMR 非常有效,当运用只需一个状况树(single state tree)时。
下面引见的要领形貌中运用了 Babel 和 React ,但这并非运用 HRM 所必需的东西。
项目设置
这里会指点你怎样用 Babel, React 和 PostCss 一同运用 HMR 去演示一个项目。为了能够随着下面走下去,须要把这些依靠增加到 package.json
中去。
为了运用 HMR,你须要以下这些依靠:
npm install --save-dev babel@6.5.2 babel-core@6.13.2 babel-loader@6.2.4 babel-preset-es2015@6.13.2 babel-preset-react@6.11.1 babel-preset-stage-2@6.13.0 css-loader@0.23.1 postcss-loader@0.9.1 react-hot-loader@3.0.0-beta.6 style-loader@0.13.1 webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.0
同时,为了到达我们演示的目标,还须要:
npm install --save react@15.3.0 react-dom@15.3.0
Babel Config
.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 Config
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 options 和concept pages 来加深明白。
我们基本想象是如许的,你的 JavaScript 进口文件在 ./src/index.js
且你运用 CSS Module 来编写款式文件。
设置文件中须要重点关注的是 devServer
和 entry
key. HotModueReplacementPlugin
一样须要被包含在 plugins
key 中。
为了到达目标,我们引入了两个模块:
react-hot-loader
增加到了进口中, 是为了能够使 React 支撑 HMR为了更好的明白 HMR 每次更新的时刻做了哪些事变,我们增加了
NamedModulePlugin
Code
// ./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
的援用:
Webpack 会暴露出
module.hot
给我们的代码,当我们设置devServer: { hot: true }
时;如许我们能够运用
module.hot
来给特定的资本弃用 HMR (这里是App.js
). 这里有一个非常重要的 APImodule.hot.accept
,用来决议怎样处置惩罚这些特定的依靠。须要注重的是,webpack2 内建支撑 ES2015 模块语法,你不须要在
module.hot.accept
中从新援用跟组件。为了到达这个目标,须要在.babelrc
设置 Babel ES2015 的预先设置:["es2015", {"modules": false}]
就像我们在之前 Babel Config 中设置的那样。须要注重,禁用 Babel 的模块功用 不单单议是为了启用 HMR。如果你不关掉这个设置,那末你会遇到须要题目。
如果你在 webpack2 的设置文件中运用 ES6 模块,而且你依据 #3 修正了
.babelrc
,那末你须要运用require
语法来竖立两个.babelrc
文件:一个放在根目次下面并设置为
"presets: ["es2015"]"
一个放在 webpack 要编译的文件夹下,比方在这个例子中,就是
src/
所以在这个案例中,module.hot.accept
会实行render
要领不管src/compoents/App.js
或许别的的依靠文件变化的时刻 ——这意味着当App.css
被引入到App.js
中今后,即使是App.css
被修正,
render
要领一样会被实行。
Index.html
进口页面须要被放在页面 dist
下面,webpack-dev-server 的运转须要这个文件。
<!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)
这个章节引见在开辟历程当中能够运用的一些东西。
须要注重,不能在临盆环境运用
Source Map
当 JS 发作非常的时刻,我们须要指点是哪个文件的哪一行出错了。然则当文件都被 webpack 打包今后,找题目会变得很贫苦。
Source Map 就是为了处理这个题目的。它有很多差别的选项,每一种都有的优点和不足。在一开始,我们运用:
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 option ,在打包的时刻会天生 Source Map.
资本映照(Source Map)
我们推荐在开辟环境启用 Source Map. 由于在 debug 或许测试的时刻很有效。Webpack 能够天生包含在代码包或许星散文件中的 inline Source Map.
在设置文件中,经由历程修正 devtools
设置来设置 Source Map 范例。现在我们支撑七种差别范例的 Source Map. 能够在详细文档中找到越发细致的引见。
一个比较好好的挑选是运用 cheap-module-source-map
,能够将源映照简化为每行映照(simplifies the Source Maps to a single mapping per line)。
Node 环境变量
实行 webpack -p
( --define process.env.NODE_EMV="production"
) 会经由历程以下的设置挪用 DefinePlugin
:
// 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(updated)
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’ 中主要有三处更新,分别是:
• 经由历程 'webpack-meger' 兼并了 `base.js`
• 把 `output` 属性移到了 `base.js` 中。我们只需体贴在 `base.js` 中之外的差别的设置就能够了
• 经由历程 `DefinePlugin` 把 `process.env.NODE_ENV` 设置为 `prod`. 如许,全部运用代码中的 `process.env.NODE_ENV` 都有一个为 `prod` 的值了。
哪些须要在差别的环境变量下坚持一致都由你来决议。这里只是经由历程一个 DEMO 来典范的申明一下怎样在差别的环境变量下坚持部份设置的一致。
能够看到,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 去处置惩罚 modules
公然门路?(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)
另一种体式格局是在开辟历程成设置 public 门路。Webpack 暴露了一个全局变量 __webpack_public_path__
来让我们到达这个目标。所以在你的进口文件中,你能够如许做:
__webpack_publick_path__ = process.en.ASSET_PATH;
怎样来做都取决于你。当我们经由历程 DefinePlugin
举行了设置今后, process.env.ASSET_PATH
在任何地方都能够直接拿来运用。