打包工具的角色
所謂打包工具在web開發中主要解決的問題是:
(1)文件依賴管理。畢竟現在都是模塊化開發,打包工具首先就是要梳理文件之間的依賴關系。
(2)資源加載管理。web本質就是html、js和css的文件組合,文件的加載順序(先后時機)和文件的加載數量(合並、嵌入、拆分)也是打包工具重點要解決的問題。
(3)效率與優化管理。提高開發效率,即寫最少的代碼,做最好的效果展示;盡可能的使用工具,減少機械coding和優化頁面效果,這個是考驗打包工具是否具備魅力的點。
打包工具的結構
由上圖可以推出,打包工具的結構應該是tool+plugins的結構。tool提供基礎能力,即文件依賴管理和資源加載管理;在此基礎上通過一系列的plugins來豐富打包工具的能力。plugins類似互聯網+的概念,文件經plugins處理之后,具備了web渲染中的某種優勢。
為什么使用webpack?
決定打包工具能走多遠的是plugins的豐富程度,而webpack目前恰恰是最豐富的,我這里對比了一下fis與webpack在npm包上數據,看完就知道為什么要使用webpack了。

webpack的工作原理
webpack處理文件的過程可以分為兩個維度:文件間的關系和文件的內容。文件間的關系處理,主要是通過文件和模塊標記方法來實現;文件內容的處理主要通過loaders和plugins來處理。
1.文件內容處理
在webpack的世界里,js是一等公民,是處理的入口,其他資源都是在js中通過類似require的方式引入。webpack雖然支持命令行操作,但是一般將配置寫在webpack.conf.js文件中,文件內容是一個配置對象,基本配置項是:entry、ouput、module、plugins屬性。
entry與output
這里引入了一個chunk的概念,chunk表示一個文件,默認情況下webpack的輸入是一個入口文件,輸出也是一個文件,這個文件就是一個chunk,chunkId就是產出時給每個文件一個唯一標識id,chunkhash就是文件內容的md5值,name就是在entry中指定的key值。
module.exports = { entry: { collection: './src/main.js' // collection為chunk的名字,chunk的入口文件是main.js }, output: { path: './dist/js', filename: '[name].[chunkhash].js' // 輸出到dist/js目錄下,以collection+chunk內容的md5值作為輸出的文件名 } };
輸出:
module
moudle對應loader(加載器 )的配置,主要對指定類型的文件進行操作,舉個例子:js類型的文件和css文件需要不同的loader來處理。最常用的加載器是eslint-loader和babel-loader。
module.exports = { entry: { collection: './src/main.js' // collection為chunk的名字,chunk的入口文件是main.js }, output: { path: './dist/js', filename: '[name].[chunkhash].js' // 輸出到dist/js目錄下,以collection+chunk內容的md5值作為輸出的文件名 } module: { rules: [ // rules為數組,保存每個加載器的配置 { test: /\.js$/, // test屬性必須配置,值為正則表達式,用於匹配文件 loader: 'babel-loader?fakeoption=true!eslint-loader', // loader屬性必須配置,值為字符串,對於匹配的文件使用babel-loader和eslint-loader處理,處理順序從右向左,先eslint-loader,后babel-loader,loader之間用!隔開,loader與options用?隔開 exclude: /node_module/, // 對於匹配的文件進行過濾,排除node_module目錄下的文件 include: './src' // 指定匹配文件的范圍 } ] } };
其中,loader的options也可以單獨使用options屬性來配置
rules: [ { test: /\.js$/, loader: 'babel-loader', options: { fakeoption: true } } ]
另外通常babel-loader的配置項可以寫在根目錄下的.babelrc文件中
{ "presets": ["stage-2"], "plugins": ["transform-runtime"] }
plugins
plugins用於擴展webpack的功能,相比着loader更加靈活,不用指定文件類型。常用的plugins有三個,html-webpack-plugin、commonChunkPlugin和ExtractTextPlugin。
var HtmlwebpackPlugin = require('html-webpack-plugin'); module.exports = { ... plugins: [ new HtmlwebpackPlugin({
filename: 'collection.html', // 入口html文件名 template: './src/index.html' // 入口html文件模板 }) ] ... };

module.exports = { ... plugins: [ // 把通過npm包引用的第三方類庫從入口文件中提取出來 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function (module, count) { // 指定范圍是js文件來自node_modules return (module.resource && /\.js$/.test(module.resource) &&module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0); } }), // 把webpack的module管理相關基礎代碼從vendor中提取到manifest new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] }) ] ... };
js是一等公民,webpack默認不產出css文件,產出css文件需要依賴ExtractTextPlugin插件來完成。
module.exports = { ... plugins: [ // 把css片段從入口js文件中提取到對應入口名的css文件中 new ExtractTextPlugin({ filename: './dist/static/css/[name].[contenthash].css' }), ] ... };
2.文件間的關系處理
理清這個過程得倒推,先看一下經webpack處理后的js文件,下面的例子中主要涉及3個產出文件,manifest是webpack的module管理代碼,vendor是第三方類庫文件,collection是入口文件,加載的順序是manifest-》vendor-》collection。
查看三個文件的內容可知:
vendor和collection的內容都是一個函數,類似jsonp請求回來的返回值。下面分別是vendor和collection中的代碼。
webpackJsonp([0],[ // chunkid為0 /* 0 */ /***/ (function(module, exports, __webpack_require__) { ... /***/ }), /* 1 */ /***/ (function(module, exports) { ... /* 2 */ ... /* 9 */ /***/ (function(module, exports, __webpack_require__) { ... /***/ }), /* 10 */, // 此處moduleid=10的模塊為空 /* 11 */ /***/ (function(module, exports) { ... /***/ }), ... ]);
webpackJsonp([1],[ // chunkid為1 /* 0 */, // moduleid為0-9的模塊均為空 /* 1 */, /* 2 */, /* 3 */, /* 4 */, /* 5 */, /* 6 */, /* 7 */, /* 8 */, /* 9 */, /* 10 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { ... }; /***/ }), /* 11 */, /* 12 */, .... /* 59 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_vue__ = __webpack_require__(14); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_validator__ = __webpack_require__(58); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_validator___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_vue_validator__); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__router__ = __webpack_require__(54); ... /***/ }), ... ], [59]); // 此處多了一個參數,參數值為[59],即moduleid=59的模塊名是入口模塊
從以上兩個文件可以發現出一個規律,
(1)每個文件的chunkid放在第一個參數;
(2)模塊都放在第二個參數,每個模塊都有對應的id,數組都是從moduleid=0開始,依次增加,如果該模塊不在該文件中,則使用空值來代替;
(3)入口文件中的函數多了一個參數,參數里面傳入了一個moduleid,視為入口模塊。
接下來,我們看一下manifest文件的內容,來看看webpackJsonp函數究竟是怎么運行的。
總的來說是利用閉包傳入了幾個自由變量:
modules:模塊本身是一個函數,modules用於存儲模塊函數數組。
installedModules:用於緩存模塊的返回值,即module.exports。
installedChunks:用於標記chunk文件是否已經被加載。
webpackJsonp:chunk文件加載后的callback函數,主要將文件中的模塊存儲到modules對象中,同時標記chunk文件的下載情況,對於入口chunk來說,等所有的模塊都放入modules之后,執行入口模塊函數。
__webpack_require__:模塊加載函數,加載的策略是:根據moduleid讀取,優先讀取緩存installedModules,讀取失敗則讀取modules,獲取返回值,然后進行緩存。
/******/ (function(modules) { // webpackBootstrap /******/ // install a JSONP callback for chunk loading /******/ var parentJsonpFunction = window["webpackJsonp"]; // webpackJsonp函數掛在window下,接收三個參數,chunkids:文件的id,moreModules: 文件中的module,executeModules:入口module /******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { /******/ // 把moreModules中的module的放入modules中,緩存起來 /******/ // 利用傳入的chunkid在installedChunks標記對應的文件已下載 /******/ var moduleId, chunkId, i = 0, resolves = [], result; /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(installedChunks[chunkId]) { /******/ resolves.push(installedChunks[chunkId][0]); /******/ } /******/ installedChunks[chunkId] = 0; /******/ } /******/ for(moduleId in moreModules) { /******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { /******/ modules[moduleId] = moreModules[moduleId]; /******/ } /******/ } /******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); /******/ while(resolves.length) { /******/ resolves.shift()(); /******/ } // 存在入口模塊,則加載對應的模塊並執行 /******/ if(executeModules) { /******/ for(i=0; i < executeModules.length; i++) { /******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]); /******/ } /******/ } /******/ return result; /******/ }; /******/ /******/ // 模塊緩存對象,存放module的exports /******/ var installedModules = {}; /******/ /******/ // chunk是否下載標記對象,key為chunkid,值為0表示已經下載 /******/ var installedChunks = { /******/ 2: 0 // 表示chunkid=2的文件已下載,其實就是manifest文件本身 /******/ }; /******/ /******/ // 模塊加載函數:先從緩存讀取,沒有則從modules中讀取module函數,執行后返回exports,最后緩存起來 /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ // This file contains only the entry chunk. /******/ // The chunk loading function for additional chunks /******/ __webpack_require__.e = function requireEnsure(chunkId) { ... /******/ }; /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = "./"; /******/ /******/ // on error function for async loading /******/ __webpack_require__.oe = function(err) { console.error(err); throw err; }; /******/ }) /************************************************************************/ /******/ ([]); //# sourceMappingURL=manifest.e7a81691a524d5b99b6b.js.map
總體而言,下圖可以簡易的描述出webpack打包過程,該過程主要分為三個階段:module構建、trunk構建和產出三個階段。