翻譯 | 上手 Webpack ? 這篇就夠了!

iKcamp發表於2017-04-13

譯者:小 boy (滬江前端開發工程師)
本文原創,轉載請註明作者及出處。
原文地址:www.smashingmagazine.com/2017/02/a-d…

JavaSript 模組化打包已混跡江湖許久。2009年,RequireJS 就提交了它的第一個版本,Browserify 接踵而至,隨後其他打包工具也開始大行其道。最終,Webpack 從其中脫穎而出。如果你對它不甚瞭解,希望我的文章能讓你上手這件強力打包工具。

什麼是模組化打包工具?

在大多數語言(JS 的最新版本 ECMAScript 2015+ 也支援,但並非支援所有瀏覽器)中,你可以將程式碼拆分至多個檔案,並且通過在業務程式碼中引用這些檔案來使用它們包含的方法。可惜的是瀏覽器並不擁有這個能力。因此,模組化打包工具應運而生,它以兩種形式為瀏覽器提供這個能力:1.非同步載入模組,並且在載入結束後執行它們。2.將需要用到的檔案拼湊成單一 JS 檔案,最終在 HTML 中使用 <script> 標籤載入該 JS 檔案。

如果沒有模組化載入及打包工具的話,你就得手動拼湊檔案或者在 HTML 中載入無數的 <script> 標籤了,而且這樣幹有一些不好的地方:

  • 你需要關心檔案的載入順序,包括哪些檔案依賴於其他檔案;還需要確認是否引入了不需要的檔案。
  • 多個 <script> 標籤意味著用多個網路請求來載入程式碼,同時也意味著更差的效能。
  • 顯然,其中有很多本可以交給計算機完成的手工活。

大多數模組化打包工具直接跟 npm 或者 Bower(譯者注:兩者都是包管理工具)整合,這樣可以讓你更容易地在業務程式碼中新增第三方依賴包(dependencies)。你僅需要安裝一下,然後寫一行程式碼引入它們,接著執行模組化打包工具,這樣就已經將第三方程式碼整合進自己的業務程式碼了。或者,如若配置正確,你可以將所有要用的三方程式碼整合進一個分開的檔案,這樣一來,當你更新業務程式碼,使用者需要更新快取的時候,他們就無需重新下載公共庫程式碼(vendor code)了。

為什麼選擇 Webpack ?

至此,你已對 Webpack 的願景有了基礎的認知,然而為什麼在各路豪傑中選擇 Webpack 呢?在我看來有這樣一些理由:

  • 小鮮肉的特性助了它一臂之力,藉此特點它可以繞開或者避免前輩們遇到的問題。
  • 上手簡單。如果只是想打包一些JS檔案,而沒有其他需求的話,你甚至都不需要一份配置檔案。
  • 它的外掛體系使其能做更多事情,從而十分強大,所以它可能是你需要的唯一構建工具。

據我所知,少有其他的模組打包和構建工具也能做到這些。但 Webpack 仍勝一籌:當你踩坑的時候有龐大的社群支援。
Browserify 的社群可能只是大,如果它不大的話,就會缺少一些 Webpack 的潛在必要特性。說了這麼多 Webpack 的優點,估計你就等上程式碼了吧?那麼我們開始。

安裝 Webpack

在使用 Webpack 前,我們首先要先把它安裝好。為此我們需要 Node.js 和 npm ,我就假設你已經安裝過它們了,實在沒有的話,請從Node.js 官網開始吧。

有兩種方式安裝 webpack (或著是其他 CLI 包):全域性安裝(globally)或者本地安裝(locally)。對於全域性安裝,雖然你可以在任意目錄下使用它,但是它不會包括在專案的依賴模組列表(dependencies)中。此外,你也不能在兩個不同的專案(有些專案可能需要投入更多工作量才能更新到最新版本,所以這些專案還需要維持老版本)中切換不同版本的 Webpack 。所以我更願意本地安裝 CLI 包,並且用相對路徑抑或是 npm 指令碼來執行它。如果你不習慣本地安裝 CLI 包,可以看一下我之前寫的關於擺脫全域性安裝 npm 包的博文。

