[译] 用 Webpack 武装本身

本文译自:Webpack your bags
这篇文章由入门到深刻的介绍了webpack的功能和使用技巧,真心值得一看。javascript

因为我英语水平有限,并且不多翻译文章,因此文中的一些语句在翻译时作了相似语义的转换,望谅解。要是有幸被转仍是但愿可以注明啊css

by the way,打个小广告。。把本身的github扔这儿好了,有时候会更新些译文或者笔记什么的html

图片描述

你可能已经据说过这个酷酷的工具-Webpack。一些人称之为相似于Gulp的工具,还有一些人则认为它相似于Browserify。若是你还没接触过它,那颇有可能会所以感到困惑。而Webpack的主页上则认为它是二者的结合,那或许更让你困惑了。html5

说实话,一开始的时候,“什么是Webpack”这个话题让我很心烦,也就没有继续研究下去了。直到后来,当我已经构建了几个项目后,才真心的为之痴迷。若是你像我同样紧随Javascript的发展步伐,你颇有可能会由于太追随潮流跨度太大而蛋疼。在经历了上面这些以后,我写下这篇文章,以便更加细致的解释Webpack是什么,以及它如此重要的缘由。java

Webpack是啥?

首先来让咱们回答最开始的问题:Webpack是个系统的构建工具,仍是打包工具?答案是二者都是--这不表明它作了这两件事(先构建资源,在分别进行打包),而是说它将二者结合在一块儿了。node

更加清晰的说明:与“构建sass文件,压缩图片,而后引用它们,再打包,再在页面上引用”相比,你只要这么作:jquery

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) // "<html><body><h1>Hello</h1></body></html>"

你的全部资源都被当作包处理,能够被import,修改,控制,最终展示在你最后的一个bundle上。webpack

为了能让上面那些有效运转,你须要在本身的Webpage配置里配置loaderloader是一个“当程序碰见XXX类型文件的时候,就作YYY”的小型插件。来看一些loader的例子:git

{
  // 若是引用了 .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转换以后,它的引用其实是这样的:github

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文件。

<!DOCTYPE html>
<html>
<body>
    <h1>My title</h1>
    <a>Click me</a>

    <script src="builds/bundle.js"></script>
</body>
</html>

运行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拥有includeexclude规则,里面能够是一段字符串、正则、回调等等。在这个例子里,咱们只想让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;
}
<!-- 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如今处于彻底独立的状态,无论什么时候何地的引用它,都能获取到全部须要的依赖并渲染出来。如今渲染咱们的按钮试试:

