[译] Webpack 前端构建集成方案

构建工具逐渐成为前端工程必备的工具,Grunt、Gulp、Fis、Webpack等等,译者有幸使用过Fis、Gulp。
前者是百度的集成化方案,提供了一整套前端构建方案,优势是基本帮你搞定了,可是灵活性相对比较低,社区也没那么大;后者提供了很是灵活的配置,简单的语法能够配置出强大的功能,流控制也减小了编译时的时间,能够和各类插件配合使用。
译者由于要使用AMD模块机制,开始接触了webpack,发现官网上讲的晦涩难懂,没法实践,而国内虽有博客也讲解一些入门的教程,可是要么篇幅太短,要么只讲各类配置贴各类代码,而后谷歌发现了国外大牛写的这篇博客,发现讲的很是通俗易懂,配合实践和代码,让译者感慨万千,瞬间打开了一扇大门。javascript

原文连接:https://blog.madewithlove.be/post/webpack-your-bags/
做者:Maxime Fabre
译者:陈坚生css


也许你已经据说过这个叫作webpack的新工具了。有些人称它是一个像gulp同样的构建工具, 有些人则认为它是像browserify同样的打包工具, 若是你并无深刻去了解它你可能就会产生疑惑。就算你仔细地研究它你也可能依旧困惑,由于webpack的官网介绍webpack的时候同时提到了这两个功能。html

一开始对"webpack 是什么"的模糊概念使得我很挫败以致于我直接关掉了webpack的网页。到了如今,我已经有了一套本身的构建系统并为此以为很开心。若是你和我同样紧跟javascript的潮流,那么错过如此好的工具将是很是惋惜的事情。(这句话翻译的很差:And if you follow closely the very fast Javascript scene, like me, you’ve probably been burnt in the past by jumping on the bandwagon too soon. )由于对webpack有了必定的实践和经验,我决定写这篇文章来更加清晰地解释“什么是webpack”还有webpack的重要性和优点。前端

什么是webpack?

首先让咱们来回答标题中的问题:webpack究竟是一个构建系统仍是一个打包工具?好吧, 它都有——但不是说它作了二者而是说它合并了二者。webpack并不构建你的资源(assets),而后分别对你的模块进行打包,它认为你的资源都是模块vue

更精确地说webpack并非构建全部的sass文件,优化你的图片,并将它们包括在一边,而是打包你全部的模块,而后在另外一个页面引用它们,像这样:java

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>"

你全部的资源都被认为是模块,所以是能够被引用的、修改、操做,最后能够被打包进你的终极模块中。node

为了使得这样可以运行,你须要在你的webpack配置中注册loaders。 Loaders 能够认为是一些小型的插件,简单地说就是让webpack在处理的时候,当遇到这种类型的文件时,作这样的操做(操做就是Loaders也就是你的配置)。如下是Loaders配置的一些例子:react

{ // When you import a .ts file, parse it with Typescript 
    test: /\.ts/, 
    loader: 'typescript',
},{
    // When you encounter images, compress them with image-webpack (wrapper around imagemin) 
    // and then inline them as data64 URLs 
    test: /\.(png|jpg|svg)/, 
    loaders: ['url', 'image-webpack'],
},{ 
    // When you encounter SCSS files, parse them with node-sass, then pass autoprefixer on them 
    // then return the results as a string of CSS 
    test: /\.scss/, 
    loaders: ['css', 'autoprefixer', 'sass'],
}

总之到食物链的末尾,全部的Loaders返回字符串。这个机制使得Webpack能够将它们引进到javascript的包中。当你的sass文件被Loaders转化后,它在内部会像这样被传递:jquery

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

为何要这样作?

当你明白webpack作了什么后,随之而来的问题大部分是这样作的好处是什么? “图片、css在个人JS中?这是什么鬼?” 好吧,思考下咱们最近一直推崇的并被教育应该这样作的,把全部的东西打包成一个文件,以减小http请求……webpack

这致使了一个很大的缺点就是大多数人把当前的全部资源都打包到一个app.js文件中,而后包含在全部的页面。这意味着任何给定页面上大部分加载的资源都是非必须的。若是你不这样作,那么你极可能要手工引入资源,致使须要维护和跟踪一个巨大的依赖书,用来记录那个页面用到了样式表A和样式表B。

