摘要:今時(shí)今日,做前端不用個(gè)好像都被時(shí)代拋棄了一樣,每天開(kāi)發(fā)的時(shí)候,該上線了,反正執(zhí)行個(gè)命令刷刷地就打包好了,你根本無(wú)需知道執(zhí)行命令之后整個(gè)過(guò)程究竟干了什么。終于有一天,我忍不住要搞清楚究竟做了什么。
今時(shí)今日,做前端不用個(gè)webpack好像都被時(shí)代拋棄了一樣,每天開(kāi)發(fā)的時(shí)候npm run dev,該上線了npm run build,反正執(zhí)行個(gè)命令刷刷地就打包好了,你根本無(wú)需知道執(zhí)行命令之后整個(gè)過(guò)程究竟干了什么。webpack就像個(gè)黑盒,你得小心翼翼遵循它的配置行事,配好了就萬(wàn)幸。這使得我很長(zhǎng)一段時(shí)間以來(lái),都對(duì)webpack畢恭畢敬,能跑起來(lái)的代碼就是最好的代碼,千萬(wàn)別亂動(dòng)配置。
終于有一天,我忍不住要搞清楚webpack究竟做了什么。
去搞清楚webpack做了什么之前,我覺(jué)得首先要思考一下我們?yōu)槭裁葱枰獁ebpack,它究竟解決了什么痛點(diǎn)。想想我們?nèi)粘0岽u的場(chǎng)景:
1.開(kāi)發(fā)的時(shí)候需要一個(gè)開(kāi)發(fā)環(huán)境,要是我們修改一下代碼保存之后瀏覽器就自動(dòng)展現(xiàn)最新的代碼那就好了(熱更新服務(wù))
2.本地寫(xiě)代碼的時(shí)候,要是調(diào)后端的接口不跨域就好了(代理服務(wù))
3.為了跟上時(shí)代,要是能用上什么ES678N等等新東西就好了(翻譯服務(wù))
4.項(xiàng)目要上線了,要是能一鍵壓縮代碼啊圖片什么的就好了(壓縮打包服務(wù))
5.我們平時(shí)的靜態(tài)資源都是放到CDN上的,要是能自動(dòng)幫我把這些搞好的靜態(tài)資源懟到CDN去就好了(自動(dòng)上傳服務(wù))
巴拉巴拉等等服務(wù),那么多你需要的服務(wù),如果你打一個(gè)響指,這些服務(wù)都有條不紊地執(zhí)行好,豈不是美滋滋!所以我們需要webpack幫我們?nèi)フ夏敲炊喾?wù),而node的出現(xiàn),賦予了我們?nèi)ゲ僮飨到y(tǒng)的能力,這才有了我們今天的幸福(kubi)生活(manong)。
所以我覺(jué)得要根據(jù)自己的需求來(lái)使用webpack,知道自己需要什么樣的服務(wù),webpack能不能提供這樣的服務(wù),如果可以,那么這個(gè)服務(wù)應(yīng)該在構(gòu)建中的哪個(gè)環(huán)節(jié)被處理。
如果與輸入相關(guān)的需求,找entry(比如多頁(yè)面就有多個(gè)入口)
如果與輸出相關(guān)的需求,找output(比如你需要定義輸出文件的路徑、名字等等)
如果與模塊尋址相關(guān)的需求,找resolve(比如定義別名alias)
如果與轉(zhuǎn)譯相關(guān)的需求,找loader(比如處理sass處理es678N)
如果與構(gòu)建流程相關(guān)的需求,找plugin(比如我需要在打包完成后,將打包好的文件復(fù)制到某個(gè)目錄,然后提交到git上)
抽絲剝繭之后,去理解這些的流程,你就能從webpack那一坨坨的配置中,定位到你需求被webpack處理的位置,最后加上相應(yīng)的配置即可。
webpack打包出來(lái)的什么webpack搞了很多東西,但最終產(chǎn)出的無(wú)非就是經(jīng)過(guò)重重服務(wù)處理過(guò)的代碼,那么這些代碼是怎樣的呢?
首先我們先來(lái)看看入口文件index.js:
console.log("index") const one = require("./module/one.js") const two = require("./module/two.js") one() two()
嗯,很簡(jiǎn)單,沒(méi)什么特別,引入了兩個(gè)模塊,最后執(zhí)行了它們一下。其中one.js和two.js的代碼也很簡(jiǎn)單,就是導(dǎo)出了個(gè)函數(shù):
// one.js module.exports = function () { console.log("one") }
// two.js module.exports = function () { console.log("two") }
好了,就是這么簡(jiǎn)單的代碼,放到webpack打包出來(lái)的是什么呢?
/******/ (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") } /***/ }) /******/ ]);
真是不忍直視……我寫(xiě)得這么簡(jiǎn)潔優(yōu)雅的代碼,經(jīng)過(guò)webpack的處理后如此不堪入目!但為了搞清楚這坨東西究竟做了什么,我不得不忍丑去將它簡(jiǎn)化了一下。
簡(jiǎn)化webpack打包出來(lái)的代碼其實(shí)進(jìn)過(guò)簡(jiǎn)化后就可以看到,這些代碼意圖十分明顯,也是我們十分熟悉的套路。
(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") } }])
這樣看可能會(huì)直觀一點(diǎn):
你會(huì)看到這不就是我們掛在嘴邊的自執(zhí)行函數(shù)嗎?然后參數(shù)是一個(gè)數(shù)組,這個(gè)數(shù)組就是我們的模塊,當(dāng)require(0)的時(shí)候就會(huì)執(zhí)行這個(gè)數(shù)組索引為0的代碼,以此類推而達(dá)到模塊化的效果。這里有個(gè)關(guān)鍵點(diǎn),就是我們明明寫(xiě)的時(shí)候是require("./module/one.js"),怎么最后出來(lái)可以變成require(1)呢?
沒(méi)有什么比自己擼一個(gè)理解得更透徹了。我們根據(jù)上面的最終打包的結(jié)果來(lái)捋一捋要做一些什么事情。
1.觀察一下,我們需要一個(gè)自執(zhí)行函數(shù),這里面需要控制的是這個(gè)自執(zhí)行函數(shù)的傳參,就是那個(gè)數(shù)組
2.這個(gè)數(shù)組是毋容置疑是根據(jù)依賴關(guān)系來(lái)形成的
3.我們要找到所有的require然后將require的路徑替換成對(duì)應(yīng)數(shù)組的索引
4.將這個(gè)處理好的文件輸出出來(lái)
ok,上代碼:
const fs = require("fs") const path = require("path") const esprima = require("esprima") const estraverse = require("estraverse") // 定義上下文 即所有的尋址都按照這個(gè)基準(zhǔn)進(jìn)行 const context = path.resolve(__dirname, "../") // 處理路徑 const pathResolve = (data) => path.resolve(context, data) // 定義全局?jǐn)?shù)據(jù)格式 const dataInfo = { // 入口文件源碼 source: "", // 分析入口文件源碼得出的依賴信息 requireInfo: null, // 根據(jù)依賴信息得出的各個(gè)模塊 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 = () => { // 各個(gè)依賴的id 從1開(kāi)始是因?yàn)?是入口文件 let id = 1 const ret = [] // 使用esprima將入口源碼解析成ast const ast = esprima.parse(dataInfo.source, {range: true}) // 使用estraverse遍歷ast estraverse.traverse(ast, { enter (node) { // 篩選出require節(jié)點(diǎn) 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路徑轉(zhuǎn)為絕對(duì)路徑 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")等對(duì)應(yīng)成require(1)模塊id */ const replace = () => { const requireInfo = dataInfo.requireInfo // 需要倒序處理,因?yàn)楸热绲谝粋€(gè)require("./module/one.js")中的路徑是在源碼字符串42-59這個(gè)區(qū)間 // 而第二個(gè)require("./module/two.js")中的路徑是在源碼字符串82-99這個(gè)區(qū)間,那么如果先替換位置較前的代碼 // 則此時(shí)源碼字符串已經(jīng)少了一截(從"./module/one.js"變成1),那第二個(gè)require的位置就不對(duì)了 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")等對(duì)應(yīng)成require(1)模塊id replace() // 輸出打包好的文件 output() console.log(JSON.stringify(dataInfo)) } main()
這里的關(guān)鍵是將入口源碼轉(zhuǎn)成ast從而分析出require的路徑在源碼字符串中所在的位置,我們這里用到了esprima去將源碼轉(zhuǎn)成ast,然后用estraverse去遍歷ast從而篩選出我們感興趣的節(jié)點(diǎn),這時(shí)我們就可以對(duì)轉(zhuǎn)化成ast的代碼為所欲為了,babel就是這樣的原理為我們轉(zhuǎn)化代碼的。
最后到這里我們可以知道,除去其他雜七雜八的服務(wù),webpack本質(zhì)上就是一個(gè)將我們平時(shí)寫(xiě)的模塊化代碼轉(zhuǎn)成現(xiàn)在瀏覽器可以直接執(zhí)行的代碼。當(dāng)然上面的代碼是非常簡(jiǎn)陋的,我們沒(méi)有去遞歸處理依賴,沒(méi)有去處理require的尋址(比如require("vue")是怎樣找到vue在哪里的)等等的細(xì)節(jié)處理,只為還原一個(gè)最簡(jiǎn)單易懂的結(jié)構(gòu)。上面的源碼可以在這里找到。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/96859.html
摘要:例如允許我們?cè)诖虬鼤r(shí)將腳本分塊利用瀏覽器緩存我們能夠有的放矢的加載資源。文章的內(nèi)容大體分為兩個(gè)方面,一方面在思路制定模塊分離的策略,另一方面從技術(shù)上對(duì)方案進(jìn)行落地。我之前提到測(cè)試之下是什么樣具體的場(chǎng)景并不重要。前言 隨著前端代碼需要處理的業(yè)務(wù)越來(lái)越繁重,我們不得不面臨的一個(gè)問(wèn)題是前端的代碼體積也變得越來(lái)越龐大。這造成無(wú)論是在調(diào)式還是在上線時(shí)都需要花長(zhǎng)時(shí)間等待編譯完成,并且用戶也不得不花額外的...
摘要:一介紹隨著社區(qū)的框架的發(fā)布,社區(qū)也終于誕生了屬于自己的前后端同構(gòu)框架。本文主要研究的運(yùn)行原理,分析它從接收一條指令,到完成指令背后所發(fā)生的一系列事情。最后,通過(guò)來(lái)檢查輸出的是否存在問(wèn)題,然后發(fā)出通知,表明可用。 showImg(https://segmentfault.com/img/bVIc9l?w=536&h=136); 一、介紹 Nuxt.js - Universal Vue.j...
摘要:面試造航母,工作擰螺絲,新公司面試技術(shù)官要求會(huì)技術(shù)棧。然而公司項(xiàng)目暫時(shí)并沒(méi)有用到,不過(guò)為了提升實(shí)戰(zhàn)經(jīng)驗(yàn),還是在業(yè)余時(shí)間搗騰出一個(gè),以下是項(xiàng)目介紹。前段為了學(xué)習(xí)小程序的開(kāi)發(fā),做了個(gè)小程序名叫口袋吉他,這也是個(gè)人興趣驅(qū)使的開(kāi)發(fā)想法。 面試造航母,工作擰螺絲,新公司面試技術(shù)官要求會(huì)react技術(shù)棧。 問(wèn):有使用過(guò)React么?答:沒(méi),只使用過(guò)Vue。又問(wèn):給你一星期能上手開(kāi)發(fā)么?答:可以(一...
摘要:顯然,要理解,首先要了解迭代器,接著了解什么是生成器。生成器上述代碼中,就是一個(gè)迭代器,循環(huán)部分就是迭代過(guò)程。迭代器和生成器的執(zhí)行效率因?yàn)樯善鬟叺吷桑哉加脙?nèi)存極少,執(zhí)行效率也更高。 顯然,要理解yield,首先要了解迭代器(iterator),接著了解什么是生成器(generator)。 迭代器 通俗的講,迭代器就是可以逐個(gè)訪問(wèn)的容器,而逐個(gè)逐步訪問(wèn)的過(guò)程成為迭代。 ite...
摘要:在上述過(guò)程再細(xì)化為瀏覽器搜索自己的緩存。至此,瀏覽器已經(jīng)得到了域名對(duì)應(yīng)的地址。具體過(guò)程如下在中這一過(guò)程如下首先是字節(jié)流,經(jīng)過(guò)解碼之后是字符流,然后通過(guò)詞法分析器會(huì)被解釋成詞語(yǔ),之后經(jīng)過(guò)語(yǔ)法分析器構(gòu)建成節(jié)點(diǎn),最后這些節(jié)點(diǎn)被組建成一棵樹(shù)。 面試的時(shí)候,我們經(jīng)常會(huì)被問(wèn)從在瀏覽器地址欄中輸入 url 到頁(yè)面展現(xiàn)的短短幾秒內(nèi)瀏覽器究竟做了什么?那么瀏覽器到底做了啥? 瀏覽器的多進(jìn)程架構(gòu)一個(gè)好的程...
閱讀 1874·2021-11-25 09:43
閱讀 3701·2021-11-24 10:32
閱讀 1096·2021-10-13 09:39
閱讀 2345·2021-09-10 11:24
閱讀 3360·2021-07-25 21:37
閱讀 3481·2019-08-30 15:56
閱讀 875·2019-08-30 15:44
閱讀 1464·2019-08-30 13:18