// 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;
}
<!-- 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 (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,以后咱们能够手动引入:

<script src="builds/vendor.js"></script>
<script src="builds/bundle.js"></script>

你也能够经过async: true,而且不提供共同依赖包的命名,来达到异步加载共同依赖的效果。

Webpack有不少这样给力的优化方案。我无法一个一个介绍它们,不过能够经过创造一个生产环境的应用来进一步学习。

飞跃到生产环境(production

首先,要在设置中添加几个插件,但要求只有当NODE_ENVproduction的时候才运行它们:

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吗?在这个配置里面,你可使用一些变量,而[hash]则会给文件提供一段随机的字符串。除此之外,咱们想要包能够被版本化,所以添加了output.chunkFilename

output: {
    path: 'builds',
    filename: production ? '[name]-[hash].js' : 'bundle.js',
    chunkFilename: '[name]-[chunkhash].js',
    publicPath: 'builds/',
},

由于没法得知每次打包生成的文件名,因此咱们只在生产环境下使用它。除此以外,咱们还想保证每次打包的时候,builds文件夹都会被清空以节约空间,所以使用了一个第三方插件:

$ npm install clean-webpack-plugin --save-dev

并将它添加到配置中:

var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
// ...
if (production) {
    plugins = plugins.concat([
        // 在打包前清空 builds/ 文件夹
        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都作了什么:

  • 在第一段代码中,后两个包很是轻量,异步请求不会占用多少HTTP带宽,因此在生产环境下Webpack将它们打包进了入口文件里

  • 全部东西都压缩过了。从322kb降到了97kb

可是这样下去,Webpack岂不是会将js文件合并成巨大的一坨吗?

是的,在这个小小的应用中是这样没错。可是你须要这么想:你不须要考虑在何时合并什么。若是你的包中含有太多的依赖,它们会被移走到异步请求包中而不会被合并起来。反之,若是它们很小,不值得独立加载,那么就会被合并。你只须要创建规则,Webpack会最大化的将其优化。没有人力劳做,不须要思考依赖关系,一切都是自动化的。

图片描述

或许你已经注意到了,我没有对HTML或CSS进行压缩。那是由于当debug模式开启的时候,css-loaderhtml-loader已经帮咱们搞好了。这也是为何Uglify是一个独立插件的缘由:在Webpack中没有js-loader这种东西,Webpack本身就是个JS loader。

抽取(extract-text-webpack-plugin

可能你已经注意到了,从这个教程一开始,Webpack打包好以后,咱们的样式就直接插在网页页面上,简直不能更难看了。能经过Webpack把打包过的CSS生成独立的文件吗?固然没问题:

$ npm install extract-text-webpack-plugin --save-dev

这个插件所作的就是我刚刚说的那些:从打出的最终包里面,提取出某一类内容分离开来单独引用。它一般被用于提取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'), // <=== 提取出来的文件
    new webpack.optimize.CommonsChunkPlugin({
        name: 'main',
        children: true, 
        minChunks: 2,
    }),
];
// ...
module.exports = {
    // ...
    plugins: plugins,
    module:  {
        loaders: [
            {
                test: /\.scss/,
                loader: ExtractPlugin.extract('style', 'css!sass'),
            },
            // ...
        ],
    }
};

ExtractPlugin.extrac方法接收两个参数,第一个参数表明当它处于已经打包好的包('style')里时,如何处理那些提取出来的东西;第二个参数表明当它在主文件('css!sass')里时,如何对待提取出的东西。当它在包里时,确定不能直接将CSS加在生成的东西后面,因此先用style-loader进行处理;而对于主文件里面的styles,则将它们放进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

若是你想提取出全部包里的样式,则须要设置ExtractTextPlugin('bundle.css', {allChunks: true})

顺带一提,你也能够自定义文件名,就跟以前说的改变js output-file名称同样:ExtractTextPlugin('[name]-[hash].css')

使用图片(url-loader&file-loader

到目前为止,咱们还没处理例如图片、字体这样的资源文件。它们在Webpack中如何工做,咱们又该如何优化?接下来,我要在网站的背景里加入图片,看起来必定酷酷的:

图片描述

把图片保存在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必定会告诉你:“你特么的想让我对JPG作啥?”,那是由于尚未加入对应的loader。有两种loader可使用:file-loaderurl-loader

  • file-loader:返回一段指向资源的URL,容许你给文件加入版本的概念(默认)

  • url-loader:以data:image/jpeg;base64的形式返回URL

两个方法不能说谁好谁坏:若是你的图片大于2M的话那你必定不但愿它直接夹杂在代码中,而是独立出去;而若是仅仅是2kb左右的小图标。那么合并在一块儿减小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进行处理。你也能够经过query传递一个Object来实现它:

{
    test: /\.(png|gif|jpe?g|svg)$/i,
    loader: 'url',
    query: {
      limit: 10000,
    }
}

来瞅一眼Webpack的输出:

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请求占有的比率,采起不一样的优化方案。还有一个叫作image-loader的loader,能够在打包前检查全部图片,避免图片的重复压缩。它有一个叫?bypassOnDebug 的参数,经过它你能够只在生产环境下启动该插件。

还有不少优秀的插件,我强烈建议你使用文末的连接去查看它们。

来个牛逼的热加载(dev-server)

咱们的生产环境以及整的差很少了,如今应该更多的关心一下本地开发。或许你以及注意到了,当人们说起开发工具的时候,老是会说起热加载:LiveReload,BrowserSync,或者其余的什么鬼东西。可是只有傻瓜才会整页的刷新,咱们则使用更高端的热加载。由于Webpack能够确切的知道你依赖树中某一点位置的代码,所以每次的改变都会据今生成一个新的文件。简单的说,就是不须要刷新页面就能将改变展示在屏幕上。

为了可以使用HMR,咱们须要一个server来启动热加载。Webpack提供的dev-server能够完成这个任务:

$ npm install webpack-dev-server --save-dev

安装下面的命令启动server,不能再简单了:

$ webpack-dev-server --inline --hot

第一个标记--inline是让Webpack把HMR逻辑直接写入页面上而不是放到iframe里,而第二个标记则开启了HMR。接下来,访问http://localhost:8080/webpack-dev-server/,嗯仍是那个正常的页面。试着修改Sass文件,MAGIC!

图片描述

你能够把webpack-dev-server做为本身本地的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 server上。Webpack提供了一个中间件,使得你能够把HMR的功能用在其余server上。

代码不干净的人都给我去罚站!(pre-loader & lint)

若是你一直跟着本教程走,那或许会有这样的疑问:为何loader都在module.loaders中而插件不在?那固然是由于还有其余能够配置进module的东西~Webpack不仅是有loader,也有pre-loader和post-loader:在main-loader运行以前和以后发动的玩意。举个栗子:我基本能够确信本身在这个文章里面写的代码很糟糕,因此使用ESLint进行代码检查:

$ npm install eslint eslint-loader babel-eslint --save-dev

新建一个确定会引起错误的.eslintrc文件:

// .eslintrc
parser: 'babel-eslint'
rules:
  quotes: 2

如今增长pre-loader,语法和以前的同样,只不过加在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的例子:每一个组件里咱们都引用了stylesheet,而它们都有相同命名的对应模板。使用一个pre-loader能够自动将有相同名称的文件做为一个module载入:

$ 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也能够很强大。在文末你能够找到更多的loader

还没看够?

如今咱们的应用还很小,当它变的庞大的时候,观测依赖树就变的很是有用了,从中能够看出咱们作的是对是错,应用的瓶颈在哪里等等。Webpack知晓这一切,不过咱们得礼貌的请教它才能知晓答案。为了作到这点,你能够经过下面的命令运行Webpack:

webpack --profile --json > stats.json

第一个标记会让Webpack生成一个profile文件,而第二个则将它转化为JSON格式。最终,讲全部的output都生成了JSON文件。如今有不少网站均可以解析这个JSON文件,不过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"
  }
}

即使是一些不须要打包和构建的Glup任务,Webpack都贴心的提供了对应的服务。下面是一个将Glup融合进Webpack的例子:

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具备Node API,所以能够很轻松了运用在其余构建体系中。不用多久你就能发现本身深爱着它没法自拔了。

无论怎样,这篇文字带你预览了Webpack可以帮你作的事情。或许你认为咱们讲了不少方面,但实际上这只是个表皮而已:multiple entry points, prefetching, context replacement等等都尚未涉及到。Webpack是个强大的工具,也所以比那些传统的工具更加难懂。但一旦你知道如何使用它,它就会为你鸣奏最悦耳动听的声音。我曾在一些项目里使用过它,它提供的强大的优化和自动化让我深深不能自拔。

资源

相关文章
相关标签/搜索