不管方法是正确的仍是错误的。 想象一下webpack做为一个中间者,它不止是一个构建系统或者一个打包工具,它是一个顽皮的智能模块包装系统。一旦被很好地配置,它会比你更加了解你的栈,因此它会比你更加清楚如何更好地优化。

让咱们一块儿构建一个简单的APP

为了让你更简单地了解webpack的优势,咱们将一块儿构建一个小型的App并对资源进行打包。对于本教程,我建议运行Node4或者Node5以及NPM3的平行依赖树以免在使用webpack时遇到坑爹地问题。若是你尚未NPM3,你能够经过

npm install npm@3 -g

来安装。

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

我也建议你添加node_modules/.bin 到你的环境变量中以免每次都输入 node_modules/.bin/webpack 来运行命令。后面的全部例子我将不会使用node_modules/.bin这个命令了。

基本的使用

让咱们建立咱们的项目并安装webpack,同时咱们引入jQuery以便后面使用。

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

如今,让咱们建立项目的入口,并使用es2015:

src/index.js

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

而后建立咱们的webpack配置,文件名为webpack.config.js, webpack的配置文件是一个javascript,而且须要export成一个object(对象)

webpack.config.js

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

在这里,entry告诉webpack那些文件是你应用的入口。入口文件位于你依赖树的顶部。而后咱们告诉它去编译咱们的文件到__builds__这个文件夹中并使用((bundle.js这个名字。接下来咱们建立咱们的index.html:

<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>webpack</title>
    </head>
    <body>
        <h1>my title</h1>
        <a href="">click me</a>
        <script src="builds/bundle.js"></script>
    </body>
    </html>

运行webpack,若是一切运行正常,咱们会收到一段信息告诉咱们webpack成功编译打包到了bundle.js中:

Version: webpack 1.13.1
Time: 382ms
    Asset    Size  Chunks             Chunk Names
bundle.js  267 kB       0  [emitted]  main
   [0] ./src/index.js 58 bytes {0} [built]
    + 1 hidden modules

在这里你能够看到webpack告诉你你的bundle.js包含了咱们的入口文件index.js同时还有一个隐藏的模块。这个隐藏的模块即是jquery,webpack默认会隐藏不属于你的模块,若是要看全部被webpack隐藏的模块,咱们能够向webpack传参 --display-modules:

>webpack --display-modules
Hash: 20aea3445ac35ac27c32
Version: webpack 1.13.1
Time: 382ms
    Asset    Size  Chunks             Chunk Names
bundle.js  267 kB       0  [emitted]  main
   [0] ./src/index.js 58 bytes {0} [built]
   [1] ./~/jquery/dist/jquery.js 258 kB {0} [built]

你也能够运行 webpack --watch 让webpack去监听你的文件,一旦有改变则自动编译。

创建咱们的第一个Loader

还记得咱们讨论过的webpack如何引进css和html以及其余全部类型的资源吗?在那里适合?若是你有投身到这几年的web组件化发展的事业中(angular2, vue, react, polymer, x-tag等等),你应该据说过关于构建webapp的一个新的概念,不适用单一集成的ui模块,而是将ui分解为多个小型的可重用的ui。如今为了让组件真正独立,他们须要可以将全部依赖都引入他们自身中。想象一下一个按钮确定有html、一些脚本让它可以交互,固然也须要一些样式。最好能在须要到这个组件的时候全部这些资源才被加载。只有当咱们引入这个按钮的时候,咱们才拿到相关的资源。

让咱们来写button组件。首先,我假设大多数人都习惯了es2015,咱们将添加第一个Loader: babel。安装Loader于webpack中须要作两件事情:**npm install {whatever}-loader, 而后添加它到你webpack配置中,即module.loaders。以下所示:

$ npm install babel-loader --save-dev

因为babel-loader并不会自动安装babel, 咱们须要本身安装babel-core还有es2015 preset:

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

而后咱们建立.babelrc来告诉babel应该用哪种preset,文件是json格式,在本例子中,咱们告诉它使用es2015 preset

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

如今已经配置并安装好babel了。咱们须要babel运行在全部的以.js结尾的文件中,可是因为webpack会遍历包括第三方在内的全部依赖包,所以咱们要防止babel运行在如jquery这样的第三方库中。Loaders能够拥有一个include或者一个exclude规则,它能够是一个字符串、一个正则表达、一个回调函数或者其余任何你想要的。在本例子中,咱们想要babel只运行在咱们的文件上,所以咱们将include咱们的资源文件夹:

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

如今咱们能够重写咱们的index.js(咱们在以前引入了babel)。而且接下来的例子咱们也将使用es6

写一个小型的组件

如今咱们开始写一个小型的button组件, 它将有一些scss样式,一个html模板,还有一些行为。咱们将安装咱们须要的东西。手下咱们须要mustache,一个很是轻量级的模板渲染库,同时还有sasss和html的Loaders。同时,因为Loader能够像管道同样将处理后的结果顺序传递下去,咱们将须要一个cssloader来处理sass Loader处理后的结果。如今,咱们有了咱们的css, 有不少方式能够处理他们,此次咱们使用的是style-loader,它能够动态地将css注入到页面中去。

$ npm install mustache --save
$ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev

咱们由右到左以‘!’为分割向配置文件传递loader以告诉webpack如何将匹配到的文件顺序传递给Loaders,你也可使用数组来进行传递,固然顺序也要是由右到左

module.exports = {
    entry: './src',
    output: {
        path: 'builds',
        filename: '[name].js',
    },
    module: {
        loaders: [
            {
                test: /\.js/,
                loader: 'babel',
                include: __dirname + '/src',
            }
        ],
        {
            test: '\.scss',
            loader: 'style!css!sass',
            // loaders: ['style', 'css', 'sass'],
        },
        {
            test: /\.html/,
            loader: 'html',
        }
    }
};

loaders已经配置安装好了,咱们能够开始写咱们的按钮了

src/Components/Button.scss

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

src/Components/Button.html

<a href="{{link}}" class="button">{{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();

        $(node).html(
            Mustache.render(template, {text})
            );

        $('.button').click(this.onClick.bind(this));
    }
}

你的button.js如今是100%自引用而且在那里均可以被引用, 如今咱们只须要将button渲染到咱们的页面来

src/index.js

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

运行webpack,刷新页面,你应该能够看到咱们丑陋的按钮,并有对应的行为,(这一步有问题,编译成功了,可是没法new一个,提示 _Button2.default is not a constructor 错误)
至此你学会了如何创建loaders以及如何定义应用每一部分的依赖。如今貌似还不不出有什么用处,让咱们更加深刻到例子中去。

代码分割

上面的例子会一直引用button,固然,这并无什么问题,但咱们并不老是一直须要咱们的按钮。也许在一些页面没有按钮须要渲染。在这种状况下,咱们不想去引入按钮的样式、模板等。这个时候就是代码分割出场的时候了(code spliting)。代码分割即是webpack用来解决以前所说的单集成模块 VS 不可维护的引用的问题。分割点(split points):你的代码被分割为多个文件并被按需请求加载。语法很是简单:

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回调中的东西会被分隔到一个数据块,一个隔离的文件,webpack会在须要的时候,经过ajax请求去加载。这意味着,咱们会看到以下面的依赖树:

bundle.js
|- jquery.js
|- index.js // our main file
chunk1.js
|- some-big-libray.js
|- index-chunk.js // the code in the callback

而且咱们不用去引入chunk1.js或者去加载它,webpack已经帮咱们作了这些事情。这意味着咱们能够经过各类各样的逻辑去分割咱们的代码。在接下来的例子中,咱们只想在页面有连接的时候去加载咱们的button组件

src/index.js

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

        button.render('a');
    });
}

