Webpack 你的包


Webpack你的包

本文轉載自:衆成翻譯
譯者:Hugo
鏈接:http://www.zcfy.cc/article/921
原文:https://blog.madewithlove.be/post/webpack-your-bags/

Webpack你的包

也許你已經聽說過這個叫做webpack非常酷的新工具。如果你沒深入的瞭解過它你可能會感到困惑,因爲有些人稱它爲類似Gulp的構建工具,還有人稱它爲類似Browserify的打包工具。如果你已經深入瞭解過它,你也可能因爲主頁作爲Webpack呈現而感到困惑。

老實說,最初“what Webpack is”這個話題嚇得我關掉了標籤頁。畢竟我已經有了一個構建系統,而且非常滿意。如果你像我一樣緊隨Javascript的快速發展,你可能已經因爲跟隨潮流太緊受到傷害。經驗更多的我覺得應該寫這樣的一篇文章,爲大家解釋一下webpack到底是什麼,更重要的是,到底webpack有什麼了不起的地方能得到這麼多的關注。

什麼是Webpack?

現在讓我們回答一下引文中提到的問題:webpack是一個構建系統或者模塊打包工具嗎?好吧,都是,但我的意思不是它會把兩件事情都做,而是它會把兩者結合起來。Webpack不會創建你的資源,然後分別打包你的模塊,它是在考慮如何讓你的資源本身自動模塊化。

準確的說例如構建你所有的sass文件,將圖片優化並放在一起,然後打包你的模塊,之後在頁面上引用,這些事情Webpack不會做,作爲替代,你的代碼是這樣的:

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返回的結果都是字符串。Webpack能夠將它們包裹進Javascript模塊中。只要你的sass文件被loaders轉換過,內部就應該類似這樣:

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

我爲什麼要這麼做?

一旦你理解了Webpack做了什麼,很有可能第二個問題就會在頭腦中出現:這樣做有什麼好處?“圖片和CSS?在我的JS裏邊?弄啥來!?”。這樣考慮一下:很長一段時間,我們學到的是將所有的東西連結在一個單獨的文件裏;爲了節省我們的HTTP請求,yada yada.

這導致了一個非常大的問題,現在的開發者都將所有的資源打包到一個‘app.js’文件中,之後在所有的頁面中引用這個文件。這意味着任何頁面的加載過程中都浪費了很多時間去加載大量不需要的資源。如果你不這樣做,你就很可能會在特定頁面上手動去引用資源,這就會生成一個大而混亂的依賴樹要去維持和跟蹤:哪個頁面上已經有了這個依賴?樣式表A和B在作用在哪些頁面上?

沒有哪種處理方式是絕對正確或錯誤的。將Webpack看成是兩面兼顧的-不僅僅是一個構建或打包工具,更是一個了不起的,聰明的模塊打包系統。一旦正確配置,它會比你更瞭解你的工作棧,更明白如何去優化。

讓我們一起創建一個小的應用

爲了更容易的讓你理解Webpack的好處,我們會創建一個非常小的應用,然後打包它的資源。這個教程中我建議運行node4(或5)和NPM3去和Webpack一起工作,這會省去很多麻煩。如果你還沒安裝NPM3,你可以通過運行npm install npm@3 -g來安裝它。

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

我也建議你在PATH變量裏添加node_modules/.bin,省去了每次都要指定node_modules/.bin/webpack。之後所有例子中我運行的所有命令都不會出現node_modules/.bin

基本的引導

讓我們開始創建我們的項目,安裝Webpack,之後我們也會引入jQuery論證一些東西。

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

現在創建應用入口,現在使用ES5:

src/index.js

var $ = require('jquery');

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

在名爲webpack.config.js的文件裏配置Webpack。Webpack的配置就是Javascript,需要作爲一個對象被導出:

webpack.config.js

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

這裏,entry告訴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,如果一切正常那我們就應該會得到一條信息,告訴我們它正確地編譯了我們的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包含了我們的入口文件和一個隱藏模塊,隱藏模塊是jQuery,Webpack會將不屬於你的模塊隱藏掉。如果想查看Webpack編譯的所有模塊,可以加入--display-modules標記:

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

你也可以運行webpack --watch監控文件的修改然後根據需要去自動重新編譯。

設置我們的第一個loader

現在還記得我們是怎樣討論關於Webpack能夠去導入CSS和HTML以及其他多種類型的嗎?如果你一直關注着這些年Web組件的大變動(Angular 2, Vue, React, Polymer, X-Tag, etc.),你可能已經聽說了這個概念,你的應用不再是一個個相互連通的UI,取而代之的是可維護的,獨立的,可複用的UI:web組件(我這裏簡化說明,你應該懂)。現在爲了讓組件真正的獨立化,它們需要將自己的需求和自己一起打包。想象一個按鈕組件:它肯定有一些HTML,然後一些JS實現交互,可能還有一些樣式。如果這些東西只在我們需要的時候加載,感覺會很好,是不是?只有當我們引入Button組件時,我們纔會得到相關資源。

