Webpack經典入門

赤~日華發表於2017-11-01

原文: Webpack your bags

October 16th, 2015 by Maxime Fabre
(中文譯本更新於2017年10月22日,由於webpack已更新至3.8.1,因此譯文有改動,譯文最後有各分段程式碼包地址,如有需要請自行下載)

Webpack經典入門

Webpack your bags

之前你可能已經聽說過這個叫webpack的很酷的工具,如果你沒仔細瞭解過這個工具,你可能會有些困惑,因為有人說它像 Gulp 之類的構建工具,也有人說它像 Browserify 之類的模組管理工具,如果你有去仔細地瞭解一下,你可能還是會不明白到底是怎麼一回事,因為官網上把webpack說成是這兩者。

說實話,開始的時候我對於“webpack到底是什麼“ 很模糊,並且感覺很受挫,最後我直接就把網頁關了,畢竟我在這之前已經有一個構建系統的工具了,而且我用得也非常嗨皮,如果你像我一樣一直密切跟蹤javascript的發展的話,可能你早就因為在各種流行的東西上頻繁地跳來跳去而灰飛煙滅了。還好現在我有些經驗了,覺得可以寫一篇文章來給那些還處在混沌中的小夥伴們,更清楚地解釋一下webpack到底是什麼,更重要的是它到底什麼地方那麼出色以至於值得我們投入那麼多的精力。

1. Webpack是什麼?

Webpack到底是一個構建系統還是一個模組管理器?好的,現在我馬上來回答一下───答案是兩個都是,當然我不是說它兩個事兒都幹,我的意思是它把兩者有機地連線起來了,webpack不是先構建你的資源,然後再管理你的模組,而是把你的資源本身當做模組。

更準確的說,它不是先編譯你所有的scss檔案,再優化所有的圖片,之後在一個地方引進來,再管理所有的模組,最後在另一個地方引到你的頁面上。假設你是下面這樣:

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) // "Hello"複製程式碼

如上面所示,你的所有的資源(無論是scss,html或者其它)都可以被當成模組,而且這些模組可以被import(引入),modify(修改),manipulate(操作),最後被打包到你的最終包(bundle)

為了達到這個目的,得在你的webpack的配置檔案裡註冊載入器(loaders),載入器就是當你遇到某種檔案的時候,對它做相應處理的一種外掛,下面是一些載入器的例子:

{
  // 當你匯入(import)一個a.ts檔案時,系統將會用Typescript載入器去解析它
  test: /\.ts/,
  loader: 'typescript',
},
{
  // 當你的專案裡面有圖片時,系統將會用image-webpack載入器把它們壓縮
  // 並且它們會被關聯到data64 URLs
  test: /\.(png|jpg|svg)/,
  loaders: ['url', 'image-webpack'],
},
{
  // 當你的專案裡面有scss檔案時,系統將會通過node-sass載入器去解析,並且自動補充字首
  // 最後返回css(可以直接被系統解析)
  test: /\.scss/,
  loaders: ['css', 'autoprefixer', 'sass'],
}複製程式碼

最後所有的loader返回的都是string,這樣webpack最終可以把資源都包裝成javascript模組。就像這個例子裡scss檔案被loaders轉換後,看起來差不多是這樣子:

export default 'body{font-size:12px}';複製程式碼

Webpack經典入門

2. 世界那麼大,你為什麼要用它?

一旦你明白webpack是什麼後,很可能就會想到第二個問題:用它能有什麼好處呢?“把Image和CSS放在我的JS裡?什麼鬼?”好吧,之前很長一段時間我一直被告訴要把所有檔案都放在一個檔案裡,這樣就保證不浪費我們的HTTP請求。

但這樣會導致一個很大的缺點,就是現在大多數人都把所有的資源打包到一個單獨的app.js檔案裡,然後把這個檔案引入到每一個頁面。這就意味著渲染每一個頁面的時候大部分時間都浪費在載入一大堆根本就沒用到的資源上。但是如果你不這麼做,那麼你很有可能得手動把這些資源引入到指定的頁面,這就會導致需要一大團亂七八糟的依賴樹去維護和跟蹤一些問題例如:哪些頁面需要依賴這個檔案?修改a.css和b.css會影響到哪些頁面?

因此這兩個方法都是不對,但也不全是錯的。如果我們把webpack當做一箇中介───它不僅僅是一個構建系統或者一個打包工具,而是一個邪惡的智慧打包系統,正確地配置後,它甚至比你還了解你的系統,而且它也比你清楚怎麼樣才能最好優化你的系統。

3. 我們來做一個小的app

為了讓你更容易地理解webpack帶來的好處,我們做一個小的app,然後用webpack來打包我們app的資源,在做之前我建議用Node 4及以上版本和NPM 3及以上版本,因為良好的依賴關係在你使用webpack的時候會避免很多讓人頭疼的問題,如果你的NPM版本不夠新,你可以通過 npm install npm -g 來更新。

$ node --version
v6.11.2
$ npm --version
5.4.2複製程式碼

同時我也建議你把 node_modules/.bin 加到你的PATH環境變數裡,以避免每次都手動打 node_modules/.bin/webpack ,後面的所有例子都不會顯示我要執行的命令列的 node_modules/.bin 部分(全域性安裝 webpack 則忽略此步驟)。

備註:全域性安裝 webpack 命令列:通過 npm install webpack -g 來更新。

基本引導

現在開始,先建立個名為webpack-your-bags的資料夾,在資料夾下安裝webpack,並且也加上jquery好在後面證明一些東西

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

現在讓我們來建立一個app入口,用現在的純ES5

路徑:webpack-your-bags/src/index.js

var $ = require('jquery');
$('body').html('Hello');
複製程式碼

建立webpack的配置檔案webpack.config.js,webpack.config.js是javascript,需要匯出(export) 一個物件(object)
路徑:webpack-your-bags/webpack.config.js

var path = require("path");//用於處理目錄的物件,提高開發效率(__dirname需要引入path後才可以使用)
var ROOT_PATH = path.resolve(__dirname);//獲取當前整個模組檔案所在目錄的完整絕對路徑
var BUILDS_PATH = path.resolve(ROOT_PATH, "builds");//獲取我們的builds目錄完整絕對路徑
module.exports = {
    entry: './src',
    output: {
        path: BUILDS_PATH,
        filename: 'bundle.js',
    },
};複製程式碼