注意当使用require的时候,若是你想要默认的导出时,你须要手动的包裹它(default)。缘由在于require没法同时处理default和正常的导出,因此你须要显示申明想要用哪个。而import则有一个系统来解决这个问题,因此它知道如何处理。(eg. import foo from 'bar' vs import {baz} from 'bar')

如今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]
    [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中,并只有当咱们页面中有链接的时候才会加载进来。如今为了让webpack知道到哪里去ajax咱们的数据块,咱们须要配置下咱们的文件:

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

publishPath告诉webpack到哪里去找资源, 至此,咱们运行webpack,因为页面有链接,所以webpack加载了button组件。注意: 咱们能够对数据块进行命名来替代默认的1.bundle.js:

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

        button.render('a');
    }, 'button');
}

尝试了,发现并无什么用……是我打开的方式不对么

添加第二个组件

src/Components/Header.scss

.header {
  font-size: 3rem;
}

src/Components/Header.html

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

src/Components/Header.js

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

export default class Header {
    render(node) {
        const text = $(node).text();

        $(node).html(
            Mustache.render(template, {text})
        );
    }
}

而后在应用中渲染它:

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

再次运行webpack查看依赖状况,你会发现两个组件都须要jquery、mustache,意味着这些依赖模块被重复定义于咱们的数据块中,这并非咱们想要的。默认状况webpack并不对此进行优化。可是webpack能够经过插件的形式提供强力的优化方案。

