(三)webpack入门——webpack功能集合的demo

此篇文章来源于https://blog.madewithlove.be/post/webpack-your-bags/ ,翻译的不准确的地方请见谅。
 
到目前为止你也许听过这个新的很酷的称为webpack的工具。如果你了解得不多,你可能被一些认为webpack跟构建工具Gulp一样的人以及一些认为webpack和打包工具Browserify一样的人给误导了。另外一个方面,如果你去看了看webpack,你仍然可能被迷惑了,因为主页呈现webpack是两者的结合,即Gulp和Browserify的结合。
 
老实说,鼠标第一次划过“what Webpack is”的时候,我是挫败的,然后关掉了tab页。毕竟我已经有一个构建系统,而且非常满意。如果你密切关注快速发展的Javascript场景,就像我一样,你或许快速的投入到发展的潮流中。现在对webpack有更多的经验,我感觉我有必要为那些仍然保持中立态度的人写一篇文章解释清楚什么是webpack,更重要的是它是如此的强大以至于得到了很多关注。
 
什么是webpack?
通过介绍让我们马上来回答这个问题:Webpack是一个构建系统或者模块打包工具?更恰当的说,它两者都是——我并是不是说他做了两者的事,我的意思是说它将两者结合了。Webpack不去构建你的资源,并且它将你的模块分开打包,它认为你的资源是模块本身。
更准确的说webpack不是构建你所有的Sass文件,优化你所有的所有图片,所有模块都打包,然后一股脑的都加载到页面。你可以像下面这样做:
import stylesheet from 'styles/my-styles.scss';
import logo from 'img/my-logo.svg';
import someTemplate from 'html/some-template.html';
 
console.log(stylesheet); // "body{font-size:12px}"
console.log(logo); // "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]"
console.log(someTemplate) // "<html><body><h1>Hello</h1></body></html>"
你所有的资源都被看作是一个个的模块,它们可以被包含导入、修改、维护,最终被打包进你的打包文件。
为了使它工作起来,你需要在Webpack配置文件中注册loaders。Loaders都是很小的插件,他们最基本的功能都是:当你遇到这种类型的文件,用这种方式去处理。下面是一些loaders的例子:
{
    // 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链返回的都是string类型的类容,这样就允许Webpack把返回结果包裹成Javascript模块。每个例子中的Sass文件都被loaders转译了,转译后的内容也许看起来是这个样子:
export default 'body{font-size:12px}';
 
为什么要这么做呢?
一旦你懂了Webpack是什么你脑海中可能会出现第二个问题:这种方法有什么好处?好处体现在images和css文件中或者JS文件中?考虑这个:在很长的一段时间我们被教导和训练将所有的东西都放进一个单个的文件中,只是为了减少HTTP请求。
 
现在很多人都是将所有的资源打包到单文件例如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
基础的引导
让我们开始创建我们的项目并安装Webpack,我们也会拉取jQuery稍后做展示用。
$ npm init -y
$ npm install jquery --save
$ npm install webpack --save-dev
现在让我们创建app的入口文件,用ES5的语法:
src/index.js
var $ = require('jquery');
$('body').html('Hello');
然后在webpac.config.js文件中创建Webpack配置。Webpack配置就是Javascript,需要导入一个对象。
module.exports = {
    entry: './src',
    output: {
        path: 'builds',
        filename: 'bundle.js',
    },
};
entry 告诉Webpack在你的应用中哪些文件是入口文件。这些就是最主要的文件,位于依赖树的最上层。然后我们告诉它编译的打包文件放到builds文件夹下并且命名为bundle.js。让我们来设置相应的index.html页面:
<!DOCTYPE html>
<html>
 <head></head>
 <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
    Asset    Size  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等),你也许听过这样一个idea,你的app不是一个相互联系的整体的UI,而是一系列可以维护可重用自包含的UI:web组件化(我在这里简化,但是你已经明白了)。现在为了让组件真正的自包含,你需要将它所依赖的都包含进来。想象一下一个button组件:它有HTML模版,也有交互用的JS,也许还有一些样式。如果在我们需要的时候这些都被加载,这是非常好的,对吗?当我们仅仅导入Button组件的同时,我们也会得到它所有关联的资源。
 