這裡,entry會告訴webpack哪個檔案是你的app的入口點。這些是你的主檔案,他們在依賴樹的頂端,然後我們告訴它編譯我們的資源到放在 builds 目錄(路徑:webpack-your-bags/builds)的bundle.js檔案下,我現在再建立相應的 index.html
路徑:webpack-your-bags/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: 65d56cd1e7dddf04958b
Version: webpack 3.8.1
Time: 250ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  271 kB       0  [emitted]  [big]  main
   [0] ./src/index.js 51 bytes {0} [built]
    + 1 hidden module複製程式碼

這裡webpack會告訴你bundle.js包含了我們的入口點(index.js)和一個隱藏的模組,這個隱藏的模組就是jquery,預設情況下webpack會把第三方模組給隱藏掉,如果想要看見webpack編譯的所有的模組 ,我們可以加 --display-modules 引數

$ webpack --display-modules
Hash: 65d56cd1e7dddf04958b
Version: webpack 3.8.1
Time: 263ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  271 kB       0  [emitted]  [big]  main
   [0] ./src/index.js 51 bytes {0} [built]
   [1] ./node_modules/jquery/dist/jquery.js 268 kB {0} [built]
複製程式碼

如果你覺得每次修改都要重新執行webpack太麻煩,你也可以執行 webpack --watch 來自動監視檔案的變化,如果檔案有發生變化,便會自動重新編譯。

搭建我們的第一個loader

還記得我們討論過webpack可以匯入CSS和HTML還有各種各樣的檔案嗎?什麼時候能派上用場呢?好的,如果你過去幾年都朝著web元件的方向大躍進(Angular 4, Vue, React, Polymer, X-Tag, etc.)。那麼估計你聽過這樣一個觀點:你的app如果是用一套可重用,自包含的UI元件搭建起來的話,會比一個單獨的內聚的UI更容易維護───這個可重用的元件就是web 元件(在這裡我說的比較簡單,你能理解就行),現在為了讓元件變成真正的自包含,需要把元件需要的所有的東西封裝到他們自己的內部,比如我們來考慮一個button,它的裡面肯定有一些HTML,而且還有一些JS來保證它的互動性,可能還得再來一些CSS,這些東西如果需要的時候再一起載入進來就顯得非常完美了,也就是隻有在我們匯入button元件的時候,我們才會去載入這些資原始檔。

現在我們來寫一個button,首先假設你已經熟悉了ES2015(即ES6,javascript的新標準),因為有些瀏覽器還不支援ES6,所以我們需要babel來為我們把ES6轉為瀏覽器支援的ES5 ,我們先加入babel的載入器(loader)。想要在webpack裡安裝一個loader,有兩步需要做:1. npm install {whatever}-loader ;2.把它加到你的webpack配置檔案(webpack.config.js)裡的 module.loaders 部分,好的,那現在我們要加babel,所以先安裝:

$ npm install babel-loader --save-dev

我們也得安裝babel本身,因為現在我們這個例子載入器不會自動安裝它們,因此我們需要裝babel-core這個包和它的預設 es2015(也就是在程式碼被執行前執行的轉換器版本):

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

--save-dev : 專案開發過程中需要依賴的包,釋出之後無需依賴的包,例如我們的babel,開發過程需要babel為我們把書寫的es6轉為es5,釋出之後由於我們所有書寫的es6程式碼都被轉為es5,因此無需繼續依賴;
--save : 專案釋出後依然需要依賴的包,例如我們jquery

我們現在建立 .babelrc 檔案,告訴babel用哪個預設,這是一個json檔案告訴babel在你的程式碼上執行哪種轉換器,現在我們告訴它用es2015:
路徑:webpack-your-bags/.babelrc

{
"presets": ["es2015"] 
}
複製程式碼

現在babel配置完了,我們可以更新webpack的配置(也就是webpack.config.js檔案):我們想要什麼?我們想babel在我們所有以.js結尾的檔案上執行, 但是由於webpack會遍歷所有的依賴,但是我們想避免babel執行在第三方程式碼上,如jquery,所以我們可以再稍微過濾一下,loaders既可以有 include 也可以是 exclude,可以是字串(string),正規表示式(regex),也可以是一個回撥(callback),隨便你用哪個。因為我們想讓babel只執行在我們的檔案上,所以我們只需要include到我們自己的source目錄也就是src資料夾下:

var path = require("path");//用於處理目錄的物件,提高開發效率(__dirname需要引入path後才可以使用)
var ROOT_PATH = path.resolve(__dirname);//獲取當前整個模組檔案所在目錄的完整絕對路徑
var BUILDS_PATH = path.resolve(ROOT_PATH, "builds");//獲取我們的builds目錄完整絕對路徑
var SRC_PATH = path.resolve(ROOT_PATH, "src");//獲取到我們的資源目錄src的完整路徑
module.exports = {
    entry: './src',
    output: {
        path: BUILDS_PATH,
        filename: 'bundle.js',
    },
    module: {
        loaders: [{
            test: /\.js/,
            loader: 'babel-loader',
            include: SRC_PATH,
        }],
    }
};
複製程式碼

現在我們可以用ES6重寫一下我們的 index.js,由於我們引入了babel,從這裡開始後面的所有例子都用ES6。

import $ from 'jquery';
$('body').html('Hello');
複製程式碼

寫個小元件

現在我們來寫一個小的Button元件,它需要有一些SCSS樣式(SCSS是SASS 3 引入新的語法,SASS是CSS的一種前處理器),一個HTML模板,和一些JS互動,那麼我們先安裝一些我們需要的東西。首先我們用一個非常輕量的模板包Mustache(前後端分離的上古框架,瞭解就好),我們也需要給SCSS和HTML配置loaders,因為結果會通過管道(pipe)從一個loader傳到另一個,這裡有點繞,怎麼說呢,類似淨水器,我們寫的SCSS通過第一個loader淨化之後變成CSS,CSS通過管道(pipe)流向第二個loader,CSS再通過第二個loader又變成可以被 import 的STYLE模組等等等過程,好了,所以我們需要一個 sass-loader 載入器來“淨化”SCSS,一旦我們得到了CSS,會有多種方式處理它,這裡,我們用一個 style-loader 載入器,它會接收一段CSS,然後動態地把它插入到頁面。