下面來寫我們的button;首先,我假設你們中大多數已經習慣了ES2015,添加第一個loader:Babel。在Webpack中安裝一個loader你需要做兩件事:npm install {whatever}-loader,之後把它添加到Webpack配置中的module.loaders部分。這裏我們想安裝babel,所以:

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

我們也需要安裝Babel,因爲loader不會安裝它。我們需要babel-core這個包還有es2015 preset:

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

之後我們要創建一個.babelrc文件去告訴Babel去使用那個preset。這是一個簡單的JSON文件,允許你設置什麼Babel轉換器會運行在你的代碼上,在我們的例子裏我們告訴它使用es2015preset。

.babelrc
{
“presets”: [“es2015”]
}

現在Babel安裝和配置好了,我們可以去更新我們的配置:我們想要什麼?我們想讓Babel去運行所有後綴爲.js的文件,但是我們不想讓Babel運行在jQuery代碼上,我們可以過濾掉它。Loader可以同時有includeexclude規則。可以是一個字符串,正則,或者回調函數,任何你想要的。在這個例子裏,我們想讓Babel只運行在我們自己的文件上,所以我們只include我們自己資源的目錄:

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

在導入Babel之後,我們可以用ES6重寫index.js。之後所有例子都會用ES6來寫。

import $ from 'jquery';

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

編寫一個小組件

現在我們來寫一個Button組件,其中會有一些SCSS樣式,一個HTML模板,和一些行爲。所以我們根據需求安裝一些東西。首先是Mustache,這是一個輕量的模板包,我們也需要一些轉換Sass和HTML文件的loaders。同時,我們也需要一個CSS loader處理從Sass loader傳出的結果。現在,一旦我們有了自己的CSS,就有多種方式去處理,暫時的我們使用一個叫style-loader的loader,它會把一小段CSS動態注入到頁面中。

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

現在爲了告訴Webpack將代碼從一個loader傳到另一個loader,我們寫了一串loaders,從右到左,用一個!分隔。你也可以將一個數組賦給loaders屬性來代替loader

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

現在我們有了loaders,來寫我們的button:

src/Components/Button.scss

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

src/Components/Button.html

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

src/Components/Button.js

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

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已經是100%獨立的,無論在什麼時候,什麼地方被引用,都會被正確渲染。現在我們只需要在頁面上渲染Button:

src/index.js

import Button from ‘./Components/Button’;

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

我們試着運行Webpack然後刷新頁面,你應該能看到我們的button已經起作用了。

你已經學到了如何安裝loaders和如何去定義應用各部分之間的依賴關係。現在這看起來可能影響不是很大,但是讓我們繼續吧。

代碼分割

這個例子很好,但是也許我們並不是總會用到button。也許在一些頁面上沒有一個a標籤需要去渲染一個button,在這些例子裏,我們不想去導入所有的Button樣式,模板,Mustache以及所有東西,對嗎?這時候就要用到代碼分割了。代碼分割是Webpack對“整塊包”VS“不可維護的手工導入”問題的迴應。這是由你在代碼裏定義的,“分割點”:代碼中能被分割成單獨文件的地方,之後能根據需求被引用。語法非常簡單:

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中 - 一個只有在我們需要時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會根據需要導入它。這意味着你可以用各種邏輯將你的代碼包裹成chunks,這也是我們接下來要做的。只有當我們的頁面有一個鏈接時我們才需要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不會去處理默認導出還是普通導出,所以你必須去指定返回值。然而import會去處理這些事情。(例如. import foo from 'bar') vs import {baz} from 'bar').

相應的,Webpack的輸出應該不同了。我們通過--display-chunks命令去運行它,看一下模塊和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這個chunk中,並且只有被頁面引用時纔會加載。現在爲了讓Webpack知道通過AJAX加載它們的時候去哪裏找到對應的chunks,我們必須在配置文件中加入一小行代碼:

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

output.publicPath配置項告訴Webpack去哪裏可以找到頁面上引用到的構建資源(我們這裏就是/builds/)。如果現在訪問我們的頁面,就會發現一切都在正常運行,更重要的是我們會看到,一旦我們在頁面上引用模塊,Webpack會正確的加載我們的chunk:

如果我們沒有在頁面上引用模塊,Webpack只會加載bundle.js。這能讓你聰明的將應用中的厚重邏輯分解開,讓每一個頁面只加載它真正需要的。提醒一下,我們也可以給我們的分割點命名,用更有意義的chunk名字去代替1.bundle.js。你可以向require.ensure中傳入第三個參數實現命名:

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

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

代替1.bundle.js生成的是button.bundle.js

添加第二個組件

現在已經非常好了,但是讓我們來添加第二個組件看一下是否正常工作:

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

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

現在用--display-chunks --display-modules指令查看一下Webpack的結果:

$ 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相對於loaders的不同之處是,它不是隻運行在某種特定文件裏,而是起到了類似於管道的作用,它們運行在所有文件上並且表現出預設的行爲,它們並不必然和轉換相關。Webpack用一些插件來執行所有各種各樣的優化。在這個例子中讓我們感興趣的是CommonChunksPlugin:它分析你的chunks的循環依賴關係,在其他的地方提取它們。它可以是一個完全獨立的文件(像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的名字。

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>
<script src="builds/bundle.js"></script>

你也可以通過不去提供一個共同的chunk名字去異步加載共同依賴,而不是去指定async: true.Webpack有很多這種強大,智能的優化。我不能一一列舉,但作爲練習,我們來試着爲我們的應用創建一個生產版本。

生產和更多

好的第一步,我們在配置項中添加幾個plugins,但是我們只想在NODE_ENVproduction的時候才加載它們,所以要在配置中添加一些邏輯。既然它是一個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',

第一個設置項切換loaders的debug模式,這意味着在本地環境不會包含多餘的代碼讓你去輕鬆地調試。第二個是關於sourcemaps的生成。Webpack有幾種方式去渲染sourcemaps, eval在本地環境中是最好的。在生產環境中我們並不關心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提供了一些plugins供你去使用來調整你的模塊和chunks。NPM上也有幾個用戶貢獻的插件實現了各種功能。文章最後有可用plugins的鏈接。

理想情況下,你會想讓你的生產資源有版本號。還記得我們設置output.filenamebundle.js?在這個選項中有幾種變量可供我們使用,其中一個是[hash],對應的是最終生成的bundle內容的hash,我們來修改一下代碼。我們也想讓我們的chunks版本化,所以我們添加output.chunkFilename來做同樣的事情:

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

因爲在這個簡化的應用中,我們沒有辦法去動態檢索已編譯bundle的名字,所以在這個例子中,我們只會在生產環境中給資源設定版本。我們也想在生產環境建立之前去清理我們的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將它們合併到入口文件中了。第二點,每部分都被適當地壓縮了。我們從三個三個HTTP請求322kb的資源,優化到了一個HTTP請求97kb的資源。

但Webpack的重點不是去掉了一個大JS文件嗎?

是的,是這樣的,但是這僅僅當我們的應用很小的時候會發生。現在考慮一下:你不必考慮什麼時候,什麼地方要去合併什麼。如果你的chunks忽然添加了以來,chunk就會被異步加載而不是被合併;如果這些chunks太類似,不值得去單獨加載,它們就會被合併,等等。你只需要制定規則,之後,Webpack會用最好的方式自動的優化你的應用。沒有體力勞動,不用去考慮哪裏添加了依賴,哪裏需要依賴,所有的事情都自動化。

你也許注意到我並沒有爲了壓縮HTML和CSS去安裝什麼,這是因爲我們之前提到的debug如果被設置爲falsecss-loaderhtml-loader默認會爲我們做這些事情。這也是爲什麼Uglify是一個單獨的plugin : Webpack中沒有js-loader,因爲它自己就是JS loader。

抽取

現在也許你已經注意到了,在教程的一開始我們的樣式就被注入了頁面中,這導致了FOUAP(醜陋的頁面)。那如果我們現在把Webpack中的樣式集中起來放到一個最終的CSS文件裏,情況會不會變好呢?當然可以,我們需要藉助一個外部plugin的幫助:

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

這個plugin做的工作就是我剛剛說的:從最終生成的包中抽取特定類型的內容,傳到其它地方,最常用的就是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'),
            },
            // ...
        ],
    }
};

現在這個extract方法有兩個參數:一是當我們在一個chunk中時如何處理提取的內容('style'),二是當我們在主文件中要做些什麼('css!sass')。現在,如果我們在一個chunk中,我們不能魔法般的把CSS添加到已經生成的文件中,所以這裏我們和之前一樣用到了styleloader,對於在主文件中的所有樣式,把它們全部放到一個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

如果你也想把chunks中的樣式提取,你可以傳入ExtractTextPlugin('bundle.css', {allChunks: true})。提示一下這裏你也可以在文件名中使用變量,所以如果你想把樣式表版本化你可以像Javascript文件那樣傳入ExtractTextPlugin('[name]-[hash].css')

