webpack使用


webpack使用

Webpack your bags 原文: Webpack your bags(by Maxime Fabre)

Webpack your bags

之前你可能已经听说过这个很酷的叫webpack工具,如果你没仔细了解过这个工具,你可能会有些困惑,因为有人说他它是像Gulp之类构建工具,也有人说它是像Browserify之类的bundler,如果你仔细了解一下,你可能还是会不明白是怎么回事儿,因为官网上把webpack说成是这两者。

说实话,开始的时候对于**“webpack到底是什么“很模糊,感觉很受挫,然后我直接就把网页关了,毕竟我已经有一个构建系统了,而且我用得也非常哈皮,如果你紧赶javascript的时髦的话,像我这样,你可能在估计很快因为在各种流行的东西中频繁地跳来跳去而烧的灰飞烟灭了。 现在我有些经验了,我觉得我应该写这篇文章给那些还处在混沌中的小伙伴儿,更清楚解释一下webpack到底是什么,更重要的是它的什么地方那么棒以至于值得我们投入更多的精力。

Webpack是什么?

好的,下面我们来回答一下,介绍里面提到的那个问题,Webpack是一个构建系统还是一个模块bundler(捆绑器),恩-,两个都是 - 我不是说它两个事儿都干,我的意思是它把两者连接起来了,webpack不是先构建你的资源(assets),然后再bundle你的模块,它把你的资源本身当做模块。

更准确的说,它不是先编译你的所有的Sass文件,再优化所有的图片,再在一个地方include进来,然后bundle所有的模块,在另一个地方include到你的page上。假设你在是下面这样:

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,modify,manipulate(操作),最后被打包到你最后的bundle

为了达到这个目的,得在webpack configuration里注册loaders,loader就是当你遇到某种文件的时候,对它做相应的一种处理的一种插件,下面是一些loader的例子:

{
  // 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'],
}

最后在食物链的终点所有的loader返回的都是string,这样可以使webpack把资源最后包装成javascript模块。这个例子里你的Sass文件被loaders转换后,里面看起来差不多是这样。

export default 'body{font-size:12px}';

那你究竟为什么要用它呢?

一旦你明白webpack是做什么的,很有可能就会想到第二个问题:用它能有什么好处呢?images和CSS?还在我的JS里?玩啥呢哥们儿。好的,看下这个:很长时间我一直被告诉要把所有文件都放在一个文件里,这样来保证不浪费我们的request。

但这样导致一个最大的缺点,就是现在大多数人都把所有的资源(assets)bundle到一个单独的app.js文件里,然后把他include每一个页面,这就意味着渲染任意一个页面的时候大部分时间都浪费在加载一大堆根本用不上的资源上。如果你不这么做,那么你很有可能得手动把这些资源include到指定的页面,这就会引出一大团乱遭的依赖树去维护和跟踪如:哪些页面这个需要依赖?哪些页面style A 和Style B 起作用。

这两个方法都不对,也都不是错的。我们把webpack当做一个中间者-他不仅仅是一个构建(build)系统或者一个bundler,它是一个邪恶小精灵般的智能打包(packing)系统,正确地配置后,它甚至比你都了解你的系统栈(stack),而且它比你清楚怎么样最优优化你的系统。

我们来做一个小的app

为了让你更容易地理解webpack带来的好处,我们做一个小的app,然后用它来bundle 资源(assets),在本篇文章里我建议用Node 4 (或 5)和NPM 3因为扁平的依赖树在你用webpack的时候会避免很多让人头疼的麻烦,如果你还没有NPM3,你可以通过 npm install npm@3 -g 来安装。

$ node --version
v5.7.1
$ npm --version
3.6.0

同时我也建议你把 node_modules/.bin 加到你的PATH variable(环境变量里)以避免每次都手动打 node_modules/.bin/webpack ,后面的所有例子都不会显示我要执行的命令行的 node_modules/.bin 部分。

基本引导

我们开始,先创建个project,安装上webpack,我们也加上jquery好在后面证明一些东西。

$ npm init -y
$ npm install jquery --save
$ npm install webpack --save-dev

现在来我们创建一个app入口,用纯ES5 (in plain ES5)

src/index.js

var $ = require('jquery');

$('body').html('Hello');