不管怎樣,在示例專案中,我們就使用 npm 指令碼。接下來,先本地安裝示例專案。首先:建立一個用來實驗和學習 Webpack 的目錄。 我在 GitHub 上有一個倉庫,你可以將它 clone 到本地,然後在分支間切換來進行下面的學習,或者從零開始建立一個新專案,此後可以與我的倉庫程式碼進行對照。

經過命令列選擇,一進到專案目錄,你將用 npm init 命令來初始化專案。接下來要填的資訊一點都不重要(譯者注:一路回車即可),除非你想把專案釋出到 npm 上。

至此 package.json 檔案準備就緒(它是通過 npm init 命令建立的),在此檔案中,你可以儲存依賴包資訊。我們通過 npm install webpack -D-D--save-dev 命令的簡寫,它的作用是將 npm 包作為開發環境的依賴包安裝,並將依賴資訊儲存到 package.json 檔案中)命令將 Webpack 作為依賴包安裝。

我們需要一個簡單的應用來開啟運用 Webpack 之旅。所謂的簡單就是:首先執行 npm install lodash -S-S == --save) 安裝 Lodash,如此一來我們的簡單應用就有一個依賴包可以用來載入了。接著我們建立一個 src 目錄,再於該目錄中建立名為 main.js 的檔案,其內容如下:

var map = require('lodash/map');

function square(n) {
    return n*n;
}

console.log(map([1,2,3,4,5,6], square));複製程式碼

很簡單對吧?我們僅僅建立了一個包含整數1至6的小陣列,然後用 Loadash 庫中的 map 函式建立了一個新陣列,這個新陣列中的數字是原陣列中數字的平方。最後,我們在控制檯中列印這個新陣列。執行命令 node src/main.js 就能看到結果:[1, 4, 9, 16, 25, 36]。你瞧,其實 Node.js 都能執行這個檔案。

但如果我們想打包這個小指令碼,其中還包括我們能跑在瀏覽器的 Lodash 程式碼,使用 Webpack 應該從哪入手?如何做到?

Webpack 命令列

若不想在配置檔案上浪費時間,使用 Webpack 命令列是最容易的上手方式。如果不啟用配置檔案的話,最簡潔的命令需要包含輸入檔案(input file)路徑和輸出檔案(output file)路徑。Webpack 會讀取輸入檔案,追蹤它的依賴關係樹,並將所有依賴檔案打包進一個檔案,最終在你指定的輸出路徑下輸出該檔案。在本例中,輸入路徑是 src/main.js ,我們要將打包後的檔案輸出到 dist/bundle.js 下。為此,我們先新增 npm 指令碼(我們並沒有全域性安裝 Webpack ,所以不能直接在命令列中執行)。編輯 package.json 檔案的 "scripts" 部分如下:

"scripts": {
    "build": "webpack src/main.js dist/bundle.js",
}複製程式碼

現在,執行 npm run build 命令,Webpack 就會執行了。很快,執行完畢的時候會生成 dist/bundle.js 檔案。然後你便可以用 Node.js (通過 node dist/bundle.js 命令)執行該檔案了。也可以藉助簡單的 HTML 將其跑在瀏覽器上,之後可在控制檯中看到同樣的執行結果。

在繼續探索 Webpack 前,我們先把構建指令碼調整得更專業一點:在重新構建(rebuilding)前刪除 dist 目錄及其內容,此外,我們再新增一些用於直接執行 bundle 檔案的指令碼。首先,安裝 del-cli 工具,這樣就不用在刪除目錄的時候顧慮作業系統的區別了(見諒,因為我用的是 Windows)。執行 npm install del-cli -D 命令即可。接著更新 npm 指令碼如下:

"scripts": {
    "prebuild": "del-cli dist -f",
    "build": "webpack src/main.js dist/bundle.js",
    "execute": "node dist/bundle.js",
    "start": "npm run build -s && npm run execute -s"
  }複製程式碼

我們保持 "build" 配置同之前一樣,但增加了 "prebuild" 配置用以清除目錄,這條配置所執行的命令會在每次 "build" 命令執行之前執行。同時增加的還有 "execute" 配置:使用 Node.js 執行已經打包好的指令碼。此外,使用 "start" 配置可以通過一條命令執行以上所有命令(-s 的作用僅僅是不讓 npm 指令碼在控制檯列印一些沒用的東西)。執行 npm start 命令,就可以在控制檯裡看到 Webpack 的輸出資訊,緊接著列印的是平方後的陣列。

