標籤:

webpack 打包JS 的運行原理

Webpack自動化構建實踐指南 - 掘金

一、打包原理

最近一直在學習 webpack 的相關知識,當清晰地領悟到 webpack 就是不同 loaderplugin 組合起來打包之後,只作為工具使用而言,算是入門了。當然,在過程中碰到數之不盡的坑,也產生了想要深入一點了解 webpack 的原理(主要是掉進坑能靠自己爬出來)。因而就從簡單的入手,先看看使用 webpack 打包後的 JS 文件是如何載入吧。

友情提示,本文簡單易懂,就算沒用過 webpack 問題都不大。如果已經了解過相關知識的朋友,不妨快速閱讀一下,算是溫故知新 。

簡單配置

既然需要用到 webpack,還是需要簡單配置一下的,這裡就簡單貼一下代碼,首先是 webpack.config.js:

const path = require(path);nconst webpack = require(webpack);n//用於插入html模板nconst HtmlWebpackPlugin = require(html-webpack-plugin);n//清除輸出目錄,免得每次手動刪除nconst CleanWebpackPlugin = require(clean-webpack-plugin);nnmodule.exports = {n entry: {n index: path.join(__dirname, index.js),n },n output: {n path: path.join(__dirname, /dist),n filename: js/[name].[chunkhash:4].jsn },n module: {},n plugins: [n new CleanWebpackPlugin([dist]),n new HtmlWebpackPlugin({n filename: index.html,n template: index.html,n }),n //持久化moduleId,主要是為了之後研究載入代碼好看一點。n new webpack.HashedModuleIdsPlugin(),n new webpack.optimize.CommonsChunkPlugin({n name: manifest,n })n ]n};n

這是我能想到近乎最簡單的配置,用到的兩個額外下載的插件都是十分常用的,也已經在注釋中簡單說明了。

之後是兩個簡單的 js 文件:

// test.jsnconst str = test is loaded;nmodule.exports = str;nn// index.jsnconst test = require(./src/js/test);nconsole.log(test);n

這個就不解釋了,貼一下打包後,項目的目錄結構應該是這樣的:

至此,我們的配置就完成了。

index.js 開始看代碼

先從打包後的 index.html 文件看看兩個 JS 文件的載入順序:

<body>nt<script type="text/javascript" src="js/manifest.2730.js"></script>nt<script type="text/javascript" src="js/index.5f4f.js"></script>n</body>n

可以看到,打包後 js 文件的載入順序是先 manifest.js,之後才是 index.js,按理說應該先看 manifest.js 的內容的。然而這裡先賣個關子,我們先看看 index.js 的內容是什麼,這樣可以帶著問題去了解 manifest.js,也就是主流程的邏輯到底是怎樣的,為何能做到模塊化。

// index.jsnnwebpackJsonp([0], {n "JkW7": (function(module, exports, __webpack_require__) {n const test = __webpack_require__("zFrx");n console.log(test);n }),n "zFrx": (function(module, exports) {n const str = test is loaded;n module.exports = str;n })n}, ["JkW7"]);n

刪去各種奇怪的注釋後剩下這麼點內容,首先應該關注到的是 webpackJsonp 這個函數,可以看見是不在任何命名空間下的,也就是 manifest.js 應該定義了一個掛在 window 下的全局函數,index.js 往這個函數傳入三個參數並調用。

第一個參數是數組,現在暫時還不清楚這個數組有什麼作用。

第二個參數是一個對象,對象內都是方法,這些方法看起來至少接受兩個參數(名為 zFrx 的方法只有兩個形參)。看一眼這兩個方法的內部,其實看見了十分熟悉的東西, module.exports,儘管看不見 require, 但有一個樣子類似的 __webpack_require__,這兩個應該是模塊化的關鍵,先記下這兩個函數。

第三個參數也是一個數組,也不清楚是有何作用的,但我們觀察到它的值是 JkW7,與參數2中的某個方法的鍵是一致的,這可能存在某種邏輯關聯。

至此,index.js 的內容算是過了一遍,接下來應當帶著問題在 manifest.js 中尋找答案。

manifest.js 代碼閱讀