$ npm install mustache --save
$ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev
複製程式碼

為了讓webpack用管道(pipe)把東西從一個loader傳到另一個loader,我們簡傳入幾個loader方向由右到左,用一個 ! 分開,或者你可以用一個陣列通過 loaders 屬性,不是 loader:

{
test: /\.js/,
loader: 'babel-loader',
include: SRC_PATH,
}, {
test: /\.scss/,
loader: 'style-loader!css-loader!sass-loader',
// Or
//loaders: ['style-loader', 'css-loader', 'sass-loader'],
}, {
test: /\.html/,
loader: 'html-loader',
}
複製程式碼

現在我們把loaders準備好了,我們來寫一個button:
路徑:webpack-your-bags/src/Components/Button.scss

.button {background: tomato; color: white; }
複製程式碼

路徑:webpack-your-bags/src/Components/Button.html

<a class="button" href="{{link}}">{{text}}</a>
複製程式碼

路徑:webpack-your-bags/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現在是百分之百的完整體了,無論什麼時候匯入,無論執行在什麼地方,它都有所有需要的東西,然後正確地渲染到那個地方,現在我們只需要把我們的Button通過index.js渲染到我們的網頁上:

// import $ from 'jquery';
// $('body').html('Hello');

import Button from './Components/Button';
const button = new Button('google.com');
button.render('a');
複製程式碼

我們來執行一下webpack,然後重新整理一下index.html頁面,你應該就能看見你的挺醜的button出來了。

Webpack經典入門

現在你已經學會怎麼配置loader和怎麼給你的app的每一個部分定義依賴,但是現在這些可能看起來已經不太重要,因為我們要把這個例子再做一下改進。

程式碼拆分

上面這個例子還不錯因為什麼都有,但我們有時可能不需要我們的Button,因為有些介面如果沒有 a 標籤的話,那就不需要我們做無謂的把 a 標籤渲染成 Button 的操作了。也就是說我們根本就不需要匯入我們上面定製的Button的樣式,模板,Mustache 和一切相關的東西了對吧?這個時候我們就需要程式碼拆分了。
程式碼拆分是webpack對我們去手動把Button匯入需要的介面相關繁瑣操作的解決方案,也就是有了webpack你根本不用去找哪個頁面需要匯入,哪個頁面不用匯入。程式碼拆分實質是在你的程式碼裡定義分割點:你的程式碼可以輕鬆拆分開到部分單獨檔案中,然後按需載入,語法非常簡單:

import $ from 'jquery';

// 這是一個拆分點
require.ensure([], () => {
  // 這裡所有的程式碼都是需要被匯入的
  // 在一個單獨的頁面裡
  const library = require('some-big-library');
  $('foo').click(() => library.doSomething());
});
複製程式碼

在 require.ensure 的回撥(即() => {})裡的任何東西都會被拆分成程式碼塊(chunk) ─── 頁面需要載入的時候會通過ajax單獨載入的包,這意味著我們的包基本上有這些:

bundle.js
|- jquery.js
|- index.js // 我們所有檔案都要匯入的程式碼
chunk1.js
|- some-big-libray.js
|- index-chunk.js // 在回撥裡面的程式碼
複製程式碼

你無需匯入 chunk1.js ,webpack只有在需要它的時候才會去載入它,這意味著你可以把你的程式碼按照各種邏輯分成多塊,我們將在下面修改我們的 index.js ,讓只在頁面裡有 a 標籤的時候才載入 Button:

if (document.querySelectorAll('a').length) {
    require.ensure([], () => {
        const Button = require('./Components/Button').default;
        const button = new Button('google.com');
        button.render('a');
    });
}
複製程式碼

注意,用 require 的時候,如果你想獲取Button物件 export default 的東西你需要通過 .default 去手動獲取它,因為 require 不會同時處理 export default 和 exports.obj,所以你必須指定返回哪個,不過 import 可以處理這個,所以它已經知道了(例如: import foo from 'bar'獲取到'bar'物件export default的東西, import {baz} from 'bar'獲取到'bar'物件的exports.baz的東西)。這裡的東西有點複雜,如果不是很理解又想理解可以去深入瞭解一下ES6和nodeJS。

現在 webpack 的 output 也應該會相應地不同了,我們來用webpack --display-modules --display-chunks執行一下,看一下哪個模組在哪個chunk裡

$ webpack --display-modules --display-chunks
Hash: c419d385603afdd301ab
Version: webpack 3.8.1
Time: 1489ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js   307 kB       0  [emitted]  [big]
  bundle.js  6.48 kB       1  [emitted]         main
chunk    {0} 0.bundle.js 305 kB {1} [rendered]
    [1] ./src/Components/Button.js 1.92 kB {0} [built]
    [2] ./node_modules/jquery/dist/jquery.js 268 kB {0} [built]
    [3] ./src/Components/Button.html 70 bytes {0} [built]
    [4] ./node_modules/mustache/mustache.js 19.4 kB {0} [built]
    [5] ./src/Components/Button.scss 1.16 kB {0} [built]
    [6] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {0} [built]
    [7] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
    [8] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} [built]
    [9] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} [built]
chunk    {1} bundle.js (main) 615 bytes [entry] [rendered]
    [0] ./src/index.js 615 bytes {1} [built]
   [0] ./src/index.js 615 bytes {1} [built]
   [1] ./src/Components/Button.js 1.92 kB {0} [built]
   [2] ./node_modules/jquery/dist/jquery.js 268 kB {0} [built]
   [3] ./src/Components/Button.html 70 bytes {0} [built]
   [4] ./node_modules/mustache/mustache.js 19.4 kB {0} [built]
   [5] ./src/Components/Button.scss 1.16 kB {0} [built]
   [6] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {0} [built]
   [7] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
   [8] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} [built]
   [9] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} [built]
複製程式碼

