打包工具的角色
所謂打包工具在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構建和產出三個階段。

