摘要:今時今日,做前端不用個好像都被時代拋棄了一樣,每天開發的時候,該上線了,反正執行個命令刷刷地就打包好了,你根本無需知道執行命令之后整個過程究竟干了什么。終于有一天,我忍不住要搞清楚究竟做了什么。
今時今日,做前端不用個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能不能提供這樣的服務,如果可以,那么這個服務應該在構建中的哪個環節被處理。
如果與輸入相關的需求,找entry(比如多頁面就有多個入口)
如果與輸出相關的需求,找output(比如你需要定義輸出文件的路徑、名字等等)
如果與模塊尋址相關的需求,找resolve(比如定義別名alias)
如果與轉譯相關的需求,找loader(比如處理sass處理es678N)
如果與構建流程相關的需求,找plugin(比如我需要在打包完成后,將打包好的文件復制到某個目錄,然后提交到git上)
抽絲剝繭之后,去理解這些的流程,你就能從webpack那一坨坨的配置中,定位到你需求被webpack處理的位置,最后加上相應的配置即可。
webpack打包出來的什么webpack搞了很多東西,但最終產出的無非就是經過重重服務處理過的代碼,那么這些代碼是怎樣的呢?
首先我們先來看看入口文件index.js:
console.log("index") const one = require("./module/one.js") const two = require("./module/two.js") one() two()
嗯,很簡單,沒什么特別,引入了兩個模塊,最后執行了它們一下。其中one.js和two.js的代碼也很簡單,就是導出了個函數:
// one.js module.exports = function () { console.log("one") }
// two.js module.exports = function () { console.log("two") }
好了,就是這么簡單的代碼,放到webpack打包出來的是什么呢?
/******/ (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的處理后如此不堪入目!但為了搞清楚這坨東西究竟做了什么,我不得不忍丑去將它簡化了一下。
簡化webpack打包出來的代碼其實進過簡化后就可以看到,這些代碼意圖十分明顯,也是我們十分熟悉的套路。
(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)呢?
沒有什么比自己擼一個理解得更透徹了。我們根據上面的最終打包的結果來捋一捋要做一些什么事情。
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) { ${content} },` /** * 獲取模塊信息 */ 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()}([ ${allModules} ])` 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在哪里的)等等的細節處理,只為還原一個最簡單易懂的結構。上面的源碼可以在這里找到。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96859.html
摘要:例如允許我們在打包時將腳本分塊利用瀏覽器緩存我們能夠有的放矢的加載資源。文章的內容大體分為兩個方面,一方面在思路制定模塊分離的策略,另一方面從技術上對方案進行落地。我之前提到測試之下是什么樣具體的場景并不重要。前言 隨著前端代碼需要處理的業務越來越繁重,我們不得不面臨的一個問題是前端的代碼體積也變得越來越龐大。這造成無論是在調式還是在上線時都需要花長時間等待編譯完成,并且用戶也不得不花額外的...
摘要:一介紹隨著社區的框架的發布,社區也終于誕生了屬于自己的前后端同構框架。本文主要研究的運行原理,分析它從接收一條指令,到完成指令背后所發生的一系列事情。最后,通過來檢查輸出的是否存在問題,然后發出通知,表明可用。 showImg(https://segmentfault.com/img/bVIc9l?w=536&h=136); 一、介紹 Nuxt.js - Universal Vue.j...
摘要:面試造航母,工作擰螺絲,新公司面試技術官要求會技術棧。然而公司項目暫時并沒有用到,不過為了提升實戰經驗,還是在業余時間搗騰出一個,以下是項目介紹。前段為了學習小程序的開發,做了個小程序名叫口袋吉他,這也是個人興趣驅使的開發想法。 面試造航母,工作擰螺絲,新公司面試技術官要求會react技術棧。 問:有使用過React么?答:沒,只使用過Vue。又問:給你一星期能上手開發么?答:可以(一...
摘要:顯然,要理解,首先要了解迭代器,接著了解什么是生成器。生成器上述代碼中,就是一個迭代器,循環部分就是迭代過程。迭代器和生成器的執行效率因為生成器邊迭代邊生成,所以占用內存極少,執行效率也更高。 顯然,要理解yield,首先要了解迭代器(iterator),接著了解什么是生成器(generator)。 迭代器 通俗的講,迭代器就是可以逐個訪問的容器,而逐個逐步訪問的過程成為迭代。 ite...
摘要:在上述過程再細化為瀏覽器搜索自己的緩存。至此,瀏覽器已經得到了域名對應的地址。具體過程如下在中這一過程如下首先是字節流,經過解碼之后是字符流,然后通過詞法分析器會被解釋成詞語,之后經過語法分析器構建成節點,最后這些節點被組建成一棵樹。 面試的時候,我們經常會被問從在瀏覽器地址欄中輸入 url 到頁面展現的短短幾秒內瀏覽器究竟做了什么?那么瀏覽器到底做了啥? 瀏覽器的多進程架構一個好的程...
閱讀 1847·2021-11-25 09:43
閱讀 3688·2021-11-24 10:32
閱讀 1076·2021-10-13 09:39
閱讀 2328·2021-09-10 11:24
閱讀 3344·2021-07-25 21:37
閱讀 3464·2019-08-30 15:56
閱讀 858·2019-08-30 15:44
閱讀 1449·2019-08-30 13:18