恭喜!你剛剛完成了 example1 分支裡所有的事情。這個分支就在我之前提到的倉庫中。

Webpack 配置檔案

跟使用 Webpack 命令列上手一樣有趣的是,一旦開始使用更多 Webpack 的功能, 你就會想要放棄通過命令列傳遞 Webpack 配置引數,轉而投入配置檔案的懷抱。使用配置檔案雖然會更佔位置,但與此同時增加了可讀性,因為它是由 JS 寫成的。

那我們就來建立配置檔案吧。在根目錄下建立一個新檔案 webpack.config.js。Webpack 預設尋找該檔案,但如果想給配置檔案取別的名字或者將配置檔案放在其他目錄,你可以通過傳遞 --config [filename] 引數來做到。

在本教程中,我們使用預設檔名。現在,我們試著讓配置檔案起作用,達到與僅使用命令列同樣的效果。為此,我們需要在配置檔案中添置如下程式碼:

module.exports = {
    entry: './src/main.js',
    output: {
        path: './dist',
        filename: 'bundle.js'
    }
};複製程式碼

如此前一樣,我們規定輸入和輸出檔案。因為這不是 JSON 檔案而是 JS 檔案,所以我們需要把配置物件(configuration object )匯出,故使用 module.exports。雖然現在還看不出寫這些配置會比用命令好多少,但文章結尾你肯定會愛上這裡的一切。

接下來,移除 package.json 檔案中給 Webpack 傳的配置,像這樣:

"scripts": {
    "prebuild": "del-cli dist -f",
    "build": "webpack",
    "execute": "node dist/bundle.js",
    "start": "npm run build -s && npm run execute -s"
}複製程式碼

像之前一樣執行 npm start 命令,執行結果是不是似曾相識呢?以上就是分支 example2 中需要做的事情。

Webpack 載入器(Loaders)

我們主要通過兩種方式增強 Webpack: 載入器(loaders)和外掛(plugins)。我們先講載入器,外掛稍後再議。載入器用以轉換或操作特定型別的檔案,你可以將多個載入器串聯在一起來處理一種型別的檔案。例如,規定 .js 字尾的檔案要先通過 ESLint 檢查,再通過 Babel 把 ES2015 語法轉換為 ES5 語法。ESLint 發出的警報將會在控制檯列印出來,而遇到語法錯誤的時候則會阻止 Webpack 繼續打包。

我們這裡就不設定語法檢查了,但要通過設定 Babel 來把程式碼轉化成 ES5。當然我們得先有些 ES2015 程式碼吧?把 main.js 檔案的程式碼改成下面的樣子:

import { map } from 'lodash';

console.log(map([1,2,3,4,5,6], n => n*n));複製程式碼

實際上這段程式碼和之前做的事情一樣,但有兩點:其一,使用箭頭函式替代了之前定義的 square 函式。其二,使用了 ES2015 中的 import 語法載入 lodash 庫中的 map 函式,但這將會把整個 Lodash 庫的程式碼打包到我們的輸出檔案中,而不是引入僅僅包含 map 函式相關程式碼的 'lodash/map' 庫。如果樂意的話,你也可以把第一行改成 import map from 'lodash/map'; 但我寫成這樣有我的理由:

  • 在更具規模的應用裡,你可能要用到 Lodash 庫的很多部分,所以你最好全載入進來。
  • 如果你正在用 Backbone.js 框架,會發現僅打包你需要的函式是很困難的,因為根本就沒有文件告訴你函式依賴哪些函式。
  • 在 Webpack 的下一個大版本中,開發者打算加入一個叫 tree-shaking 的東西,tree-shaking 會排除掉引入模組中沒有用到的部分。所以那也是一種辦法。
  • 這樣寫是為了舉一個例子,好讓你理解我之前提到的要點。

(注:Lodash 這兩種載入方式都可以用,因為它的開發者明確規定可以這麼做,而不是所有的庫都可以通過這種載入方式工作。)