在一个webpack.config.js文件里建webpack configuration,webpack configuration是javascript,需要export 一个object

webpack.config.js

module.exports = {
    entry:  './src',
    output: {
        path: 'builds',
        filename: 'bundle.js',
    },
};

这里,entry告诉webpack那个文件是你的app的入口点。这些是你的主文件,他们在依赖树的顶端,然后我们告诉它编译我们的bundle 到bundle.js文件放在builds目录下,再创建相应的index HTML

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

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

跑一下webpack,如果一切正常,会看见一个信息告诉我们正确地编译了bundle.js

$ webpack
Hash: d41fc61f5b9d72c13744
Version: webpack 1.12.14
Time: 301ms
AssetSize  Chunks Chunk Names
bundle.js  268 kB   0  [emitted]  main
   [0] ./src/index.js 53 bytes {0} [built]
+ 1 hidden modules

这里webpack告诉你你的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]
   [1] ./~/jquery/dist/jquery.js 259 kB {0} [built]

你也可以运行 webpack --watch 来自动监视文件的变化,如果有变化,自动重新编译。 搭建我们的第一个loader 现在还记得我们怎么讨论webpack可以导入CSS和HTML还有各种文件的吗?什么时候能排上用场呢?好的,如果你跟随着过去几年朝着web组件方向的大跃进(Angular 2, Vue, React, Polymer, X-Tag, etc.)估计你听过这样一个观点:你的app是如果用一套可重用,自包含的UI组件搭建的话相比较于一个单独的内聚的UI将会变得更容易维护,这个可重用的组件就是web 组件(在这里我说的比较简单),现在为了让组件变成真正的自包含,需要把组件需要的所有的东西封装到他们自己的内部,我们来考虑一个button,它里面肯定有一些HTML,而且还有一些JS来保证它的交互性,还得来一些style,这些东西如果需要的时候一起加载进来会非常棒,只有在我们导入button组件的时候,我们才会得到所有的这些资源文件(asset)。

来我们写一个button,首先我先假设你们已经熟悉了ES2015 ,我们先加入babel这个loader。想要在webpack里安装一个loader,有两步需要做:1. npm install {whatever}-loader,2. 把它加到你的Webpack configuration里的module.loaders部分 我们开始,现在我们要加babel,所以:

$ npm install babel-loader --save-dev

我们也得安装 Babel ,因为现在我们这个case,loader不会安装它,我们需要装babel-core packagees2015 preset:

$ npm install babel-core babel-preset-es2015 --save-dev.

我们现在创建.babelrc文件,告诉bable用那个preset,这是一个json文件让你配置Babel在你的code上执行哪种变体,我们现在这个我们我告诉它用es2015preset

.babelrc { "presets": ["es2015"] }

现在babel配完了,我们可以更新我们的配置:我们想要什么?我们想babel在我们所有的.js结尾的文件上运行,但是由于webpack遍历所有的依赖,我们想避免babel运行在第三方代码上,如jquery,所以我们可以再稍微过滤一下,loaders既可以有include也可以由exclude,可以是string,regex(正则表达式),一个callback(回调),随便用哪个。因为我们想让babel值运行在我们的文件上,所以我们只include我们自己的source目录:

module.exports = {
    entry:  './src',
        output: {
            path: 'builds',
             filename: 'bundle.js',
            },
            module: {
            loaders: [
            {
                test:   /\.js/,
                loader: 'babel',
                include: __dirname + '/src',
            }
        ],
    }
};

我们可以用ES6重写一下我们的index.js这个小文件,由于我们引入了babel,从这里开始后面的所有例子都用ES6.

import $ from 'jquery';

$('body').html('Hello');

写个小组件

我们来写一个小的Button 组件,它有一些SCSS style,一个HTML template,和一些behavior,那么我们安装一些我们需要的东西。首先我们用一个非常轻量的模板包Mustache ,我们也需要给Sass和HTML用的loaders,因为结果会通过管道(pipe)从一个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用管道(pipe)把东西从一个loader传到另一个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',
}

现在我们把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';

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现在是百分之百自包含的,无论什么时候导入,无论运行在什么context,它有所有需要的东西,然后正确地render(渲染),现在我们只需要把我们的Button render到我们的网页上(page):

src/index.js

js import Button from ‘./Components/Button’;