由於沒有配置任何壓縮 js 的選項,因此 manifest.js 的源碼大約在 150 行左右,簡化後為 28 行(已經跑過代碼,實測沒問題)。鑒於精簡後的代碼真的不多,因而先貼代碼,大家帶著剛才提出的問題,先看看能找到幾個答案:

(function(modules) {n window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {n var moduleId, result;n for (moduleId in moreModules) {n if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {n modules[moduleId] = moreModules[moduleId];n }n }n if (executeModules) {n for (i = 0; i < executeModules.length; i++) {n result = __webpack_require__(executeModules[i]);n }n }n return result;n };n var installedModules = {};nn function __webpack_require__(moduleId) {n if (installedModules[moduleId]) {n return installedModules[moduleId].exports;n }n var module = installedModules[moduleId] = {n exports: {}n };n modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);n return module.exports;n }n})([]);n

首先應該看到的是,manifest.js 內部是一個 IIFE,就是自執行函數咯,這個函數會接受一個空數組作為參數,該數組被命名為 modules。之後看到我們在 index.js 中的猜想,果然在 window 上掛了一個名為 webpackJsonp 的函數。它接受的三個參數,分別名為chunkIds, moreModules, executeModules。對應了 index.js 中調用 webpackJsonp 時傳入的三個參數。而 webpackJsonp 內究竟是有怎樣的邏輯呢?

先不管定義的參數,webpackJsonp 先是 for in 遍歷了一次 moreModules,將 moreModules 內的所有方法都存在 modules, 也就是自執行函數執行時傳入的數組。

之後是一個條件判斷:

if (executeModules) {n for (i = 0; i < executeModules.length; i++) {n result = __webpack_require__(executeModules[i]);n }n}n

判斷 executeModules, 也就是第三個參數是否存在,如存在即執行 __webpack_require__ 方法。在 index.js 調用 webpackJsonp 方法時,這個參數當然是存在的,因而要看看 __webpack_require__ 方法是什麼了。

__webpack_require__ 接受一個名為 moduleId 的參數。方法內部首先是一個條件判斷,先不管。接下來看到賦值邏輯

var module = installedModules[moduleId] = {n exports: {}n};n

結合剛才的條件判斷,可以推測出 installedModules 是一個緩存的容器,那麼前面的代碼意思就是如果緩存中有對應的 moduleId,那麼直接返回它的 exports,不然就定義並賦值一個吧。接著先偷看一下 __webpack_require__ 的最後的返回值,可以看到函數返回的是 module.exports,那麼 module.exports 又是如何被賦值的呢? 看看之後的代碼:

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);n

剛才我們知道 modules[moduleId] 就是 moreModules 中的方法,此處就是將 this 指定為 module.exports,再把module, module.exports, __webpack_require__ 傳入去作為參數調用。這三個參數是不是很熟悉?之前我們看 index.js 裡面代碼時,有一個疑問就是模塊化是如何實現的。這裡我們已經看出了眉目。

其實 webpack 就是將每一個 js 文件封裝成一個函數,每個文件中的 require 方法對應的就是 __webpack_require____webpack_require__ 會根據傳入的 moduleId 再去載入對應的代碼。而當我們想導出 js 文件的值時,要麼用 module.exports,要麼用 exports,這就對應了module, module.exports兩個參數。少接觸這塊的童鞋,應該就能理解為何導出值時,直接使用 exports = xxx 會導出失敗了。簡單舉個例子:

const module = {n exports: {}n};nnfunction demo1(module) {n module.exports = 1;n}nndemo1(module);nconsole.log(module.exports); // 1nnfunction demo2(exports) {n exports = 2;n}nndemo2(module.exports);nconsole.log(module.exports); // 1n

粘貼這段代碼去瀏覽器跑一下,可以發現兩次列印出來都是1。這和 wenpack 打包邏輯是一模一樣的。

梳理一下打包後代碼執行的流程,首先 minifest.js 會定義一個 webpackJsonp 方法,待其他打包後的文件(也可稱為 chunk)調用。當調用 chunk 時,會先將該 chunk 中所有的 moreModules, 也就是每一個依賴的文件也可稱為 module (如 test.js)存起來。之後通過 executeModules 判斷這個文件是不是入口文件,決定是否執行第一次 __webpack_require__。而 __webpack_require__ 的作用,就是根據這個 modulerequire 的東西,不斷遞歸調用 __webpack_require____webpack_require__函數返回值後供 require 使用。當然,模塊是不會重複載入的,因為 installedModules 記錄著 module 調用後的 exports 的值,只要命中緩存,就返回對應的值而不會再次調用 modulewebpack 打包後的文件,就是通過一個個函數隔離 module 的作用域,以達到不互相污染的目的。