让我们写我们的button组件;首先,我猜想大多数人都还是习惯ES2015语法的,因此我们加入我们的第一个loader:Babel. 为了在Webpack中安装一个loader你需要做两件事情:用 npm install {whatever}-loader命令安装loader,然后将它加入到 Webpack配置文件中的 module.loaders部分,所以让我们用这种方式来加入babel到我们的项目中来吧:
$ npm install babel-loader --save-dev
我们需要安装Babel本身,因为在这种情况下babel-loader不会安装Babel插件本身。我们需要安装 babel-core 包和 es2015 预调装置
$ npm install babel-core babel-preset-es2015 --save-dev
接下来我们将会创建一个命名为 .babelrc 的文件,这个文件用来告诉Babel使用预调装置。 它是一个简单的JSON文件告诉Babel转译成可以运行的代码——在我们的案例中我们使用 es2015 预调装置。
.babelrc
{ "presets": ["es2015"] }
现在Babel已经安装和配置了我们需要更新我们的配置文件:我们想要做什么?我们想要Babel去运行所有以.js 结尾的文件,但是Webpack遍历所有的依赖,我们希望避免执行第三方代码比如像jQuery,所以我们可以过滤它。Loaders有 include 和 exclude 两个规则。它可以配置成一个字符串、正则表达式、一个或调或者其他你希望的,我们希望Babel只执行我们自己的文件,所以我们仅 include 我们自己的资源文件夹:
module.exports = {  
entry: './src',
    output: {
        path: 'builds',
        filename: 'bundle.js',
    },
module: {
        loaders: [{
            test: /\.js/,
            loader: 'babel',
            include: /src/,
        }],
    }
};
Babel已经安装配置完成,我们可以用ES6语法重写 index.js 文件。从现在开始所有的例子都用ES6语法。
import $ from 'jquery';
$('body').html('Hello');
写一个小的组件
现在让我们写一个小Button组件,它将会有SCSS样式,HTML模板,和一些行为。所以我们要安装一些我们需要的东西。首先我们需要安装Mustache,它是一个非常轻量级的模板包,但是我们也需要用来加载Sass和HTML的loaders。结果是从一个加载器传送到另外一个加载器,我们需要一个CSS加载器来处理Sass 加载器处理的结果。现在,一旦我们有了CSS就有很多方式去处理他们了,目前为止我们将会使用称作 style-loader 的 加载器来处理CSS,  style-loader 将CSS动态的注入到页面。
$ npm install mustache --save
$ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev
现在为了告诉Webpack传送数据是从一个加载器到另一个加载器,我们简单的用感叹号“!”来将一些列的加载器从右到左分隔开。你也可以给loaders  属性配置一个array值来替代 loader 属性配置:
{
    test: /\.js/,
    loader: 'babel',
    include: /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';
 
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是百分百自包含的,无论何时被导入,在哪种上下文环境中,它都是可以运行的,它是可以被正确使用和渲染的。现在我们只需要将Button渲染在我们的页面:
src/index.js
import Button from './Components/Button';
const button = new Button('google.com');
button.render('a'); 
运行webpack,然后刷行页面,你就可以点击我们的按钮了。
 
你现在已经学会了怎么设置loader是以及在你的应用中如何定义每个部分的依赖了.这看起来好像并没有多少东西,来让我们进一步推进我们的例子。
 
代码分离
这个例子好像很好,还包含了很多东西,但是也许我们不是总是需要我们的button。也许在一些页面上根本就没有一个 a 标签来供 button渲染在上面,在这个例子中,我们也许不会导入所有的button样式、模版、Mustache或者其他的,对吗? 所以代码分离就上场了。Webpack代码分离处理了打包到一个文件和手动引入文件不可维护的问题。在你的代码中定义你的想法,你的代码很容易的分隔到单个文件中,然后按需加载。语法很简单:
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 的回调函数里面的内容将会被分离进一个chunk中——即单独打包并通过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会自动加载。这意味着你可以用各个部分的逻辑来包裹分块的代码,我们例子中就是这么做的。在页面有 a 标签的时候我们才需要我们的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 的时候 你想得到默认的export需要加上 .default 属性手动获得。由于require不能处理默认和正常的exports的原因所以你不得不明确指定返回值。 而 import  是会处理的,所以不需要额外手动获得对应的module。
 
因此Webpack的输出现在应该是不一样的。让我们加上 --display-chunks 标志来运行webpack命令,看看哪些模块是在chuns中:
$ 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 分组中,只有我们在页面嵌入1.bundle.js,它才会加载。当加载Ajax的时候,为了让Webpack知道去哪儿寻找chunks,我们不得不加一点内容到我们的配置文件中:
path: 'builds',
filename: 'bundle.js',
publicPath: 'builds/',
output.publicPath 选项告诉Webpack在页面视图中去哪儿寻找构建了的资源(在我们的例子中就是 builds/).现在我们如果访问我们的也页面将会看到一切正常工作,但我们发现更重要的是,我们的也买年有一个锚,Webpack才能正确加载chunk:
 
 
如果页面没有加锚,只会加载bundle.js .这允许在你的应用程序中明智的将复杂的逻辑分离,并且让每个页面仅仅加载它们真正需要的内容。注意,我们命名我们的分离文件名,将1.bundle.js替换成更有意义的chunk名。你仅仅给 require.ensure 加第三个参数以及在webpack配置文件中配置 chunkFilename 就可以做到:
src/index.js
require.ensure([], () = >{
    const Button = require('./Components/Button').default;
    const button = new Button('google.com');
 
    button.render('a');
},
'button');
webpack.config.js
output: {
        path:     'builds',
        filename: 'bundle.js',
        chunkFilename: '[name].bundle.js',
        publicPath: 'builds/',
},
生成的 button.bundle.js 文件将会替代 1.bundle.js 文件。
 
增加第二个组件
现在一切都已经很完美了,但是我们要增加第二个组件来看看它是怎么工作的:
src/Components/Header.scss
.header {
    font-size: 3rem;
}
src/Components/Header.html
<headerclass="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 命令加上  --display-modules  --display-chunks 标志后的输出内容:

$ 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 , 这意味着这些依赖在我们的chunks中是重复的,这并不是我们想要的。默认情况下Webpack会执行很小的优化。但是它包含了很多很牛掰的东西帮你扭转乾坤,它们在插件列表中。
 
功能上,plugins 不同于加载器,plugins不是仅仅的执行一系列的特定文件,也不是成为一个管道,它们执行所有的文件以及更高级的操作,不一定需要关联的去转换。Webpack将一些插件用来执行各种各样的优化。其中令我们感兴趣的一个插件是CommonChunksPlugin: 它分析所有的循环依赖,在某个地方提取它们。它可以是一个完全独立的文件(就像vendor.js)或许它可以成为你的主要文件。
 
在我们的例子中我们更喜欢将普通的依赖移到我们的入口文件中,因为如果所有的页面都需要jQuery 和 Mustache, 我们不妨移入进去。 所以让我们来更新我们的配置:
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: {
        // ...
    }
};
如果我们再次运行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,
}),
到现在我们的主chunk不存在,Webpack会创建一个 builds/vendor.js的文件,我们需要手动导入到HTML中:
<script src="builds/vendor.js"></script>
你也可以不提供普通的chunk名,然后指定特殊的 async: true属性,就可以创建一个被异步加载的普通依赖。Webpack有很多这样强大的只能优化功能。我不可能将它们所有的都命名但是作为联系我们来试试创建我们应用程序的生产版本。
 