const button = new Button(‘google.com’); button.render(‘a’); 

我们来运行一下webpack,然后刷新一下页面,你应该就能看见你的挺丑的button出来了。

现在你已经学了怎么配置loader和怎么给你的app的每一个部分定义dependency(依赖),现在可能看起来不太重要这些,但现在我们来把我们的例子再做一下改进:

code分片(splitting)

这个例子还不错什么都有,但可能我们不总需要我们的Button,可能有些界面不会有 a(link)来让我们渲染Button用,在这种情况下,我们不需要导入所有的Button的style(式样),template(模板),Mustache 和一切相关的东西,对吧?“Monolithic bundle”(单片绑定) VS “Unmaintainable manual imports”(不好维护的手动导入). 这种idea是,你在你的代码里定义split point(分割点):你的code中可以轻松拆分开到单独文件中的部分,然后按需加载,语法非常简单:

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 callback 里的任何东西都会被拆分成chunk(代码块)-一个webpack需要的时候会通过ajax单独加载的bundle,这意味着我们基本上有这些:

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只有在需要它的时候才会加载它,这意味着你可以把你的code按照各种逻辑分成多块,我们将在下面的例子中这么做,我们只想在page里有link的的时候才需要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 export 你需要通过.default手动获取它, 因为require不会同时处理default 和 normal(正常) exports,所以你必须指定返回哪个,不过import有一个系统处理这个,所以它已经知道了(例如: import foo from 'bar')vs import {baz} from 'bar').

webpack的output现在应该会相应地不同了,我们来用--display-chunks flag 来运行一下,来看一下哪个模块在哪个chunk里

$ 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,只有page上有anchor (锚:超链接的意思)的时候才会加载,为了让webpack知道用ajax加载的时候在哪能找到chunks,我们必须在我们的配置中加几行:

path:   'builds',
filename:   'bundle.js',
publicPath: 'builds/',

output.publicPath选项告诉webpack相对于当前的page(我们的case是在 /builds/)在哪能找到built(构建后的)assets (资源),现在访问我们的page我们会看到一切都正常工作,但更重要的是,我们能看到,由于页面上有anchor (锚:超链接的意思),webpack实时地加载加载了chunk:

如果我们page上没有anchor,只有bundle.js会被加载 ,这点可以让你智能地把你的app里的大片逻辑拆分开,让每个页面值加载它真正需要的,我们也可以给我们的split point命名,不用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. 加入第二个组件 现在啥都有了非常cool了已经,我们再来加一个组件来看看好不好使:

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})
        );
    }
}

我们在我们的application里把它render(渲染)一下:

// If we have an anchor, render the Button component on it
if (document.querySelectorAll('a').length) {
    require.ensure([], () => {
        const Button = require('./Components/Button').default;
        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').default;

        new Header().render('h1');
    });
}

现在用--display-chunks --display-modules flags(参数标记)来看一下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]
[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,也就是说这两个依赖会在chunk里有重复,wepack默认做很少优化,但是他会以plugin的形式来给webpack提供强大的功能。

plugin跟laoder不同,它不是对指定的文件像pipe一样执行一些操作,他们对所有文件进行处理,做一些更高级的操作,但不一定非得是transformation(转换),webpack有一把plugin可以做各种优化,此时我们比较感兴趣的一个是CommonChunksPlugin:它分析你的chunk的递归依赖,然后把它们抽出来放在别的地方,可以使一个完全独立的文件(如vendor.js)或者也可以是你的主(main)文件。

我们现在的case,我们需要把公用的依赖移到我们的entry(入口) 文件,如果所有的文件需要jQuery 和 Mustache,我们也可以把它往上层移动,我们来更新一下我们的configuration:

var webpack = require('webpack');

module.exports = {
    entry:   './src',
    output:  {
      // ...
    },
    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
        }),
    ],
    module:  {
      // ...
    }
};

如果我们重新run一下 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,
}),

由于vendor 这个 chunk还不存在,webpack会创建一个builds/vendor.js,我们手动把它导入到我们的HTML:

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

你也可以通过不提供一个公用的(common) chunk name或者是指定async: true来让公用的dependency被异步加载,webpack有非常多的智能优化。作为练习我们来给我们的app建一个product version

生产环境相关插件配置

