mini?webpack打包基礎(chǔ)解決包緩存和環(huán)依賴
正文
本文帶你實(shí)現(xiàn) webpack 最基礎(chǔ)的打包功能,同時(shí)解決包緩存和環(huán)依賴的問(wèn)題 ~
發(fā)車,先來(lái)看示例代碼。
index.js 主入口文件
我們這里三個(gè)文件,index.js 是主入口文件:
// filename: index.js import foo from './foo.js' foo(); //filename: foo.js import message from './message.js' function foo() { console.log(message); } // filename: message.js const message = 'hello world' export default message;
接下來(lái),我們會(huì)創(chuàng)建一個(gè) bundle.js 打包這三個(gè)文件,打包得到的結(jié)果是一個(gè) JS 文件,運(yùn)行這個(gè) JS 文件輸出的結(jié)果會(huì)是 'hello world'。
bundle.js 就是 webpack 做的事情,我們示例中的 index.js 相當(dāng)于 webpack 的入口文件,會(huì)在 webpack.config.js 的 entry 里面配置。
讓我們來(lái)實(shí)現(xiàn) bundle.js 的功能。
讀主入口文件
最開始的,當(dāng)然是讀主入口文件了:
function createAssert(filename) { const content = fs.readFileSync(filename, { encoding: 'utf-8' }); return content; } const content = createAssert('./example/index.js');
接下來(lái),需要做的事情就是把 import 語(yǔ)法引入的這個(gè)文件也找過(guò)來(lái),在上圖中,就是 foo.js,同時(shí)還得把 foo.js 依賴的也找過(guò)來(lái),依次遞推。
現(xiàn)在得把 foo.js 取出來(lái),怎么解析 import foo from './foo.js' 這句,把值取出來(lái)呢?
把這行代碼解析成 ast 會(huì)變成:
接下來(lái)的思路就是把上面的代碼轉(zhuǎn)化成 ast,接著去取上圖框框里那個(gè)字段。
對(duì)依賴文件進(jìn)行讀取操作
const fs = require('fs'); const babylon = require('babylon'); const traverse = require('babel-traverse').default; function createAssert(filename) { const dependencies = []; const content = fs.readFileSync(filename, { encoding: 'utf-8' }); const ast = babylon.parse(content, { sourceType: 'module', }); traverse(ast, { ImportDeclaration: ({node}) => { dependencies.push(node.source.value); } }) console.log(dependencies); // [ './foo.js' ] return content; }
上面我們做的事情就是把當(dāng)前的文件讀到,然后再把當(dāng)前文件的依賴加到一個(gè)叫做 dependencies 的數(shù)組里面去。
然后,這里的 createAssert 只返回源代碼還不夠,再完善一下:
let id = 0; function getId() { return id++; } function createAssert(filename) { const dependencies = []; const content = fs.readFileSync(filename, { encoding: 'utf-8' }); const ast = babylon.parse(content, { sourceType: 'module', }); traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); } }) return { id: getId(), code: content, filename, dependencies, mapping: {}, }; }
假如對(duì)主入口文件 index.js 調(diào)用,得到的結(jié)果會(huì)是(先忽略 mapping):
我們不能只對(duì)主入口文件做這件事,得需要對(duì)所有在主入口這鏈上的文件做,上面 createAssert 針對(duì)一個(gè)文件做,我們基于這個(gè)函數(shù),建一個(gè)叫做 crateGraph 的函數(shù),里面進(jìn)行遞歸調(diào)用。
不妨先直接看結(jié)果,來(lái)了解這個(gè)函數(shù)是做什么的。
運(yùn)行這個(gè)函數(shù),得到的結(jié)果如下圖所示:
mapping 字段做了當(dāng)前項(xiàng) dependencies 里的文件和其他項(xiàng)的映射,這個(gè),我們?cè)诤竺鏁?huì)用到。
function createGraph(entry) { const modules = []; createGraphImpl( path.resolve(__dirname, entry), ); function createGraphImpl(absoluteFilePath) { const assert = createAssert(absoluteFilePath); modules.push(assert); assert.dependencies.forEach(relativePath => { const absolutePath = path.resolve( path.dirname(assert.filename), relativePath ); const id = createGraphImpl(absolutePath); assert.mapping[relativePath] = child.id; }); return assert.id } return modules; }
大家可以注意到,截圖中,數(shù)組中每一項(xiàng)的 code 就是我們的源代碼,但是這里面還留著 import 語(yǔ)句,我們先使用 babel 把它轉(zhuǎn)成 commonJS 。
做的也比較簡(jiǎn)單,就是用 babel 修改 createAssert 中返回值的 code:
const code = transformFromAst(ast, null, { presets: ['env'], }).code
截取其中一項(xiàng),結(jié)果變成了:
接下來(lái)要做的一步剛上來(lái)會(huì)比較難以理解,最關(guān)鍵的是我們會(huì)重寫 require 函數(shù),非常的巧妙,不妨先看:
我們新建一個(gè)函數(shù) bundle 來(lái)處理 createGraph 函數(shù)得到的結(jié)果。
function bundle(graph) { let moduleStr = ''; graph.forEach(module => { moduleStr += ` ${module.id}: [ // require,module,exports 作為參數(shù)傳進(jìn)來(lái) // 在下面我們自己定義了,這里記作【位置 1】 function(require, module, exports) { ${module.code} }, ${JSON.stringify(module.mapping)} ], ` }) const result = ` (function(modules){ function require(id) { const [fn, mapping] = modules[id]; // 這其實(shí)就是一個(gè)空對(duì)象, // 我們導(dǎo)出的那個(gè)東西會(huì)掛載到這個(gè)對(duì)象上 const module = { exports: {} } // fn 就是上面【位置 1】 那個(gè)函數(shù) fn(localRequire, module, module.exports) // 我們使用 require 是 require(文件名) // 所有這里要做一層映射,轉(zhuǎn)到 require(id) function localRequire(name) { return require(mapping[name]) } return module.exports; } require(0); })({${moduleStr}}) ` return result; }
最終的使用就是:
const graph = createGraph('./example/index.js'); const res = bundle(graph);
res 就是最終打包的結(jié)果,復(fù)制整段到控制臺(tái)運(yùn)行,可見成功輸出了 'hello world':
于是基本的功能就完成了,也就是 webpack 最基本的功能。
接下來(lái)解決包緩存的問(wèn)題,目前來(lái)說(shuō),import 過(guò)的文件,會(huì)被轉(zhuǎn)成 require 函數(shù)。每一次都會(huì)重新調(diào)用 require 函數(shù),現(xiàn)在先辦法把已經(jīng)調(diào)用過(guò)的緩存起來(lái):
function createGraph(entry) { const modules = []; const visitedAssert = {}; // 增加了這個(gè)對(duì)象 createGraphImpl( path.resolve(__dirname, entry), ); function createGraphImpl(absoluteFilePath) { // 如果已經(jīng)訪問(wèn)過(guò)了,那就直接返回 if (visitedAssert[absoluteFilePath]) { return visitedAssert[absoluteFilePath] } const assert = createAssert(absoluteFilePath); modules.push(assert); visitedAssert[absoluteFilePath] = assert.id; assert.dependencies.forEach(relativePath => { const absolutePath = path.resolve( path.dirname(assert.filename), relativePath ); // 優(yōu)化返回值,只返回 id 即可 const childId = createGraphImpl(absolutePath); assert.mapping[relativePath] = childId; }); return assert.id } return modules; } function bundle(graph) { let moduleStr = ''; graph.forEach(module => { moduleStr += ` ${module.id}: [ function(require, module, exports) { ${module.code} }, ${JSON.stringify(module.mapping)} ], ` }) const result = ` (function(modules){ // 增加對(duì)已訪問(wèn)模塊的緩存 let cache = {}; console.log(cache); function require(id) { if (cache[id]) { console.log('直接從緩存中取') return cache[id].exports; } const [fn, mapping] = modules[id]; const module = { exports: {} } fn(localRequire, module, module.exports) cache[id] = module; function localRequire(name) { return require(mapping[name]) } return module.exports; } require(0); })({${moduleStr}}) ` return result; }
解決依賴成環(huán)問(wèn)題
這個(gè)問(wèn)題比較經(jīng)典,如下所示,這個(gè)例子來(lái)自于 Node.js 官網(wǎng):
// filename: a.js console.log('a starting'); exports.done = false; const b = require('./b.js'); console.log('in a, b.done = %j', b.done); exports.done = true; console.log('a done');
// filename: b.js console.log('b starting'); exports.done = false; const a = require('./a.js'); console.log('in b, a.done = %j', a.done); exports.done = true; console.log('b done');
// filename: main.js console.log('main starting'); const a = require('./a.js'); const b = require('./b.js'); console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
目前我們只支持額外把 import 語(yǔ)句引用的文件加到依賴項(xiàng)里,還不夠,再支持一下 require。做的也很簡(jiǎn)單,就是 解析 AST 的時(shí)候再加入 require 語(yǔ)法的解析就好:
traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); }, CallExpression ({ node }) { if (node.callee.name === 'require') { dependencies.push(node.arguments[0].value) } } })
然后,如果這樣,我們直接運(yùn)行,按照現(xiàn)在的寫法處理不了這種情況,會(huì)報(bào)錯(cuò)棧溢出:
但是我們需要改的也特別少。先看官網(wǎng)對(duì)這種情況的解釋:
When main.js loads a.js, then a.js in turn loads b.js. At that point, b.js tries to load a.js. In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading, and its exports object is provided to the a.js module.
解決方法就是這句話:『an unfinished copy of the a.js exports object is returned to the b.js module』。也就是,提前返回一個(gè)未完成的結(jié)果出來(lái)。我們需要做到也很簡(jiǎn)單,只需要把緩存的結(jié)果提前就好了。
之前我們是這么寫的:
fn(localRequire, module, module.exports) cache[id] = module;
接著改為:
cache[id] = module; fn(localRequire, module, module.exports)
這樣就解決了這個(gè)問(wèn)題:
到現(xiàn)在我們就基本了解了它的實(shí)現(xiàn)原理,實(shí)現(xiàn)了一個(gè)初版的 webpack,撒花~
明白了它的實(shí)現(xiàn)原理,我才知道為什么網(wǎng)上說(shuō) webpack 慢是因?yàn)橐阉械囊蕾嚩枷仁占槐?,且看我們?createGraph 。它確實(shí)是做了這件事。
但是寫完發(fā)現(xiàn),這個(gè)題材不適合寫文章,比較適合視頻或者直接看代碼,你覺得呢??_?
所有的代碼在這個(gè)倉(cāng)庫(kù)
以上就是mini webpack打包基礎(chǔ)解決包緩存和環(huán)依賴的詳細(xì)內(nèi)容,更多關(guān)于mini webpack包緩存環(huán)依賴的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于JavaScript?中?if包含逗號(hào)表達(dá)式
這篇文章主要介紹了?關(guān)于JavaScript?中?if包含逗號(hào)表達(dá)式,有時(shí)會(huì)看到JavaScript中if判斷里包含英文逗號(hào)?“,”,這個(gè)是其實(shí)是逗號(hào)表達(dá)式。在if條件里,只有最后一個(gè)表達(dá)式起判斷作用。下面來(lái)看看文章的具體介紹吧2021-11-11Javascript實(shí)現(xiàn)的分頁(yè)函數(shù)
Javascript實(shí)現(xiàn)的分頁(yè)函數(shù)...2006-12-12JS創(chuàng)建對(duì)象常用設(shè)計(jì)模式工廠構(gòu)造函數(shù)及原型
本篇帶來(lái)你一定熟知的、用于創(chuàng)建對(duì)象的三種設(shè)計(jì)模式:工廠模式、構(gòu)造函數(shù)模式、原型模式,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-07-07Performance 內(nèi)存監(jiān)控使用技巧詳解
這篇文章主要為大家介紹了Performance 內(nèi)存監(jiān)控使用技巧詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10