生产环境和超越

首先,我们将会加入一些plugins到我们的应用程序中,但是我们只希望在NODE_ENV 等于 production的时候加载它们,所以让我们在配置文件中增加一些逻辑。它只是一个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也有几个设置用来对生成环境进行开关的:
module.exports = {
    debug: !production,
    devtool: production ? false: 'eval',
第一个设置debug模式的开关,这意味着很容易的在本地调试代码。 第二个设置是关于sourcemaps生成的。Webpack有几种方式去渲染sourcemaps, 在本地  eveal仅仅是其中最好的一种。在生产环境中我们没必要关心sourcemaps,所以我们关闭了它。现在让我们加上生产环境的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提供了很多插件供你使用。这里也有一些第三方插件,它们可以在NPM上找到来完成各种各样的需求。plugins的链接可以在文章最后找到。
 
现在生成环境最理想的是让资源版本化。现在你还记得我们设置 output.filename 的值为  bundle.js?   然而这里有一些变量你可以用在你的option中,其中一个是 [hash] ,对应最终包的内容的hash值,所以让我们改变我们的代码。我们也希望我们的chunks被版本化所以我们将会增加一个 output.chunkFilename 就像下面这样:
output: {
    path: 'builds',
    filename: production ? '[name]-[hash].js': 'bundle.js',
    chunkFilename: '[name]-[chunkhash].js',
    publicPath: 'builds/',
},
在我们简单的app中我们真的没有办法动态的检索编译后的文件名,例如,在生产环境中我们仅仅版本化资源文件。在一个生成环境构建之前我们也许希望我们的builds文件夹是清空的,所以让我们安装第三方插件来做这个事情:
$ npm install clean-webpack-plugin --save-dev
将它加入到我们的配置中:
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做了些什么: 首先我们的例子已经很轻了,两个异步的chunks不值得发起HTTP请求,所以Webpack将他们合并到了入口文件中。其次一切都被正确的压缩了。我们从一个需要发起3个HTTP请求及总共322kb的文件到只需要一个HTTP请求及压缩到97kb的文件。
 
Webpack是避免了一个大的JS文件的生成? 是的,但是这仅仅发生在我们的app很小的时候。现在考虑这个:你不必要考虑合适何地合并哪些内容。如果你的chunks突然开始有很多依赖,chunk会被移动到异步chunk中,而不是合并;如果开始寻找到类似的chunks,他们将会被合并。你仅仅需要设置一些规则,Webpack以尽可能最好的方式自动优化你的应用程序。不需要手动操作,不需要考虑依赖什么,哪里被组要这些问题,一切都是自动的。
 
你也许已经发现了我没有设置任何东西去压缩我们的HTML和CSS, 那是因为 css-loader 和 html-loader已经主要到我们前面提到的 debug选项被设置为 false 了。这也是为什么Uglify是一个单独的插件的原因: 因为在Webpack中没有 js-loader , Webpack自身就是 JS loader。
 
抽取 
也许你已经注意到了,在教程开始,SCSS文件编译后的内容是一个个用style标签包裹写入HTML页面的,有一个SCSS就用一个style标签来包裹并写入 ,这样会有很多的style标签被插入HTML中,这样不是很好,那有没有办法将这些文件合并然后在连接到html中呢? 答案是肯定的,我们需要外部的plugin来做到:
$ 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'), // <=== 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'),
        },
        // ...
        ],
    }
};
extrect方法有两个参数:第一个是说明来抽取 style类型的内容,第二个参数说明在主文件中如何处理。所有的style是都整合到了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