可以看到我們的入口( bundle.js )現在只有一些webpack的邏輯,其它的東西(jQuery, Mustache, Button) 都在 0.bundle.js ,只有頁面上有 a 標籤的時候才會載入 0.bundle.js,為了讓webpack知道用ajax載入的時候在哪能找到chunks,我們必須在我們的配置中加一行:

output: {
    path: BUILDS_PATH,
    filename: 'bundle.js',
    publicPath: 'builds/',
},
複製程式碼

output.publicPath 選項告訴 webpack 相對於當前的檔案在哪能找到構建後的資源,現在訪問我們的頁面我們會看到一切都正常工作,但更重要的是,我們能看到,由於頁面上有 a 標籤,因此webpack準確地載入了我們拆分出來的程式碼塊 0.bundle.js:

Webpack經典入門

如果我們的頁面上沒有 a 標籤,就只有 bundle.js 會被載入,這點可以讓你智慧地把你的app裡的大片邏輯拆分開,讓每個頁面只載入它真正需要的,我們也可以給我們拆分出來的程式碼包的Chunk Names命名 ,我們可以用語義的名字,你可以通過傳給 require.ensure 第三個引數來指定:

require.ensure([], () => {
const Button = require('./Components/Button').default;
const button = new Button('google.com');
button.render('a');
}, 'button');
複製程式碼

這樣就會生成程式碼包的 Chunk Names 就是 button 而不是空白了:

$ webpack
Hash: 50ed6a7993b581f0bf0a
Version: webpack 3.8.1
Time: 1524ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js   307 kB       0  [emitted]  [big]  button
  bundle.js  6.49 kB       1  [emitted]         main
   [0] ./src/index.js 625 bytes {1} [built]
   [1] ./src/Components/Button.js 1.92 kB {0} [built]
   [3] ./src/Components/Button.html 70 bytes {0} [built]
   [5] ./src/Components/Button.scss 1.16 kB {0} [built]
   [6] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {0} [built]
    + 5 hidden modules
複製程式碼

加入第二個元件

現在什麼都有了已經非常酷了,不過我們再來加一個元件看看好不好使:
路徑:webpack-your-bags/src/Components/Header.scss

.header {
  font-size: 3rem;
}
複製程式碼

路徑:webpack-your-bags/src/Components/Header.html

<header class="header">{{text}}</header>
複製程式碼

路徑:webpack-your-bags/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 })

        );

    }

}
複製程式碼

我們在我們的 index.js 裡把它渲染一下:

// 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');
    });
}
複製程式碼

現在用 --display-chunks --display-modules 來看一下 webpack 的 output:

$ webpack --display-modules --display-chunks
Hash: 66f9e900ac553f5d66eb
Version: webpack 3.8.1
Time: 1646ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js   306 kB       0  [emitted]  [big]
1.bundle.js   307 kB       1  [emitted]  [big]
  bundle.js  6.56 kB       2  [emitted]         main
chunk    {0} 0.bundle.js 305 kB {2} [rendered]
    [2] ./src/Components/Header.js 1.7 kB {0} [built]
    [3] ./node_modules/jquery/dist/jquery.js 268 kB {0} {1} [built]
    [4] ./node_modules/mustache/mustache.js 19.4 kB {0} {1} [built]
    [5] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} {1} [built]
    [6] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} {1} [built]
    [7] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} {1} [built]
   [11] ./src/Components/Header.html 62 bytes {0} [built]
   [12] ./src/Components/Header.scss 1.16 kB {0} [built]
   [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
chunk    {1} 1.bundle.js 305 kB {2} [rendered]
    [1] ./src/Components/Button.js 1.92 kB {1} [built]
    [3] ./node_modules/jquery/dist/jquery.js 268 kB {0} {1} [built]
    [4] ./node_modules/mustache/mustache.js 19.4 kB {0} {1} [built]
    [5] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} {1} [built]
    [6] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} {1} [built]
    [7] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} {1} [built]
    [8] ./src/Components/Button.html 70 bytes {1} [built]
    [9] ./src/Components/Button.scss 1.16 kB {1} [built]
   [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
chunk    {2} bundle.js (main) 601 bytes [entry] [rendered]
    [0] ./src/index.js 601 bytes {2} [built]
   [0] ./src/index.js 601 bytes {2} [built]
   [1] ./src/Components/Button.js 1.92 kB {1} [built]
   [2] ./src/Components/Header.js 1.7 kB {0} [built]
   [3] ./node_modules/jquery/dist/jquery.js 268 kB {0} {1} [built]
   [4] ./node_modules/mustache/mustache.js 19.4 kB {0} {1} [built]
   [5] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} {1} [built]
   [6] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {0} {1} [built]
   [7] ./node_modules/style-loader/lib/urls.js 3.01 kB {0} {1} [built]
   [8] ./src/Components/Button.html 70 bytes {1} [built]
   [9] ./src/Components/Button.scss 1.16 kB {1} [built]
  [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
  [11] ./src/Components/Header.html 62 bytes {0} [built]
  [12] ./src/Components/Header.scss 1.16 kB {0} [built]
  [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
複製程式碼

能看見這裡有一個問題:我們的兩個元件都會用到 jQuery 和 Mustache,也就是說這兩個依賴在chunk裡有重複,雖然webpack 預設會做一點點的優化,但是它也可以以 plugin(外掛) 的形式來給 webpack 提供更強大的功能。

plugin 跟 loader 不同,它不是對指定的檔案執行一些操作,而是對所有檔案進行處理,做一些更高階的操作,但不一定非得像 loader 一樣是轉換,webpack 中有自帶 plugin 可以做各種優化,此時我們比較感興趣的一個是 CommonChunksPlugin:它分析你的chunk的遞迴依賴,然後把它們抽出來放在別的地方,可以是一個完全獨立的檔案或者是你的主檔案。

在我們現在的例子中,我們需要把公用的依賴移到我們的entry(入口) 檔案,如果所有的檔案需要jQuery 和 Mustache,我們也可以把它往上層移動,我們來更新一下我們的配置檔案webpack.config.js :

......
var webpack = require('webpack');
module.exports = {
    entry:   './src',
    output:  {
      // ...
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name:      'main', // 將依賴移到我們的主檔案
            children:  true, // 在所有被拆分的程式碼塊中尋找共同的依賴關係
            minChunks: 2, // 在被提取之前,一個依賴要出現多少次(也就是一個依賴會在遍歷所有拆分的程式碼塊時被重複發現多少次)
        }),
    ],
    module:  {
      // ...
    }
};
複製程式碼