二、非同步載入

簡單配置

webpack 的配置就不貼出來了,就是確定一下入口,提取 webpack 運行時需要用到的 minifest.js 而已。這裡簡單貼一下 html 模板與需要的兩個 js 文件:

<!--index.html-->n<!doctype html>n<html lang="en">n<body>n <p class="p">Nothing yet.</p>n <button class="btn">click</button>n</body>n</html>nnn//index.jsnconst p = document.querySelector(.p);nconst btn = document.querySelector(.btn);nbtn.addEventListener(click, function() {n //只有觸發事件才回家再對應的js 也就是非同步載入 n require.ensure([], function() {n const data = require(./src/js/test);n p.innerHTML = data;n })n})nn//test.jsnconst data = success!;nmodule.exports = data;n

這樣配置示例配置就完成了。可能有小夥伴不太熟悉 require.ensure,簡單地說,就是告訴 webpack,請懶載入 test.js,別一打開頁面就給我下載下來。相關的知識不妨看這裡。

打包完的目錄架構畫風是這樣的:

至此,配置就完成啦~

index.js 開始探索

先用瀏覽器打開 index.html,查看資源載入情況,能發現只載入了 index.jsminifest.js

之後點擊按鈕,會再加多一個 0.7f0a.js

可以說明代碼是被分割了的,只要當對應的條件觸發時,瀏覽器才會去載入指定的資源。而無論之後我們點擊多少次,0.7f0a.js 文件都不會重複載入,此時小本本應記下第一個問題:如何做到不重複載入。

按照載入順序,其實是應該先砍 minifest.js 的,但不妨先看看 index.js 的代碼,帶著問題有助於尋找答案。代碼如下:

webpackJsonp([1], {n "JkW7":n (function(module, exports, __webpack_require__) {n const p = document.querySelector(.p);n const btn = document.querySelector(.btn);nn btn.addEventListener(click, function() {n __webpack_require__.e(0).then((function() {n const data = __webpack_require__("zFrx");n p.innerHTML = data;n }).bind(null, __webpack_require__)).catch(__webpack_require__.oe)n })n })n}, ["JkW7"]);n

可能有些小夥伴已經忘記了上一篇文章的內容,__webpack_require__ 作用是載入對應 module 的內容。這裡提一句, module 其實就是打包前,import 或者 require 的一個個 js 文件,如test.jsindex.js。後文說到的 chunk 是打包後的文件,即 index.ad23.jsmanifest.473d.js0.7f0a.js文件。一個 chunk 可能包含若干 module

回憶起相關知識後,我們看看非同步載入到底有什麼不同。index.js 中最引入注目的應該是 __webpack_require__.e 這個方法了,傳入一個數值之後返回一個 promise。這方法當 promise 決議成功後執行切換文本的邏輯,失敗則執行 __webpack_require__.oe。因而小本本整理一下,算上剛才的問題,需要為這些問題找到答案:

  • 如何做到不重複載入。
  • __webpack_require__.e 方法的邏輯。
  • __webpack_require__.oe 方法的邏輯。

minifest.js 中尋找答案

我們先查看一下 __webpack_require__.e 方法,為方法查看起見,貼一下對應的代碼,大家不妨先試著自己尋找一下剛才問題的答案。

var installedChunks = {n 2: 0n};nn__webpack_require__.e = function requireEnsure(chunkId) {n var installedChunkData = installedChunks[chunkId];n if (installedChunkData === 0) {n return new Promise(function(resolve) {n resolve();n });nn }n if (installedChunkData) {n return installedChunkData[2];n }nn var promise = new Promise(function(resolve, reject) {n installedChunkData = installedChunks[chunkId] = [resolve, reject];n });n installedChunkData[2] = promise;n var head = document.getElementsByTagName(head)[0];n var script = document.createElement(script);n script.src = "js/" + chunkId + "." + {n "0": "7f0a",n "1": "ad23"n }[chunkId] + ".js";n script.onerror = script.onload = onScriptComplete;nn function onScriptComplete() {n script.onerror = script.onload = null;n var chunk = installedChunks[chunkId];n if (chunk !== 0) {n if (chunk) {n chunk[1](new Error(Loading chunk + chunkId + failed.));n }n installedChunks[chunkId] = undefined;n }n };n head.appendChild(script);n return promise;n};n