靜態資源的處理

現在我們的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會跟你說“我TM的怎麼搞JPG啊”,因爲我們沒安裝對應的loader。有兩個loaders可以幫我們處理靜態資源:file-loaderurl-loader:
- 第一個不會做特殊處理,僅僅返回資源的URL,在這過程中允許你去給文件規定版本(這是默認行爲)。
- 第二個會將資源內聯到一個data:image/jpeg;base64URL中

事實上兩種方式沒有絕對好壞之分:如果你的背景是一張2Mb的圖片,你不會想內聯它,單獨的加載它是更好的方式。另一方面,如果是一個4Kb的小圖標,內聯是更好的方式,還可以節約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。該語法被稱爲查詢字符串,使用它去配置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現在能依據HTTP請求大小來智能的優化任何靜態資源。有一些loaders能讓你做進一步的優化,比如 image-loader會在打包圖片之前把imagemin傳給它們。它甚至還有一個?bypassOnDebug查詢字符串,能讓你只在生產環境中做這件事情。還有很多類似的plugins,我支持你去看一下文章末尾的列表。

熱替換

現在我們的產品構建方式已經設定好,我們來專注於本地開發。你也許注意到在我們提到構建工具時經常出現一個問題:重載:LiveReload, BrowserSync,無論頁面是什麼內容。讓整個頁面全部刷新是笨蛋做的事,讓我們借用叫做 hot module replacement或者hot reload的工具來改善這種狀況。思路是,既然Webpack清楚的知道每個模塊在我們的依賴樹中的位置,其中的改變應該表現爲藉助新文件簡單地對樹的一部分的修改。簡單點說:你的改變在頁面沒有重載的情況下顯示在了屏幕上。

爲了實現HMR的使用,我們需要給資源建一個服務器。我們可以利用Webpack中的dev-server實現,安裝它:

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

現在運行dev server,非常簡單,只要運行下邊的命令:

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

第一個命令告訴Webpack在頁面中包含HMR邏輯(代替在iframe中顯示頁面),第二個打開HMR。現在讓我們在這個地址http://localhost:8080/webpack-dev-server/訪問web-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的服務器。Webpack提供了一箇中間件讓你能夠在其他的服務器上實現HMR。

輕量化

你過你仔細地看了這篇文化在那個你也許會注意到一些奇怪的東西:爲什麼loaders都放進 module.loaders但plugins沒有?當然這是因爲還有其它的東西你可以放進module!Webpack不僅僅有loaders,它還有pre-loaders和post-loaders:在主loaders之前或之後執行的loaders。舉個例子:我確信這篇文章中的代碼寫的很差,所以讓我們來應用ESLint:

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

我們要建一個我知道會失敗的簡單的.eslintrc文件

.eslintrc

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

現在添加pre-loader,我們之前使用過相同的語法,但是是在module.preLoaders

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文件,第二個是使用JSON格式去生成,最後將所有東西輸出到JSON文件。現在有許多網站去分析這些profile文件,但是Webpack提供了一個官方版本去分析這份信息。去Webpack Analyze導入你的JSON文件。現在進入Modules標籤應該能看到你的依賴樹的一個可視化圖像:

圓點越紅,在你最終的生成包中問題性就越大。在我們的例子中,jQuery被標註爲問題性的因爲它是所有模塊中被重用最多的,看一下所有標籤裏的內容,你不會從我們的應用中學到很多,但是這個工具對於你觀察你的依賴樹和最終的生成包是非常重要的。就像我說的,其他的服務提供了你的profile文件的內在分析,另一個我喜歡用的是Webpack Visualizer ,它依據生成報中內容所佔空間生成了一個環形表,我們的是這樣的:

That’s all folks

現在我知道在我的例子裏,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"
  }
}

如果你在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也擁有NodeAPI,使它能夠輕鬆地使用在其他構建系統中,你會發現它無處不在。

總之,我認爲這對你來說是對Webpack的一個非常好的審視。你也許會覺得這篇文章覆蓋了很多內容,但我們僅接觸了表面的東西:多個入口,預先提取,上下文切換,等等。Webpack是一個非常出色的工具,這當然也會比傳統的構建工具多了複雜的配置語法,我不會否認。但一旦你知道怎樣去馴服它,你能看到它非常棒的表現。我在幾個項目中使用了它,他提供瞭如此強大的優化和自動化能力,以至於我不敢向我再讓我去敲着腦袋去決定資源在何時何處被引用了。

資源


發佈了51 篇原創文章 · 獲贊 33 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章