现在运行wbpack命令,现在非常确定我们有一个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

你也可以提取chunks的styles,可以通过ExtractTextPlugin('bundle.css', {allChunks: true})选项来设置。注意一下,你也可以额给你的文件名设置成变量,所以如果你需要讲你的样式表版本化的话你仅需要做的是这么配置一下就行

ExtractTextPlugin('[name]-[hash].css')

在Webpack中使用images

现在我们所有的Javascript文件都很好,但是有一个主题我们还没有谈到多少,那就是固定资源:图片,文字字体等。

在Webpack环境中是怎么工作的及如何最好的优化他们?让我们在网络上找一张图片,我们将会用这张图片作为页面背景图,因为我已经看到有人在Geocities有人这么做了,并且他看起来非常酷:

让我们将这张图片保存在 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.  这里有两个本地loaders,我们可以用来处理固定资源:file-loader 和 url-loader :第一个loader仅仅只会返回一个URL资源并没有任何其他改变,允许在处理过程中对文件版本化;第二个loader将会将资源内联成一个 data:image/jpeg;base64 URL。
 
实际中它非黑即白:如果你的背景是一个2M的图片,你不想内联它就会单独加载它。从另一方面看如果它是一个2kb的小图标,最好用内联的方式处理来减少HTTP请求,所以我们设置这两个loader:
$ npm install url-loader file-loader --save-dev
{
    test: /\.(png|gif|jpe?g|svg)$/i,
    loader: 'url?limit=10000',
},
在这里,我们通过查询参数 limit 告诉 url-loader: 如果资源小于10kb就内联资源,否则就调用 file-loader 并关联它。这个语法称为查询字符串,你使用它来配置loaders,作为一种选择你也可以通过一个对象来配置loaders:
{
    test: /\.(png|gif|jpe?g|svg)$/i,
    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,因为我们的图片文件小于配置的大小,它被内联了。这以为着我们访问我们的页面的时候,我们可以看到一个小狗。

这是很强大的因为它意味着Webpack现在通过判断大小或者请求来优化任何固定资源。这里有一个很好的loader你可以通过管道传输进一步推动一些可优化的地方,一个非常常用的loader是image-loader,它通过imagemin在打包之前压缩所有的图片文件。它甚至有个?bypassOnDebug 查询参数允许你仅在生产环境来压缩图片。这里有很多像这样的插件,我鼓励你去看看文章最后列表里面的内容。

 
现在我们的生产环境受到很多关注,现在让我们把焦点放在本地开发上。到目前为止你也许注意到了一个很大的问题,通常提到的构建工具是要在线上加载,线上重新加载,浏览器同步。但是整个页面都需要刷新,让我们进一步设置热模块替换和热重载。它的思想是这样的,自从Webpack准确的知道依赖树中每个木块的位置,一个修改就会被依赖树中简单修补的新文件替代。更同属的说:你的修改将不需要重新刷新页面在浏览器中也会出现。
 

为了让HMR被使用,我们需要一个服务器,热资源将从它提供。我们可以使用Webpack提供的 dev-server, 让我们来加载它:

$ npm install webpack-dev-server --save-dev
让我们来运行 dev-server:
$ webpack-dev-server --inline --hot
第一个标志告诉我们内联HMR的逻辑然后插入页面(而不是在iframe页面呈现),第二个标志启用HMR. 现在让我们访问 web-server :http://localhost:8080/webpack-dev-server/。你看到的是普通的页面,但是你尝试修改你的Sass文件,奇迹产生了:

 

你可以将webpack-dev-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.

彻底弄明白或者糊里糊涂的?

如果你密切关注本教程你也许发现了一些不奇怪的东西: 为什么loaders被包裹在module.loaders但是plugins不是呢?这是因为你可以将其他的东西放在module中!Webpack不仅仅只有loaders,它也有pre-loaders 和post-loadrs:这些loaders运行在主loaders之前或者之后。让我们看一个例子:我确定在这篇文章中写的代码是惊人的,所以在转换代码之前将ESLint应用到我们的代码中:
 
$ npm install eslint eslint-loader babel-eslint --save-dev

然后创建以 .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的例子:每一个组件我们现在引用的样式列表都是同一个名字,模板名也都是同一个。现在让我们使用一个pre-loader来自动加载和模块名相同的任何文件:

$ 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是非常完美的,post-loaders也是一样的。文章的最后面去看看loaders变量列表,你将会发现很多实用案例。
 
你想知道更多吗?

现在我们的应用程序还是相当小的,但是随着他开始变得越来越大它也许对依赖树到底是什么的深刻理解非常有帮助。我们可能做的对还是错,什么是我们的应用程序的瓶颈等等。现在在内部,Webpack知道各种事情,但是你的礼貌的问它它知道的将会向你展示。你可以像下面这样做来生产一个profile文件:

webpack --profile --json > stats.json
第一个标志告诉Webpack生成一个profile文件,第二个标志告诉Webpack生成的文件内容是JSON格式,最后将他输出到一个JSON文件中。现在有很多各种网站去分析这样的profile文件,但是Webpack提供了一个官方网站去分析它。 所以去Webpack Analyze 网站并且导入上面生成的JSON文件。现在打开Modules tab 你将会看到下面呈现的依赖树的样子:
 
 
红点,说明你最后的包有问题。在我们的例子中jQuery是有问题的因为他们是所有模块中最大的,依次看看所有的tabs,尽管在我们小的应用程序中你没有学到太多但是这是一个非常重要的工具来深刻理解你的树依赖及最后的打包。现在就像我说的,其他服务提供了洞察你的概要文件的能力,另外一个我喜欢的是 Webpack Visualizer   ,它向我们展示了一张圈圈图来说明我们打包的内容,下面就是我们的案例:
 
现在我知道在我的案例中,Webpack已经替代了Grunt或者Gulp: 很多我使用过的,现在它们还是得手动处理,剩下的我仅适用NPM脚本。过去的每个例子中一个普通的任务即适用Aglio将API文档转换成HTML,像下面这样做将会很容易:
package.json
{
    "scripts": {
        "build": "webpack",
        "build:api": "aglio -i docs/api/index.apib -o docs/api/index.html"
    }
}
如果无论如何在你的Gulp堆栈中有有一些复杂的任务,他们跟绑定和资源没有关联,Wepback和其他的构建系统相处得很好。下面的例子如何在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();
    });
});
差不多就是这样,自从Wwbpack有Node API 后, 在其他构建系统中它很容易被使用,在任何情况下你都可以发现在任何地方它挂载着包装器。
 
任何情况下,我想这是一个足够好的鸟瞰让你知道Webpack会为你做什么。你也许回想我们在这篇文章中已经覆盖了很多,但是我们只是触及表面:多入口,预提取、上下文替换等都没有提到。Webpack是一个而令人难忘的强大的工具,我不决绝它。但是你一旦知道怎么去驯服它,进入你耳朵的将是性能甜美的声音。我在几个项目中使用过它,它提供了如此强大的智能化和自动化功能。
 
参考资源:
posted @ 2016-06-26 15:08  ErduYang  阅读(17665)  评论(6编辑  收藏  举报