Webpack经典入门

7,034 阅读22分钟

原文: Webpack your bags

October 16th, 2015 by Maxime Fabre
(中文译本更新于2017年10月22日,由于webpack已更新至3.8.1,因此译文有改动,译文最后有各分段代码包地址,如有需要请自行下载)


Webpack your bags

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

说实话,开始的时候我对于“webpack到底是什么“ 很模糊,并且感觉很受挫,最后我直接就把网页关了,毕竟我在这之前已经有一个构建系统的工具了,而且我用得也非常嗨皮,如果你像我一样一直密切跟踪javascript的发展的话,可能你早就因为在各种流行的东西上频繁地跳来跳去而灰飞烟灭了。还好现在我有些经验了,觉得可以写一篇文章来给那些还处在混沌中的小伙伴们,更清楚地解释一下webpack到底是什么,更重要的是它到底什么地方那么出色以至于值得我们投入那么多的精力。

1. Webpack是什么?

Webpack到底是一个构建系统还是一个模块管理器?好的,现在我马上来回答一下───答案是两个都是,当然我不是说它两个事儿都干,我的意思是它把两者有机地连接起来了,webpack不是先构建你的资源,然后再管理你的模块,而是把你的资源本身当做模块。

更准确的说,它不是先编译你所有的scss文件,再优化所有的图片,之后在一个地方引进来,再管理所有的模块,最后在另一个地方引到你的页面上。假设你是下面这样:

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"

如上面所示,你的所有的资源(无论是scss,html或者其它)都可以被当成模块,而且这些模块可以被import(引入),modify(修改),manipulate(操作),最后被打包到你的最终包(bundle)

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

{
  // 当你导入(import)一个a.ts文件时,系统将会用Typescript加载器去解析它
  test: /\.ts/,
  loader: 'typescript',
},
{
  // 当你的项目里面有图片时,系统将会用image-webpack加载器把它们压缩
  // 并且它们会被关联到data64 URLs
  test: /\.(png|jpg|svg)/,
  loaders: ['url', 'image-webpack'],
},
{
  // 当你的项目里面有scss文件时,系统将会通过node-sass加载器去解析,并且自动补充前缀
  // 最后返回css(可以直接被系统解析)
  test: /\.scss/,
  loaders: ['css', 'autoprefixer', 'sass'],
}

最后所有的loader返回的都是string,这样webpack最终可以把资源都包装成javascript模块。就像这个例子里scss文件被loaders转换后,看起来差不多是这样子:

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


2. 世界那么大,你为什么要用它?

一旦你明白webpack是什么后,很可能就会想到第二个问题:用它能有什么好处呢?“把Image和CSS放在我的JS里?什么鬼?”好吧,之前很长一段时间我一直被告诉要把所有文件都放在一个文件里,这样就保证不浪费我们的HTTP请求。

但这样会导致一个很大的缺点,就是现在大多数人都把所有的资源打包到一个单独的app.js文件里,然后把这个文件引入到每一个页面。这就意味着渲染每一个页面的时候大部分时间都浪费在加载一大堆根本就没用到的资源上。但是如果你不这么做,那么你很有可能得手动把这些资源引入到指定的页面,这就会导致需要一大团乱七八糟的依赖树去维护和跟踪一些问题例如:哪些页面需要依赖这个文件?修改a.css和b.css会影响到哪些页面?

因此这两个方法都是不对,但也不全是错的。如果我们把webpack当做一个中介───它不仅仅是一个构建系统或者一个打包工具,而是一个邪恶的智能打包系统,正确地配置后,它甚至比你还了解你的系统,而且它也比你清楚怎么样才能最好优化你的系统。

3. 我们来做一个小的app

为了让你更容易地理解webpack带来的好处,我们做一个小的app,然后用webpack来打包我们app的资源,在做之前我建议用Node 4及以上版本和NPM 3及以上版本,因为良好的依赖关系在你使用webpack的时候会避免很多让人头疼的问题,如果你的NPM版本不够新,你可以通过 npm install npm -g 来更新。

$ node --version
v6.11.2
$ npm --version
5.4.2

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

备注:全局安装 webpack 命令行:通过 npm install webpack -g 来更新。

基本引导

现在开始,先创建个名为webpack-your-bags的文件夹,在文件夹下安装webpack,并且也加上jquery好在后面证明一些东西

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

现在让我们来创建一个app入口,用现在的纯ES5

路径:webpack-your-bags/src/index.js

var $ = require('jquery');
$('body').html('Hello');

创建webpack的配置文件webpack.config.js,webpack.config.js是javascript,需要导出(export) 一个对象(object)
路径:webpack-your-bags/webpack.config.js

var path = require("path");//用于处理目录的对象,提高开发效率(__dirname需要引入path后才可以使用)
var ROOT_PATH = path.resolve(__dirname);//获取当前整个模块文件所在目录的完整绝对路径
var BUILDS_PATH = path.resolve(ROOT_PATH, "builds");//获取我们的builds目录完整绝对路径
module.exports = {
    entry: './src',
    output: {
        path: BUILDS_PATH,
        filename: 'bundle.js',
    },
};