插件(plugins)和loaders不一样,loaders只执行与特定类型的文件,plugins执行于全部的文件并提供更多丰富的功能。webpack拥有大量的插件来处理各类各样的优化。CommonChunksPlugin能够用来解决这个问题的插件, 它经过递归分析你的依赖包,找到公用的模块并将它们分离成一个独立的文件中,固然你也能够写入到入口文件中。

在接下来的例子中,咱们将公用的模块放到了咱们的入口文件中,由于若是全部的页面有引用了jquery和mustache,咱们就把它们放到顶端。接下来让咱们更新下咱们的配置:

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

若是咱们再次运行webpack, 咱们能够发现公用的组件已经被提取到了顶部:

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改成'vender‘:

new webpack.optimize.CommonsChunkPlugin({
    name:      'verder', // 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
})

因为该数据块尚未建立出来,webpack会自动建立builds/verder.js的文件,而后供咱们在html中引用,这一步笔者试了,发现没法建立vender这个依赖,全部公用依赖也没有被提取出来,不知道是否是windows的问题。

你还可使得公用模块文件以异步请求的方式加载进来,设置属性async: true即可以了。webpack还有大量的功能强大智能化的插件,我没法一个个介绍它们,可是做为练习,让咱们为应用建立一个生产环境

生产和超越

首先,咱们将添加几个插件到咱们的配置中去,但咱们只想要在生产环境中去加载并使用这些插件。因此咱们要添加逻辑来控制咱们的配置。

var webpack    = require('webpack');
var production = process.env.NODE_ENV === 'production';

var plugins = [
    new webpack.optimize.CommonsChunkPlugin({
        name:      'main', // Move dependencies to our main file
        children:  true, // Look for common dependencies in all children,
        minChunks: 2, // How many times a dependency must come up before being extracted
    }),
];

if (production) {
    plugins = plugins.concat([
       // Production plugins go here
    ]);
}

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

webpack 有多个设置咱们能够在生产环境中关掉:

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

debug意味着不会打包过多的代码以让你在本地调试的时候更加容易,第二个是关于资源映射的方式(sourcemaps generation),webpack有几个方式来渲染sourcemaps,eval是在本地开发中最好的一种,但在生产环境中,咱们并不在乎这些,因此在生产环境中咱们禁止了它。接下来咱们能够添加生产环境中用到的插件:

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还提供了不少其余的插件供你去协调你的模块和数据块。同时在npm上也有自由开发者开发贡献出来的拥有强大功能的插件。具体能够参考文章最后的连接。

如今你但愿你生产环境下的资源能按版本发布。还记得咱们为bundle.js设置过的output.filename属性吗?这里有几个变量供你使用,一个是[hash], 和最终生成的bundle.js内容的哈希值保持一致。咱们也想咱们的数据块(chunks)也版本话,咱们将设置output.chunkFilename属性来实现一样的功能:

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

在咱们这个简单的应用中并无一个方法来动态检索编译后文件的名字啊,咱们将只在生产环境中使用版本化的资源。同时咱们想在生产环境中清空咱们的打包环境,让咱们添加一个三方插件:

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

将这个插件配置到webpack中:

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

// ...

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

        // Cleanup the builds/ folder before
        // compiling our final assets
        new CleanPlugin('builds'),

好了,咱们已经作了一些优化的方案,让咱们来比较下结果:

$ webpack
                bundle.js   314 kB       0  [emitted]  main
1-21660ec268fe9de7776c.js  4.46 kB       1  [emitted]
2-fcc95abf34773e79afda.js  4.15 kB       2  [emitted]
$ NODE_ENV=production webpack
main-937cc23ccbf192c9edd6.js  97.2 kB       0  [emitted]  main

因此webpack到底作了什么:一开始,因为咱们的例子很是的简单轻量级,咱们的两个异步数据块不值得使用两个异步请求去获取,因此webpack将它们合并回了入口文件中;其次,全部的文件都被合理地压缩了。咱们从本来的3个请求总大小为322kb变成了一个97kb大小的文件。