該方法中接受一個名為 chunkId 的參數,返回一個 promise,印證了我們閱讀 index.js 時的猜想,也確認了傳入的數字是 chunkId。之後變數 installedChunkData 被賦值為對象 installedChunks 中鍵為 chunkId 的值,可以推想出 installedChunks 對象其實就是記錄已載入 chunk 的地方。此時我們尚未載入對應模塊,理所當然是 undefined

之後我們想跳過兩個判斷,查看一下 __webpack_require__.e 方法返回值的 promise 是怎樣的:

var promise = new Promise(function(resolve, reject) {n installedChunkData = installedChunks[chunkId] = [resolve, reject];n});ninstalledChunkData[2] = promise;n

可以看到 installedChunkDatainstalledChunks[chunkId] 被重新賦值為一個數組,存放著返回值 promiseresolvereject,而令人不解的是,為何將數組的第三項賦值為這個 promise呢?

其實此前有一個條件判斷:

if (installedChunkData) {n return installedChunkData[2];n}n

那你明白為什麼了嗎?在此例中1,假設網路很差的情況下,我們瘋狂點擊按鈕,為避免瀏覽器發出若干個請求,通過條件判斷都返回同一個 promise,當它決議後,所有掛載在它之上的 then 方法都能得到結果運行下去,相當於構造了一個隊列,返回結果後按順序執行對應方法,此處還是十分巧妙的。

之後就是創造一個 script 標籤插入頭部,載入指定的 js 了。值得關注的是 onScriptComplete 方法中的判斷:

var chunk = installedChunks[chunkId];nif (chunk !== 0) {n ...n}n

明明 installedChunks[chunkId] 被賦值為數組,它肯定不可能為0啊,這不是鐵定失敗了么?先別急,要知道 js 文件下載成功之後,先執行內容,再執行 onload 方法的,那麼它的內容是什麼呢?

webpackJsonp([0], {n "zFrx":n (function(module, exports) {n const data = success!;n module.exports = data;n })n});n

可以看到,和 index.js 還是很像的。這個 js 文件的 chunkId 是0。它的內容很簡單,只不過是 module.exports 出去了一些東西。關鍵還是 webpackJsonp 方法,此處截取關鍵部分:

var resolves = [];nnfor (; i < chunkIds.length; i++) {n chunkId = chunkIds[i];n if (installedChunks[chunkId]) {n resolves.push(installedChunks[chunkId][0]);n }n installedChunks[chunkId] = 0;n}nnwhile (resolves.length) {n resolves.shift()();n}n

當它執行的時候,會判斷 installedChunks[chunkId] 是否存在,若存在則往數組中 push(installedChunks[chunkId][0]) 並將 installedChunks[chunkId] 賦值為0; 。還得記得數組的首項是什麼嗎?是 __webpack_require__.e 返回 promiseresolve!之後執行這個 resolve。當然, webpackJsonp 方法會將下載下來文件所有的 module 存起來,當 __webpack_require__ 對應 modulIde 時,返回對應的值。

讓我們目光返回 __webpack_require__.e 方法。

已知對應的 js 文件下載成功後,installedChunks[chunkId] 被賦值為0。文件執行完或下載失敗後都會觸發 onScriptComplete 方法,在該方法中,如若 installedChunks[chunkId] !== 0,這是下載失敗的情況,那麼此時 installedChunks[chunkId] 的第二項是返回 promisereject,執行這個 reject 以拋出錯誤:

if (chunk !== 0) {n if (chunk) {n chunk[1](new Error(Loading chunk + chunkId + failed.));n }n installedChunks[chunkId] = undefined;n}n

當再次請求同一文件時,由於對應的 module 已經被載入,因而直接返回一個成功的 promise 即可,對應的邏輯如下:

