本文转载自:众成翻译
译者:Hugo
链接:http://www.zcfy.cc/article/921
原文:https://blog.madewithlove.be/post/webpack-your-bags/
也许你已经听说过这个叫做webpack非常酷的新工具。如果你没深入的了解过它你可能会感到困惑,因为有些人称它为类似Gulp的构建工具,还有人称它为类似Browserify的打包工具。如果你已经深入了解过它,你也可能因为主页作为Webpack呈现而感到困惑。
老实说,最初“what Webpack is”这个话题吓得我关掉了标签页。毕竟我已经有了一个构建系统,而且非常满意。如果你像我一样紧随Javascript的快速发展,你可能已经因为跟随潮流太紧受到伤害。经验更多的我觉得应该写这样的一篇文章,为大家解释一下webpack到底是什么,更重要的是,到底webpack有什么了不起的地方能得到这么多的关注。
现在让我们回答一下引文中提到的问题:webpack是一个构建系统或者模块打包工具吗?好吧,都是,但我的意思不是它会把两件事情都做,而是它会把两者结合起来。Webpack不会创建你的资源,然后分别打包你的模块,它是在考虑如何让你的资源本身自动模块化。
准确的说例如构建你所有的sass文件,将图片优化并放在一起,然后打包你的模块,之后在页面上引用,这些事情Webpack不会做,作为替代,你的代码是这样的:
import stylesheet from 'styles/my-styles.scss';
import logo from 'img/my-logo.svg';
import someTemplate from 'html/some-template.html';
console.log(stylesheet); // "body{font-size:12px}"
console.log(logo); // "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]"
console.log(someTemplate) // "Hello
"
你的每一份资源都被看做单独的模块,之后能被导入,修改,操作,然后能被打入最终的包中。
为了让上面的代码能够工作,你要在Webpack配置文件中注册loaders。Loaders是一些小插件,基础功能是遇到这种类型文件时,使用这个插件。下面是一些loaders的例子:
{
// When you import a .ts file, parse it with Typescript
test: /\.ts/,
loader: 'typescript',
},
{
// When you encounter images, compress them with image-webpack (wrapper around imagemin)
// and then inline them as data64 URLs
test: /\.(png|jpg|svg)/,
loaders: ['url', 'image-webpack'],
},
{
// When you encounter SCSS files, parse them with node-sass, then pass autoprefixer on them
// then return the results as a string of CSS
test: /\.scss/,
loaders: ['css', 'autoprefixer', 'sass'],
}
最后所有的loaders返回的结果都是字符串。Webpack能够将它们包裹进Javascript模块中。只要你的sass文件被loaders转换过,内部就应该类似这样:
`export default 'body{font-size:12px}';`
一旦你理解了Webpack做了什么,很有可能第二个问题就会在头脑中出现:这样做有什么好处?“图片和CSS?在我的JS里边?弄啥来!?”。这样考虑一下:很长一段时间,我们学到的是将所有的东西连结在一个单独的文件里;为了节省我们的HTTP请求,yada yada.
这导致了一个非常大的问题,现在的开发者都将所有的资源打包到一个‘app.js’文件中,之后在所有的页面中引用这个文件。这意味着任何页面的加载过程中都浪费了很多时间去加载大量不需要的资源。如果你不这样做,你就很可能会在特定页面上手动去引用资源,这就会生成一个大而混乱的依赖树要去维持和跟踪:哪个页面上已经有了这个依赖?样式表A和B在作用在哪些页面上?
没有哪种处理方式是绝对正确或错误的。将Webpack看成是两面兼顾的-不仅仅是一个构建或打包工具,更是一个了不起的,聪明的模块打包系统。一旦正确配置,它会比你更了解你的工作栈,更明白如何去优化。
为了更容易的让你理解Webpack的好处,我们会创建一个非常小的应用,然后打包它的资源。这个教程中我建议运行node4(或5)和NPM3去和Webpack一起工作,这会省去很多麻烦。如果你还没安装NPM3,你可以通过运行npm install npm@3 -g
来安装它。
$ node --version
v5.7.1
$ npm --version
3.6.0
我也建议你在PATH变量里添加node_modules/.bin
,省去了每次都要指定node_modules/.bin/webpack
。之后所有例子中我运行的所有命令都不会出现node_modules/.bin
。
让我们开始创建我们的项目,安装Webpack,之后我们也会引入jQuery论证一些东西。
$ npm init -y
$ npm install jquery --save
$ npm install webpack --save-dev
现在创建应用入口,现在使用ES5:
src/index.js
var $ = require('jquery');
$('body').html('Hello');
在名为webpack.config.js
的文件里配置Webpack。Webpack的配置就是Javascript,需要作为一个对象被导出:
webpack.config.js
module.exports = {
entry: './src',
output: {
path: 'builds',
filename: 'bundle.js',
},
};
这里,entry
告诉Webpack那个文件是应用的入口。这些都是主文件,在你的依赖树的顶端。然后我们告诉它将我们的包编译到builds
文件夹下的bundle.js
里。之后将我们的index HTML写成这样:
<html>
<body>
<h1>My titleh1>
<a>Click mea>
<script src="builds/bundle.js">script>
body>
html>
我们运行Webpack,如果一切正常那我们就应该会得到一条信息,告诉我们它正确地编译了我们的bundle.js
。
$ webpack
Hash: d41fc61f5b9d72c13744
Version: webpack 1.12.14
Time: 301ms
Asset Size Chunks Chunk Names
bundle.js 268 kB 0 [emitted] main
[0] ./src/index.js 53 bytes {0} [built]
+ 1 hidden modules
这里你可以看到Webpack告诉你,你的bundle.js
包含了我们的入口文件和一个隐藏模块,隐藏模块是jQuery,Webpack会将不属于你的模块隐藏掉。如果想查看Webpack编译的所有模块,可以加入--display-modules
标记:
$ webpack --display-modules
bundle.js 268 kB 0 [emitted] main
[0] ./src/index.js 53 bytes {0} [built]
[1] ./~/jquery/dist/jquery.js 259 kB {0} [built]
你也可以运行webpack --watch
监控文件的修改然后根据需要去自动重新编译。
现在还记得我们是怎样讨论关于Webpack能够去导入CSS和HTML以及其他多种类型的吗?如果你一直关注着这些年Web组件的大变动(Angular 2, Vue, React, Polymer, X-Tag, etc.),你可能已经听说了这个概念,你的应用不再是一个个相互连通的UI,取而代之的是可维护的,独立的,可复用的UI:web组件(我这里简化说明,你应该懂)。现在为了让组件真正的独立化,它们需要将自己的需求和自己一起打包。想象一个按钮组件:它肯定有一些HTML,然后一些JS实现交互,可能还有一些样式。如果这些东西只在我们需要的时候加载,感觉会很好,是不是?只有当我们引入Button组件时,我们才会得到相关资源。
下面来写我们的button;首先,我假设你们中大多数已经习惯了ES2015,添加第一个loader:Babel。在Webpack中安装一个loader你需要做两件事:npm install {whatever}-loader
,之后把它添加到Webpack配置中的module.loaders
部分。这里我们想安装babel,所以:
`$ npm install babel-loader --save-dev`
我们也需要安装Babel,因为loader不会安装它。我们需要babel-core
这个包还有es2015
preset:
`$ npm install babel-core babel-preset-es2015 --save-dev`
之后我们要创建一个.babelrc
文件去告诉Babel去使用那个preset。这是一个简单的JSON文件,允许你设置什么Babel转换器会运行在你的代码上,在我们的例子里我们告诉它使用es2015
preset。
.babelrc
{
“presets”: [“es2015”]
}
现在Babel安装和配置好了,我们可以去更新我们的配置:我们想要什么?我们想让Babel去运行所有后缀为.js
的文件,但是我们不想让Babel运行在jQuery代码上,我们可以过滤掉它。Loader可以同时有include
和exclude
规则。可以是一个字符串,正则,或者回调函数,任何你想要的。在这个例子里,我们想让Babel只运行在我们自己的文件上,所以我们只include
我们自己资源的目录:
module.exports = {
entry: './src',
output: {
path: 'builds',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
}
],
}
};
在导入Babel之后,我们可以用ES6重写index.js
。之后所有例子都会用ES6来写。
import $ from 'jquery';
$('body').html('Hello');
现在我们来写一个Button组件,其中会有一些SCSS样式,一个HTML模板,和一些行为。所以我们根据需求安装一些东西。首先是Mustache,这是一个轻量的模板包,我们也需要一些转换Sass和HTML文件的loaders。同时,我们也需要一个CSS loader处理从Sass loader传出的结果。现在,一旦我们有了自己的CSS,就有多种方式去处理,暂时的我们使用一个叫style-loader
的loader,它会把一小段CSS动态注入到页面中。
$ npm install mustache --save
$ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev
现在为了告诉Webpack将代码从一个loader传到另一个loader,我们写了一串loaders,从右到左,用一个!
分隔。你也可以将一个数组赋给loaders
属性来代替loader
:
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.scss/,
loader: 'style!css!sass',
// Or
loaders: ['style', 'css', 'sass'],
},
{
test: /\.html/,
loader: 'html',
}
现在我们有了loaders,来写我们的button:
src/Components/Button.scss
.button {
background: tomato;
color: white;
}
src/Components/Button.html
`<a class="button" href="{{link}}">{{text}}a>`
src/Components/Button.js
import $ from 'jquery';
import template from './Button.html';
import Mustache from 'mustache';
import './Button.scss';
export default class Button {
constructor(link) {
this.link = link;
}
onClick(event) {
event.preventDefault();
alert(this.link);
}
render(node) {
const text = $(node).text();
// Render our button
$(node).html(
Mustache.render(template, {text})
);
// Attach our listeners
$('.button').click(this.onClick.bind(this));
}
}
你的Button.js
已经是100%独立的,无论在什么时候,什么地方被引用,都会被正确渲染。现在我们只需要在页面上渲染Button:
src/index.js
import Button from ‘./Components/Button’;
const button = new Button(‘google.com’);
button.render(‘a’);
我们试着运行Webpack然后刷新页面,你应该能看到我们的button已经起作用了。
你已经学到了如何安装loaders和如何去定义应用各部分之间的依赖关系。现在这看起来可能影响不是很大,但是让我们继续吧。
这个例子很好,但是也许我们并不是总会用到button。也许在一些页面上没有一个a
标签需要去渲染一个button,在这些例子里,我们不想去导入所有的Button样式,模板,Mustache以及所有东西,对吗?这时候就要用到代码分割了。代码分割是Webpack对“整块包”VS“不可维护的手工导入”问题的回应。这是由你在代码里定义的,“分割点”:代码中能被分割成单独文件的地方,之后能根据需求被引用。语法非常简单:
import $ from 'jquery';
// This is a split point
require.ensure([], () => {
// All the code in here, and everything that is imported
// will be in a separate file
const library = require('some-big-library');
$('foo').click(() => library.doSomething());
});
在require.ensure
回调函数中的内容都会被分割到一个chunk中 - 一个只有在我们需要时Webpack才会通过AJAX请求装载的包。这意味着我们基本上会有这个:
bundle.js
|- jquery.js
|- index.js // our main file
chunk1.js
|- some-big-libray.js
|- index-chunk.js // the code in the callback
你不用必须到处导入chunk1.js
。Webpack会根据需要导入它。这意味着你可以用各种逻辑将你的代码包裹成chunks,这也是我们接下来要做的。只有当我们的页面有一个链接时我们才需要Button组件:
src/index.js
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
const Button = require('./Components/Button').default;
const button = new Button('google.com');
button.render('a');
});
}
提醒一点,在使用require
的时候如果你想得到的是默认导出的内容,那你就需要手动的通过.default
抓取它。原因是require
不会去处理默认导出还是普通导出,所以你必须去指定返回值。然而import
会去处理这些事情。(例如. import foo from 'bar')
vs import {baz} from 'bar'
).
相应的,Webpack的输出应该不同了。我们通过--display-chunks
命令去运行它,看一下模块和chunks的对应关系。
$ webpack --display-modules --display-chunks
Hash: 43b51e6cec5eb6572608
Version: webpack 1.12.14
Time: 1185ms
Asset Size Chunks Chunk Names
bundle.js 3.82 kB 0 [emitted] main
1.bundle.js 300 kB 1 [emitted]
chunk {0} bundle.js (main) 235 bytes [rendered]
[0] ./src/index.js 235 bytes {0} [built]
chunk {1} 1.bundle.js 290 kB {0} [rendered]
[1] ./src/Components/Button.js 1.94 kB {1} [built]
[2] ./~/jquery/dist/jquery.js 259 kB {1} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[4] ./~/mustache/mustache.js 19.4 kB {1} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {1} [built]
你可以看到,我们的入口文件(bundle.js
)现在只包含了一些Webpack逻辑,其他的东西(jQuery,Mustache,Button)在1.bundle.js
这个chunk中,并且只有被页面引用时才会加载。现在为了让Webpack知道通过AJAX加载它们的时候去哪里找到对应的chunks,我们必须在配置文件中加入一小行代码:
path: 'builds',
filename: 'bundle.js',
publicPath: 'builds/',
output.publicPath
配置项告诉Webpack去哪里可以找到页面上引用到的构建资源(我们这里就是/builds/)。如果现在访问我们的页面,就会发现一切都在正常运行,更重要的是我们会看到,一旦我们在页面上引用模块,Webpack会正确的加载我们的chunk:
如果我们没有在页面上引用模块,Webpack只会加载bundle.js
。这能让你聪明的将应用中的厚重逻辑分解开,让每一个页面只加载它真正需要的。提醒一下,我们也可以给我们的分割点命名,用更有意义的chunk名字去代替1.bundle.js
。你可以向require.ensure
中传入第三个参数实现命名:
require.ensure([], () => {
const Button = require('./Components/Button').default;
const button = new Button('google.com');
button.render('a');
}, 'button');
代替1.bundle.js
生成的是button.bundle.js
。
现在已经非常好了,但是让我们来添加第二个组件看一下是否正常工作:
src/Components/Header.scss
.header {
font-size: 3rem;
}
src/Components/Header.html
`<header class="header">{{text}}header>`
src/Components/Header.js
import $ from 'jquery';
import Mustache from 'mustache';
import template from './Header.html';
import './Header.scss';
export default class Header {
render(node) {
const text = $(node).text();
$(node).html(
Mustache.render(template, {text})
);
}
}
在我们的应用中渲染它:
// If we have an anchor, render the Button component on it
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
const Button = require('./Components/Button');
const button = new Button('google.com');
button.render('a');
});
}
// If we have a title, render the Header component on it
if (document.querySelectorAll('h1').length) {
require.ensure([], () => {
const Header = require('./Components/Header');
new Header().render('h1');
});
}
现在用--display-chunks --display-modules
指令查看一下Webpack的结果:
$ webpack --display-modules --display-chunks
Hash: 178b46d1d1570ff8bceb
Version: webpack 1.12.14
Time: 1548ms
Asset Size Chunks Chunk Names
bundle.js 4.16 kB 0 [emitted] main
1.bundle.js 300 kB 1 [emitted]
2.bundle.js 299 kB 2 [emitted]
chunk {0} bundle.js (main) 550 bytes [rendered]
[0] ./src/index.js 550 bytes {0} [built]
chunk {1} 1.bundle.js 290 kB {0} [rendered]
[1] ./src/Components/Button.js 1.94 kB {1} [built]
[2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
chunk {2} 2.bundle.js 290 kB {0} [rendered]
[2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
[4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
[9] ./src/Components/Header.js 1.62 kB {2} [built]
[10] ./src/Components/Header.html 64 bytes {2} [built]
[11] ./src/Components/Header.scss 1.05 kB {2} [built]
[12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
你会发现一个争议点:我们的组件都需要jQuery和Mustache,这意味着这些依赖在我们的chunks中重复了,这并不是我们想要的。Webpack默认会做很小的优化。但是它内置了很多能量去帮助你扭转这种情况,以plugins的形式。
Plugins相对于loaders的不同之处是,它不是只运行在某种特定文件里,而是起到了类似于管道的作用,它们运行在所有文件上并且表现出预设的行为,它们并不必然和转换相关。Webpack用一些插件来执行所有各种各样的优化。在这个例子中让我们感兴趣的是CommonChunksPlugin:它分析你的chunks的循环依赖关系,在其他的地方提取它们。它可以是一个完全独立的文件(像vendor.js
)或者是你的主文件。
在我们的例子中我们想把共同的依赖转移到入口文件,因为如果所有的页面都需要jQuery和Mustache,我们不妨把它移动。所以我们要更新配置项:
var webpack = require('webpack');
module.exports = {
entry: './src',
output: {
// ...
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'main', // 将依赖转移到主文件中
children: true, // 在所有子文件中寻找依赖项
minChunks: 2, // 一个依赖出现多少次会被抽取
}),
],
module: {
// ...
}
};
如果我们重新运行Webpack,可以看到它看起来好多了。这里的 main
是默认chunk的名字。
chunk {0} bundle.js (main) 287 kB [rendered]
[0] ./src/index.js 550 bytes {0} [built]
[2] ./~/jquery/dist/jquery.js 259 kB {0} [built]
[4] ./~/mustache/mustache.js 19.4 kB {0} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {0} [built]
chunk {1} 1.bundle.js 3.28 kB {0} [rendered]
[1] ./src/Components/Button.js 1.94 kB {1} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
chunk {2} 2.bundle.js 2.92 kB {0} [rendered]
[9] ./src/Components/Header.js 1.62 kB {2} [built]
[10] ./src/Components/Header.html 64 bytes {2} [built]
[11] ./src/Components/Header.scss 1.05 kB {2} [built]
[12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
如果我们制定name: 'vendor'
:
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
children: true,
minChunks: 2,
}),
只要这个chunk不存在,Webpack会创建一个builds/vendor.js
,之后我们就可以在HTML中手动引用它:
<script src="builds/vendor.js">script>
<script src="builds/bundle.js">script>
你也可以通过不去提供一个共同的chunk名字去异步加载共同依赖,而不是去指定async: true
.Webpack有很多这种强大,智能的优化。我不能一一列举,但作为练习,我们来试着为我们的应用创建一个生产版本。
好的第一步,我们在配置项中添加几个plugins,但是我们只想在NODE_ENV
是 production
的时候才加载它们,所以要在配置中添加一些逻辑。既然它是一个JS文件,很容易就可以做到了:
var webpack = require('webpack');
var production = process.env.NODE_ENV === 'production';
var plugins = [
new webpack.optimize.CommonsChunkPlugin({
name: 'main', // Move dependencies to our main file
children: true, // Look for common dependencies in all children,
minChunks: 2, // How many times a dependency must come up before being extracted
}),
];
if (production) {
plugins = plugins.concat([
// Production plugins go here
]);
}
module.exports = {
entry: './src',
output: {
path: 'builds',
filename: 'bundle.js',
publicPath: 'builds/',
},
plugins: plugins,
// ...
};
第二步,Webpack也有几项我们能够在生产环境中关闭的设置:
module.exports = {
debug: !production,
devtool: production ? false : 'eval',
第一个设置项切换loaders的debug
模式,这意味着在本地环境不会包含多余的代码让你去轻松地调试。第二个是关于sourcemaps的生成。Webpack有几种方式去渲染sourcemaps, eval
在本地环境中是最好的。在生产环境中我们并不关心sourcemaps,所以我们禁用它们。现在我们添加生产环境的plugins:
if (production) {
plugins = plugins.concat([
// This plugin looks for similar chunks and files
// and merges them for better caching by the user
new webpack.optimize.DedupePlugin(),
// This plugins optimizes chunks and modules by
// how much they are used in your app
new webpack.optimize.OccurenceOrderPlugin(),
// This plugin prevents Webpack from creating chunks
// that would be too small to be worth loading separately
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 51200, // ~50kb
}),
// This plugin minifies all the Javascript code of the final bundle
new webpack.optimize.UglifyJsPlugin({
mangle: true,
compress: {
warnings: false, // Suppress uglification warnings
},
}),
// This plugins defines various variables that we can set to false
// in production to avoid code related to them from being compiled
// in our final bundle
new webpack.DefinePlugin({
__SERVER__: !production,
__DEVELOPMENT__: !production,
__DEVTOOLS__: !production,
'process.env': {
BABEL_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
]);
}
这些是我最常用的,但Webpack提供了一些plugins供你去使用来调整你的模块和chunks。NPM上也有几个用户贡献的插件实现了各种功能。文章最后有可用plugins的链接。
理想情况下,你会想让你的生产资源有版本号。还记得我们设置output.filename
为 bundle.js
?在这个选项中有几种变量可供我们使用,其中一个是[hash]
,对应的是最终生成的bundle内容的hash,我们来修改一下代码。我们也想让我们的chunks版本化,所以我们添加output.chunkFilename
来做同样的事情:
output: {
path: 'builds',
filename: production ? '[name]-[hash].js' : 'bundle.js',
chunkFilename: '[name]-[chunkhash].js',
publicPath: 'builds/',
},
因为在这个简化的应用中,我们没有办法去动态检索已编译bundle的名字,所以在这个例子中,我们只会在生产环境中给资源设定版本。我们也想在生产环境建立之前去清理我们的builds目录(节省空间),所以让我们去安装一个第三方插件做这件事情:
`$ npm install clean-webpack-plugin --save-dev`
将它添加到配置项:
var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
// ...
if (production) {
plugins = plugins.concat([
// Cleanup the builds/ folder before
// compiling our final assets
new CleanPlugin('builds'),
好的,我们已经做了一些漂亮的优化,来对比一下结果:
$ webpack
bundle.js 314 kB 0 [emitted] main
1-21660ec268fe9de7776c.js 4.46 kB 1 [emitted]
2-fcc95abf34773e79afda.js 4.15 kB 2 [emitted]
$ NODE_ENV=production webpack
main-937cc23ccbf192c9edd6.js 97.2 kB 0 [emitted] main
Webpack做了什么:首先因为我们的实例非常轻量化,我们的两个异步chunks不值得单独使用HTTP请求,所以Webpack将它们合并到入口文件中了。第二点,每部分都被适当地压缩了。我们从三个三个HTTP请求322kb的资源,优化到了一个HTTP请求97kb的资源。
但Webpack的重点不是去掉了一个大JS文件吗?
是的,是这样的,但是这仅仅当我们的应用很小的时候会发生。现在考虑一下:你不必考虑什么时候,什么地方要去合并什么。如果你的chunks忽然添加了以来,chunk就会被异步加载而不是被合并;如果这些chunks太类似,不值得去单独加载,它们就会被合并,等等。你只需要制定规则,之后,Webpack会用最好的方式自动的优化你的应用。没有体力劳动,不用去考虑哪里添加了依赖,哪里需要依赖,所有的事情都自动化。
你也许注意到我并没有为了压缩HTML和CSS去安装什么,这是因为我们之前提到的debug
如果被设置为false
,css-loader
和html-loader
默认会为我们做这些事情。这也是为什么Uglify是一个单独的plugin : Webpack中没有js-loader
,因为它自己就是JS loader。
现在也许你已经注意到了,在教程的一开始我们的样式就被注入了页面中,这导致了FOUAP(丑陋的页面)。那如果我们现在把Webpack中的样式集中起来放到一个最终的CSS文件里,情况会不会变好呢?当然可以,我们需要借助一个外部plugin的帮助:
`$ npm install extract-text-webpack-plugin --save-dev`
这个plugin做的工作就是我刚刚说的:从最终生成的包中抽取特定类型的内容,传到其它地方,最常用的就是CSS。我们来设置它:
var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
var ExtractPlugin = require('extract-text-webpack-plugin');
var production = process.env.NODE_ENV === 'production';
var plugins = [
new ExtractPlugin('bundle.css'), // <=== where should content be piped
new webpack.optimize.CommonsChunkPlugin({
name: 'main', // Move dependencies to our main file
children: true, // Look for common dependencies in all children,
minChunks: 2, // How many times a dependency must come up before being extracted
}),
];
// ...
module.exports = {
// ...
plugins: plugins,
module: {
loaders: [
{
test: /\.scss/,
loader: ExtractPlugin.extract('style', 'css!sass'),
},
// ...
],
}
};
现在这个extract
方法有两个参数:一是当我们在一个chunk中时如何处理提取的内容('style'
),二是当我们在主文件中要做些什么('css!sass'
)。现在,如果我们在一个chunk中,我们不能魔法般的把CSS添加到已经生成的文件中,所以这里我们和之前一样用到了style
loader,对于在主文件中的所有样式,把它们全部放到一个builds/bundle.css
文件中。我们测试一下,为我们的应用添加一段主样式:
src/styles.scss
body {
font-family: sans-serif;
background: darken(white, 0.2);
}
src/index.js
import './styles.scss';
// Rest of our file
运行Webpack可以确认我们现在有了一个bundle.css
文件,我们可以在HTML中导入:
$ webpack
bundle.js 318 kB 0 [emitted] main
1-a110b2d7814eb963b0b5.js 4.43 kB 1 [emitted]
2-03eb25b4d6b52a50eb89.js 4.1 kB 2 [emitted]
bundle.css 59 bytes 0 [emitted] main
如果你也想把chunks中的样式提取,你可以传入ExtractTextPlugin('bundle.css', {allChunks: true})
。提示一下这里你也可以在文件名中使用变量,所以如果你想把样式表版本化你可以像Javascript文件那样传入ExtractTextPlugin('[name]-[hash].css')
。
现在我们的Javascript文件已经处理完毕,但是还有一个没有讨论过的主题就是静态资源:图片,字体,等等。在Webpack中这些是怎样工作的,我们怎样去优化它们呢?我们从网上找一张照片作为我们的页面背景,因为我看到有人在Geocities上这样做而且看起来很酷:
我们把图片保存为img/puppy.jpg
,对应的更新我们的Sass文件:
src/styles.scss
body {
font-family: sans-serif;
background: darken(white, 0.2);
background-image: url('../img/puppy.jpg');
background-size: cover;
}
现在如果你这样做了,Webpack会跟你说“我TM的怎么搞JPG啊”,因为我们没安装对应的loader。有两个loaders可以帮我们处理静态资源:file-loader
和url-loader
:
- 第一个不会做特殊处理,仅仅返回资源的URL,在这过程中允许你去给文件规定版本(这是默认行为)。
- 第二个会将资源内联到一个data:image/jpeg;base64
URL中
事实上两种方式没有绝对好坏之分:如果你的背景是一张2Mb的图片,你不会想内联它,单独的加载它是更好的方式。另一方面,如果是一个4Kb的小图标,内联是更好的方式,还可以节约HTTP请求,所以两个都安装:
`$ npm install url-loader file-loader --save-dev`
{
test: /\.(png|gif|jpe?g|svg)$/i,
loader: 'url?limit=10000',
},
这里,我们向url-loader
中传入了一个limit
查询参数来告诉它:如果资源大小小于10kb就内联它,其他的情况就是用file-loader
。该语法被称为查询字符串,使用它去配置loaders,或者你也可以通过一个对象来配置loaders:
{
test: /\.(png|gif|jpe?g|svg)$/i,
loader: 'url',
query: {
limit: 10000,
}
}
我们运行一下
bundle.js 15 kB 0 [emitted] main
1-b8256867498f4be01fd7.js 317 kB 1 [emitted]
2-e1bc215a6b91d55a09aa.js 317 kB 2 [emitted]
bundle.css 2.9 kB 0 [emitted] main
我们可以看到这里没提到JPG,因为图片小于我们配置的大小,它被内联了。这意味着如果访问页面,我们就会沐浴在狗大人的荣光之下。
这是非常强大的,因为这意味着Webpack现在能依据HTTP请求大小来智能的优化任何静态资源。有一些loaders能让你做进一步的优化,比如 image-loader会在打包图片之前把imagemin
传给它们。它甚至还有一个?bypassOnDebug
查询字符串,能让你只在生产环境中做这件事情。还有很多类似的plugins,我支持你去看一下文章末尾的列表。
现在我们的产品构建方式已经设定好,我们来专注于本地开发。你也许注意到在我们提到构建工具时经常出现一个问题:重载:LiveReload, BrowserSync,无论页面是什麽内容。让整个页面全部刷新是笨蛋做的事,让我们借用叫做 hot module replacement或者hot reload的工具来改善这种状况。思路是,既然Webpack清楚的知道每个模块在我们的依赖树中的位置,其中的改变应该表现为借助新文件简单地对树的一部分的修改。简单点说:你的改变在页面没有重载的情况下显示在了屏幕上。
为了实现HMR的使用,我们需要给资源建一个服务器。我们可以利用Webpack中的dev-server
实现,安装它:
`$ npm install webpack-dev-server --save-dev`
现在运行dev server,非常简单,只要运行下边的命令:
`$ webpack-dev-server --inline --hot`
第一个命令告诉Webpack在页面中包含HMR逻辑(代替在iframe中显示页面),第二个打开HMR。现在让我们在这个地址http://localhost:8080/webpack-dev-server/
访问web-server。你会看到你通常的页面,但是现在试着改一下其中的一个Sass文件,见证神奇的时刻:
你可以将webpack-dev-server当作本地服务器来使用。如果你计划一直用它来实现HMR,你可以这样设置:
output: {
path: 'builds',
filename: production ? '[name]-[hash].js' : 'bundle.js',
chunkFilename: '[name]-[chunkhash].js',
publicPath: 'builds/',
},
devServer: {
hot: true,
},
现在无论什么时候运行 webpack-dev-server
,它都会在HMR模式。提示一下,这里我们用webpack-dev-server
当作热替换资源的服务器,你也可以把它用作其他的用途,比如Express的服务器。Webpack提供了一个中间件让你能够在其他的服务器上实现HMR。
你过你仔细地看了这篇文化在那个你也许会注意到一些奇怪的东西:为什么loaders都放进 module.loaders
但plugins没有?当然这是因为还有其它的东西你可以放进module
!Webpack不仅仅有loaders,它还有pre-loaders和post-loaders:在主loaders之前或之后执行的loaders。举个例子:我确信这篇文章中的代码写的很差,所以让我们来应用ESLint:
`$ npm install eslint eslint-loader babel-eslint --save-dev`
我们要建一个我知道会失败的简单的.eslintrc
文件
.eslintrc
parser: 'babel-eslint'
rules:
quotes: 2
现在添加pre-loader,我们之前使用过相同的语法,但是是在module.preLoaders
:
module.preLoaders
:
module: {
preLoaders: [
{
test: /\.js/,
loader: 'eslint',
}
],
现在我们运行Webpack,绝对会失败:
$ webpack
Hash: 33cc307122f0a9608812
Version: webpack 1.12.2
Time: 1307ms
Asset Size Chunks Chunk Names
bundle.js 305 kB 0 [emitted] main
1-551ae2634fda70fd8502.js 4.5 kB 1 [emitted]
2-999713ac2cd9c7cf079b.js 4.17 kB 2 [emitted]
bundle.css 59 bytes 0 [emitted] main
+ 15 hidden modules
ERROR in ./src/index.js
/Users/anahkiasen/Sites/webpack/src/index.js
1:8 error Strings must use doublequote quotes
4:31 error Strings must use doublequote quotes
6:32 error Strings must use doublequote quotes
7:35 error Strings must use doublequote quotes
9:23 error Strings must use doublequote quotes
14:31 error Strings must use doublequote quotes
16:32 error Strings must use doublequote quotes
18:29 error Strings must use doublequote quotes
来举另一个pre-loader的例子:对每个组件我们导入的样式表是一样的名字,模板也是。我们来使用pre-loader去自动的将重名的文件当作模块导入:
`$ npm install baggage-loader --save-dev`
{
test: /\.js/,
loader: 'baggage?[file].html=template&[file].scss',
}
这告诉Webpack:如果你遇到重名的HTML文件,把它当作template
导入,并且将任何的Sass文件用同样的名字导入。我们现在可以将我们的组件改为:
import $ from 'jquery';
import template from './Button.html';
import Mustache from 'mustache';
import './Button.scss';
这样:
import $ from 'jquery';
import Mustache from 'mustache';
你可以看到pre-loaders非常强大,post-loaders也是一样。去看一下文章末尾的可用loaders清单,你肯定会在其中发现很多可用示例。
现在我们的应用还很小,但是当它变大的时候,如果我们能够清楚的了解实际的依赖树就会非常有用处。我们可能要做的是错还是对,我们应用的瓶颈在哪,等等。现在在内部,Webpack知道所有这些事情,但是你必须客气的请教它所知道的东西。你可以通过运行下边的命令来生成一个 profile文件来做这件事情:
`webpack --profile --json > stats.json`
第一个命令告诉Webpack生成profile文件,第二个是使用JSON格式去生成,最后将所有东西输出到JSON文件。现在有许多网站去分析这些profile文件,但是Webpack提供了一个官方版本去分析这份信息。去Webpack Analyze导入你的JSON文件。现在进入Modules标签应该能看到你的依赖树的一个可视化图像:
圆点越红,在你最终的生成包中问题性就越大。在我们的例子中,jQuery被标注为问题性的因为它是所有模块中被重用最多的,看一下所有标签里的内容,你不会从我们的应用中学到很多,但是这个工具对于你观察你的依赖树和最终的生成包是非常重要的。就像我说的,其他的服务提供了你的profile文件的内在分析,另一个我喜欢用的是Webpack Visualizer ,它依据生成报中内容所占空间生成了一个环形表,我们的是这样的:
现在我知道在我的例子里,Webpack已经替代了 Grunt或者Gulp:之前我用它们做的事情Webpack都能帮我做了,剩下的我只是用了NPM脚本。在我们过去的例子里的一个常见的任务是使用Aglio将我们的API文档转换成HTML,像这样做就能够简单地完成:
package.json
{
"scripts": {
"build": "webpack",
"build:api": "aglio -i docs/api/index.apib -o docs/api/index.html"
}
}
如果你在Gulp工作栈中有一个非常复杂的任务与打包或资源无关,Webpack能够使用其他的构建系统漂亮地处理。这里的例子是将Webpack整合进Gulp中:
var gulp = require('gulp');
var gutil = require('gutil');
var webpack = require('webpack');
var config = require('./webpack.config');
gulp.task('default', function(callback) {
webpack(config, function(error, stats) {
if (error) throw new gutil.PluginError('webpack', error);
gutil.log('[webpack]', stats.toString());
callback();
});
});
类似的还有很多,因为Webpack也拥有NodeAPI,使它能够轻松地使用在其他构建系统中,你会发现它无处不在。
总之,我认为这对你来说是对Webpack的一个非常好的审视。你也许会觉得这篇文章覆盖了很多内容,但我们仅接触了表面的东西:多个入口,预先提取,上下文切换,等等。Webpack是一个非常出色的工具,这当然也会比传统的构建工具多了复杂的配置语法,我不会否认。但一旦你知道怎样去驯服它,你能看到它非常棒的表现。我在几个项目中使用了它,他提供了如此强大的优化和自动化能力,以至于我不敢向我再让我去敲着脑袋去决定资源在何时何处被引用了。
Webpack documentation
List of loaders
List of plugins
Sources for this article
Our Webpack configuration package