[譯] 用 Webpack 武裝本身

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

因爲我英語水平有限,並且不多翻譯文章,因此文中的一些語句在翻譯時作了相似語義的轉換,望諒解。要是有幸被轉仍是但願可以註明啊php

by the way,打個小廣告。。把本身的github扔這兒好了,有時候會更新些譯文或者筆記什麼的css

圖片描述

你可能已經據說過這個酷酷的工具-Webpack。一些人稱之爲相似於Gulp的工具,還有一些人則認爲它相似於Browserify。若是你還沒接觸過它,那頗有可能會所以感到困惑。而Webpack的主頁上則認爲它是二者的結合,那或許更讓你困惑了。html

說實話,一開始的時候,「什麼是Webpack」這個話題讓我很心煩,也就沒有繼續研究下去了。直到後來,當我已經構建了幾個項目後,才真心的爲之癡迷。若是你像我同樣緊隨Javascript的發展步伐,你頗有可能會由於太追隨潮流跨度太大而蛋疼。在經歷了上面這些以後,我寫下這篇文章,以便更加細緻的解釋Webpack是什麼,以及它如此重要的緣由。html5

Webpack是啥?

首先來讓咱們回答最開始的問題:Webpack是個系統的構建工具,仍是打包工具?答案是二者都是--這不表明它作了這兩件事(先構建資源,在分別進行打包),而是說它將二者結合在一塊兒了。java

更加清晰的說明:與「構建sass文件,壓縮圖片,而後引用它們,再打包,再在頁面上引用」相比,你只要這麼作:node

import stylesheet from 'styles/my-styles.scss';
import logo from 'img/my-logo.svg';
import someTemplate from 'html/some-template.html';

console.log(stylesheet); // "body{font-size:12px}"
console.log(logo); // "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]"
console.log(someTemplate) // "<html><body><h1>Hello</h1></body></html>"

你的全部資源都被當作包處理,能夠被import,修改,控制,最終展示在你最後的一個bundle上。jquery

爲了能讓上面那些有效運轉,你須要在本身的Webpage配置裏配置loaderloader是一個「當程序碰見XXX類型文件的時候,就作YYY」的小型插件。來看一些loader的例子:webpack

{
  // 若是引用了 .ts 文件, 將會觸發 Typescript loader
  test: /\.ts/,
  loader: 'typescript',
},
{
  // 若是引用了png|jpg|svg圖片,則會用 image-webpack 進行壓縮 (wrapper around imagemin)
  // 並轉化成 data64 URL 格式
  test: /\.(png|jpg|svg)/,
  loaders: ['url', 'image-webpack'],
},
{
  // 若是使用了 SCSS files, 則會用 node-sass 解析, 最終返回CSS格式
  test: /\.scss/,
  loaders: ['css', 'autoprefixer', 'sass'],
}

最終在食物鏈的最底端,全部的loader都返回string,這樣Webpack就能夠將它們加入到javascript模塊中去。當你的Sass文件被loader轉換以後,它的引用其實是這樣的:git

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

圖片描述

究竟爲何要這麼作?

在你理解了Webpack是作什麼的以後,第二個問題就接踵而至:使用它有什麼好處?「把圖片和CSS扔進個人js裏?什麼鬼?」其實在好久以前,爲了減小HTTP request請求,咱們都被教育要把全部東西寫在一個文件裏面。

到了如今,與之相似的是,不少人把全部東西打包進app.js。這兩種方法都有一個很大的負面影響:不少時候人們在下載的是他們用不到的資源。但若是你不這麼作吧,你就得手動的在每一個頁面引用相應的資源,最終會混亂成一坨:哪一個頁面已經引用了它所依賴的資源?

這些方法沒有絕對的對錯。把Webpage當作一箇中間件--不只僅是打包或構建工具,而是個聰明的模塊打包系統。只要你設置正確,它會比你還要清楚使用的技術棧,並更好的優化它們。

來讓咱們一塊兒構建一個簡單的App

爲了讓你更快捷的理解使用Webpack的好處,咱們會構建一個簡單的App,並將資源打包進去。在這裏教程中我推薦使用Node4(或5),以及NPM3做爲包管理工具,以便在使用Webpack的時候避免大量的麻煩。若是你還沒裝NPM3,能夠經過npm install npm@3 -g來安裝。

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