如果我們重新執行一下 webpack --display-modules --display-chunks,我們可以看到現在比之前好多了,因為 0.bundle.js 和 1.bundle.js 的共同部分都被移到了 bundle.js 。

$ webpack --display-modules --display-chunks
Hash: 22fbf6bdd63c4bbbb096
Version: webpack 3.8.1
Time: 1554ms
      Asset     Size  Chunks                    Chunk Names
0.bundle.js  3.36 kB       0  [emitted]
1.bundle.js  3.58 kB       1  [emitted]
  bundle.js   310 kB       2  [emitted]  [big]  main
chunk    {0} 0.bundle.js 3.12 kB {2} [rendered]
    [7] ./src/Components/Header.js 1.7 kB {0} [built]
   [11] ./src/Components/Header.html 62 bytes {0} [built]
   [12] ./src/Components/Header.scss 1.16 kB {0} [built]
   [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
chunk    {1} 1.bundle.js 3.37 kB {2} [rendered]
    [6] ./src/Components/Button.js 1.92 kB {1} [built]
    [8] ./src/Components/Button.html 70 bytes {1} [built]
    [9] ./src/Components/Button.scss 1.16 kB {1} [built]
   [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
chunk    {2} bundle.js (main) 303 kB [entry] [rendered]
    [0] ./src/index.js 601 bytes {2} [built]
    [1] ./node_modules/style-loader/lib/urls.js 3.01 kB {2} [built]
    [2] ./node_modules/jquery/dist/jquery.js 268 kB {2} [built]
    [3] ./node_modules/mustache/mustache.js 19.4 kB {2} [built]
    [4] ./node_modules/css-loader/lib/css-base.js 2.26 kB {2} [built]
    [5] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {2} [built]
   [0] ./src/index.js 601 bytes {2} [built]
   [1] ./node_modules/style-loader/lib/urls.js 3.01 kB {2} [built]
   [2] ./node_modules/jquery/dist/jquery.js 268 kB {2} [built]
   [3] ./node_modules/mustache/mustache.js 19.4 kB {2} [built]
   [4] ./node_modules/css-loader/lib/css-base.js 2.26 kB {2} [built]
   [5] ./node_modules/style-loader/lib/addStyles.js 9.41 kB {2} [built]
   [6] ./src/Components/Button.js 1.92 kB {1} [built]
   [7] ./src/Components/Header.js 1.7 kB {0} [built]
   [8] ./src/Components/Button.html 70 bytes {1} [built]
   [9] ./src/Components/Button.scss 1.16 kB {1} [built]
  [10] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
  [11] ./src/Components/Header.html 62 bytes {0} [built]
  [12] ./src/Components/Header.scss 1.16 kB {0} [built]
  [13] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
複製程式碼

你也可以通過不提供一個公用的塊名(chunk name)或者是指定 async: true 來讓公用的dependency被非同步載入,webpack有非常多這樣強大的智慧優化。我不可能全列出來,但作為練習,讓我們嘗試來給我們的app建一個生產版本。

生產和優化

好吧,首先,我們加幾個 plugin 到配置檔案中,但我們只想在 NODE_ENV 等於 production 時才載入這些 plugin ,所以讓我們來給配置檔案加一些邏輯,因為只是一個 js 檔案,所以比較簡單:

......
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_PATH,
        filename:   'bundle.js',
        publicPath: 'builds/',
    },
    plugins: plugins,
    ......
};
複製程式碼

第二步,Webpack有一些設定我們也可以在生產中關閉:

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

        new webpack.LoaderOptionsPlugin({
            debug: true
        }),
        
        ......
    ]);
}

module.exports = {
	//debug:   !production,
    //loaders的除錯模式已經在webpack3及以上版本完全移除,為了保持與舊的loaders的相容性,loaders可以通過LoaderOptionsPlugin外掛切換到除錯模式
    devtool:production ? false : 'eval',
    ......
    }
複製程式碼

第1個設定為非debug模式,這意味著系統不會處理太多的程式碼,讓你可以更輕鬆地除錯本地的資源,第2個是控制 sourcemap 生成的,webpack有幾個方法可以呈現 sourcemap,自帶的 eval 是最好的,生產環境我們不太關心 sourcemap 的事,所以我們把它禁用掉,下面加一下我們的生產外掛(plugins):

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

        // 這個外掛會尋找類似的塊和檔案然後進行合併
        //new webpack.optimize.DedupePlugin(),
        //webpack3版本之後這個外掛已經被移除

        // 這個外掛會根據模組在你的程式中使用的次數來優化模組
        //new webpack.optimize.OccurenceOrderPlugin(),
        //webpack2版本之後這個外掛的功能已經是預設開啟,無需呼叫

        // 這個外掛可以防止webpack建立太小以至於不值得單獨載入的塊
        new webpack.optimize.MinChunkSizePlugin({
            minChunkSize: 51200, // ~50kb
        }),

        //這個外掛會最小化所有最終資源的javascript程式碼
        new webpack.optimize.UglifyJsPlugin({
            mangle:   true,
            compress: {
                warnings: false, //  阻止難看的警告
            },
        }),
        //loaders的最小化模式也會在webpack3或者以後的版本中移除

        // 這個外掛允許我們可以在生產中設定各種為錯誤的變數,以避免它們被編譯在我們的最後打包的程式碼中
        new webpack.DefinePlugin({
            __SERVER__:      !production,
            __DEVELOPMENT__: !production,
            __DEVTOOLS__:    !production,
            'process.env':   {
                BABEL_ENV: JSON.stringify(process.env.NODE_ENV),
            },
        }),

    ]);
}
複製程式碼

上面這幾個是我最常用的,不過webpack提供很多外掛(plugin)用來優化處理你的modules和chunks,npm 上也有其他人寫的有各種功能的外掛(plugin),自行按需選擇。