無論如何,ES2015 程式碼現已在手,我們要把它轉化成 ES5 程式碼,這樣它們就能在老式瀏覽器(事實上,在新版瀏覽器裡 ES2015 的支援度還不錯)裡跑起來了。因此,我們需要 Babel 及在 Webpack 中執行 Babel 的配套設施。至少要有 babel-core(Babel 的核心功能庫),babel-loader(babel-core 的 Webpack 載入器介面),babel-preset-es2015(裡面有 ES2015 到 ES5 的轉化規則,這是 Babel 需要得知的)。同時我們引進 babel-plugin-transform-runtimebabel-polyfill ,儘管它們實現方式有點不同,但都用於改變 Babel 新增語法填充(polyfills)和輔助函式(helper functions)的方式。正是因此,它們適應於不同種類的專案。你可能不想把它們倆都引入,二者擇一即可,但我在這把它倆都引入,這樣無論你選擇哪個,都能知道引入的方式。想知道更多的話,請訪問 polyfill 和 runtime transform 的官方文件吧。

不管怎樣,先安裝它們:npm i -D babel-core babel-loader babel-preset-es2015 babel-plugin-transform-runtime babel-polyfill。再為它們配置 Webpack。首先,新增一個部分用於增新增載器。更新 webpack.config.js 如下:

module.exports = {
    entry: './src/main.js',
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            …
        ]
    }
};複製程式碼

我們增加了一個 module 屬性,其中包含了 rules 屬性。rules 是一個陣列,這個陣列囊括每個載入器的配置。我們將把 babel-loader 相關配置加到這裡。對於每一個載入器,我們都要配置至少兩個引數:testloadertest 通常是一個正規表示式,它用以驗證(test)每個檔案的絕對路徑。我們一般只驗證檔案字尾,例如:/\.js$/驗證所有以 .js 結尾的檔案。在這裡,我們把這個引數設為 /\.jsx?$/ 這樣可以匹配到 .js 檔案和 .jsx 檔案,以便使用 React。接下來配置 loader 引數,它描述了在相應的 test 引數下,應該使用哪一個載入器處理檔案。

將載入器的名字所拼成的字串傳入該引數即可奏效,其中,名字用感嘆號隔開,例如 'babel-loader!eslint-loader'eslint-loader 會比 babel-loader 先執行,因為 Webpack 的讀取順序是從右到左。如果某個載入器有特殊引數配置,你可以使用 query string 語法。比如,要給 Babel 配置一個 fakeoption 引數為 true,我們得把前面的例子改為 'babel-loader?fakeoption=true!eslint-loader'。如果你覺得更易閱讀和維護的話,也可以使用 use 替代 loader 配置,這樣可以傳入一個陣列替代此前的字串。把之前的例子改為:use: ['babel-loader?fakeoption=true', 'eslint-loader'],更有甚者,你可以把它們寫成多行以提高可讀性。

目前我們只用 Babel loader ,所以我們的配置檔案看起來像下面這個樣子:

…
rules: [
    { test: /\.jsx?$/, loader: 'babel-loader' }
]
…複製程式碼

如果只用一個載入器,我們還可以這樣配置來替代 query string 的寫法:使用 options 配置物件,它就是一個鍵值對 map。因此,對於 fakeoption 的例子,我們的配置檔案可以寫成這樣:

…
rules: [
    {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        options: {
            fakeoption: true
        }
    }
]
…複製程式碼

用上面這種方式來配置我們的 Babel 載入器:

…
rules: [
    {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        options: {
            plugins: ['transform-runtime'],
            presets: ['es2015']
        }
    }
]
…複製程式碼

預設(presets)用於把 ES2015 特性轉成 ES5,我們也給 Babel 設定了已經安裝的 transform-runtime 外掛。如此前所言,該外掛並非必要,這裡是為了演示。我們也可以另建 .babelrc 檔案獨立配置這些引數,但那樣不利於演示 Webpack。一般我推薦使用 .babelrc 檔案,但在這裡我們還是保持不變。

萬事俱備,只欠東風。我們需要告知 Babel 跳過處理 node_modules 中的檔案,這樣可以提高我們的構建速度。添置 exclude 屬性以告知載入器忽略目標目錄下的檔案,它的值是一個正規表示式,因此我們這樣寫:/node_modules/

…
rules: [
    {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
            plugins: ['transform-runtime'],
            presets: ['es2015']
        }
    }
]
…複製程式碼

