今時今日,作前端不用個webpack好像都被時代拋棄了同樣,天天開發的時候npm run dev,該上線了npm run build,反正執行個命令刷刷地就打包好了,你根本無需知道執行命令以後整個過程究竟幹了什麼。webpack就像個黑盒,你得當心翼翼遵循它的配置行事,配好了就萬幸。這使得我很長一段時間以來,都對webpack畢恭畢敬,能跑起來的代碼就是最好的代碼,千萬別亂動配置。
終於有一天,我忍不住要搞清楚webpack究竟作了什麼。前端
去搞清楚webpack作了什麼以前,我以爲首先要思考一下咱們爲何須要webpack,它究竟解決了什麼痛點。想一想咱們平常搬磚的場景:
1.開發的時候須要一個開發環境,要是咱們修改一下代碼保存以後瀏覽器就自動展示最新的代碼那就行了(熱更新服務)
2.本地寫代碼的時候,要是調後端的接口不跨域就行了(代理服務)
3.爲了跟上時代,要是能用上什麼ES678N等等新東西就行了(翻譯服務)
4.項目要上線了,要是能一鍵壓縮代碼啊圖片什麼的就行了(壓縮打包服務)
5.咱們平時的靜態資源都是放到CDN上的,要是能自動幫我把這些搞好的靜態資源懟到CDN去就行了(自動上傳服務)
巴拉巴拉等等服務,那麼多你須要的服務,若是你打一個響指,這些服務都有條不紊地執行好,豈不是美滋滋!因此咱們須要webpack幫咱們去整合那麼多服務,而node的出現,賦予了咱們去操做系統的能力,這纔有了咱們今天的幸福(kubi)生活(manong)。
因此我以爲要根據本身的需求來使用webpack,知道本身須要什麼樣的服務,webpack能不能提供這樣的服務,若是能夠,那麼這個服務應該在構建中的哪一個環節被處理。vue
抽絲剝繭以後,去理解這些的流程,你就能從webpack那一坨坨的配置中,定位到你需求被webpack處理的位置,最後加上相應的配置便可。node
webpack搞了不少東西,但最終產出的無非就是通過重重服務處理過的代碼,那麼這些代碼是怎樣的呢?
首先咱們先來看看入口文件index.js:webpack
console.log('index') const one = require('./module/one.js') const two = require('./module/two.js') one() two()
嗯,很簡單,沒什麼特別,引入了兩個模塊,最後執行了它們一下。其中one.js和two.js的代碼也很簡單,就是導出了個函數:git
// one.js module.exports = function () { console.log('one') }
// two.js module.exports = function () { console.log('two') }
好了,就是這麼簡單的代碼,放到webpack打包出來的是什麼呢?es6
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ 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; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // 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 = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { console.log('index') const one = __webpack_require__(1) const two = __webpack_require__(2) one() two() /***/ }), /* 1 */ /***/ (function(module, exports) { module.exports = function () { console.log('one') } /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = function () { console.log('two') } /***/ }) /******/ ]);
真是不忍直視……我寫得這麼簡潔優雅的代碼,通過webpack的處理後如此不堪入目!但爲了搞清楚這坨東西究竟作了什麼,我不得不忍醜去將它簡化了一下。github
其實進過簡化後就能夠看到,這些代碼意圖十分明顯,也是咱們十分熟悉的套路。web
(function (modules) { const require = function (moduleId) { const module = {} module.exports = null modules[moduleId].call(module, module, require) return module.exports } require(0) })([ function (module, require) { console.log('index') const one = require(1) const two = require(2) one() two() }, function (module, require) { module.exports = function () { console.log('one') } }, function (module, require) { module.exports = function () { console.log('two') } }])
這樣看可能會直觀一點:
你會看到這不就是咱們掛在嘴邊的自執行函數嗎?而後參數是一個數組,這個數組就是咱們的模塊,當require(0)的時候就會執行這個數組索引爲0的代碼,以此類推而達到模塊化的效果。這裏有個關鍵點,就是咱們明明寫的時候是require('./module/one.js'),怎麼最後出來能夠變成require(1)呢?npm
沒有什麼比本身擼一個理解得更透徹了。咱們根據上面的最終打包的結果來捋一捋要作一些什麼事情。
1.觀察一下,咱們須要一個自執行函數,這裏面須要控制的是這個自執行函數的傳參,就是那個數組
2.這個數組是毋容置疑是根據依賴關係來造成的
3.咱們要找到全部的require而後將require的路徑替換成對應數組的索引
4.將這個處理好的文件輸出出來
ok,上代碼:後端
const fs = require('fs') const path = require('path') const esprima = require('esprima') const estraverse = require('estraverse') // 定義上下文 即全部的尋址都按照這個基準進行 const context = path.resolve(__dirname, '../') // 處理路徑 const pathResolve = (data) => path.resolve(context, data) // 定義全局數據格式 const dataInfo = { // 入口文件源碼 source: '', // 分析入口文件源碼得出的依賴信息 requireInfo: null, // 根據依賴信息得出的各個模塊 modules: null } /** * 讀取文件 * @param {String} path */ const readFile = (path) => { return new Promise((resolve, reject) => { fs.readFile(path, function (err, data) { if (err) { console.log(err) reject(err) return } resolve(data) }) }) } /** * 分析入口源碼 */ const getRequireInfo = () => { // 各個依賴的id 從1開始是由於0是入口文件 let id = 1 const ret = [] // 使用esprima將入口源碼解析成ast const ast = esprima.parse(dataInfo.source, {range: true}) // 使用estraverse遍歷ast estraverse.traverse(ast, { enter (node) { // 篩選出require節點 if (node.type === 'CallExpression' && node.callee.name === 'require' && node.callee.type === 'Identifier') { // require路徑,如require('./index.js'),則requirePath = './index.js' const requirePath = node.arguments[0] // 將require路徑轉爲絕對路徑 const requirePathValue = pathResolve(requirePath.value) // 如require('./index.js')中'./index.js'在源碼的位置 const requirePathRange = requirePath.range ret.push({requirePathValue, requirePathRange, id}) id++ } } }) return ret } /** * 模塊模板 * @param {String} content */ const moduleTemplate = (content) => `function (module, require) {\n${content}\n},` /** * 獲取模塊信息 */ const getModules = async () => { const requireInfo = dataInfo.requireInfo const modules = [] for (let i = 0, len = requireInfo.length; i < len; i++) { const file = await readFile(requireInfo[i].requirePathValue) const content = moduleTemplate(file.toString()) modules.push(content) } return modules } /** * 將入口文件如require('./module/one.js')等對應成require(1)模塊id */ const replace = () => { const requireInfo = dataInfo.requireInfo // 須要倒序處理,由於好比第一個require('./module/one.js')中的路徑是在源碼字符串42-59這個區間 // 而第二個require('./module/two.js')中的路徑是在源碼字符串82-99這個區間,那麼若是先替換位置較前的代碼 // 則此時源碼字符串已經少了一截(從'./module/one.js'變成1),那第二個require的位置就不對了 const sortRequireInfo = requireInfo.sort((item1, item2) => item1.requirePathRange[0] < item2.requirePathRange[0]) sortRequireInfo.forEach(({requirePathRange, id}) => { const start = requirePathRange[0] const end = requirePathRange[1] const headerS = dataInfo.source.substr(0, start) const endS = dataInfo.source.substr(end) dataInfo.source = `${headerS}${id}${endS}` }) } /** * 輸出打包好的文件 */ const output = async () => { const data = await readFile(pathResolve('./template/indexTemplate.js')) const indexModule = moduleTemplate(dataInfo.source) const allModules = [indexModule, ...dataInfo.modules].join('') const result = `${data.toString()}([\n${allModules}\n])` fs.writeFile(pathResolve('./build/output.js'), result, function (err) { if (err) { throw err; } }) } const main = async () => { // 讀取入口文件 const data = await readFile(pathResolve('./index.js')) dataInfo.source = data.toString() // 獲取依賴信息 dataInfo.requireInfo = getRequireInfo() // 獲取模塊信息 dataInfo.modules = await getModules() // 將入口文件如require('./module/one.js')等對應成require(1)模塊id replace() // 輸出打包好的文件 output() console.log(JSON.stringify(dataInfo)) } main()
這裏的關鍵是將入口源碼轉成ast從而分析出require的路徑在源碼字符串中所在的位置,咱們這裏用到了esprima去將源碼轉成ast,而後用estraverse去遍歷ast從而篩選出咱們感興趣的節點,這時咱們就能夠對轉化成ast的代碼隨心所欲了,babel就是這樣的原理爲咱們轉化代碼的。
到這裏咱們能夠知道,除去其餘雜七雜八的服務,webpack本質上就是一個將咱們平時寫的模塊化代碼轉成如今瀏覽器能夠直接執行的代碼。固然上面的代碼是很是簡陋的,咱們沒有去遞歸處理依賴,沒有去處理require的尋址(好比require('vue')是怎樣找到vue在哪裏的)等等的細節處理,只爲還原一個最簡單易懂的結構。上面的源碼能夠在這裏找到。