这里,entry会告诉webpack哪个文件是你的app的入口点。这些是你的主文件,他们在依赖树的顶端,然后我们告诉它编译我们的资源到放在 builds 目录(路径:webpack-your-bags/builds)的bundle.js文件下,我现在再创建相应的 index.html
路径:webpack-your-bags/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: 65d56cd1e7dddf04958b
Version: webpack 3.8.1
Time: 250ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  271 kB       0  [emitted]  [big]  main
   [0] ./src/index.js 51 bytes {0} [built]
    + 1 hidden module

这里webpack会告诉你bundle.js包含了我们的入口点(index.js)和一个隐藏的模块,这个隐藏的模块就是jquery,默认情况下webpack会把第三方模块给隐藏掉,如果想要看见webpack编译的所有的模块 ,我们可以加 --display-modules 参数

$ webpack --display-modules
Hash: 65d56cd1e7dddf04958b
Version: webpack 3.8.1
Time: 263ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  271 kB       0  [emitted]  [big]  main
   [0] ./src/index.js 51 bytes {0} [built]
   [1] ./node_modules/jquery/dist/jquery.js 268 kB {0} [built]

如果你觉得每次修改都要重新执行webpack太麻烦,你也可以运行 webpack --watch 来自动监视文件的变化,如果文件有发生变化,便会自动重新编译。

搭建我们的第一个loader

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

现在我们来写一个button,首先假设你已经熟悉了ES2015(即ES6,javascript的新标准),因为有些浏览器还不支持ES6,所以我们需要babel来为我们把ES6转为浏览器支持的ES5 ,我们先加入babel的加载器(loader)。想要在webpack里安装一个loader,有两步需要做:1. npm install {whatever}-loader ;2.把它加到你的webpack配置文件(webpack.config.js)里的 module.loaders 部分,好的,那现在我们要加babel,所以先安装:

$ npm install babel-loader --save-dev

我们也得安装babel本身,因为现在我们这个例子加载器不会自动安装它们,因此我们需要装babel-core这个包和它的预设 es2015(也就是在代码被执行前执行的转换器版本):

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

--save-dev : 项目开发过程中需要依赖的包,发布之后无需依赖的包,例如我们的babel,开发过程需要babel为我们把书写的es6转为es5,发布之后由于我们所有书写的es6代码都被转为es5,因此无需继续依赖;
--save : 项目发布后依然需要依赖的包,例如我们jquery

我们现在创建 .babelrc 文件,告诉babel用哪个预设,这是一个json文件告诉babel在你的代码上执行哪种转换器,现在我们告诉它用es2015:
路径:webpack-your-bags/.babelrc

{
"presets": ["es2015"] 
}

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

var path = require("path");//用于处理目录的对象,提高开发效率(__dirname需要引入path后才可以使用)
var ROOT_PATH = path.resolve(__dirname);//获取当前整个模块文件所在目录的完整绝对路径
var BUILDS_PATH = path.resolve(ROOT_PATH, "builds");//获取我们的builds目录完整绝对路径
var SRC_PATH = path.resolve(ROOT_PATH, "src");//获取到我们的资源目录src的完整路径
module.exports = {
    entry: './src',
    output: {
        path: BUILDS_PATH,
        filename: 'bundle.js',
    },
    module: {
        loaders: [{
            test: /\.js/,
            loader: 'babel-loader',
            include: SRC_PATH,
        }],
    }
};

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

import $ from 'jquery';
$('body').html('Hello');

写个小组件

现在我们来写一个小的Button组件,它需要有一些SCSS样式(SCSS是SASS 3 引入新的语法,SASS是CSS的一种预处理器),一个HTML模板,和一些JS交互,那么我们先安装一些我们需要的东西。首先我们用一个非常轻量的模板包Mustache(前后端分离的上古框架,了解就好),我们也需要给SCSS和HTML配置loaders,因为结果会通过管道(pipe)从一个loader传到另一个,这里有点绕,怎么说呢,类似净水器,我们写的SCSS通过第一个loader净化之后变成CSS,CSS通过管道(pipe)流向第二个loader,CSS再通过第二个loader又变成可以被 import 的STYLE模块等等等过程,好了,所以我们需要一个 sass-loader 加载器来“净化”SCSS,一旦我们得到了CSS,会有多种方式处理它,这里,我们用一个 style-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-loader',
include: SRC_PATH,
}, {
test: /\.scss/,
loader: 'style-loader!css-loader!sass-loader',
// Or
//loaders: ['style-loader', 'css-loader', 'sass-loader'],
}, {
test: /\.html/,
loader: 'html-loader',
}

现在我们把loaders准备好了,我们来写一个button:
路径:webpack-your-bags/src/Components/Button.scss

.button {background: tomato; color: white; }

路径:webpack-your-bags/src/Components/Button.html

<a class="button" href="{{link}}">{{text}}</a>

路径:webpack-your-bags/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();

        // 渲染我们的按钮
        $(node).html(
            Mustache.render(template, {text})
        );

        // 增加监听事件
        $('.button').click(this.onClick.bind(this));
    }
}

你的Button现在是百分之百的完整体了,无论什么时候导入,无论运行在什么地方,它都有所有需要的东西,然后正确地渲染到那个地方,现在我们只需要把我们的Button通过index.js渲染到我们的网页上:

// import $ from 'jquery';
// $('body').html('Hello');

import Button from './Components/Button';
const button = new Button('google.com');
button.render('a');

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


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

