[譯] 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,有興趣的同窗也能夠看看。

相關文章
相關標籤/搜索