我還要推薦你把node_modules/.bin放進你的PATH變量,以免每次都要輸入node_modules/.bin/webpack。在下面了例子裏我輸入的指令都不會再包含node_modules/.bin

基礎指引(setup)

從建立項目安裝Webpack開始。咱們同時也安裝了jQuery以便支持後續操做。

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

如今來作一個App的入口:

// src/index.js
var $ = require('jquery');

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

讓咱們在webpack.config.js文件裏進行的Webpack配置。Webpack配置實質上是Javascript,而且在最後export出去一個Object:

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

在這裏,entry告訴Webpack哪些文件是應用的入口文件。它們是你的主要文件,在依賴樹的最頂端。以後,咱們告訴Webpack把資源打包在builds文件夾下的bundle.js文件裏。讓咱們編寫index HTML文件。

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

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

運行Webpack。若是一切正確那就能夠看見下面的信息:

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

在這段信息裏能夠看出,bundle.js包含了index.js和一個隱藏的模塊。隱藏的模塊是jQuery。在默認模式下Webpack隱藏的模塊都不是你寫的。若是想要顯示它們,咱們能夠在運行Webpack的時候使用--display-modules

$ webpack --display-modules
bundle.js  268 kB       0  [emitted]  main
   [0] ./src/index.js 53 bytes {0} [built]
   [3] ./~/jquery/dist/jquery.js 259 kB {0} [built]

你還可使用webpack --watch,在改變代碼的時候自動進行打包。

設置第一個loader(loader-01)

還記得Webpack能夠處理各類資源的引用嗎?該怎麼搞?若是你跟隨了這些年Web組件發展的步伐(Angular2,Vue,React,Polymer,X-Tag等等),那麼你應該知道,與一堆UI相互鏈接組合而成的App相比,使用可維護的小型可複用的UI組件會更好:web component。

爲了確保組件可以保持獨立,它們須要在本身內部打包須要的資源。想象一個按鈕組件:除了HTML以外,還須要js以便和外部結合。噢對或許還須要一些樣式。若是可以在須要這個按鈕組件的時候,加載全部它所依賴的資源的話那就太讚了。當咱們import按鈕組件的時候,就獲取到了全部資源。

開始編寫這個按鈕組件吧。首先,假設你已經習慣了ES2015語法,那麼須要安裝第一個loader:Babel。安裝好一個loader你須要作下面這兩步:首先,經過npm install {whatever}-loader安裝你須要的loader,而後,將它加到Webpage配置的module.loaders裏:

$ npm install babel-loader --save-dev

loader並不會幫咱們安裝Babel因此咱們要本身安裝它。須要安裝babel-core包和es2015預處理包。

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

新建.babelrc文件,裏面是一段JSON,告訴Babel使用es2015進行預處理。

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

如今,Babel已經被安裝並配置完成,咱們要更新Webpack配置。咱們想要Babel運行在全部以.js結尾的文件裏,可是要避免運行在第三方依賴包例如jQuery裏面。loader擁有includeexclude規則,裏面能夠是一段字符串、正則、回調等等。在這個例子裏,咱們只想讓Babel在咱們本身的文件裏運行,所以使用include包含本身的資源文件夾:

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

如今,咱們能夠用ES6語法重寫index.js了:

// index.js
import $ from 'jquery';

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

寫個小組件(loader-02)

來寫個按鈕組件吧,它將包含一些SCSS樣式,HTML模板和一些操做。因此咱們要安裝須要的工具。首先安裝Mustache這個輕量級的模板庫,而後安裝處理Sass和HTML的loader。一樣的,爲了處理Sass loader返回的結果,還要安裝CSS loader。一旦獲取到了CSS文件,咱們就能夠用不少種方式來處理。目前使用的是一個叫style-loader的東西,它可以把CSS插入到包中。

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

爲了可以讓Webpack依次處理不一樣loader的返回結果,咱們能夠將loader經過!連接到一塊兒,獲取使用loaders並對應一個由loader組成的數組:

{
    test: /\.js/,
    loader: 'babel',
    include: __dirname + '/src',
},
{
    test: /\.scss/,
    loader: 'style!css!sass',
    // Or
    loaders: ['style', 'css', 'sass'],
},
{
    test: /\.html/,
    loader: 'html',
}

有了loader,咱們來寫寫按鈕:

// src/Components/Button.scss
.button {
  background: tomato;
  color: white;
}
<!-- src/Components/Button.html -->
<a class="button" href="{{link}}">{{text}}</a>
// src/Components/Button.js
import $ from 'jquery';
import template from './Button.html';
import Mustache from 'mustache';
import './Button.scss';

export default class Button {
    constructor(link) {
        this.link = link;
    }

    onClick(event) {
        event.preventDefault();
        alert(this.link);
    }

    render(node) {
        const text = $(node).text();
        // Render our button
        $(node).html(
            Mustache.render(template, {text})
        );
        // Attach our listeners
        $('.button').click(this.onClick.bind(this));
    }
}

你的Button.js如今處於徹底獨立的狀態,無論什麼時候何地的引用它,都能獲取到全部須要的依賴並渲染出來。如今渲染咱們的按鈕試試:

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

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

運行Webpack,刷新頁面,馬上就能看見咱們這個難看的按鈕了。

圖片描述

如今你已經學習瞭如何安裝loader,以及定義各個依賴配置。看起來好像也沒啥。但讓咱們來深刻擴展一下這個例子。

代碼分離(require.ensure

上面的例子還不錯,但咱們並不老是須要這個按鈕。或許有的頁面沒有能夠用來渲染按鈕的a,咱們並不想在這樣的頁面引用按鈕的資源文件。這種時候代碼分離就能起到做用了。代碼分離是Webpack對於「整塊所有打包」vs「難以維護的手動引導」這個問題而給出的解決方案。這須要在你的代碼中設定「分離點」:代碼能夠據此分離成不一樣區域進行按需加載。它的語法很簡單:

import $ from 'jquery';

// 這個是分割點
require.ensure([], () => {
  // 在這裏import進的代碼都會被打包到一個單獨的文件裏
  const library = require('some-big-library');
  $('foo').click(() => library.doSomething());
});

require.ensure中的東西都會在打包結果中分離開來--只有當須要加載它的時候Webpack纔會經過AJAX請求進行加載。也就是說咱們實際上獲得的是這樣的文件:

bundle.js
|- jquery.js
|- index.js // 入口文件
chunk1.js
|- some-big-libray.js
|- index-chunk.js // 回調中的代碼在這裏

你不須要在任何地方引用chunk1.js文件,Webpack會幫你在須要的時候進行請求。這意味着你能夠像咱們的例子同樣,根據邏輯須要引進的資源所有扔進代碼裏。

只有當頁面上有連接存在時,再引用按鈕組件:

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

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

須要注意的一點是,由於require不會同時處理default export和normal export,因此使用require引用資源裏default export的時候,須要手動加上.default。相比之下,import則能夠進行處理:

import foo from 'bar' vs import {baz} from 'bar'

此時Webpack的output將會變得更復雜了。跑下Webpack,用--display-chunks打印出來看看:

$ webpack --display-modules --display-chunks
Hash: 43b51e6cec5eb6572608
Version: webpack 1.12.14
Time: 1185ms
      Asset     Size  Chunks             Chunk Names
  bundle.js  3.82 kB       0  [emitted]  main
1.bundle.js   300 kB       1  [emitted]
chunk    {0} bundle.js (main) 235 bytes [rendered]
    [0] ./src/index.js 235 bytes {0} [built]
chunk    {1} 1.bundle.js 290 kB {0} [rendered]
    [5] ./src/Components/Button.js 1.94 kB {1} [built]
    [6] ./~/jquery/dist/jquery.js 259 kB {1} [built]
    [7] ./src/Components/Button.html 72 bytes {1} [built]
    [8] ./~/mustache/mustache.js 19.4 kB {1} [built]
    [9] ./src/Components/Button.scss 1.05 kB {1} [built]
    [10] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
    [11] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built]
    [12] ./~/style-loader/addStyles.js 7.21 kB {1} [built]