But wasn’t the point of Webpack to stem away for one big ass JS file?
可是webpack不是不提倡合并成一个文件吗?

是的,它的确不提倡,当时若是咱们的app很小,代码量不多,它是提倡这样作的。但请考虑以下状况,你不须要去考虑何时什么地方作什么合并。若是你的数据块忽然间依赖了不少模块,那么webpack会让它变成异步加载而不是合并到入口文件中, 同时若是这些模块的依赖有公用的,那么这些模块也会被抽离出来等等。你只须要设立好规则,而后,webpack变回自动提供最好的优化方案。不用手册,不用思考模块依赖的顺序,全部的东西都自动化了。

你可能发现我并无设置任何东西去压缩咱们的HTML和CSS,这是由于CSS-loader和html-loader已经默认完成了这些事情。

由于webpack是自己就是一个JS-loader,所以在webpack中没有js-loader,这也是uglify是一个独立引进来的插件的缘由。

信息抽取

如今你可能发现,一开始咱们顶一个的样式被分开几段插入到页面从而致使了FOUAP(Flash of Ugly Ass Page),若是咱们能够把全部的样式都合并到一个文件中不是更好吗?是的,咱们可使用另外一个插件:

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

这个组件作了我刚才说的事情,它收集了你最后的bundle后内容里全部的样式,并将它们合成到一个文件中。

让咱们把它引入:

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:

(这一段翻译得很差,请看上面的原文)

如今能够看到 extract 方法传入了两个参数: 第一个是当咱们在style数据块中咱们要对引出的内容作什么;第二是当咱们在入口文件css!sass中要作的事情。若是咱们在一个数据块中,咱们不能简单地把咱们的CSS添加到咱们的css文件中,因此咱们在此以前使用style加载器,但对于在入口函数找到的全部样式,咱们将它们传递到builds/bundle.css文件中。让咱们为应用添加一个主样式表。

问题:这里遇到一个问题,每次修改主样式表(styles.scss)后,若是是有监听的话,webpack的自动重编译是会出错的,须要从新保存一次脚本才能让其正确编译成功,不知道是什么问题致使的。

src/styles.scss

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

src/index.js

import './styles.scss';