關於生產環境下的資源的另一個方面是,我們有時想給我們的打包後的資源加版本,還記得在上面我們把 output.filename 設定為 bundle.js 吧,這有幾個變數可以在命名的時候用,其中一個是 [hash] ,代表最終資原始檔的版本,同時用 output.chunkFilename 給chunk也加上版本,現在讓我們來修改我們的配置檔案:

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

由於沒有什麼特別好的辦法能動態地獲取這個簡化版app編譯後的bundle的名字(由於加了版本號),所以只是在生產環境上給資源加版本,多次編譯後目錄裡帶版本的最終包會越來越多,所以為了節省空間,還得加一個第三方外掛來每次清理我們之前的生產版本(output 裡面的 path):

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

並把它新增到我們的配置裡:

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

// ...

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

        // 清理以前的版本/資料夾
        // 編譯我們的最終資源
        new CleanPlugin('builds'),
        ......
        }
        ......
複製程式碼

OK,做完了幾個小優化,下面比較一下結果:

$ webpack
Hash: 8b942bcf553d3d782e6c
Version: webpack 3.8.1
Time: 1542ms
                    Asset     Size  Chunks                    Chunk Names
0-294790a46bbc09afc580.js  4.34 kB       0  [emitted]
1-24fa8b23bd35ecac8a8f.js  4.57 kB       1  [emitted]
                bundle.js   348 kB       2  [emitted]  [big]  main
......
複製程式碼
$ NODE_ENV=production webpack
clean-webpack-plugin: D:\other\webpack-your-bags\builds has been removed.
Hash: 22674bf33e7cdefb2776
Version: webpack 3.8.1
Time: 2973ms
                       Asset    Size  Chunks             Chunk Names
main-22674bf33e7cdefb2776.js  101 kB       0  [emitted]  main
......
複製程式碼

那麼webpack到底做了什麼呢?首先,由於我們的例子是非常輕量級的,所以我們的兩個非同步塊不是HTTP請求,因此Webpack將它們合併到入口。而且所有的東西都被最小化了,我們從總共356KB的3個HTTP請求到101KB的1個HTTP請求。

但webpack竟然能毫無徵兆地就把一些大的js檔案給消除掉?

是的,的確是這樣,但這只是因為我們的應用程式非常的小才發生的。現在考慮到一點:你根本想不到什麼被合併,並且在什麼地方什麼時候被合併。如果你拆分的程式碼塊突然有了更多的依賴,那麼這些拆分塊將被移動到一個非同步塊,而不是被合併。 如果這些拆分塊過於相似以至於不值得單獨載入,它們將被合併,你僅僅需要配置一下,webpack就會用最好的方式自動優化你的應用程式。不用你去手動操作,也不用去想依賴在哪些時候或者哪些地方需要被依賴,一切都是全自動。

Webpack經典入門

你可能已經注意到我們沒有使用任何設定來最小化我們的HTML和CSS,那是因為在預設情況下,如果除錯(debug)選項是我們之前所說的false的話, css-loader 和 html-loader 就會去幫我們做這件事。這也是為什麼UglifyJsPlugin是一個單獨的外掛(而不是一個loader)的原因:因為在webpack中沒有js-loader,因為webpack中本身就是JS的載入器。

提取

好吧,現在你可能已經注意到,由於本教程的一開始,我們的樣式就已經在網頁裡面,並且做出了一個非常醜的按鈕。現在如果我們可以把所有的樣式通過webpack構建出一個最終的CSS檔案,豈不是很好?我們當然可以啦,讓我們在一些外部的外掛(plugin)的幫助下實現這種操作:

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

這個外掛具體是做什麼的我來說一下:就是從你的最終的資源中提取一些指定型別的內容,這個經常用在CSS上,因此讓我們來配置一下webpack.config.js(在這裡切合版本的更替,我們將用module.rules替換module.loaders,因為在未來module.loaders將完全被module.rules代替):

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:  {
		rules: [{
			test: /\.js/,
			loader: 'babel-loader',
			include: SRC_PATH,
		},{
			test: /\.html/,
			loader: 'html-loader',
		},{
			test: /\.scss$/,
			use: ExtractPlugin.extract({
				fallback: 'style-loader',
				use: ['css-loader', 'sass-loader']
			})
		}],
        // loaders: [{
		// 	test: /\.js/,
		// 	loader: 'babel-loader',
		// 	include: SRC_PATH,
		// },{
		// 	test: /\.html/,
		// 	loader: 'html-loader',
		// },
		// {
		// 	test: /\.scss/,
		// 	loader: 'style-loader!css-loader!sass-loader',
		// 	// Or
		// 	// loaders: ['style-loader', 'css-loader', 'sass-loader'],
		// }
		// ]
    }
};
複製程式碼

現在 rules 的 extract 方法有兩個引數:第一個是當我們在一個程式碼塊('style-loader')中,未提取的CSS該怎麼辦,二是當資源在一個主檔案('css-loader!sass-loader')中時該怎麼轉換。現在如果我們在一個程式碼塊中,那麼在使用 loader 前我們不能只是奇蹟般地讓這個程式碼塊中的CSS追加到生成的CSS中,但對於那些在主檔案中找到的樣式,它們就應該被打包到一個builds/bundle.css檔案中。現在讓我們來測試一下,新增一個小主樣式表到我們的應用程式中:
路徑:webpack-your-bags/src/styles.scss

body {
  font-family: sans-serif;
  background: darken(white, 0.2);
}
複製程式碼

修改我們的 index.js :

import './styles.scss';
......
複製程式碼

讓我們執行webpack,果然我們現在有一個bundle.css,現在我們可以在我們的HTML檔案匯入:

$ webpack
Hash: 6890269852e3f4363514
Version: webpack 3.8.1
Time: 1607ms
                    Asset      Size  Chunks                    Chunk Names
0-4a6171ca8018a700cec4.js   4.26 kB       0  [emitted]
1-ffe316264a9c9fafbb4c.js   4.58 kB       1  [emitted]
                bundle.js    348 kB       2  [emitted]  [big]  main
               bundle.css  70 bytes       2  [emitted]         main