首先,我们加几个plugin到configuration,但我们想只有在NODE_ENV 等于production才加载这些plugin,所以让我们来给configuration加一些逻辑,因为只是一个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的设置在production上关掉:

module.exports = {
    debug:   !production,
    devtool: production ? false : 'eval',

第一个设置把转为非debug模式,非debug模式会去掉debug模式中产生的方便调试的code,第2个是控制sourcemap 生成的,webpack有几个方法可以render sourcemap,自带的里面eval是最好的,生产环境我们不太关心sourcemap的事,我们把它disable掉,下面加一下production 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提供很多plugin用来优化处理你的modules和chunks,NPM上也有个人用户写的plugin,有各种功能的插件,自行按需选择。

另关于production assets的另一个方面是,我们有时想给asset加版本,还记得在上面我把output.filename设置为bundle.js吧,这有几个变量可以在option里用,其中一个是[hash]代表最后bundle文件的版本,同时用output.chunkFilename给chunk也加上版本:

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

由于没有什么特别好的办法能动态地获取这个简化版app编译后的bundle的名字(由于加了版本号),所以只是在production上给asset加版本,多次编译后目录里带版本的bundle会越来越多,还得加一个第三方插件来每次production build 之前清理一下build folder(output 里面的path):

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

把它加到configuration里

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

So what has Webpack done: first of all since our example is very lightweight, our two async chunks weren’t worth the HTTP request so Webpack merged them back up to the entry point. Second everything was properly minified. We went from a total of 3 HTTP requests for 322kb to a single HTTP request for 97kb.

But wasn’t the point of Webpack to stem away for one big ass JS file?

Yes, yes it is, but this only happened because our app is very small. Now consider this: you didn’t have to think about what to merge, when or where. If your chunks suddenly start having more dependencies, the chunk will be moved to an async chunk instead of being merged; and if these chunks start looking too similar to be worth loading separately, they would be merged, etc. You just setup the rules, and from then on, Webpack will automatically optimize your application in the best way possible. No manual labor, no thinking about what dependencies go where or are needed where, everything is automatic.

You may have noticed I didn’t setup anything to minify our HTML and CSS, that’s because the css-loader and html-loader already take care of that by default if the debug option we mentioned earlier is false. This is also the reason why Uglify is a separate plugin: because there is no js-loader in Webpack, since Webpack itself is the JS loader.

extract-text-webpack-plugin

Ok now as you may have noticed, since the beginning of this tutorial our styles have been injected live into the webpage which causes a FOUAP (Flash of Ugly Ass Page). Now wouldn’t it be nice if we could just, pipe all the styles that Webpack has gathered in the current build to a final CSS file? Sure we can, let’s call in some help from an external plugin:

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

What this plugin does is exactly what I just said: gather a certain type of content from your final bundle, and pipe it elsewhere, most common use case being for CSS. So let’s set it up:

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'),
            },
            // ...
        ],
    }
};

Now the extract method takes two arguments: first is what to do with the extracted contents when we’re in a chunk (‘style’), second is what to do when we’re in a main file (‘css!sass’). Now if we’re in a chunk, we can’t just magically append our CSS to the generated one so we use the style loader here as before, but for all the styles that are found in the main file, pipe them to a builds/bundle.css file. Let’s test it out, let’s add a small main stylesheet for our application:

src/styles.scss

body {
  font-family: sans-serif;
  background: darken(white, 0.2);
}

src/index.js

import './styles.scss';

Let’s run Webpack and sure enough we now have a bundle.css file that we can import in our 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

If you’d like to also extract the styles of the chunks, you can pass the ExtractTextPlugin(‘bundle.css’, {allChunks: true}) option. Do note that you can also use variables here in your filename, so if you wanted a versioned stylesheet you’d just do ExtractTextPlugin(’[name]-[hash].css’) same as for the Javascript file.

Images all the people

Now this is good for all our Javascript files but one topic we haven’t talked about much is concrete assets: images, fonts, etc. How do those work in the context of Webpack and how can we best optimize them? Let’s take a picture from the web that we’ll use as our page background because I’ve seen people doing it on Geocities and it looks so cool:

puppy

Let’s save this image in img/puppy.jpg, and update our Sass file accordingly:

src/styles.scss

body {
    font-family: sans-serif;
    background: darken(white, 0.2);
    background-image: url('../img/puppy.jpg');
    background-size: cover;
}