若是你想导出全部的样式,你也能够向ExtractTextPlugin传参(’bundle.css', {allChunks: true})。若是你想在你的文件名中使用变量,你也能够传入 [name]-[hash].css。

图片处理

脚本处理已经基本能够,可是咱们尚未处理如图片、字体等资源。在webpack中要怎么处理这些资源并获得最好的优化?接下来让咱们下载一张图片并让它做为咱们的背景,由于我以为它很酷:

将这张图片保存在img/puppy.png& 并更新咱们的sass文件:

body {
    font-family: sans-serif;
    background-color: #333;
    background-image: url('../img/puppy.jpg');
    background-size: cover;
}

若是你这样作的话,webpack会和你说:“我tmd要我怎么处理jpg这东西?”,由于咱们没有一个Loader用来处理它。有两个自带的加载器能够用来处理这些资源,一个是file-loader,另外一个是url-loader,第一个不会作什么改变,只会返回一个url,并能够版本化设置,第二个能够将资源转化为base64的url

这两个加载器各有优缺点:若是你的背景图片是2mb大的图片,你不会想将它做为base64引入到样式表中而更加倾向于单独去加载它。若是它只是一个2kb的图片,那么则引入它从而减小http请求次数会更好:

因此咱们把这两个加载器都安装上:

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

咱们在这里向url-loader传递了限制类的参数,告诉它:若是资源文件小于10kb则引入,不然,使用file-loader去处理它。语法使用的是查询字符串,你也可使用对象去配置加载器:

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

好了,让咱们来试试看

bundle.js   15 kB       0  [emitted]  main
1-b8256867498f4be01fd7.js  317 kB       1  [emitted]
2-e1bc215a6b91d55a09aa.js  317 kB       2  [emitted]
               bundle.css  2.9 kB       0  [emitted]  main

咱们能够看到并无提到jpg文件,由于咱们的puppy图片过小了,它被直接引入到bundle.css文件中了。

webpack会智能地根据大小或者http请求来优化资源文件。还有不少加载器能够更好地处理,最经常使用的一个是image-loader,能够在合并的时候对图片进行压缩,甚至能够设置?bypassOnDebug让你只在生产环境中使用。像这样的插件还有不少,我鼓励你在文章的末尾去看看这些插件。

实时监听编译

咱们的生产环境已经搭建好了,接下来就是实时重载:LiveReload、BrowserSync,这多是你想要的。可是刷新整个页面很消耗性能,让咱们使用更吊的装备hot module replacement或者叫作hot reload。因为webpack知道咱们依赖树的每个模块的位置,修改的时候就能够很简单地替换树上的某一块文件。更清晰地说,当你修改文件的时候,浏览器不用刷新整个页面就能够看到实时变化。

要使用HMR,咱们须要一个支持hot assets的服务器。Webpack有一个dev-server供咱们使用,安装下:

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

而后运行该服务器:

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

第一个参数告诉webpack将HMR逻辑引入到页面中(而不使用一个iframe去包含页面),第二个参数是启动HMR(hot module reload)。如今让咱们访问web-server的地址:http://localhost:8080/webpack-dev-server/ 。尝试改变文件,会看到浏览器上实时的变化

你可使用这个插件做为你的本地服务器。若是你计划一直使用它来作HMR,你能够将其配置到webpack中。

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

配置后,不管咱们何时运行ewbpack-dev-server,它都会在HMR模式。固然,还有不少配置供你配置,例如提供一个中间件供你在express服务器中使用HMR模式。

规范的代码

若是你一直跟着本文实践,你确定发现了奇怪的地方:为何Loaders被放在了Module.loaders中,而plugins却没有?这固然是由于还有其余东西你能够放在module中!Webpack不只有loaders,它也有pre-loaders和post-loaders:它们会在主加载器加载前/加载后执行。来个例子,很明显个人代码很是糟糕,因此在转化前咱们使用eslint来检测咱们的代码:

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

建立一个小型的eslintrc文件:

.eslintrc

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

如今咱们添加咱们的preloader,咱们使用和以前同样的语法:

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

在举另外一个例子,如今咱们的组件都会引入一样名字的样式表以及模板。让咱们使用一个预加载器来自动加载:

$ npm install baggage-loader --save-dev
{
    test: /\.js/,
    loader: 'baggage?[file].html=template&[file].scss',
}

这告诉webpack,若是你定义了一个一样名字的html文件,会把它以template的名字引入,一样的也会引入同名的sass文件。如今咱们能够修改咱们的组件:

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

pre-loader的功能强大,post-loader也同样,你也能够从文章末尾看到不少有用的加载器并使用它们。

你还想了解更多吗?

如今咱们的应用还很小,但随着应用的增大,了解正式的依赖树状况是颇有用的。能够帮助咱们了解咱们作的是否正确,咱们的应用的瓶颈在哪里。webpack知道全部这些事情,但咱们须要告诉他显示给咱们看,咱们能够处处一个profile文件:

webpack --profile --json > stats.json

第一个参数告诉webpack生成一个profile 文件,第一个指定生成的格式。有多个站点提供分析并可视化这些文件的功能,webpack官方也提供解析这些信息的功能。因此你能够到webpack analysis引入你的文件。选择modules 标签而后即可以看到你的可视化依赖树。另外一个我比较喜欢的是webpack visualizer
用圆环图的形式表示你的包大小占据状况。

That's all folks

我知道在个人案例中,Webpack已经彻底取代了Grunt或者gulp了,大部分功能已经由webpack来渠道,剩下的值经过npm script。过去使用Aglio转化咱们的API文档为html咱们使用的是任务型,如今能够这样作:

package.json

{
  "scripts": {
    "build": "webpack",
    "build:api": "aglio -i docs/api/index.apib -o docs/api/index.html"
  }
}

不管你在gulp中有多么复杂不关乎打包的任务,Webpack均可以很好地配合。提供一个在Gulp中集成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是一个强大的工具,固然代价是更多的配置须要你去写。不过一旦你知道如何驯服它,它会给你最好的优化方案。我在几个项目中使用了它,它也提供了强大的优化方案和自动化,让我没法不用它。

资源

译者拓展连接:

备注

开发过程遇到的问题能够查看原文下的评论或和译者交流学习。

译者英文水平有限,若是哪里翻译的很差欢迎指正,相关的代码可参考译者的demo2demo6demo4是使用Webpack + Vue写的DEMO,有兴趣的同窗也能够看看。