此外,我們本應使用 include 屬性來描述我們僅讀 src 目錄,但我覺得應該保持原樣。於是,你應該可以再次執行 npm start 命令,然後獲取為瀏覽器準備的 ES5 程式碼了。若想使用 polyfill 替代 transform-runtime 外掛,你需要做一兩處改動。首先刪除 plugins: ['transform-runtime], 這行(如果不打算再用了,你也可以直接用 npm 解除安裝該外掛)。接下來,編輯 Webpack 配置檔案的 entry 部分如下:

entry: [
    'babel-polyfill',
    './src/main.js'
],複製程式碼

我們把描述單一入口的字串替換成了描述多入口的陣列,新添的入口乃 語法填充(polyfill)。我們將其置於首位,這樣語法填充將會率先出現在打包後的檔案裡,因為我們在程式碼裡使用語法填充前,要確保它們已經存在。

除了藉助 Webpack 配置檔案,我們本可以通過在 src/main.js 的首行加上 import 'babel-polyfill; 來達到相同的目的。而我們卻使用了配置檔案,除了用於服務本例,更是為了用作一個演示多入口打包至單一檔案的範例。好吧,那便是倉庫裡的 example3 分支。容我再說一遍,你可以執行 npm start 命令來確認專案正常執行。

另一個例子:Handlebars 載入器

我們再為專案添置一個載入器:Handlebars。Handlebars 載入器用以將 Handlebars 模版編譯成函式,當你在 JS 中引入(import)一個 Handlebars 檔案時,該檔案編譯成的函式就會被引入 JS 檔案。這便是我喜歡 Webpack 載入器的地方:即便引入非 JS 檔案,該檔案也會在打包時被轉化為 JS 裡可用的東西。接下來的例子將會使用另一個載入器:允許引入圖片檔案並將圖片檔案轉化成 base64 編碼的 URL 字串,該字串可被用於在 JS 中為頁面新增內聯圖片。這也意味著,如果你串聯多個載入器,其中一個甚至能優化把圖片的檔案大小。

同樣,我們首先安裝這個載入器:執行 npm install -D handlebars-loader 命令。當你用的時候會發現 Handlebars 本身也是不可或缺的:執行 npm install -D handlebars 命令。這樣你就可以在不更新載入器版本的情況下控制 Handlebars 的版本,它們可以分別獨立迭代。

二者現已安裝完畢,我們弄一個 Handlebars 模板來用。在 src 目錄下建立一個 numberlist.hbs 檔案,其內容如下:

<ul>
  {{#each numbers as |number i|}}
    <li>{{number}}</li>
  {{/each}}
</ul>複製程式碼

該模板描繪了一個陣列(變數名為 numbers ,也可以是別的變數名),建立了一個無序列表。

接下來,我們調整此前的 JS 檔案來使用模板輸出一個列表,不再止步於列印陣列本身。main.js 看起來會像下面一樣:

import { map } from 'lodash';
import template from './numberlist.hbs';

let numbers = map([1,2,3,4,5,6], n => n*n);

console.log(template({numbers}));複製程式碼

可惜目前為止 Webpack 並不知道如何引入 numberlist.hbs ,因為它並非 JS 檔案。我們可以在 import 的路徑前加點東西通知 Webpack 要使用 Handlebars 載入器:

import { map } from 'lodash';
import template from 'handlebars-loader!./numberlist.hbs';

let numbers = map([1,2,3,4,5,6], n => n*n);

console.log(template({numbers}));複製程式碼

通過給路徑增新增載器名字,並將名字和路徑以感嘆號隔開的字首,我們告知 Webpack 那個檔案應該使用那個載入器。這樣,我們不必在配置檔案裡添置任何東西。然而,在頗有規模的專案裡,你極有可能載入不止一個模板,所以,在配置檔案裡告知 Webpack 我們使用 Handlebars ,以免去引入模板時在路徑前新增字首,這樣做會更有意義。那我們就更新一下配置檔案:

…
rules: [
    {/* babel loader config… */},
    { test: /\.hbs$/, loader: 'handlebars-loader' }
]
…複製程式碼

這部分相當簡單。我們所需要做的就是指定用 handlebars-loader 去處理以 .hbs 結尾的檔案,僅此而已。我們搞定了 Handlebars 同時也搞定了 example4 分支。現在,一旦執行 npm start ,你會看到 Webpack 打包輸出如下內容:

<ul>
<li>1</li>
<li>4</li>
<li>9</li>
<li>16</li>
<li>25</li>
<li>36</li>
</ul>複製程式碼

Webpack 外掛

外掛是另一種用來自定義 Webpack 功能的方式。你可以更自由地把它們新增到 Webpack 工作流(workflow)中,因為,除載入特殊檔案型別之外,它們幾乎不受限制。它們可被植入到任何地方,正因如此,他們更加強勁。我很難定義 Webpack 外掛到底能做多少事情,因此我僅給出一個 npm 上的搜尋結果列表 npm packages that have “webpack-plugin”,那應該不失為一個好的答案。

本教程中我們只接觸兩個外掛(其中一個馬上揭曉)。行文已至此你也知道我的風格,過多的例子我們就不需要了。我們首先上 HTML Webpack Plugin ,它的作用很純粹:生成 HTML 檔案 —— 終於可以開始進軍瀏覽器了!

在使用該外掛之前,我們首先更新 npm 指令碼來執行一個能夠測試示例應用的簡單伺服器。先安裝一個伺服器:執行 npm i -D http-server 命令。接著,仿照下面的程式碼將此前的 execute 指令碼改成 server 指令碼。

"scripts": {
  "prebuild": "del-cli dist -f",
  "build": "webpack",
  "server": "http-server ./dist",
  "start": "npm run build -s && npm run server -s"
},
…複製程式碼

Webpack 完成構建後,npm start 會同時啟動一個 web 伺服器,將瀏覽器跳轉到 localhost:8080 可以訪問到你的頁面。自然,我們仍然需要靠外掛來建立該頁面,所以接下來,我們需要安裝外掛:npm i -D html-webpack-plugin

安裝完畢以後,我們移步 webpack.config.js 並作如下修改:

var HtmlwebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: [
        'babel-polyfill',
        './src/main.js'
    ],
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,
                options: { plugins: ['transform-runtime'], presets: ['es2015'] }
            },
            { test: /\.hbs$/, loader: 'handlebars-loader' }
        ]
    },
    plugins: [
        new HtmlwebpackPlugin()
    ]
};複製程式碼