Now if you do this, Webpack will legitimately tell you “What the fuck do I do with a JPG”, because we have no loader for it. There are two native loaders we can use to handle concrete assets: the file-loader and the url-loader: - The first one will just return a URL to the asset without any particular change, allowing you in the process to version the file (this is the default behavior). - The second one will inline the asset to a data:image/jpeg;base64 URL

In reality it’s not black and white: if your background is a 2Mb image you don’t want to inline that and it would be preferable to load it separately. If on the other hand it’s a small icon of 2kb it’s better to inline it and spare the HTTP request, so let’s setup both:

$ npm install url-loader file-loader –save-dev

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

Here, we’re passing a limit query parameter to the url-loader which tells it: if the asset is smaller than 10kb inline it, else, fallback to the file-loader and reference it. That syntax is called a query string, you use it to configure loaders, alternatively you can also configure loaders through an object:

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

Ok let’s give it a shot:

    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

As we can see there is no mention of a JPG because our puppy image is smaller than the configured size, it has been inlined. Which means if we visit our page we can bask in the gloriousness of our puppies overlords.

We’ll do it live dammit

Now that our production build is taken care of, let’s focus more on our local development. You may have noticed one big hole so far that usually comes up when mentioning build tools: live reloading: LiveReload, BrowserSync, whatever your cup of tea is. But having the whole page refresh that’s for suckers, let’s take it one step further with what is called hot module replacement or hot reload. It’s the idea that, since Webpack knows exactly the position of each module in our dependency tree, a change in it can be represented by simply patching that part of the tree with the new file. More clearly: your changes appear live on your screen without the page reloading.

In order for HMR to be used, we need a server from which the hot assets will be served. Webpack comes with a dev-server we can leverage for that, so let’s install it:

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

Now to run the dev server, nothing simpler, just run the following command:

$ webpack-dev-server –inline –hot

The first flag tells Webpack to inline the HMR logic into the page (instead of presenting the page in an iframe) and the second enables HMR. Now let’s visit the web-server at http://localhost:8080/webpack-dev-server/. You’ll see your usual page, but now try to modify one of the Sass files and, magic:

hot reload

You can use the webpack-dev-server as your own local server. If you plan to always use it for HMR, you can say so in your configuration:

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

Now whenever we run webpack-dev-server it’ll already be in HMR mode. Note that we used webpack-dev-server here to serve the hot assets but you can use several other options, like an Express server. Webpack provides a middleware that you can use to plug in HMR capabilities to other servers.

Get clean or die lintin

If you’ve been following this tutorial closely you may have noticed something weird: why are loaders nested in module.loaders but plugins are not? That’s because there are other things you can put into module of course! Webpack doesn’t just have loaders, it also has pre-loaders and post-loaders: loaders that are executed on the code before or after our main loaders. Let’s take an example: I’m sure the code I wrote for this article is horrendous, so let’s apply ESLint to our code before we transform it:

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

And let’s create a minimalistic .eslintrc file that I know will fail:

.eslintrc

parser: 'babel-eslint'
rules:
  quotes: 2

Now to add our pre-loader, we simply use the same syntax as before, but in module.preLoaders:

module:  {
    preLoaders: [
        {
            test: /\.js/,
            loader: 'eslint',
        }
    ],

And now if we run Webpack, sure enough it fails:

$ 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

Let’s take another example of pre-loader: for every component we currently import its stylesheet of the same name, and its template of the same name. Let’s use a pre-loader to automatically load any files bearing the same name as a module:

$ npm install baggage-loader –save-dev

{
    test: /\.js/,
    loader: 'baggage?[file].html=template&[file].scss',
}

This tells Webpack: if you encounter an HTML file of the same name, import it as template, and also import any Sass file of the same name. We can now change our components from this:

import $ from 'jquery';
import template from './Button.html';
import Mustache from 'mustache';
import './Button.scss';

To this:

import $ from 'jquery';
import Mustache from 'mustache';

As you can see pre-loaders can be pretty powerful, same goes for post-loaders. Take a look at the list of available loaders at the end of this article and you’ll surely find a lot of use cases in it.

Demonstrations

Boilerplate for express+react app with webpack builder

Boilerplate for express+angular app with webpack builder