正如你所見的那樣,咱們的入口bundle.js值包含了一些邏輯,而其餘東西(jQuery,Mustache,Button)都被打包進了1.bundle.js,而且只在須要的時候纔會被引用。如今爲了可以讓Webpack在AJAX的時候找到這些資源,咱們須要改下配置裏的output

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

output.publicPath告訴Webpack,從當前頁面的位置出發哪裏能夠找到須要的資源(在這個例子裏是/builds/)。當咱們加載頁面的時候一切正常,並且可以看見Webpack已經根據頁面上預留的「錨」加載好了包。

圖片描述

若是頁面上缺乏「錨」(代指link),那麼只會加載bundle.js。經過這種方式,你能夠作到在真正須要資源的時候才進行加載,避免讓本身的頁面變成笨重的一坨。順帶一提,咱們能夠改變分割點的名字,不使用1.bundle.js而使用更加語義化的名稱。經過require.ensure的第三個參數來實現:

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

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

這樣的話就會生成button.bundle.js而不是1.bundle.js

再加個組件(CommonChunksPlugin

來讓咱們再加個組件吧:

// src/Components/Header.scss
.header {
  font-size: 3rem;
}
<!-- src/Components/Header.html -->
<header class="header">{{text}}</header>
// src/Components/Header.js
import $ from 'jquery';
import Mustache from 'mustache';
import template from './Header.html';
import './Header.scss';

export default class Header {
    render(node) {
        const text = $(node).text();
        $(node).html(
            Mustache.render(template, {text})
        );
    }
}

將它在應用中渲染出來:

// 若是有連接,則渲染按鈕組件
if (document.querySelectorAll('a').length) {
    require.ensure([], () => {
        const Button = require('./Components/Button');
        const button = new Button('google.com');

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

// 若是有標題,則渲染標題組件
if (document.querySelectorAll('h1').length) {
    require.ensure([], () => {
        const Header = require('./Components/Header');

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

瞅瞅使用了--display-chunks --display-modules標記後Webpack的output輸出:

$ webpack --display-modules --display-chunks
Hash: 178b46d1d1570ff8bceb
Version: webpack 1.12.14
Time: 1548ms
      Asset     Size  Chunks             Chunk Names
  bundle.js  4.16 kB       0  [emitted]  main
1.bundle.js   300 kB       1  [emitted]
2.bundle.js   299 kB       2  [emitted]
chunk    {0} bundle.js (main) 550 bytes [rendered]
    [0] ./src/index.js 550 bytes {0} [built]
chunk    {1} 1.bundle.js 290 kB {0} [rendered]
    [14] ./src/Components/Button.js 1.94 kB {1} [built]
    [15] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
    [16] ./src/Components/Button.html 72 bytes {1} [built]
    [17] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
    [18] ./src/Components/Button.scss 1.05 kB {1} [built]
    [19] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
    [20] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
    [21] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
chunk    {2} 2.bundle.js 290 kB {0} [rendered]
    [22] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
    [23] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
    [24] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
    [25] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
    [26] ./src/Components/Header.js 1.62 kB {2} [built]
   [27] ./src/Components/Header.html 64 bytes {2} [built]
   [28] ./src/Components/Header.scss 1.05 kB {2} [built]
   [29] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]

能夠看出一點問題了:這兩個組件都須要jQuery和Mustache,這樣的話就形成了包中的依賴重複,這可不是咱們想要的。儘管Webpack會在默認狀況下進行必定的優化,但還得靠插件來加足火力搞定它。

插件和loader的不一樣在於,loader只對一類特定的文件有效,而插件每每面向全部文件,而且並不老是會引發轉化。Webpack提供了不少插件供你優化。在這裏咱們使用CommonChunksPlugin插件:它會分析你包中的重複依賴並提取出來,生成一個徹底獨立的文件(例如vendor.js),甚至生成你的主文件。

如今,咱們想要把共同的依賴包從入口中剔除。若是全部的頁面都用到了jQuery和Mustache,那麼就要把它們提取出來。更新下配置吧:

var webpack = require('webpack');

module.exports = {
    entry: './src',
    output:  {
      // ...
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'main', // 將依賴移到咱們的主文件中
            children: true, // 再在全部的子文件中檢查依賴文件
            minChunks: 2, // 一個依賴重複幾回會被提取出來
        }),
    ],
    module:  {
      // ...
    }
};

再跑次Webpack,能夠看出如今就好多了。其中,main是咱們的默認依賴。

chunk    {0} bundle.js (main) 287 kB [rendered]
    [0] ./src/index.js 550 bytes {0} [built]
    [30] ./~/jquery/dist/jquery.js 259 kB {0} [built]
    [31] ./~/mustache/mustache.js 19.4 kB {0} [built]
    [32] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
    [33] ./~/style-loader/addStyles.js 7.21 kB {0} [built]
chunk    {1} 1.bundle.js 3.28 kB {0} [rendered]
    [34] ./src/Components/Button.js 1.94 kB {1} [built]
    [35] ./src/Components/Button.html 72 bytes {1} [built]
    [36] ./src/Components/Button.scss 1.05 kB {1} [built]
    [37] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
chunk    {2} 2.bundle.js 2.92 kB {0} [rendered]
    [38] ./src/Components/Header.js 1.62 kB {2} [built]
   [39] ./src/Components/Header.html 64 bytes {2} [built]
   [40] ./src/Components/Header.scss 1.05 kB {2} [built]
   [41] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]

若是咱們改變下名字name: 'vendor'

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    children: true,
    minChunks: 2,
}),

Webpack會在沒有該文件的狀況下自動生成builds/vendor.js,以後咱們能夠手動引入:

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

你也能夠經過async: true,而且不提供共同依賴包的命名,來達到異步加載共同依賴的效果。

Webpack有不少這樣給力的優化方案。我無法一個一個介紹它們,不過能夠經過創造一個生產環境的應用來進一步學習。

飛躍到生產環境(production

首先,要在設置中添加幾個插件,但要求只有當NODE_ENVproduction的時候才運行它們:

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

var plugins = [
    new webpack.optimize.CommonsChunkPlugin({
        name: 'main',
        children: true,
        minChunks: 2,
    }),
];

if (production) {
    plugins = plugins.concat([
       // 生產環境下須要的插件
    ]);
}

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

Webpack也提供了一些能夠切換生產環境的設置:

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

設置中的第一行代表在開發環境下,將開啓debug模式,代碼再也不混作一團,利於本地調試。第二行則用來生產資源地圖(sourcemaps)。Webpack有一些方法能夠生成sourcemaps,而eval則是在本地表現最讚的一個。在生產環境下,咱們並不關心sourcemaps,所以關閉了這個選項。

如今來添加生產環境下的插件吧:

if (production) {
    plugins = plugins.concat([
        // 這個插件用來尋找相同的包和文件,並把它們合併在一塊兒
        new webpack.optimize.DedupePlugin(),

        // 這個插件根據包/庫的引用次數來優化它們
        new webpack.optimize.OccurenceOrderPlugin(),

        // 這個插件用來阻止Webpack把太小的文件打成單獨的包
        new webpack.optimize.MinChunkSizePlugin({
            minChunkSize: 51200, // ~50kb
        }),

        // 壓縮js文件
        new webpack.optimize.UglifyJsPlugin({
            mangle: true,
            compress: {
                warnings: false, // 禁止生成warning
            },
        }),

        // 這個插件提供了各類可用在生產環境下的變量
        // 經過設置爲false,可避免生產環境下調用到它們
        new webpack.DefinePlugin({
            __SERVER__: !production,
            __DEVELOPMENT__: !production,
            __DEVTOOLS__: !production,
            'process.env': {
                BABEL_ENV: JSON.stringify(process.env.NODE_ENV),
            },
        }),

    ]);
}

我廣泛使用的差很少就這麼多了,不過Webpack還提供了很是多的插件,你能夠本身去研究它們。也能夠在NPM上找到不少用戶本身貢獻的插件。插件的連接在文末提供。

還有一個關於生產環境的優化是給資源提供版本的概念。還記得output.filename裏的bundle.js嗎?在這個配置裏面,你可使用一些變量,而[hash]則會給文件提供一段隨機的字符串。除此之外,咱們想要包能夠被版本化,所以添加了output.chunkFilename

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

由於沒法得知每次打包生成的文件名,因此咱們只在生產環境下使用它。除此以外,咱們還想保證每次打包的時候,builds文件夾都會被清空以節約空間,所以使用了一個第三方插件:

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

並將它添加到配置中:

var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
// ...
if (production) {
    plugins = plugins.concat([
        // 在打包前清空 builds/ 文件夾
        new CleanPlugin('builds'),

作完這些漂亮的優化,來比較下結果的不一樣吧:

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

來看看Webpack都作了什麼:

  • 在第一段代碼中,後兩個包很是輕量,異步請求不會佔用多少HTTP帶寬,因此在生產環境下Webpack將它們打包進了入口文件裏

  • 全部東西都壓縮過了。從322kb降到了97kb

可是這樣下去,Webpack豈不是會將js文件合併成巨大的一坨嗎?

是的,在這個小小的應用中是這樣沒錯。可是你須要這麼想:你不須要考慮在何時合併什麼。若是你的包中含有太多的依賴,它們會被移走到異步請求包中而不會被合併起來。反之,若是它們很小,不值得獨立加載,那麼就會被合併。你只須要創建規則,Webpack會最大化的將其優化。沒有人力勞做,不須要思考依賴關係,一切都是自動化的。

圖片描述

或許你已經注意到了,我沒有對HTML或CSS進行壓縮。那是由於當debug模式開啓的時候,css-loaderhtml-loader已經幫咱們搞好了。這也是爲何Uglify是一個獨立插件的緣由:在Webpack中沒有js-loader這種東西,Webpack本身就是個JS loader。

抽取(extract-text-webpack-plugin

可能你已經注意到了,從這個教程一開始,Webpack打包好以後,咱們的樣式就直接插在網頁頁面上,簡直不能更難看了。能經過Webpack把打包過的CSS生成獨立的文件嗎?固然沒問題:

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

這個插件所作的就是我剛剛說的那些:從打出的最終包裏面,提取出某一類內容分離開來單獨引用。它一般被用於提取CSS文件:

var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
var ExtractPlugin = require('extract-text-webpack-plugin');
var production = process.env.NODE_ENV === 'production';

var plugins = [
    new ExtractPlugin('bundle.css'), // <=== 提取出來的文件
    new webpack.optimize.CommonsChunkPlugin({
        name: 'main',
        children: true, 
        minChunks: 2,
    }),
];
// ...
module.exports = {
    // ...
    plugins: plugins,
    module:  {
        loaders: [
            {
                test: /\.scss/,
                loader: ExtractPlugin.extract('style', 'css!sass'),
            },
            // ...
        ],
    }
};

ExtractPlugin.extrac方法接收兩個參數,第一個參數表明當它處於已經打包好的包('style')裏時,如何處理那些提取出來的東西;第二個參數表明當它在主文件('css!sass')裏時,如何對待提取出的東西。當它在包裏時,確定不能直接將CSS加在生成的東西后面,因此先用style-loader進行處理;而對於主文件裏面的styles,則將它們放進builds/bundle.css文件。咱們來給應用加一個主樣式:

// src/styles.scss
body {
  font-family: sans-serif;
  background: darken(white, 0.2);
}
// src/index.js
import './styles.scss';

// Rest of our file

跑下Webpack,就能看見已經生成了bundle.css,能夠把它引用進HTML裏:

$ webpack
                bundle.js    318 kB       0  [emitted]  main
1-a110b2d7814eb963b0b5.js   4.43 kB       1  [emitted]
2-03eb25b4d6b52a50eb89.js    4.1 kB       2  [emitted]
               bundle.css  59 bytes       0  [emitted]  main

若是你想提取出全部包裏的樣式,則須要設置ExtractTextPlugin('bundle.css', {allChunks: true})

順帶一提,你也能夠自定義文件名,就跟以前說的改變js output-file名稱同樣:ExtractTextPlugin('[name]-[hash].css')

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

到目前爲止,咱們還沒處理例如圖片、字體這樣的資源文件。它們在Webpack中如何工做,咱們又該如何優化?接下來,我要在網站的背景里加入圖片,看起來必定酷酷的:

圖片描述

把圖片保存在img/puppy.jpg,更新下Sass文件:

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

若是僅僅是這樣,Webpack必定會告訴你:「你特麼的想讓我對JPG作啥?」,那是由於尚未加入對應的loader。有兩種loader可使用:file-loaderurl-loader

  • file-loader:返回一段指向資源的URL,容許你給文件加入版本的概念(默認)

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

兩個方法不能說誰好誰壞:若是你的圖片大於2M的話那你必定不但願它直接夾雜在代碼中,而是獨立出去;而若是僅僅是2kb左右的小圖標。那麼合併在一塊兒減小HTTP請求會更好。所以,咱們兩個都要設置:

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

在這裏,咱們給url-loader了一個limit參數,這樣,當文件大小小於10kb的時候,會採起行內樣式,不然的話,會轉到file-loader進行處理。你也能夠經過query傳遞一個Object來實現它:

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

來瞅一眼Webpack的輸出:

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

輸出裏面沒有JPG圖像,那是由於咱們的小狗圖片比配置裏限制的大小要小,所以被加到了行內。訪問頁面,你就能看見這隻可愛的小狗了。

圖片描述

這是一個很是強大的功能,它意味着Webpack能夠智能的根據資源的大小和HTTP請求佔有的比率,採起不一樣的優化方案。還有一個叫作image-loader的loader,能夠在打包前檢查全部圖片,避免圖片的重複壓縮。它有一個叫?bypassOnDebug 的參數,經過它你能夠只在生產環境下啓動該插件。

還有不少優秀的插件,我強烈建議你使用文末的連接去查看它們。

來個牛逼的熱加載(dev-server)

咱們的生產環境以及整的差很少了,如今應該更多的關心一下本地開發。或許你以及注意到了,當人們說起開發工具的時候,老是會說起熱加載:LiveReload,BrowserSync,或者其餘的什麼鬼東西。可是隻有傻瓜纔會整頁的刷新,咱們則使用更高端的熱加載。由於Webpack能夠確切的知道你依賴樹中某一點位置的代碼,所以每次的改變都會據今生成一個新的文件。簡單的說,就是不須要刷新頁面就能將改變展示在屏幕上。

爲了可以使用HMR,咱們須要一個server來啓動熱加載。Webpack提供的dev-server能夠完成這個任務:

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

安裝下面的命令啓動server,不能再簡單了:

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

第一個標記--inline是讓Webpack把HMR邏輯直接寫入頁面上而不是放到iframe裏,而第二個標記則開啓了HMR。接下來,訪問http://localhost:8080/webpack-dev-server/,嗯仍是那個正常的頁面。試着修改Sass文件,MAGIC!

圖片描述

你能夠把webpack-dev-server做爲本身本地的server。若是你打算一直使用HMR,就須要這麼配置:

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

這樣的話,無論咱們何時運行webpack-dev-server,都會是HMR模式。值得一提的是,咱們在這裏使用webpack-dev-server對資源進行熱加載,但也可使用在其餘地方例如Express server上。Webpack提供了一箇中間件,使得你能夠把HMR的功能用在其餘server上。

代碼不乾淨的人都給我去罰站!(pre-loader & lint)

若是你一直跟着本教程走,那或許會有這樣的疑問:爲何loader都在module.loaders中而插件不在?那固然是由於還有其餘能夠配置進module的東西~Webpack不僅是有loader,也有pre-loader和post-loader:在main-loader運行以前和以後發動的玩意。舉個栗子:我基本能夠確信本身在這個文章裏面寫的代碼很糟糕,因此使用ESLint進行代碼檢查:

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

新建一個確定會引起錯誤的.eslintrc文件:

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

如今增長pre-loader,語法和以前的同樣,只不過加在module.preLoaders裏:

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

啓動Webpack,而後淡定的看它失敗:

$ webpack
Hash: 33cc307122f0a9608812
Version: webpack 1.12.2
Time: 1307ms
                    Asset      Size  Chunks             Chunk Names
                bundle.js    305 kB       0  [emitted]  main
1-551ae2634fda70fd8502.js    4.5 kB       1  [emitted]
2-999713ac2cd9c7cf079b.js   4.17 kB       2  [emitted]
               bundle.css  59 bytes       0  [emitted]  main
    + 15 hidden modules

ERROR in ./src/index.js

/Users/anahkiasen/Sites/webpack/src/index.js
   1:8   error  Strings must use doublequote  quotes
   4:31  error  Strings must use doublequote  quotes
   6:32  error  Strings must use doublequote  quotes
   7:35  error  Strings must use doublequote  quotes
   9:23  error  Strings must use doublequote  quotes
  14:31  error  Strings must use doublequote  quotes
  16:32  error  Strings must use doublequote  quotes
  18:29  error  Strings must use doublequote  quotes

再舉個pre-loader的例子:每一個組件裏咱們都引用了stylesheet,而它們都有相同命名的對應模板。使用一個pre-loader能夠自動將有相同名稱的文件做爲一個module載入:

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

經過這樣的方式告知Webpack,若是碰見和配置相同的HTML文件,則將它做爲template 引入,同時引入和它同名的Sass文件。這樣就能改寫組件文件:

將:

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

改成:

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

你看,pre-loaders也能夠很強大。在文末你能夠找到更多的loader

還沒看夠?

如今咱們的應用還很小,當它變的龐大的時候,觀測依賴樹就變的很是有用了,從中能夠看出咱們作的是對是錯,應用的瓶頸在哪裏等等。Webpack知曉這一切,不過咱們得禮貌的請教它才能知曉答案。爲了作到這點,你能夠經過下面的命令運行Webpack:

webpack --profile --json > stats.json

第一個標記會讓Webpack生成一個profile文件,而第二個則將它轉化爲JSON格式。最終,講全部的output都生成了JSON文件。如今有不少網站均可以解析這個JSON文件,不過Webpack官方提供了一個解碼的網站Webpack Analyze。將JSON文件導入,進入Modules板塊,就能夠看見本身依賴樹的可視化圖像:

圖片描述

小圓點越紅,則證實在打包的時候越困難。在這個例子中,jQuery做爲最大的文件而成爲罪魁禍首。再瞅瞅網站上的其餘模塊。或許你沒法從這個小小的例子裏學到不少東西,可是這個工具在分析依賴樹和包的時候真的很是有用。

我以前提過,如今有不少服務提供能夠對profile文件進行分析。其中一個是Webpack Visualizer,它能夠以餅狀圖的形式告知你各個文件佔據了多大的比重:

圖片描述

就先講到這兒吧

對我而言,Webpack已經取代了Grunt或者Gulp:大部分的功能可使用Webpack替代,其餘的則使用NPM腳本就夠了。在之前,每一個任務中咱們都要經過Aglio,把API文檔轉換爲HTML,而如今只須要這麼作:

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

即使是一些不須要打包和構建的Glup任務,Webpack都貼心的提供了對應的服務。下面是一個將Glup融合進Webpack的例子:

var gulp = require('gulp');
var gutil = require('gutil');
var webpack = require('webpack');
var config = require('./webpack.config');

gulp.task('default', function(callback) {
  webpack(config, function(error, stats) {
    if (error) throw new gutil.PluginError('webpack', error);
    gutil.log('[webpack]', stats.toString());

    callback();
  });
});

由於Webpack具備Node API,所以能夠很輕鬆了運用在其餘構建體系中。不用多久你就能發現本身深愛着它沒法自拔了。

無論怎樣,這篇文字帶你預覽了Webpack可以幫你作的事情。或許你認爲咱們講了不少方面,但實際上這只是個表皮而已:multiple entry points, prefetching, context replacement等等都尚未涉及到。Webpack是個強大的工具,也所以比那些傳統的工具更加難懂。但一旦你知道如何使用它,它就會爲你鳴奏最悅耳動聽的聲音。我曾在一些項目裏使用過它,它提供的強大的優化和自動化讓我深深不能自拔。

資源

本文轉載於猿2048:[譯] 用 Webpack 武裝本身

相關文章
相關標籤/搜索