我們有作兩處改動:其一在檔案頂部引入新安裝的外掛,其二在配置物件尾部添置了一個 plugins 部分,並在此處傳入了外掛的例項物件。

目前我們並沒有為該外掛例項傳入配置物件,預設使用它的基礎模板,除了我們打包好的指令碼檔案以外,該基礎模版並沒有包含很多東西。在執行 npm start 後在瀏覽器訪問相應 URL ,你會看到一空白頁,但若在開發者工具中開啟控制檯,應該會看到裡面列印出了 HTML。

我們可能要獲得模板並將 HTML 吐(spit out)到頁面上而不是控制檯裡,這樣一個“正常人”就能真正從頁面上得到資訊了。我們先在 src 目錄下建立 index.html 檔案,這樣就能定義自己的模板了。預設情況下,該外掛用的是 EJS 模板語法,不過,你也可以配置該外掛使其使用其它受到支援的模板語言。在這裡我們就用 EJS 因為用什麼語法都沒有實質區別,index.html 的內容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
    <h2>This is my Index.html Template</h2>
    <div id="app-container"></div>
</body>
</html>複製程式碼

請注意幾點:

  • 我們將為外掛傳入一個配置物件來定義標題(僅僅因為我們能做到)。
  • 沒有具體指定該在哪裡插入我們的指令碼檔案,因為該外掛預設會在 body 元素結尾前新增指令碼。
  • 這裡 div 的 id 並非特定,我們在這裡隨便取了一個。

現在我們得到了想要的模板,最終不會只是一個空白頁了。接下來更新 main.js ,把 HTML 結構加入那個 div 裡以替代此前列印在控制檯裡。為此,我們僅需更新 main.js 的最後一行:document.getElementById("app-container").innerHTML = template({numbers});

同時,我們也需要更新 Webpack 配置檔案,為外掛傳入兩個引數。配置檔案現在應改成這樣:

var HtmlwebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: [
        'babel-polyfill',
        './src/main.js'
    ],
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,
                options: { plugins: ['transform-runtime'], presets: ['es2015'] }
            },
            { test: /\.hbs$/, loader: 'handlebars-loader' }
        ]
    },
    plugins: [
        new HtmlwebpackPlugin({
            title: 'Intro to webpack',
            template: 'src/index.html'
        })
    ]
};複製程式碼

template 配置指定了模板檔案的位置,title 配置被傳入了模板。現在,執行 npm start,你將會在瀏覽器裡看到下面的內容:

假如你一直跟著做的話,example5 分支便在此結束。不同外掛傳入的引數或者配置項也大異其趣,其原因在於外掛種類繁多且涵蓋範圍廣闊,但殊途同歸的是,他們最終都會被新增到 webpack.config.jsplugins 陣列中。同樣,也有其他方式可以處理 HTML 頁面的生成和檔名填充,一旦你開始為打包後的檔案新增清快取雜湊值(cache-busting hashes)字尾,這些事情就會變得非常簡單。

觀察示例倉庫,你會發現有一個 example6 分支,在該分支裡我通過新增外掛實現了 JS 程式碼壓縮,但這不是必須的,除非你想改動 UglifyJS 配置。如果你不爽 UglifyJS 的預設配置,可將倉庫切換 (check out)至該分支下(只需要檢視 webpack.config.js )去找到如何使用該外掛並加以配置。但如果預設配置正合你意,你只需要在命令列執行 webpack 時傳入 -p 引數。該引數是 production 的簡寫,與使用 --optimize-minimize--optimize-occurence-order 引數的效果一樣,前者用以壓縮 JS 程式碼,後者用以優化已引入模組的順序,著眼於稍小的檔案尺寸和稍快的執行速度。在示例倉庫完成一段時間後我才知道 -p 這個引數,所以我決定儲存該外掛示例,可以用來提醒你還有更簡單的方法(除了新增外掛之外)。另一可供使用的快捷命令引數是 -d-d 會展示更多 Webpack 列印出的資訊,並且可不借助其他引數生成資料圖(source map)。還有很多其他命令列快捷引數可供使用。

懶載入資料塊

懶載入(lazy-loading)模組是我在 RequireJS 中用得舒適但在 Browserify 中難以工作的模組。一個頗具規模的 JS 檔案固然可以從減少網路請求中受益,但也幾乎坐實了在一次會話中,某些使用者不必用到的程式碼會被下載下來。

Webpack 可以將打包檔案拆分成可被懶載入的若干塊(chunks),而且還不需要任何配置。你僅需要從兩種書寫方式中挑一種來書寫程式碼,剩下的則交給 Webpack。這兩種方式其一基於 CommonJS ,其二則基於 AMD。如果使用前者懶載入,需要這樣寫:

require.ensure(["module-a", "module-b"], function(require) {
    var a = require("module-a");
    var b = require("module-b");
    // …
});複製程式碼

require.ensure 需要確保模組是可用的(但並非執行模組),然後傳入一個由模組名構成的陣列,接著傳入一個回撥函式(callback)。真正想要在回撥函式裡使用模組,你需要顯式 require 陣列裡傳入的相應模組。

私以為這種方式相麻煩,所以,我們來看 AMD 的寫法。

require(["module-a", "module-b"], function(a, b) {
    // …
});複製程式碼

AMD 模式下,使用 require 函式,傳入包含依賴模組名的陣列,接著再傳入回撥函式。該回撥函式的引數就是依賴模組的引用,它們的排列順序與依賴模組在陣列中的排列順序相同。

Webpack 2 同時也支援 System.import,其藉助於 promises 而非回撥函式。儘管將回撥內容包裹在 promise 下並非難事,但我仍以為該提升非常有用。不過需要注意的是, System.import 現已過時,較新的規範推薦使用 import()。不過,這裡告誡一下, Babel (以及 TypeScript)會在你使用System.import的時候丟擲語法異常。你可以藉助於 babel-plugin-dynamic-import-webpack 外掛,但該外掛將會將其轉化為 require.ensure,而不是讓 Babel 合法處理新 import 或者任之由 Webpack 處置。我認為 AMD 或 require.ensure 在很久之後才會被棄置,且 Webpack 直到第三個版本才會支援 System.import ,那還遠著呢,所以用你順眼的那個就好了。