var installedChunkData = installedChunks[chunkId];nif (installedChunkData === 0) {n return new Promise(function(resolve) {n resolve();n });n}n

最後看一下 __webpack_require__.oe 方法:

__webpack_require__.oe = function(err) { console.error(err); throw err; };n

特別簡單對吧?最後整理一下流程:當非同步請求文件發起時,先判斷該 chunk 是否已被載入,是的話直接返回一個成功的 promise,讓 then 執行的函數 require 對應的 module 即可。不然則構造一個 script 標籤載入對應的 chunk,下載成功後掛載該 chunk 內所有的 module。下載失敗則列印錯誤。

三、代碼打包優化

基礎配置

CommonsChunkPlugin 插件,是一個可選的用於建立一個獨立文件(又稱作 chunk)的功能,這個文件包括多個入口 chunk 的公共模塊。通過將公共模塊拆出來,最終合成的文件能夠在最開始的時候載入一次,便存起來到緩存中供後續使用。這個帶來速度上的提升,因為瀏覽器會迅速將公共的代碼從緩存中取出來,而不是每次訪問一個新頁面時,再去載入一個更大的文件。

簡單來說,這有點像封裝函數。把不變的與變化的分開,使得不變的可以高效復用,變化的靈活配置。接下來會根據這個原則優化我們的項目,現在先看看虛擬的項目長成什麼樣吧~

新建一個 index.html 模板與入口 index.js文件,簡單配置如下:

index.html :

<!doctype html>n<html lang="en">n<head>n <meta charset="UTF-8">n <meta name="viewport"n content="width_=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">n <meta http-equiv="X-UA-Compatible" content="ie=edge">n <title>Document</title>n</head>n<body>n <div id="app">n <p>{{ vue_test }}</p>n </div>n <div class="jq_test"></div>n</body>n</html>n

index.js:

import Vue from vue;nimport $ from jquery;nnnew Vue({n el: #app,n data: {n vue_test: vue is loaded!n }n})nn$(function() {n $(.jq_test).html(jquery is loaded!)n})n

為演示起見,代碼十分簡單,相信不用多加解釋。接下來先簡單配置一下 webpack.config.js,代碼如下:

const path = require(path);nconst webpack = require(webpack);nconst HtmlWebpackPlugin = require(html-webpack-plugin);nconst CleanWebpackPlugin = require(clean-webpack-plugin);nconst BundleAnalyzerPlugin = require(webpack-bundle-analyzer).BundleAnalyzerPlugin;nnmodule.exports = {n entry: {n index: path.join(__dirname, index.js)n },n output: {n path: path.join(__dirname, /dist),n filename: js/[name].[chunkhash].jsn },n resolve: { alias: { vue: vue/dist/vue.js } },n plugins: [n new CleanWebpackPlugin([./dist]),n new HtmlWebpackPlugin({n filename: index.html,n template: index.html,n inject: truen }),n new BundleAnalyzerPlugin(),n ]n};n

CleanWebpackPlugin 主要用於清除 dist 目錄下的文件,這樣每次打包就不必手動清除了。HtmlWebpackPlugin 則是為了在 dist 目錄下新建 html 模板並自動插入依賴的 jsBundleAnalyzerPlugin 主要是為了生成打包後的 js 文件包含的依賴,如此時進行打包,則生成:

可以看到生成的 index.js 文件包含了 vuejquery

首次優化

一般而言,我們項目中的類庫變化較少,業務代碼倒是多變的。需要想辦法把類庫抽離出來,把業務代碼單獨打包。這樣加傷 hash 後瀏覽器就能緩存類庫的 js 文件,優化用戶體驗。此時我們的主角 CommonsChunkPlugin 就正式登場了。我們在 webpack.config.js 文件的 plugins 中添加 CommonsChunkPlugin,配置如下:

plugins: [n //...此前的代碼n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: function(module) {n return (n module.resource &&n /.js$/.test(module.resource) &&n module.resource.indexOf(n path.join(__dirname, ./node_modules)n ) === 0n )n }n }),n]n

上述配置,是通過 CommonsChunkPlugin 生成一個名為 vendorjs 文件,它抽取入口文件也就是 index.js 中來源於 node_modules 的依賴組成。此例中就是 vuejquery。打包出來畫風是這樣的:

此時看上去解決了我們的問題,將依賴的類庫抽取抽來獨立打包,加上緩存就能被瀏覽器緩存了。然而事情沒那麼簡單,不行你隨意改一下入口的 index.js 代碼,再次打包:

絕望地發現 vendor.js 文件的 hash 改變了。簡單說,這是因為模塊標識產生了變化所導致的,更具體的原因可以查看相關的中文文檔~修正的方法其實也挺簡單,就是再使用 CommonsChunkPlugin 抽取一次模塊,將不變的類庫沉澱下來,將變化的抽離出去。因而添如下代碼:

plugins: [n //...此前的代碼n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: function(module) {n return (n module.resource &&n /.js$/.test(module.resource) &&n module.resource.indexOf(n path.join(__dirname, ./node_modules)n ) === 0n )n }n }),n new webpack.optimize.CommonsChunkPlugin({n name: manifest,n chunks: [vendor, index]n })n]n

打包後, dist/js 目錄下多出一個名為 manifestjs 文件,此時你無論如何改變 index.js 的代碼,打包後的 vendor.jshash 都不再會改變了。

然而稍等,當你想拍拍手收工的時候,思考一下這樣的場景:隨著項目不斷迭代,vendor 中的依賴不斷被添加與刪除,使得它的 hash 會不斷變化,這顯然不符合我們的利益,這到底如何解決呢?

再次優化

既然 CommonsChunkPlugin 是可以按照我們的需求抽取模塊,而依賴的外部模塊可能是不斷變化的,那麼為何不將基礎的依賴模塊抽取出來作為一個文件,其他的依賴如插件等作為另一個文件呢?

簡單說,如我們的項目中 vue 是基本的依賴,必須用到它,而 jquery 等則是後加的類庫,之後可能變更。那麼將 vue 獨立打包一個文件,有利於瀏覽器緩存,因為無論此後添加更多的類庫或刪去 jquery 時, vue 文件的緩存依然是生效的。因而我們可以這麼做,首先新建一個入口:

entry: {n index: path.join(__dirname, index.js),n vendor: [vue],n},n

此處主要是用於指明需要獨立打包的依賴有哪些。之後在 plugins 中做如下修改:

plugins: [n //...此前的代碼n new webpack.HashedModuleIdsPlugin(),n new webpack.optimize.CommonsChunkPlugin({n name: vendor,n minChunks: Infinity,n }),n new webpack.optimize.CommonsChunkPlugin({n name: common,n minChunks: function(module) {n return (n module.resource &&n /.js$/.test(module.resource) &&n module.resource.indexOf(n path.join(__dirname, ./node_modules)n ) === 0n )n },n chunks: [index],n }),n new webpack.optimize.CommonsChunkPlugin({n name: manifest,n chunks: [vendor, common, index]n })n]n

插件 HashedModuleIdsPlugin,是用於保持模塊引用的 module id 不變。而 CommonsChunkPlugin 則提取入口指定的依賴獨立打包,minChunks: Infinity,的用意是讓插件別管其他,就按照設置的數組提取文件就好。之後修改一下原來的 vendor,重命名為 common,指定它從入口 index.js 中抽取來自 node_modules 的依賴。最後就是抽取 webpack 運行時的函數及其模塊標識組成 manifest。運行一下 webpack,構建出來如圖:

可以看到 vuejquery 被分開打包成了兩個文件,我們嘗試添加一下新的依賴 vuex,打包後結果如下:

如此一來,我們的優化目的就達到了,不變的都提取出來,變化的可以動態配置~

小結

webpack 插件 CommonsChunkPlugin 就介紹到這裡了,然而優化還是有很多的,比如開啟壓縮,去除注釋等。而當項目體積逐漸增大時,CommonsChunkPlugin 就不一定是提取代碼的最優解了。在打包速度與控制構建的精細程度來說,結合 DLLPlugin 會有更好的表現。根據不同的場景組合不同的插件以達到我們的目的,本來就是 webpack 的魅力之一。


推薦閱讀:

淺析 Webpack 插件化設計
Webpack工程化解決方案easywebpack
你的Tree-Shaking並沒什麼卵用
Webpack 之 Loader 的使用
基於 Webpack 3 的 Vue.js 工程項目腳手架

TAG:webpack |