渣翻译,原文链接
构建Kanban
到目前为至,我们已经有了一个运行良好的Kanban应用, 本章的目地是在此基础上设置不错的产品级的版本.使用各种技术来控制打包后的大小并加入浏览器端缓存.
优化构建大小
我们运行npm run build
,能看到下面的结果|:
> webpack
Hash: 807faffbf966eb7f08fc
Version: webpack 1.12.13
Time: 3967ms
Asset Size Chunks Chunk Names
bundle.js 1.12 MB 0 [emitted] app
+ 337 hidden modules
1.12MB的打包大小是很大的!但我们能通过几种手段减少打包后的体积,我们可以压缩我们的脚本,也能让React优化它自身.同样也可以使用gzip进行内容压缩.
压缩
压缩可以转化我们的代码到一个较小的格式而不会失去原本的功能.通常是使用一些预置的代码进行格式转换.有时这样做可能会在不经意间破坏代码,这就是为什么我们在store中明确的指定id的原因了.
最简单的压缩方式是调用webpack -p
(-p
为 production
).其它方式,为了获得更多的控制我们可以直接使用插件. 默认的Uglify将输出大量的警告并且在这个情况不会提供有用的东西,我们将禁用它.增加下面的部分到我们的Webpack的配置:
webpack.config.js
if(TARGET === 'build') {
leanpub-start-delete
module.exports = merge(common, {});
leanpub-end-delete
leanpub-start-insert
module.exports = merge(common, {
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
});
leanpub-end-insert
}
当我们现在执行npm run build
会看到下面的信息:
> webpack
Hash: ff80bbb1bdd7df271313
Version: webpack 1.12.13
Time: 9159ms
Asset Size Chunks Chunk Names
bundle.js 368 kB 0 [emitted] app
+ 337 hidden modules
考虑到使用插件后会增加工作,它需要较长的时间.但从好的方面说构建后的包要小的多.
设置process.env.NODE_ENV
我们能进一步的压缩构建的体积.React依赖process.env.NODE_ENV
进行优化.如果我们设置React到production
, React将以优化了的方式进行构建, 这样做会禁用一些检查(如,属性类型检查).最重要的是它可以减小包的体积并提升性能.
在Webpack配置中,可以增加下面代码到我们的插件部分:
webpack.config.js
...
if(TARGET === 'build') {
module.exports = merge(common, {
plugins: [
leanpub-start-insert
// Setting DefinePlugin affects React library size!
// DefinePlugin replaces content "as is" so we need some extra quotes
// for the generated code to make sense
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
// You can set this to JSON.stringify('development') for your
// development target to force NODE_ENV to development mode
// no matter what
}),
leanpub-end-insert
...
]
});
}
这样做很有用,如果在执行时我们有一部分代码被判定为false,这个压缩后的构建将移除这一部分.
再次运行npm run build
,你能看到被优化的结果:
> webpack
Hash: cde2c1861fbd65f03c3b
Version: webpack 1.12.13
Time: 9032ms
Asset Size Chunks Chunk Names
bundle.js 307 kB 0 [emitted] app
+ 333 hidden modules
我们的构建的包从最初的1.12MB到368KB,最后成为307KB.如果我们在用gzip压缩它.体积大概还会减少40%.并且gzip被浏览器很好的支持.
我们还能稍稍的改善一下.我们可以分离app
和vendor
包,并且为他们增加的文件名增加hash值.
分离app
和vendor
包
分离我们的应用到两个特定的包的好处是可以让我们更好的用客户端缓存.例如.我们可以只需要频繁的修改app
包.在本例中,假设vendor
包已经被加载了,客户端将只获取app
包.
这个方案不会和加载单独的包一样快,因为他有一个额外的请求.感谢客户端缓存技术,我们可以不需要在每次请求时都重载所有数据.如果有某些包没有改变,将不会重新加载.本例中app
被修改后只会下载这个包.
定义一个vendor
入口点
当开发这个应用的时候,我们会分离我们的dependencies
和devDependencies
.这样做会变得很方便.它让我们只把dependencies
加入打包.这样可以避免我们把开发环境相关的包(如Webpack)加到最终的构建中.
如果你打开package.json,dependencies
列表会包含:alt, alt-container, alt-utils, node-uuid, react, react-addons-update, react-dnd, react-dnd-html5-backend, 和 react-dom如果你还有其它的包则需要将它们移到devDependencies
中.
我们还需要定义一个vendor
入口点.考虑到alt-utils在执行时会有一些问题,我们可以简单的从vendor
包中把它排除出去.你可以用相似的手段处理其它可能有问题的依赖.
webpack.config.js
const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
const NpmInstallPlugin = require('npm-install-webpack-plugin');
leanpub-start-insert
// Load *package.json* so we can use `dependencies` from there
const pkg = require('./package.json');
leanpub-end-insert
const TARGET = process.env.npm_lifecycle_event;
const PATHS = {
app: path.join(__dirname, 'app'),
build: path.join(__dirname, 'build')
};
process.env.BABEL_ENV = TARGET;
const common = {
entry: {
app: PATHS.app
},
resolve: {
extensions: ['', '.js', '.jsx']
},
output: {
path: PATHS.build,
leanpub-start-delete
filename: 'bundle.js'
leanpub-end-delete
leanpub-start-insert
// Output using entry name
filename: '[name].js'
leanpub-end-insert
},
...
};
if(TARGET === 'build') {
module.exports = merge(common, {
leanpub-start-insert
// Define vendor entry point needed for splitting
entry: {
vendor: Object.keys(pkg.dependencies).filter(function(v) {
// Exclude alt-utils as it won't work with this setup
// due to the way the package has been designed
// (no package.json main).
return v !== 'alt-utils';
})
},
leanpub-end-insert
plugins: [
...
]
});
}
这告诉Webpack我们想从entry中分离出vendor
级的依赖.
除此之外,可以使用动态加载require.ensure
来达到目地.
在此执行构建脚本npm run build
,经过一段时间会看到下面的结果:
> webpack
Hash: 192a0643b9245a61a6e0
Version: webpack 1.12.13
Time: 14745ms
Asset Size Chunks Chunk Names
app.js 307 kB 0 [emitted] app
vendor.js 286 kB 1 [emitted] vendor
[0] multi vendor 112 bytes {1} [built]
+ 333 hidden modules
现在我们分离出了app
和vendor
包.如果你检查这些文件,你会发现一些问题,app.js包含vendor的所有依赖.我们需要让Webpack避免这种情况.这就需要设置CommonsChunkPlugin
了
设置CommonsChunkPlugin
CommonsChunkPlugin
让我们可以提取我们vendor
中所需的代码.此外我们使用它提取一个manifest,这个文件让Webpack知道怎么映射哪个模块使用哪个文件.我们下一步需要基于此来建立一个长期的缓存.设置如下:
webpack.config.js
...
if(TARGET === 'build') {
module.exports = merge(common, {
// Define vendor entry point needed for splitting
entry: {
...
},
plugins: [
leanpub-start-insert
// Extract vendor and manifest files
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest']
}),
leanpub-end-insert
...
]
});
}
运行npm run build
会看到下面的输出:
> webpack
Hash: 3a08642b633ebeafa62f
Version: webpack 1.12.13
Time: 11044ms
Asset Size Chunks Chunk Names
app.js 21.3 kB 0, 2 [emitted] app
vendor.js 286 kB 1, 2 [emitted] vendor
manifest.js 743 bytes 2 [emitted] manifest
[0] multi vendor 112 bytes {1} [built]
+ 333 hidden modules
现在可以看到app包对比vendor包要小的多.这不是分开的目地,我们最终要在客户端缓存它们.要达到这个目地,我们需要在文件名中加入一个hash值.
增加hash值到文件名
Webpack提供占位符来让我们使用不同类型的hash值.最有用的是:
[name]
- 反回入口名称[hash]
- 反回创建的hash[chunkhash]
- 反回一段特定的hash
使用这个占位符你可以得到文件名如:
app.d587bbd6e38337f5accd.js
vendor.dc746a5db4ed650296e1.js
如果文件内容是不同的,hash也将随之改变,这就会清理客户端对它的缓存,更准确的说是浏览器将会发送一个新的请求.意味着如果只有app
发生改变,则只需要再次请求这个文件.
我们能使用占位符像如下配置:
webpack.config.js
if(TARGET === 'build') {
module.exports = merge(common, {
// Define vendor entry point needed for splitting
entry: {
...
},
leanpub-start-insert
output: {
path: PATHS.build,
filename: '[name].[chunkhash].js',
chunkFilename: '[chunkhash].js'
},
leanpub-end-insert
plugins: [
...
]
});
}
现在你运行npm run build
,会看到如下结果:
> webpack
Hash: 7ddb226a34540aa401bc
Version: webpack 1.12.13
Time: 8741ms
Asset Size Chunks Chunk Names
app.5b758fea66f30faf0f0e.js 21.3 kB 0, 2 [emitted] app
vendor.db9a3343cf47e4b3d83c.js 286 kB 1, 2 [emitted] vendor
manifest.19a8a5985bb61f546ce3.js 763 bytes 2 [emitted] manifest
[0] multi vendor 112 bytes {1} [built]
+ 333 hidden modules
现在我们的文件增加了hash值,为了验证它是工作的,你可以尝试改变app/index.jsx并写一个console.log,之后再次运行build,应该会看到只有app
和manifest
相关的包是被改变的.
其它方式改善构建的方式是通过CDN的方式加载一些流行的依赖,如React.这样会进一步减少vendor
包的体积.意味着如果用户使用早期的CDN,缓存同样生效.
通过html-webpack-plugin生成index.html
虽然现在我们有了一个不错的包,但还是有一个问题.我们当前的index.html
不能正确的指向构建的包.我们必需手动添加脚本标签,幸运的是有一个较好的方式.通过Node.js API触发Webpack.构建输出它的API包含了这些资源名子.之后把它们加入模板中.
其它好的方案是使用Webpack的插件和为此目地设计的模板.html-webpack-plugin
和html-webpack-template
一起为我们提供帮助并可以为我们执行大量繁重的工作.首先安装它们:
npm i html-webpack-plugin html-webpack-template --save-dev
为了加入它到我们的工程,我们需要调整我们的配置.在些期间,我们可以移除build/index.html
系统会自动生成它:
webpack.config.js
...
leanpub-start-insert
const HtmlWebpackPlugin = require('html-webpack-plugin');
leanpub-end-insert
...
const common = {
...
module: {
...
leanpub-start-delete
}
leanpub-end-delete
leanpub-start-insert
},
plugins: [
new HtmlWebpackPlugin({
template: 'node_modules/html-webpack-template/index.ejs',
title: 'Kanban app',
appMountId: 'app',
inject: false
})
]
leanpub-end-insert
};
if(TARGET === 'start' || !TARGET) {
module.exports = merge(common, {
devtool: 'eval-source-map',
devServer: {
leanpub-start-delete
contentBase: PATHS.build,
leanpub-end-delete
...
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new NpmInstallPlugin({
save: true // --save
})
]
});
}
...
现在运行npm run build
,输出的文件中会包含一个index.html:
> webpack
Hash: 7ddb226a34540aa401bc
Version: webpack 1.12.13
Time: 9200ms
Asset Size Chunks Chunk Names
app.5b758fea66f30faf0f0e.js 21.3 kB 0, 2 [emitted] app
vendor.db9a3343cf47e4b3d83c.js 286 kB 1, 2 [emitted] vendor
manifest.19a8a5985bb61f546ce3.js 763 bytes 2 [emitted] manifest
index.html 648 bytes [emitted]
[0] multi vendor 112 bytes {1} [built]
+ 333 hidden modules
Child html-webpack-plugin for "index.html":
+ 3 hidden modules
尽管在我们的项目中添加了一些配置,我们现在不必担心整合的事情。如果需要更多的灵活性,也可以实现一个自定义的模板。
清理构建
当前的配置没有清理构建目录的功能.我们可以增加这个插件清理这个目录.安装并且如下配置它:
npm i clean-webpack-plugin --save-dev
webpack.config.js
...
leanpub-start-insert
const CleanPlugin = require('clean-webpack-plugin');
leanpub-end-insert
...
if(TARGET === 'build') {
module.exports = merge(common, {
...
plugins: [
leanpub-start-insert
new CleanPlugin([PATHS.build]),
leanpub-end-insert
...
]
});
}
After this change, our build directory should remain nice and tidy when building. See clean-webpack-plugin for further options.
修改后,build
目录能保持干净,其它选项可以看clean-webpack-plugin
分离CSS
我们现在已经有了一个好很的构建配置,但要怎么处理CSS呢?根据我们的配置,它被内联到JavaScript中!虽然在开发过程中能带来方便.但它不是一个好主意.因为这样做不利于我们缓存这些CSS.在一些情况下可能会出现文档使样式短暂失效(FOUC)的问题.
Webpack提供一个分CSS包的方法.可以使用ExtractTextPlugin.它运行在编译期,并且在HMR(热插拔)中不好使.因为我们只是用它来发布打包.所以是没问题的.
需要一些配置才能使它工作:
npm i extract-text-webpack-plugin --save-dev
下一步,还需要移除我们在common
中关于CSS的相关声明,之后需要分离build
和dev
配置如下:
webpack.config.js
...
leanpub-start-insert
const ExtractTextPlugin = require('extract-text-webpack-plugin');
leanpub-end-insert
...
const common = {
entry: {
app: PATHS.app
},
resolve: {
extensions: ['', '.js', '.jsx']
},
module: {
loaders: [
leanpub-start-delete
{
// Test expects a RegExp! Note the slashes!
test: /\.css$/,
loaders: ['style', 'css'],
// Include accepts either a path or an array of paths.
include: PATHS.app
},
leanpub-end-delete
{
test: /\.jsx?$/,
loaders: ['babel?cacheDirectory'],
include: PATHS.app
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'node_modules/html-webpack-template/index.html',
title: 'Kanban app',
appMountId: 'app',
inject: false
})
]
};
if(TARGET === 'start' || !TARGET) {
module.exports = merge(common, {
devtool: 'eval-source-map',
devServer: {
...
},
leanpub-start-insert
module: {
loaders: [
// Define development specific CSS setup
{
test: /\.css$/,
loaders: ['style', 'css'],
include: PATHS.app
}
]
},
leanpub-end-insert
plugins: [
...
]
});
}
if(TARGET === 'build') {
module.exports = merge(common, {
...
output: {
...
},
leanpub-start-insert
module: {
loaders: [
// Extract CSS during build
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style', 'css'),
include: PATHS.app
}
]
},
leanpub-end-insert
plugins: [
new CleanPlugin([PATHS.build], {
verbose: false // Don't write logs to console
}),
leanpub-start-insert
// Output extracted CSS to a file
new ExtractTextPlugin('[name].[chunkhash].css'),
leanpub-end-insert
...
]
});
}
之后运行npm run build
会看到如下信息:
> webpack
clean-webpack-plugin: .../kanban_app/build has been removed.
Hash: 32b800b64e76fbd5d447
Version: webpack 1.12.13
Time: 9125ms
Asset Size Chunks Chunk Names
app.341186f43191211bee0c.js 16.5 kB 0, 2 [emitted] app
vendor.db9a3343cf47e4b3d83c.js 286 kB 1, 2 [emitted] vendor
manifest.c3a4ec998b7ccca9268f.js 763 bytes 2 [emitted] manifest
app.341186f43191211bee0c.css 1.16 kB 0, 2 [emitted] app
index.html 713 bytes [emitted]
[0] multi vendor 112 bytes {1} [built]
+ 333 hidden modules
Child html-webpack-plugin for "index.html":
+ 3 hidden modules
Child extract-text-webpack-plugin:
+ 2 hidden modules
现在我们分离出了CSS文件.并且JavaScript包又小了一些.我们还能避免FOUC问题.浏览器不需要等JavaScript加载样式信息.反而它能单独处理CSS从而避免文档样式短暂失效(FOUC)的问题.
改善缓存行为
这是一个小问题,注意到生成的app.js和app.css的文件名区块相同.这意味着如果JavaScript和CSS改变相关的内容,这个hash值同样会改变.这不是我们想要的,因为它会使我们的缓存失效.即使我们不想这么做.
一种解决方式是把样式加到它自已的区块中.我们需要从当前的区块中解耦出样式,并定义它为自定义的区块通过如下配置:
app/index.jsx
leanpub-start-delete
import './main.css';
leanpub-end-delete
...
webpack.config.js
...
const PATHS = {
app: path.join(__dirname, 'app'),
leanpub-start-delete
build: path.join(__dirname, 'build')
leanpub-end-delete
leanpub-start-insert
build: path.join(__dirname, 'build'),
style: path.join(__dirname, 'app/main.css')
leanpub-end-insert
};
...
const common = {
entry: {
leanpub-start-delete
app: PATHS.app
leanpub-end-delete
leanpub-start-insert
app: PATHS.app,
style: PATHS.style
leanpub-end-insert
},
...
}
...
现在运行npm run build
会看到如下信息:
Hash: 22e7d6f1b15400035cbb
Version: webpack 1.12.13
Time: 9271ms
Asset Size Chunks Chunk Names
app.5d41daf72705cb65cd89.js 16.4 kB 0, 3 [emitted] app
style.0688e2aa1fa6c618dcdd.js 38 bytes 1, 3 [emitted] style
vendor.ec174332c803122d2dba.js 286 kB 2, 3 [emitted] vendor
manifest.035b449d16a98df2cb4f.js 788 bytes 3 [emitted] manifest
style.0688e2aa1fa6c618dcdd.css 1.16 kB 1, 3 [emitted] style
index.html 770 bytes [emitted]
[0] multi vendor 112 bytes {2} [built]
+ 333 hidden modules
Child html-webpack-plugin for "index.html":
+ 3 hidden modules
Child extract-text-webpack-plugin:
+ 2 hidden modules
这一步后,我们已经成功地从JavaScript分离出样式。更改它不应该影响JavaScript区块hash值,反之亦然.如果你仔细观察会发现一个文件名为style.64acd61995c3afbc43f1.js,它是Webpack生成的看起来如下:
webpackJsonp([1,3],[function(n,c){}]);
从技术上讲它是多余的。它可以在 HtmlWebpackPlugin 模板文件装载时被排除。但这种解决方案是不够好的。理想情况下 Webpack 根本不应该生成这些文件。
生成统计分析
生成统计分析是理解 Webpack 好的方式.看以看到我们包的构成.
为了得到结果我们需要调整一下我们的配置:
package.json
{
...
"scripts": {
leanpub-start-insert
"stats": "webpack --profile --json > stats.json",
leanpub-end-insert
...
},
...
}
现在执行npm run stats
可以在你的工程根止录中看到* stats.json * ,我们能传递它到一个在线工具中查看更多结果.并可帮助你进一步优化输出.
部署
npm run build
提供了一些静态的托管.一个巧妙的方法是把它放到GitHub的页面上。
托管在 GitHub 上
我们可以使用gh-pages
达到这个目地.首先把它指向到你的构建目录,之后把内容推到gh-pages
分支,让我们执行
npm i gh-pages --save-dev
还需要一个入口点在package.json:
package.json
{
...
"scripts": {
leanpub-start-insert
"deploy": "gh-pages -d build",
leanpub-end-insert
...
},
...
}
如果你执行npm run deploy
并且没出现问题,你能把应用托管到GitHub页面.通过https://<name>.github.io/<project>
(github.com// 到GitHub)
总结
略.