擴充我們的程式碼,令其停滯兩秒,然後再將 Handlebars 模板懶載入進來並輸出到螢幕上。為此,我們移除頂部 import 模板的語句,然後將最後一行包裹到 setTimeout 和 AMD 模式的 require 中引入模板。

執行 npm start ,你會發現生成了另外一個名為 1.bundle.js 的資原始檔(asset)。在瀏覽器開啟該頁面,然後在開發者工具中監聽網路流量,2秒之後你會發現新的資原始檔最終被載入並且執行了。以上這些實現起來並不困難,但提升使用者體驗可不止一點。

注意,這些二級打包檔案(sub-bundles)或曰資料塊(chunks),內部囊括了他們的所有依賴模組(dependencies),但不包含其主資料塊(parent chunks)已引入的依賴模組。(你可以有多個入口檔案,每個都懶載入一個資料塊,因此該資料塊在其主資料塊中載入的依賴模組也會不同。)

建立公共庫資料塊 (Vendor Chunk)

我們再說一個優化的點:公共庫資料塊。你可以定義一個單獨用以打包的 bundle,該 bundle 中存放不常改動的 “common” 庫或第三方程式碼。該策略可使使用者獨立快取你的公共庫檔案,以區別於業務程式碼,以便在你迭代應用時讓使用者無需重新下載該庫檔案。

為此,我們使用 Webpack 官方外掛:CommonsChunkPlugin。它已附帶在 Webpack 中,所以我們無需安裝。僅對 webpack.config.js 稍作修改即可:

var HtmlwebpackPlugin = require('html-webpack-plugin');
var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
    entry: {
        vendor: ['babel-polyfill', 'lodash'],
        main: './src/main.js'
    },
    output: {
        path: './dist',
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,
                options: { plugins: ['transform-runtime'], presets: ['es2015'] }
            },
            { test: /\.hbs$/, loader: 'handlebars-loader' }
        ]
    },
    plugins: [
        new HtmlwebpackPlugin({
            title: 'Intro to webpack',
            template: 'src/index.html'
        }),
        new UglifyJsPlugin({
            beautify: false,
            mangle: { screw_ie8 : true },
            compress: { screw_ie8: true, warnings: false },
            comments: false
        }),
        new CommonsChunkPlugin({
            name: "vendor",
            filename: "vendor.bundle.js"
        })
    ]
};複製程式碼

我們在第三行引入該外掛。此後,在 entry 部分修改配置,將其換成了一個物件字面量(literal),用以指定多入口。vendor 入口記錄了會在公共庫資料塊中——這裡包含了 polyfill 和 Lodash ——被引入的庫並將我們的主要入口放置在 main 入口裡。接著,我們僅需將 CommonsChunkPlugin 新增到 plugins 部分,指定 “vendor” 資料塊作為該外掛生成資料塊的索引,同時指定 vendor.bundle.js 檔案用以存放公共庫程式碼(譯者注:這裡外掛配置中的 name: "vendor" 對應 entry 中的 vendor 入口,入口陣列中指定的依賴模組即最終存放於 vendor.bundle.js 檔案中的依賴模組)。

通過指定 “vendor” 資料塊,該外掛將拉取此資料塊所有的依賴模組,並將其存放於公共庫資料塊內,這些依賴模組在一個單獨入口檔案裡被指定。如果不在入口物件字面量中指定資料塊名,外掛會基於多入口檔案之間公用的依賴模組來生成獨立檔案。

執行 Webpack ,你將看到3份 JS 檔案:bundle.js, 1.bundle.jsvendor.bundle.js。如果願意的話也可以執行 npm start 命令來在瀏覽器中檢視結果。看起來 Webpack 甚至會把自身載入不同模組的主要程式碼放進公共庫資料塊,此舉極為實用。

至此我們結束了 example8 分支之旅,同時本篇教程也接近尾聲。我所談頗多,但僅讓你對 Webpack 的能力淺嘗輒止。Webpack 實現了更簡便的 CSS module、清快取、圖片優化等等很多事情——多到即便書鉅著一本,我也無法說窮道盡,且在我成書之前,大多數已寫的內容也將被更新替代。So,嘗試一下 Webpack 吧,且告訴我它有沒有提升工作流。祝吾主保佑,程式設計愉快!

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

相關文章