......複製程式碼

Webpack經典入門

這個時候重新整理頁面可能會出錯,因為你必須將所有CSS檔案合併到一個檔案裡面去,你可以傳遞選項ExtractTextPlugin('bundle.css', {allChunks: true}) ,你也可以在這裡的檔名使用變數,如果你想要一個版本的樣式你只需要做ExtractTextPlugin('[name]-[hash].css')

載入圖片

現在對我們所有的JavaScript檔案來說是非常完美的了,但我們還沒有談論的一個話題是具體的資源:影象、字型等在webpack裡是如何工作的並且是如何經過優化的呢?下面讓我們從網站上拷一張圖片,並且我們將使用它作為我們的網頁背景,因為我見過人們把它放在Geocities上而且看起來挺酷的:

Webpack經典入門

讓我們儲存這一張圖片在 webpack-your-bags/img/puppy.jpg ,並相應地更新我們的styles.scss:

body{
	font-family:sans-serif;
	background:url('../img/puppy.jpg') no-repeat 0 0;
	background-size:50%;
}
複製程式碼

如果你這樣做, webpack 會理直氣壯地告訴你“它嗎的到底是做個什麼png出來”,因為我們沒有載入它。這裡有兩個原生的載入器,我們可以用它們來處理具體的資源: file-loader 和 url-loader : 第一個將只返回一個沒有什麼特別變化的資源的URL,讓你得到一個在這個過程中產生的版本檔案(這是預設的行為) ;第二個將資源內聯到一個 data:image/jpeg;base64 的地址。

事實上這兩個外掛不是對立的關係:比如你的背景是一個2MB的影象,那就不要內聯,最好單獨載入它。另一方面如果它是一個2KB的小圖示,最好內聯,節省HTTP請求。所以我們就設定了兩個:

$ npm install url-loader file-loader --save-dev

{
    test:   /\.(png|gif|jpe?g|svg)$/i,
    loader: 'url-loader?limit=10000',
},
複製程式碼

在這裡,我們向 url-loader 傳遞一個 limit 查詢引數告訴它:如果資產大於10KB或者更小則內聯,否則,就退回到了file-loader並引用它。該語法被稱為查詢字串,你用它來配置載入器(loader),或者你也可以通過一個物件配置載入器:

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

好吧,讓我們來看看結果:

$ webpack
Hash: e672f7becf7b049e759d
Version: webpack 3.8.1
Time: 1653ms
                    Asset     Size  Chunks                    Chunk Names
0-4a6171ca8018a700cec4.js  4.26 kB       0  [emitted]
1-ffe316264a9c9fafbb4c.js  4.58 kB       1  [emitted]
                bundle.js   348 kB       2  [emitted]  [big]  main
               bundle.css  2.88 kB       2  [emitted]         main
複製程式碼

正如我們看到的那樣:沒有一個JPG被打包,因為我們的小狗圖片(2KB)比配置的尺寸(10KB)小,所以它就被內聯了。這意味著如果我們訪問我們的網頁,無需載入圖片我們就可以沉浸在我們的小狗霸主的榮耀下了。

Webpack經典入門

這是非常強大的,因為這意味著webpack現在可以根據大小/HTTP請求的比值來智慧優化具體的資源。有一個良好的載入器,你甚至可以進一步的把所有東西聯絡起來,最常見的一個是image-loader,將在打包它們之前壓縮所有圖片。它甚至有一個?bypassOnDebug查詢引數讓你只在生產的時候做這種事。有很多這樣的外掛,我鼓勵你在本文的結尾看看那些外掛。

我們現在將把它做活起來

我們上面所做的建設都只是為了體現webpack的強大功能,現在讓我們更多的關注原生程式碼除錯。
可能當你提到構建工具時,經常注意到的一個比較大的缺陷:實時載入:LiveReload,BrowserSync,不需要等太久。但是當我們修改一點什麼東西的時候我們希望整個頁面會自動進行重新整理,讓它一步就做到我們想要的效果的就是所謂的模組更換或熱過載。它的想法是,既然webpack知道我們依賴樹的每個模組的位置,那麼一個小小的變化就能用新檔案簡單地修補來體現。更清楚的表達就是:你所做的修改實時地表現在頁面上但是你卻沒發現頁面已經重新重新整理過了。

為了要使用HMR(熱模組載入),我們需要一個能為我們的熱資源服務的伺服器。webpack 中有自帶的 dev-server,我們可以利用這個,所以讓我們來安裝它:

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

現在執行我們開發的伺服器,沒有比這個更簡單的了,只需要執行下面的命令:

$ webpack-dev-server

現在請讓我們訪問web伺服器 http://localhost:8080/webpack-dev-server/ 。你會看到你平時的頁面,但是現在我們修改一個SASS檔案,像變魔術一樣:

Webpack經典入門

現在,每當我們執行 webpack-dev-server 它就已經是HMR模式。請注意,我們使用 webpack-dev-server 這裡是為了服務我們的熱資源,但你也可以使用其他幾個選項,像 Express server 。webpack 提供一箇中介軟體,並且你可以使用這個中介軟體把即插即用的HMR移到其他伺服器上。

乾淨或者凋零

如果你一直在跟進這門教程,你可能已經注意到一些奇怪的東西:為什麼載入器巢狀在 module.rules(module.loaders) 但外掛卻不是呢?當然這是因為你還可以向 module 投入其它的東西了!webpack 不只是有載入器,它還具有前載入器,後載入器:在程式碼執行之前或之後,我們主要的載入器。讓我們舉個例子:我敢肯定,我寫這篇文章的程式碼是可怕的,所以我們申請在我們改造它之前 ESLint 我們的程式碼:

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

現在讓我們建立一個簡單的.eslintrc檔案,我知道等一下會執行失敗:
路徑:webpack-your-bags/.eslintrc

{
    "parser": "babel-eslint",
    "rules": {
        "quotes": 2
    }
}
複製程式碼

現在加入我們的前載入器,我們只是像以前一樣使用相同的語法,只是在 module.preLoaders 裡(module.preLoaders已經在webpack2版本之後移除,需要用到preLoader的地方可以改到rules的enfore中進行配置):