代码拆分

上面这个例子还不错因为什么都有,但我们有时可能不需要我们的Button,因为有些界面如果没有 a 标签的话,那就不需要我们做无谓的把 a 标签渲染成 Button 的操作了。也就是说我们根本就不需要导入我们上面定制的Button的样式,模板,Mustache 和一切相关的东西了对吧?这个时候我们就需要代码拆分了。
代码拆分是webpack对我们去手动把Button导入需要的界面相关繁琐操作的解决方案,也就是有了webpack你根本不用去找哪个页面需要导入,哪个页面不用导入。代码拆分实质是在你的代码里定义分割点:你的代码可以轻松拆分开到部分单独文件中,然后按需加载,语法非常简单:

import $ from 'jquery';

// 这是一个拆分点
require.ensure([], () => {
  // 这里所有的代码都是需要被导入的
  // 在一个单独的页面里
  const library = require('some-big-library');
  $('foo').click(() => library.doSomething());
});

在 require.ensure 的回调(即() => {})里的任何东西都会被拆分成代码块(chunk) ─── 页面需要加载的时候会通过ajax单独加载的包,这意味着我们的包基本上有这些:

bundle.js
|- jquery.js
|- index.js // 我们所有文件都要导入的代码
chunk1.js
|- some-big-libray.js
|- index-chunk.js // 在回调里面的代码

你无需导入 chunk1.js ,webpack只有在需要它的时候才会去加载它,这意味着你可以把你的代码按照各种逻辑分成多块,我们将在下面修改我们的 index.js ,让只在页面里有 a 标签的时候才加载 Button:

if (document.querySelectorAll('a').length) {
    require.ensure([], () => {
        const Button = require('./Components/Button').default;
        const button = new Button('google.com');
        button.render('a');
    });
}

注意,用 require 的时候,如果你想获取Button对象 export default 的东西你需要通过 .default 去手动获取它,因为 require 不会同时处理 export default 和 exports.obj,所以你必须指定返回哪个,不过 import 可以处理这个,所以它已经知道了(例如: import foo from 'bar'获取到'bar'对象export default的东西, import {baz} from 'bar'获取到'bar'对象的exports.baz的东西)。这里的东西有点复杂,如果不是很理解又想理解可以去深入了解一下ES6和nodeJS。

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

$ webpack --display-modules --display-chunks
Hash: c419d385603afdd301ab
Version: webpack 3.8.1
Time: 1489ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js   307 kB       0  [emitted]  [big]
  bundle.js  6.48 kB       1  [emitted]         main
chunk    {0} 0.bundle.js 305 kB {1} [rendered]
    [1] ./src/Components/Button.js 1.92 kB {0} [built]
    [2] ./node_modules/jquery/dist/jquery.js 268 kB {0} [built]
    [3] ./src/Components/Button.html 70 bytes {0} [built]
    [4] ./node_modules/mustache/mustache.js 19.4 kB {0} [built]
    [5] ./src/Components/Button.scss 1.16 kB {0} [built]
    [6] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {0} [built]
    [7] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
    [8] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} [built]
    [9] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} [built]
chunk    {1} bundle.js (main) 615 bytes [entry] [rendered]
    [0] ./src/index.js 615 bytes {1} [built]
   [0] ./src/index.js 615 bytes {1} [built]
   [1] ./src/Components/Button.js 1.92 kB {0} [built]
   [2] ./node_modules/jquery/dist/jquery.js 268 kB {0} [built]
   [3] ./src/Components/Button.html 70 bytes {0} [built]
   [4] ./node_modules/mustache/mustache.js 19.4 kB {0} [built]
   [5] ./src/Components/Button.scss 1.16 kB {0} [built]
   [6] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {0} [built]
   [7] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
   [8] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} [built]
   [9] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} [built]

可以看到我们的入口( bundle.js )现在只有一些webpack的逻辑,其它的东西(jQuery, Mustache, Button) 都在 0.bundle.js ,只有页面上有 a 标签的时候才会加载 0.bundle.js,为了让webpack知道用ajax加载的时候在哪能找到chunks,我们必须在我们的配置中加一行:

output: {
    path: BUILDS_PATH,
    filename: 'bundle.js',
    publicPath: 'builds/',
},

output.publicPath 选项告诉 webpack 相对于当前的文件在哪能找到构建后的资源,现在访问我们的页面我们会看到一切都正常工作,但更重要的是,我们能看到,由于页面上有 a 标签,因此webpack准确地加载了我们拆分出来的代码块 0.bundle.js:


如果我们的页面上没有 a 标签,就只有 bundle.js 会被加载,这点可以让你智能地把你的app里的大片逻辑拆分开,让每个页面只加载它真正需要的,我们也可以给我们拆分出来的代码包的Chunk Names命名 ,我们可以用语义的名字,你可以通过传给 require.ensure 第三个参数来指定:

require.ensure([], () => {
const Button = require('./Components/Button').default;
const button = new Button('google.com');
button.render('a');
}, 'button');

这样就会生成代码包的 Chunk Names 就是 button 而不是空白了:

$ webpack
Hash: 50ed6a7993b581f0bf0a
Version: webpack 3.8.1
Time: 1524ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js   307 kB       0  [emitted]  [big]  button
  bundle.js  6.49 kB       1  [emitted]         main
   [0] ./src/index.js 625 bytes {1} [built]
   [1] ./src/Components/Button.js 1.92 kB {0} [built]
   [3] ./src/Components/Button.html 70 bytes {0} [built]
   [5] ./src/Components/Button.scss 1.16 kB {0} [built]
   [6] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {0} [built]
    + 5 hidden modules

加入第二个组件

现在什么都有了已经非常酷了,不过我们再来加一个组件看看好不好使:
路径:webpack-your-bags/src/Components/Header.scss

.header {
  font-size: 3rem;
}

路径:webpack-your-bags/src/Components/Header.html

<header class="header">{{text}}</header>

路径:webpack-your-bags/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 })

        );

    }

}

我们在我们的 index.js 里把它渲染一下:

// 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 来看一下 webpack 的 output:

$ webpack --display-modules --display-chunks
Hash: 66f9e900ac553f5d66eb
Version: webpack 3.8.1
Time: 1646ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js   306 kB       0  [emitted]  [big]
1.bundle.js   307 kB       1  [emitted]  [big]
  bundle.js  6.56 kB       2  [emitted]         main
chunk    {0} 0.bundle.js 305 kB {2} [rendered]
    [2] ./src/Components/Header.js 1.7 kB {0} [built]
    [3] ./node_modules/jquery/dist/jquery.js 268 kB {0} {1} [built]
    [4] ./node_modules/mustache/mustache.js 19.4 kB {0} {1} [built]
    [5] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} {1} [built]
    [6] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} {1} [built]
    [7] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} {1} [built]
   [11] ./src/Components/Header.html 62 bytes {0} [built]
   [12] ./src/Components/Header.scss 1.16 kB {0} [built]
   [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
chunk    {1} 1.bundle.js 305 kB {2} [rendered]
    [1] ./src/Components/Button.js 1.92 kB {1} [built]
    [3] ./node_modules/jquery/dist/jquery.js 268 kB {0} {1} [built]
    [4] ./node_modules/mustache/mustache.js 19.4 kB {0} {1} [built]
    [5] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} {1} [built]
    [6] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} {1} [built]
    [7] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} {1} [built]
    [8] ./src/Components/Button.html 70 bytes {1} [built]
    [9] ./src/Components/Button.scss 1.16 kB {1} [built]
   [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
chunk    {2} bundle.js (main) 601 bytes [entry] [rendered]
    [0] ./src/index.js 601 bytes {2} [built]
   [0] ./src/index.js 601 bytes {2} [built]
   [1] ./src/Components/Button.js 1.92 kB {1} [built]
   [2] ./src/Components/Header.js 1.7 kB {0} [built]
   [3] ./node_modules/jquery/dist/jquery.js 268 kB {0} {1} [built]
   [4] ./node_modules/mustache/mustache.js 19.4 kB {0} {1} [built]
   [5] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} {1} [built]
   [6] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} {1} [built]
   [7] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} {1} [built]
   [8] ./src/Components/Button.html 70 bytes {1} [built]
   [9] ./src/Components/Button.scss 1.16 kB {1} [built]
  [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
  [11] ./src/Components/Header.html 62 bytes {0} [built]
  [12] ./src/Components/Header.scss 1.16 kB {0} [built]
  [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]

能看见这里有一个问题:我们的两个组件都会用到 jQuery 和 Mustache,也就是说这两个依赖在chunk里有重复,虽然webpack 默认会做一点点的优化,但是它也可以以 plugin(插件) 的形式来给 webpack 提供更强大的功能。

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

在我们现在的例子中,我们需要把公用的依赖移到我们的entry(入口) 文件,如果所有的文件需要jQuery 和 Mustache,我们也可以把它往上层移动,我们来更新一下我们的配置文件webpack.config.js :

......
var webpack = require('webpack');
module.exports = {
    entry:   './src',
    output:  {
      // ...
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name:      'main', // 将依赖移到我们的主文件
            children:  true, // 在所有被拆分的代码块中寻找共同的依赖关系
            minChunks: 2, // 在被提取之前,一个依赖要出现多少次(也就是一个依赖会在遍历所有拆分的代码块时被重复发现多少次)
        }),
    ],
    module:  {
      // ...
    }
};

如果我们重新运行一下 webpack --display-modules --display-chunks,我们可以看到现在比之前好多了,因为 0.bundle.js 和 1.bundle.js 的共同部分都被移到了 bundle.js 。

$ webpack --display-modules --display-chunks
Hash: 22fbf6bdd63c4bbbb096
Version: webpack 3.8.1
Time: 1554ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js  3.36 kB       0  [emitted]
1.bundle.js  3.58 kB       1  [emitted]
  bundle.js   310 kB       2  [emitted]  [big]  main
