[译] 用 Webpack 武装自己
2021-02-18 04:21
YPE html>
本文译自:Webpack your bags
这篇文章由入门到深入的介绍了webpack的功能和使用技巧,真心值得一看。由于我英语水平有限,而且很少翻译文章,所以文中的一些语句在翻译时做了类似语义的转换,望谅解。要是有幸被转还是希望能够注明啊
by the way,打个小广告。。把自己的github扔这儿好了,有时候会更新些译文或者笔记什么的
你可能已经听说过这个酷酷的工具-Webpack。一些人称之为类似于Gulp的工具,还有一些人则认为它类似于Browserify。如果你还没接触过它,那很有可能会因此感到困惑。而Webpack的主页上则认为它是两者的结合,那或许更让你困惑了。
说实话,一开始的时候,“什么是Webpack”这个话题让我很心烦,也就没有继续研究下去了。直到后来,当我已经构建了几个项目后,才真心的为之痴迷。如果你像我一样紧随Javascript的发展步伐,你很有可能会因为太追随潮流跨度太大而蛋疼。在经历了上面这些之后,我写下这篇文章,以便更加细致的解释Webpack是什么,以及它如此重要的原因。
Webpack是啥?
首先来让我们回答最开始的问题:Webpack是个系统的构建工具,还是打包工具?答案是两者都是--这不代表它做了这两件事(先构建资源,在分别进行打包),而是说它将两者结合在一起了。
更加清晰的说明:与“构建sass文件,压缩图片,然后引用它们,再打包,再在页面上引用”相比,你只要这么做:
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
"
你的所有资源都被当做包处理,可以被import,修改,控制,最终展现在你最后的一个bundle上。
为了能让上面那些有效运转,你需要在自己的Webpage配置里配置loader
。loader
是一个“当程序遇见XXX类型文件的时候,就做YYY”的小型插件。来看一些loader
的例子:
{
// 如果引用了 .ts 文件, 将会触发 Typescript loader
test: /\.ts/,
loader: ‘typescript‘,
},
{
// 如果引用了png|jpg|svg图片,则会用 image-webpack 进行压缩 (wrapper around imagemin)
// 并转化成 data64 URL 格式
test: /\.(png|jpg|svg)/,
loaders: [‘url‘, ‘image-webpack‘],
},
{
// 如果使用了 SCSS files, 则会用 node-sass 解析, 最终返回CSS格式
test: /\.scss/,
loaders: [‘css‘, ‘autoprefixer‘, ‘sass‘],
}
最终在食物链的最底端,所有的loader
都返回string
,这样Webpack就可以将它们加入到javascript模块中去。当你的Sass文件被loader转换之后,它的引用实际上是这样的:
export default ‘body{font-size:12px}‘;
究竟为什么要这么做?
在你理解了Webpack是做什么的之后,第二个问题就接踵而至:使用它有什么好处?“把图片和CSS扔进我的js里?什么鬼?”其实在很久之前,为了减少HTTP request请求,我们都被教育要把所有东西写在一个文件里面。
到了现在,与之类似的是,很多人把所有东西打包进app.js
。这两种方法都有一个很大的负面影响:很多时候人们在下载的是他们用不到的资源。但如果你不这么做吧,你就得手动的在每个页面引用相应的资源,最终会混乱成一坨:哪个页面已经引用了它所依赖的资源?
这些方法没有绝对的对错。把Webpage当做一个中间件--不仅仅是打包或构建工具,而是个聪明的模块打包系统。只要你设置正确,它会比你还要清楚使用的技术栈,并更好的优化它们。
来让我们一起构建一个简单的App
为了让你更快捷的理解使用Webpack的好处,我们会构建一个简单的App,并将资源打包进去。在这里教程中我推荐使用Node4(或5),以及NPM3作为包管理工具,以便在使用Webpack的时候避免大量的麻烦。如果你还没装NPM3,可以通过npm install npm@3 -g
来安装。
$ node --version
v5.7.1
$ npm --version
3.6.0
我还要推荐你把node_modules/.bin
放进你的PATH
变量,以避免每次都要输入node_modules/.bin/webpack
。在下面了例子里我输入的指令都不会再包含node_modules/.bin
。
基础指引(setup)
从创建项目安装Webpack开始。我们同时也安装了jQuery以便支持后续操作。
$ npm init -y
$ npm install jquery --save
$ npm install webpack --save-dev
现在来做一个App的入口:
// src/index.js
var $ = require(‘jquery‘);
$(‘body‘).html(‘Hello‘);
让我们在webpack.config.js
文件里进行的Webpack配置。Webpack配置实质上是Javascript,并且在最后export
出去一个Object:
// webpack.config.js
module.exports = {
entry: ‘./src‘,
output: {
path: ‘builds‘,
filename: ‘bundle.js‘,
},
};
在这里,entry
告诉Webpack哪些文件是应用的入口文件。它们是你的主要文件,在依赖树的最顶端。之后,我们告诉Webpack把资源打包在builds
文件夹下的bundle.js
文件里。让我们编写index HTML文件。
My title
Click me
运行Webpack。如果一切正确那就可以看见下面的信息:
$ 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
在这段信息里可以看出,bundle.js
包含了index.js
和一个隐藏的模块。隐藏的模块是jQuery。在默认模式下Webpack隐藏的模块都不是你写的。如果想要显示它们,我们可以在运行Webpack的时候使用--display-modules
:
$ webpack --display-modules
bundle.js 268 kB 0 [emitted] main
[0] ./src/index.js 53 bytes {0} [built]
[3] ./~/jquery/dist/jquery.js 259 kB {0} [built]
你还可以使用webpack --watch
,在改变代码的时候自动进行打包。
设置第一个loader(loader
-01)
还记得Webpack可以处理各种资源的引用吗?该怎么搞?如果你跟随了这些年Web组件发展的步伐(Angular2,Vue,React,Polymer,X-Tag等等),那么你应该知道,与一堆UI相互连接组合而成的App相比,使用可维护的小型可复用的UI组件会更好:web component。
为了确保组件能够保持独立,它们需要在自己内部打包需要的资源。想象一个按钮组件:除了HTML之外,还需要js以便和外部结合。噢对或许还需要一些样式。如果能够在需要这个按钮组件的时候,加载所有它所依赖的资源的话那就太赞了。当我们import按钮组件的时候,就获取到了所有资源。
开始编写这个按钮组件吧。首先,假设你已经习惯了ES2015语法,那么需要安装第一个loader:Babel。安装好一个loader你需要做下面这两步:首先,通过npm install {whatever}-loader
安装你需要的loader,然后,将它加到Webpage配置的module.loaders
里:
$ npm install babel-loader --save-dev
loader并不会帮我们安装Babel所以我们要自己安装它。需要安装babel-core
包和es2015
预处理包。
$ npm install babel-core babel-preset-es2015 --save-dev
新建.babelrc
文件,里面是一段JSON,告诉Babel使用es2015
进行预处理。
// .babelrc
{ "presets": ["es2015"] }
现在,Babel已经被安装并配置完成,我们要更新Webpack配置。我们想要Babel运行在所有以.js
结尾的文件里,但是要避免运行在第三方依赖包例如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‘,
}
],
}
};
现在,我们可以用ES6语法重写index.js
了:
// index.js
import $ from ‘jquery‘;
$(‘body‘).html(‘Hello‘);
写个小组件(loader
-02)
来写个按钮组件吧,它将包含一些SCSS样式,HTML模板和一些操作。所以我们要安装需要的工具。首先安装Mustache这个轻量级的模板库,然后安装处理Sass和HTML的loader。同样的,为了处理Sass loader返回的结果,还要安装CSS loader。一旦获取到了CSS文件,我们就可以用很多种方式来处理。目前使用的是一个叫style-loader
的东西,它能够把CSS插入到包中。
$ npm install mustache --save
$ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev
为了能够让Webpack依次处理不同loader的返回结果,我们可以将loader通过!
链接到一起,获取使用loaders
并对应一个由loader组成的数组:
{
test: /\.js/,
loader: ‘babel‘,
include: __dirname + ‘/src‘,
},
{
test: /\.scss/,
loader: ‘style!css!sass‘,
// Or
loaders: [‘style‘, ‘css‘, ‘sass‘],
},
{
test: /\.html/,
loader: ‘html‘,
}
有了loader,我们来写写按钮:
// src/Components/Button.scss
.button {
background: tomato;
color: white;
}
{{text}}
// 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
现在处于完全独立的状态,不管何时何地的引用它,都能获取到所有需要的依赖并渲染出来。现在渲染我们的按钮试试:
// src/index.js
import Button from ‘./Components/Button’;
const button = new Button(‘google.com’);
button.render(‘a’);
运行Webpack,刷新页面,立刻就能看见我们这个难看的按钮了。
现在你已经学习了如何安装loader,以及定义各个依赖配置。看起来好像也没啥。但让我们来深入扩展一下这个例子。
代码分离(require.ensure
)
上面的例子还不错,但我们并不总是需要这个按钮。或许有的页面没有可以用来渲染按钮的a
,我们并不想在这样的页面引用按钮的资源文件。这种时候代码分离就能起到作用了。代码分离是Webpack对于“整块全部打包”vs“难以维护的手动引导”这个问题而给出的解决方案。这需要在你的代码中设定“分离点”:代码可以据此分离成不同区域进行按需加载。它的语法很简单:
import $ from ‘jquery‘;
// 这个是分割点
require.ensure([], () => {
// 在这里import进的代码都会被打包到一个单独的文件里
const library = require(‘some-big-library‘);
$(‘foo‘).click(() => library.doSomething());
});
在require.ensure
中的东西都会在打包结果中分离开来--只有当需要加载它的时候Webpack才会通过AJAX请求进行加载。也就是说我们实际上得到的是这样的文件:
bundle.js
|- jquery.js
|- index.js // 入口文件
chunk1.js
|- some-big-libray.js
|- index-chunk.js // 回调中的代码在这里
你不需要在任何地方引用chunk1.js
文件,Webpack会帮你在需要的时候进行请求。这意味着你可以像我们的例子一样,根据逻辑需要引进的资源全部扔进代码里。
只有当页面上有链接存在时,再引用按钮组件:
// 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 export和normal export,所以使用require
引用资源里default export的时候,需要手动加上.default
。相比之下,import
则可以进行处理:
import foo from ‘bar‘
vs import {baz} from ‘bar‘
此时Webpack的output将会变得更复杂了。跑下Webpack,用--display-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]
[5] ./src/Components/Button.js 1.94 kB {1} [built]
[6] ./~/jquery/dist/jquery.js 259 kB {1} [built]
[7] ./src/Components/Button.html 72 bytes {1} [built]
[8] ./~/mustache/mustache.js 19.4 kB {1} [built]
[9] ./src/Components/Button.scss 1.05 kB {1} [built]
[10] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[11] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built]
[12] ./~/style-loader/addStyles.js 7.21 kB {1} [built]
正如你所见的那样,我们的入口bundle.js
值包含了一些逻辑,而其他东西(jQuery,Mustache,Button)都被打包进了1.bundle.js
,并且只在需要的时候才会被引用。现在为了能够让Webpack在AJAX的时候找到这些资源,我们需要改下配置里的output
:
path: ‘builds‘,
filename: ‘bundle.js‘,
publicPath: ‘builds/‘,
output.publicPath
告诉Webpack,从当前页面的位置出发哪里可以找到需要的资源(在这个例子里是/builds/
)。当我们加载页面的时候一切正常,而且能够看见Webpack已经根据页面上预留的“锚”加载好了包。
如果页面上缺少“锚”(代指link),那么只会加载bundle.js
。通过这种方式,你可以做到在真正需要资源的时候才进行加载,避免让自己的页面变成笨重的一坨。顺带一提,我们可以改变分割点的名字,不使用1.bundle.js
而使用更加语义化的名称。通过require.ensure
的第三个参数来实现:
require.ensure([], () => {
const Button = require(‘./Components/Button‘).default;
const button = new Button(‘google.com‘);
button.render(‘a‘);
}, ‘button‘);
这样的话就会生成button.bundle.js
而不是1.bundle.js
再加个组件(CommonChunksPlugin
)
来让我们再加个组件吧:
// src/Components/Header.scss
.header {
font-size: 3rem;
}
{{text}}
// 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 (document.querySelectorAll(‘a‘).length) {
require.ensure([], () => {
const Button = require(‘./Components/Button‘);
const button = new Button(‘google.com‘);
button.render(‘a‘);
});
}
// 如果有标题,则渲染标题组件
if (document.querySelectorAll(‘h1‘).length) {
require.ensure([], () => {
const Header = require(‘./Components/Header‘);
new Header().render(‘h1‘);
});
}
瞅瞅使用了--display-chunks --display-modules
标记后Webpack的output输出:
$ 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]
[14] ./src/Components/Button.js 1.94 kB {1} [built]
[15] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
[16] ./src/Components/Button.html 72 bytes {1} [built]
[17] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
[18] ./src/Components/Button.scss 1.05 kB {1} [built]
[19] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[20] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
[21] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
chunk {2} 2.bundle.js 290 kB {0} [rendered]
[22] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
[23] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
[24] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
[25] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
[26] ./src/Components/Header.js 1.62 kB {2} [built]
[27] ./src/Components/Header.html 64 bytes {2} [built]
[28] ./src/Components/Header.scss 1.05 kB {2} [built]
[29] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
可以看出一点问题了:这两个组件都需要jQuery和Mustache,这样的话就造成了包中的依赖重复,这可不是我们想要的。尽管Webpack会在默认情况下进行一定的优化,但还得靠插件来加足火力搞定它。
插件和loader的不同在于,loader只对一类特定的文件有效,而插件往往面向所有文件,并且并不总是会引起转化。Webpack提供了很多插件供你优化。在这里我们使用CommonChunksPlugin
插件:它会分析你包中的重复依赖并提取出来,生成一个完全独立的文件(例如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 {0} bundle.js (main) 287 kB [rendered]
[0] ./src/index.js 550 bytes {0} [built]
[30] ./~/jquery/dist/jquery.js 259 kB {0} [built]
[31] ./~/mustache/mustache.js 19.4 kB {0} [built]
[32] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
[33] ./~/style-loader/addStyles.js 7.21 kB {0} [built]
chunk {1} 1.bundle.js 3.28 kB {0} [rendered]
[34] ./src/Components/Button.js 1.94 kB {1} [built]
[35] ./src/Components/Button.html 72 bytes {1} [built]
[36] ./src/Components/Button.scss 1.05 kB {1} [built]
[37] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
chunk {2} 2.bundle.js 2.92 kB {0} [rendered]
[38] ./src/Components/Header.js 1.62 kB {2} [built]
[39] ./src/Components/Header.html 64 bytes {2} [built]
[40] ./src/Components/Header.scss 1.05 kB {2} [built]
[41] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
如果我们改变下名字name: ‘vendor‘
:
new webpack.optimize.CommonsChunkPlugin({
name: ‘vendor‘,
children: true,
minChunks: 2,
}),
Webpack会在没有该文件的情况下自动生成builds/vendor.js
,之后我们可以手动引入:
你也可以通过async: true
,并且不提供共同依赖包的命名,来达到异步加载共同依赖的效果。
Webpack有很多这样给力的优化方案。我没法一个一个介绍它们,不过可以通过创造一个生产环境的应用来进一步学习。
飞跃到生产环境(production
)
首先,要在设置中添加几个插件,但要求只有当NODE_ENV
为production
的时候才运行它们:
var webpack = require(‘webpack‘);
var production = process.env.NODE_ENV === ‘production‘;
var plugins = [
new webpack.optimize.CommonsChunkPlugin({
name: ‘main‘,
children: true,
minChunks: 2,
}),
];
if (production) {
plugins = plugins.concat([
// 生产环境下需要的插件
]);
}
module.exports = {
entry: ‘./src‘,
output: {
path: ‘builds‘,
filename: ‘bundle.js‘,
publicPath: ‘builds/‘,
},
plugins: plugins,
// ...
};
Webpack也提供了一些可以切换生产环境的设置:
module.exports = {
debug: !production,
devtool: production ? false : ‘eval‘,
}
设置中的第一行表明在开发环境下,将开启debug模式,代码不再混做一团,利于本地调试。第二行则用来生产资源地图(sourcemaps)。Webpack有一些方法可以生成sourcemaps,而eval
则是在本地表现最赞的一个。在生产环境下,我们并不关心sourcemaps,因此关闭了这个选项。
现在来添加生产环境下的插件吧:
if (production) {
plugins = plugins.concat([
// 这个插件用来寻找相同的包和文件,并把它们合并在一起
new webpack.optimize.DedupePlugin(),
// 这个插件根据包/库的引用次数来优化它们
new webpack.optimize.OccurenceOrderPlugin(),
// 这个插件用来阻止Webpack把过小的文件打成单独的包
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 51200, // ~50kb
}),
// 压缩js文件
new webpack.optimize.UglifyJsPlugin({
mangle: true,
compress: {
warnings: false, // 禁止生成warning
},
}),
// 这个插件提供了各种可用在生产环境下的变量
// 通过设置为false,可避免生产环境下调用到它们
new webpack.DefinePlugin({
__SERVER__: !production,
__DEVELOPMENT__: !production,
__DEVTOOLS__: !production,
‘process.env‘: {
BABEL_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
]);
}
我普遍使用的差不多就这么多了,不过Webpack还提供了非常多的插件,你可以自己去研究它们。也可以在NPM上找到很多用户自己贡献的插件。插件的链接在文末提供。
还有一个关于生产环境的优化是给资源提供版本的概念。还记得output.filename
里的bundle.js
吗&am
文章标题:[译] 用 Webpack 武装自己
文章链接:http://soscw.com/index.php/essay/56892.html