module:  {
        // preLoaders: [{
        //     test: /\.js/,
        //     loader: 'eslint-loader',
        // }],
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            enforce: "pre",
            loader: "eslint-loader"
        }],
}
複製程式碼

現在,如果我們執行webpack,果然失敗:

$ webpack
Hash: 891f6c68504d0f787afa
Version: webpack 3.8.1
Time: 2335ms
                    Asset     Size  Chunks                    Chunk Names
0-4a6171ca8018a700cec4.js  4.26 kB       0  [emitted]
1-ffe316264a9c9fafbb4c.js  4.58 kB       1  [emitted]
                bundle.js   348 kB       2  [emitted]  [big]  main
               bundle.css  2.88 kB       2  [emitted]         main
   [0] ./src/index.js 627 bytes {2} [built] [1 error]
   [1] ./src/styles.scss 41 bytes {2} [built]
   [7] ./src/Components/Button.js 1.92 kB {1} [built] [1 error]
   [8] ./src/Components/Header.js 1.7 kB {0} [built] [1 error]
   [9] ./src/Components/Button.html 70 bytes {1} [built]
  [10] ./src/Components/Button.scss 1.16 kB {1} [built]
  [11] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Button.scss 219 bytes {1} [built]
  [12] ./src/Components/Header.html 62 bytes {0} [built]
  [13] ./src/Components/Header.scss 1.16 kB {0} [built]
  [14] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/Components/Header.scss 199 bytes {0} [built]
  [15] ./node_modules/css-loader!./node_modules/sass-loader/lib/loader.js!./src/styles.scss 297 bytes [built]
  [16] ./img/puppy.jpg 2.8 kB [built]
    + 5 hidden modules

ERROR in ./src/Components/Button.js

D:\other\webpack-your-bags\src\Components\Button.js
   1:15  error  Strings must use doublequote  quotes
   2:22  error  Strings must use doublequote  quotes
   3:22  error  Strings must use doublequote  quotes
   4:8   error  Strings must use doublequote  quotes
  25:11  error  Strings must use doublequote  quotes
......
複製程式碼

上面報錯留給大家自己解決,溫馨提示:將報錯內容“Strings must use doublequote quotes”翻譯一下。如果暫時解決不了則註釋掉 eslint 相關內容接著進行下面內容。
讓我們執行前載入器的另一個例子:對於每個元件我們匯入其同名樣式表,並且具有相同名稱的模板。讓我們用一個前載入器來自動載入任何帶有相同的名稱的檔案作為一個模組:

$ npm install baggage-loader --save-dev

{
	test: /\.js/,
	enforce: "pre",
    loader: 'baggage-loader?[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';
複製程式碼

正如你所看到的如此強大的前載入器一樣,後載入器也是如此。看看可用的裝載機在本文末尾的列表,你一定會發現很多用例在裡面。看看這篇文章的結尾那個可用的載入器列表,你肯定會發現更多用例。

附加

關於 webpack-dev-server 我們不僅可以通過直接執行命令列 webpack-dev-server 開啟我們的web伺服器,而且可以在 package.json 檔案 scripts 中配置快捷命令列,如:

"scripts": {
  "start:dev": "webpack-dev-server"
}
複製程式碼

然後我們就可以直接執行命令列 npm run start:dev (等同於 webpack-dev-server )來開啟我們的web伺服器了。

你想了解更多嗎?

目前,我們的應用程式是相當小的所以還是看不出 webpack 的優勢,但隨著這個應用程式越來越大,webpack 就會變得有用,你也能夠得到更多的領悟,我們實際的依賴關係樹是什麼。我們可能會做對或做錯,我們的應用程式什麼時候會變得寸步難行等等等。現在應用程式的內部,webpack 知道一切,但你要有禮貌地讓它展示它所知道的。您可以通過產生一個配置檔案來執行以下命令:

webpack --profile --json > stats.json

第一個標誌(--profile)告訴 webpack 生成一個配置檔案,第二個(--json)產生它的JSON,最後一個(> stats.json) 我們讓這一切都輸出到JSON檔案。現在有很多個網站都在分析這些配置檔案,但 webpack 提供了一個官方的來一一解說這個資訊。所以去 webpack 分析和匯入你的JSON檔案。現在進入“Modules”選項卡,你應該會看到你的依賴關係樹的視覺化表示:

Webpack經典入門

一個點越紅,越對你最後的包有影響。在我們這個應用程式下,這個點是 Jquery ,因為它在我們所有的模組是最重要的。看到所有選項卡,看一下週圍,你不會在我們這個小應用程式裡學到太多,但這個工具卻是非常重要的因為它能洞察到你的依賴樹和最終資源。現在,正如我所說的,其它的服務能夠提供洞察你的配置檔案的功能,另外一個我喜歡的是 Webpack Visualizer: 它用一個餅圖表示你的應用程式的依賴佔用了一個什麼空間,當然它也能表示我們的案例:

Webpack經典入門

這是我們的全部

現在我所知道的情況是:webpack 已經完全取代 grunt 和 gulp:我之前大部分都是用它們但是現在我是用 webpack 來處理,而對於其餘的一小部分我只是用NPM指令碼。每個例子,我們過去有一個共同的任務是我們的API文件通過 Aglio 轉換為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 中還具有節點的API,因此它可以很容易地在其它構建系統中使用,在任何情況下,你會發現它的思想無處不在。

無論如何,我認為這是可以為你做的一個 webpack 的足夠好的鳥瞰圖。你可能會認為,我們已經在這篇文章中介紹了很多,但我們認為只是皮毛而已:多個入口點,預取,背景更換等等。webpack 是一個令人印象深刻並且功能強大的工具,這當然是因為它比傳統的構建工具更加不透明,我不會否認這一點。但是,一旦你知道如何馴服它,它就會發出甜美的聲音進入你的耳朵讓你回味無窮。我把它用在幾個專案裡,它提供了優化和自動化,我不能想象自己要來回敲打腦袋多少次才能知道所需要的資源什麼時候在什麼地方。

資源

備註

各分段程式碼包已經上傳到GitHub,訪問地址:

github.com/hrh94/Webpa…


相關文章