chunk    {0} 0.bundle.js 3.12 kB {2} [rendered]
    [7] ./src/Components/Header.js 1.7 kB {0} [built]
   [11] ./src/Components/Header.html 62 bytes {0} [built]
   [12] ./src/Components/Header.scss 1.16 kB {0} [built]
   [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
chunk    {1} 1.bundle.js 3.37 kB {2} [rendered]
    [6] ./src/Components/Button.js 1.92 kB {1} [built]
    [8] ./src/Components/Button.html 70 bytes {1} [built]
    [9] ./src/Components/Button.scss 1.16 kB {1} [built]
   [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
chunk    {2} bundle.js (main) 303 kB [entry] [rendered]
    [0] ./src/index.js 601 bytes {2} [built]
    [1] ./node_modules/style-loader/lib/urls.js 3.01 kB {2} [built]
    [2] ./node_modules/jquery/dist/jquery.js 268 kB {2} [built]
    [3] ./node_modules/mustache/mustache.js 19.4 kB {2} [built]
    [4] ./node_modules/css-loader/lib/css-base.js 2.26 kB {2} [built]
    [5] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {2} [built]
   [0] ./src/index.js 601 bytes {2} [built]
   [1] ./node_modules/style-loader/lib/urls.js 3.01 kB {2} [built]
   [2] ./node_modules/jquery/dist/jquery.js 268 kB {2} [built]
   [3] ./node_modules/mustache/mustache.js 19.4 kB {2} [built]
   [4] ./node_modules/css-loader/lib/css-base.js 2.26 kB {2} [built]
   [5] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {2} [built]
   [6] ./src/Components/Button.js 1.92 kB {1} [built]
   [7] ./src/Components/Header.js 1.7 kB {0} [built]
   [8] ./src/Components/Button.html 70 bytes {1} [built]
   [9] ./src/Components/Button.scss 1.16 kB {1} [built]
  [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
  [11] ./src/Components/Header.html 62 bytes {0} [built]
  [12] ./src/Components/Header.scss 1.16 kB {0} [built]
  [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]

你也可以通过不提供一个公用的块名(chunk name)或者是指定 async: true 来让公用的dependency被异步加载,webpack有非常多这样强大的智能优化。我不可能全列出来,但作为练习,让我们尝试来给我们的app建一个生产版本。

生产和优化

好吧,首先,我们加几个 plugin 到配置文件中,但我们只想在 NODE_ENV 等于 production 时才加载这些 plugin ,所以让我们来给配置文件加一些逻辑,因为只是一个 js 文件,所以比较简单:

......
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_PATH,
        filename:   'bundle.js',
        publicPath: 'builds/',
    },
    plugins: plugins,
    ......
};

第二步,Webpack有一些设置我们也可以在生产中关闭:

......
if (production) {
    plugins = plugins.concat([

        new webpack.LoaderOptionsPlugin({
            debug: true
        }),
        
        ......
    ]);
}

module.exports = {
	//debug:   !production,
    //loaders的调试模式已经在webpack3及以上版本完全移除,为了保持与旧的loaders的兼容性,loaders可以通过LoaderOptionsPlugin插件切换到调试模式
    devtool:production ? false : 'eval',
    ......
    }

第1个设置为非debug模式,这意味着系统不会处理太多的代码,让你可以更轻松地调试本地的资源,第2个是控制 sourcemap 生成的,webpack有几个方法可以呈现 sourcemap,自带的 eval 是最好的,生产环境我们不太关心 sourcemap 的事,所以我们把它禁用掉,下面加一下我们的生产插件(plugins):

if (production) {
    plugins = plugins.concat([

        // 这个插件会寻找类似的块和文件然后进行合并
        //new webpack.optimize.DedupePlugin(),
        //webpack3版本之后这个插件已经被移除

        // 这个插件会根据模块在你的程序中使用的次数来优化模块
        //new webpack.optimize.OccurenceOrderPlugin(),
        //webpack2版本之后这个插件的功能已经是默认开启,无需调用

        // 这个插件可以防止webpack创建太小以至于不值得单独加载的块
        new webpack.optimize.MinChunkSizePlugin({
            minChunkSize: 51200, // ~50kb
        }),

        //这个插件会最小化所有最终资源的javascript代码
        new webpack.optimize.UglifyJsPlugin({
            mangle:   true,
            compress: {
                warnings: false, //  阻止难看的警告
            },
        }),
        //loaders的最小化模式也会在webpack3或者以后的版本中移除

        // 这个插件允许我们可以在生产中设置各种为错误的变量,以避免它们被编译在我们的最后打包的代码中
        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),自行按需选择。

关于生产环境下的资源的另一个方面是,我们有时想给我们的打包后的资源加版本,还记得在上面我们把 output.filename 设置为 bundle.js 吧,这有几个变量可以在命名的时候用,其中一个是 [hash] ,代表最终资源文件的版本,同时用 output.chunkFilename 给chunk也加上版本,现在让我们来修改我们的配置文件:

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

由于没有什么特别好的办法能动态地获取这个简化版app编译后的bundle的名字(由于加了版本号),所以只是在生产环境上给资源加版本,多次编译后目录里带版本的最终包会越来越多,所以为了节省空间,还得加一个第三方插件来每次清理我们之前的生产版本(output 里面的 path):

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

并把它添加到我们的配置里:

var webpack     = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');

// ...

if (production) {
    plugins = plugins.concat([

        // 清理以前的版本/文件夹
        // 编译我们的最终资源
        new CleanPlugin('builds'),
        ......
        }
        ......

OK,做完了几个小优化,下面比较一下结果:

$ webpack
Hash: 8b942bcf553d3d782e6c
Version: webpack 3.8.1
Time: 1542ms
                    Asset     Size  Chunks                    Chunk Names
0-294790a46bbc09afc580.js  4.34 kB       0  [emitted]
1-24fa8b23bd35ecac8a8f.js  4.57 kB       1  [emitted]
                bundle.js   348 kB       2  [emitted]  [big]  main
......
$ NODE_ENV=production webpack
clean-webpack-plugin: D:\other\webpack-your-bags\builds has been removed.
Hash: 22674bf33e7cdefb2776
Version: webpack 3.8.1
Time: 2973ms
                       Asset    Size  Chunks             Chunk Names
main-22674bf33e7cdefb2776.js  101 kB       0  [emitted]  main
......

那么webpack到底做了什么呢?首先,由于我们的例子是非常轻量级的,所以我们的两个异步块不是HTTP请求,因此Webpack将它们合并到入口。而且所有的东西都被最小化了,我们从总共356KB的3个HTTP请求到101KB的1个HTTP请求。

但webpack竟然能毫无征兆地就把一些大的js文件给消除掉?

是的,的确是这样,但这只是因为我们的应用程序非常的小才发生的。现在考虑到一点:你根本想不到什么被合并,并且在什么地方什么时候被合并。如果你拆分的代码块突然有了更多的依赖,那么这些拆分块将被移动到一个异步块,而不是被合并。 如果这些拆分块过于相似以至于不值得单独加载,它们将被合并,你仅仅需要配置一下,webpack就会用最好的方式自动优化你的应用程序。不用你去手动操作,也不用去想依赖在哪些时候或者哪些地方需要被依赖,一切都是全自动。


你可能已经注意到我们没有使用任何设置来最小化我们的HTML和CSS,那是因为在默认情况下,如果调试(debug)选项是我们之前所说的false的话, css-loader 和 html-loader 就会去帮我们做这件事。这也是为什么UglifyJsPlugin是一个单独的插件(而不是一个loader)的原因:因为在webpack中没有js-loader,因为webpack中本身就是JS的加载器。

提取

好吧,现在你可能已经注意到,由于本教程的一开始,我们的样式就已经在网页里面,并且做出了一个非常丑的按钮。现在如果我们可以把所有的样式通过webpack构建出一个最终的CSS文件,岂不是很好?我们当然可以啦,让我们在一些外部的插件(plugin)的帮助下实现这种操作:

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

这个插件具体是做什么的我来说一下:就是从你的最终的资源中提取一些指定类型的内容,这个经常用在CSS上,因此让我们来配置一下webpack.config.js(在这里切合版本的更替,我们将用module.rules替换module.loaders,因为在未来module.loaders将完全被module.rules代替):

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:  {
		rules: [{
			test: /\.js/,
			loader: 'babel-loader',
			include: SRC_PATH,
		},{
			test: /\.html/,
			loader: 'html-loader',
		},{
			test: /\.scss$/,
			use: ExtractPlugin.extract({
				fallback: 'style-loader',
				use: ['css-loader', 'sass-loader']
			})
		}],
        // loaders: [{
		// 	test: /\.js/,
		// 	loader: 'babel-loader',
		// 	include: SRC_PATH,
		// },{
		// 	test: /\.html/,
		// 	loader: 'html-loader',
		// },
		// {
		// 	test: /\.scss/,
		// 	loader: 'style-loader!css-loader!sass-loader',
		// 	// Or
		// 	// loaders: ['style-loader', 'css-loader', 'sass-loader'],
		// }
		// ]
    }
};

现在 rules 的 extract 方法有两个参数:第一个是当我们在一个代码块('style-loader')中,未提取的CSS该怎么办,二是当资源在一个主文件('css-loader!sass-loader')中时该怎么转换。现在如果我们在一个代码块中,那么在使用 loader 前我们不能只是奇迹般地让这个代码块中的CSS追加到生成的CSS中,但对于那些在主文件中找到的样式,它们就应该被打包到一个builds/bundle.css文件中。现在让我们来测试一下,添加一个小主样式表到我们的应用程序中:
路径:webpack-your-bags/src/styles.scss

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

修改我们的 index.js :

import './styles.scss';
......

让我们运行webpack,果然我们现在有一个bundle.css,现在我们可以在我们的HTML文件导入:

$ webpack
Hash: 6890269852e3f4363514
Version: webpack 3.8.1
Time: 1607ms
                    Asset      Size  Chunks                    Chunk Names
0-4a6171ca8018a700cec4.js   4.26 kB       0  [emitted]
1-ffe316264a9c9fafbb4c.js   4.58 kB       1  [emitted]
                bundle.js    348 kB       2  [emitted]  [big]  main
               bundle.css  70 bytes       2  [emitted]         main
......


这个时候刷新页面可能会出错,因为你必须将所有CSS文件合并到一个文件里面去,你可以传递选项ExtractTextPlugin('bundle.css', {allChunks: true}) ,你也可以在这里的文件名使用变量,如果你想要一个版本的样式你只需要做ExtractTextPlugin('[name]-[hash].css')

载入图片

现在对我们所有的JavaScript文件来说是非常完美的了,但我们还没有谈论的一个话题是具体的资源:图像、字体等在webpack里是如何工作的并且是如何经过优化的呢?下面让我们从网站上拷一张图片,并且我们将使用它作为我们的网页背景,因为我见过人们把它放在Geocities上而且看起来挺酷的:


让我们保存这一张图片在 webpack-your-bags/img/puppy.jpg ,并相应地更新我们的styles.scss:

body{
	font-family:sans-serif;
	background:url('../img/puppy.jpg') no-repeat 0 0;
	background-size:50%;
}

如果你这样做, webpack 会理直气壮地告诉你“它吗的到底是做个什么png出来”,因为我们没有加载它。这里有两个原生的加载器,我们可以用它们来处理具体的资源: file-loader 和 url-loader : 第一个将只返回一个没有什么特别变化的资源的URL,让你得到一个在这个过程中产生的版本文件(这是默认的行为) ;第二个将资源内联到一个 data:image/jpeg;base64 的地址。

事实上这两个插件不是对立的关系:比如你的背景是一个2MB的图像,那就不要内联,最好单独加载它。另一方面如果它是一个2KB的小图标,最好内联,节省HTTP请求。所以我们就设置了两个:

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

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

在这里,我们向 url-loader 传递一个 limit 查询参数告诉它:如果资产大于10KB或者更小则内联,否则,就退回到了file-loader并引用它。该语法被称为查询字符串,你用它来配置加载器(loader),或者你也可以通过一个对象配置加载器:

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

好吧,让我们来看看结果:

$ webpack
Hash: e672f7becf7b049e759d
Version: webpack 3.8.1
Time: 1653ms
                    Asset     Size  Chunks                    Chunk Names
0-4a6171ca8018a700cec4.js  4.26 kB       0  [emitted]
1-ffe316264a9c9fafbb4c.js  4.58 kB       1  [emitted]
                bundle.js   348 kB       2  [emitted]  [big]  main
               bundle.css  2.88 kB       2  [emitted]         main

正如我们看到的那样:没有一个JPG被打包,因为我们的小狗图片(2KB)比配置的尺寸(10KB)小,所以它就被内联了。这意味着如果我们访问我们的网页,无需加载图片我们就可以沉浸在我们的小狗霸主的荣耀下了。


这是非常强大的,因为这意味着webpack现在可以根据大小/HTTP请求的比值来智能优化具体的资源。有一个良好的加载器,你甚至可以进一步的把所有东西联系起来,最常见的一个是image-loader,将在打包它们之前压缩所有图片。它甚至有一个?bypassOnDebug查询参数让你只在生产的时候做这种事。有很多这样的插件,我鼓励你在本文的结尾看看那些插件。

我们现在将把它做活起来

我们上面所做的建设都只是为了体现webpack的强大功能,现在让我们更多的关注本地代码调试。
可能当你提到构建工具时,经常注意到的一个比较大的缺陷:实时加载:LiveReload,BrowserSync,不需要等太久。但是当我们修改一点什么东西的时候我们希望整个页面会自动进行刷新,让它一步就做到我们想要的效果的就是所谓的模块更换或热重载。它的想法是,既然webpack知道我们依赖树的每个模块的位置,那么一个小小的变化就能用新文件简单地修补来体现。更清楚的表达就是:你所做的修改实时地表现在页面上但是你却没发现页面已经重新刷新过了。

为了要使用HMR(热模块加载),我们需要一个能为我们的热资源服务的服务器。webpack 中有自带的 dev-server,我们可以利用这个,所以让我们来安装它:

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

现在运行我们开发的服务器,没有比这个更简单的了,只需要运行下面的命令:

$ webpack-dev-server

现在请让我们访问web服务器 http://localhost:8080/webpack-dev-server/ 。你会看到你平时的页面,但是现在我们修改一个SASS文件,像变魔术一样:


现在,每当我们运行 webpack-dev-server 它就已经是HMR模式。请注意,我们使用 webpack-dev-server 这里是为了服务我们的热资源,但你也可以使用其他几个选项,像 Express server 。webpack 提供一个中间件,并且你可以使用这个中间件把即插即用的HMR移到其他服务器上。

干净或者凋零

如果你一直在跟进这门教程,你可能已经注意到一些奇怪的东西:为什么加载器嵌套在 module.rules(module.loaders) 但插件却不是呢?当然这是因为你还可以向 module 投入其它的东西了!webpack 不只是有加载器,它还具有前加载器,后加载器:在代码执行之前或之后,我们主要的加载器。让我们举个例子:我敢肯定,我写这篇文章的代码是可怕的,所以我们申请在我们改造它之前 ESLint 我们的代码:

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

现在让我们创建一个简单的.eslintrc文件,我知道等一下会运行失败:
路径:webpack-your-bags/.eslintrc

{
    "parser": "babel-eslint",
    "rules": {
        "quotes": 2
    }
}

现在加入我们的前加载器,我们只是像以前一样使用相同的语法,只是在 module.preLoaders 里(module.preLoaders已经在webpack2版本之后移除,需要用到preLoader的地方可以改到rules的enfore中进行配置):

module:  {
        // preLoaders: [{
        //     test: /\.js/,
        //     loader: 'eslint-loader',
        // }],
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            enforce: "pre",
            loader: "eslint-loader"
        }],
}

现在,如果我们运行webpack,果然失败:

$ webpack
Hash: 891f6c68504d0f787afa
Version: webpack 3.8.1
Time: 2335ms
                    Asset     Size  Chunks                    Chunk Names
0-4a6171ca8018a700cec4.js  4.26 kB       0  [emitted]
1-ffe316264a9c9fafbb4c.js  4.58 kB       1  [emitted]
                bundle.js   348 kB       2  [emitted]  [big]  main
               bundle.css  2.88 kB       2  [emitted]         main
   [0] ./src/index.js 627 bytes {2} [built] [1 error]
   [1] ./src/styles.scss 41 bytes {2} [built]
   [7] ./src/Components/Button.js 1.92 kB {1} [built] [1 error]
   [8] ./src/Components/Header.js 1.7 kB {0} [built] [1 error]
   [9] ./src/Components/Button.html 70 bytes {1} [built]
  [10] ./src/Components/Button.scss 1.16 kB {1} [built]
  [11] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
  [12] ./src/Components/Header.html 62 bytes {0} [built]
  [13] ./src/Components/Header.scss 1.16 kB {0} [built]
  [14] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
  [15] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/styles.scss 297 bytes [built]
  [16] ./img/puppy.jpg 2.8 kB [built]
    + 5 hidden modules

ERROR in ./src/Components/Button.js

D:\other\webpack-your-bags\src\Components\Button.js
   1:15  error  Strings must use doublequote  quotes
   2:22  error  Strings must use doublequote  quotes
   3:22  error  Strings must use doublequote  quotes
   4:8   error  Strings must use doublequote  quotes
  25:11  error  Strings must use doublequote  quotes
......

上面报错留给大家自己解决,温馨提示:将报错内容“Strings must use doublequote quotes”翻译一下。如果暂时解决不了则注释掉 eslint 相关内容接着进行下面内容。
让我们运行前加载器的另一个例子:对于每个组件我们导入其同名样式表,并且具有相同名称的模板。让我们用一个前加载器来自动加载任何带有相同的名称的文件作为一个模块:

$ npm install baggage-loader --save-dev

{
	test: /\.js/,
	enforce: "pre",
    loader: 'baggage-loader?[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';

正如你所看到的如此强大的前加载器一样,后加载器也是如此。看看可用的装载机在本文末尾的列表,你一定会发现很多用例在里面。看看这篇文章的结尾那个可用的加载器列表,你肯定会发现更多用例。

附加

关于 webpack-dev-server 我们不仅可以通过直接运行命令行 webpack-dev-server 开启我们的web服务器,而且可以在 package.json 文件 scripts 中配置快捷命令行,如:

"scripts": {
  "start:dev": "webpack-dev-server"
}

然后我们就可以直接运行命令行 npm run start:dev (等同于 webpack-dev-server )来开启我们的web服务器了。

你想了解更多吗?

目前,我们的应用程序是相当小的所以还是看不出 webpack 的优势,但随着这个应用程序越来越大,webpack 就会变得有用,你也能够得到更多的领悟,我们实际的依赖关系树是什么。我们可能会做对或做错,我们的应用程序什么时候会变得寸步难行等等等。现在应用程序的内部,webpack 知道一切,但你要有礼貌地让它展示它所知道的。您可以通过产生一个配置文件来运行以下命令:

webpack --profile --json > stats.json

第一个标志(--profile)告诉 webpack 生成一个配置文件,第二个(--json)产生它的JSON,最后一个(> stats.json) 我们让这一切都输出到JSON文件。现在有很多个网站都在分析这些配置文件,但 webpack 提供了一个官方的来一一解说这个信息。所以去 webpack 分析和导入你的JSON文件。现在进入“Modules”选项卡,你应该会看到你的依赖关系树的可视化表示:


一个点越红,越对你最后的包有影响。在我们这个应用程序下,这个点是 Jquery ,因为它在我们所有的模块是最重要的。看到所有选项卡,看一下周围,你不会在我们这个小应用程序里学到太多,但这个工具却是非常重要的因为它能洞察到你的依赖树和最终资源。现在,正如我所说的,其它的服务能够提供洞察你的配置文件的功能,另外一个我喜欢的是 Webpack Visualizer: 它用一个饼图表示你的应用程序的依赖占用了一个什么空间,当然它也能表示我们的案例:


这是我们的全部

现在我所知道的情况是:webpack 已经完全取代 grunt 和 gulp:我之前大部分都是用它们但是现在我是用 webpack 来处理,而对于其余的一小部分我只是用NPM脚本。每个例子,我们过去有一个共同的任务是我们的API文档通过 Aglio 转换为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 中还具有节点的API,因此它可以很容易地在其它构建系统中使用,在任何情况下,你会发现它的思想无处不在。

无论如何,我认为这是可以为你做的一个 webpack 的足够好的鸟瞰图。你可能会认为,我们已经在这篇文章中介绍了很多,但我们认为只是皮毛而已:多个入口点,预取,背景更换等等。webpack 是一个令人印象深刻并且功能强大的工具,这当然是因为它比传统的构建工具更加不透明,我不会否认这一点。但是,一旦你知道如何驯服它,它就会发出甜美的声音进入你的耳朵让你回味无穷。我把它用在几个项目里,它提供了优化和自动化,我不能想象自己要来回敲打脑袋多少次才能知道所需要的资源什么时候在什么地方。

资源

备注

各分段代码包已经上传到GitHub,访问地址:

github.com/hrh94/Webpa…