mini?webpack打包基礎解決包緩存和環(huán)依賴
正文
本文帶你實現(xiàn) webpack 最基礎的打包功能,同時解決包緩存和環(huán)依賴的問題 ~
發(fā)車,先來看示例代碼。
index.js 主入口文件
我們這里三個文件,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;
接下來,我們會創(chuàng)建一個 bundle.js 打包這三個文件,打包得到的結果是一個 JS 文件,運行這個 JS 文件輸出的結果會是 'hello world'。
bundle.js 就是 webpack 做的事情,我們示例中的 index.js 相當于 webpack 的入口文件,會在 webpack.config.js 的 entry 里面配置。
讓我們來實現(xiàn) bundle.js 的功能。
讀主入口文件
最開始的,當然是讀主入口文件了:
function createAssert(filename) { const content = fs.readFileSync(filename, { encoding: 'utf-8' }); return content; } const content = createAssert('./example/index.js');
接下來,需要做的事情就是把 import 語法引入的這個文件也找過來,在上圖中,就是 foo.js,同時還得把 foo.js 依賴的也找過來,依次遞推。
現(xiàn)在得把 foo.js 取出來,怎么解析 import foo from './foo.js' 這句,把值取出來呢?
把這行代碼解析成 ast 會變成:
接下來的思路就是把上面的代碼轉化成 ast,接著去取上圖框框里那個字段。
對依賴文件進行讀取操作
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; }
上面我們做的事情就是把當前的文件讀到,然后再把當前文件的依賴加到一個叫做 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: {}, }; }
假如對主入口文件 index.js 調用,得到的結果會是(先忽略 mapping):
我們不能只對主入口文件做這件事,得需要對所有在主入口這鏈上的文件做,上面 createAssert 針對一個文件做,我們基于這個函數(shù),建一個叫做 crateGraph 的函數(shù),里面進行遞歸調用。
不妨先直接看結果,來了解這個函數(shù)是做什么的。
運行這個函數(shù),得到的結果如下圖所示:
mapping 字段做了當前項 dependencies 里的文件和其他項的映射,這個,我們在后面會用到。
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ù)組中每一項的 code 就是我們的源代碼,但是這里面還留著 import 語句,我們先使用 babel 把它轉成 commonJS 。
做的也比較簡單,就是用 babel 修改 createAssert 中返回值的 code:
const code = transformFromAst(ast, null, { presets: ['env'], }).code
截取其中一項,結果變成了:
接下來要做的一步剛上來會比較難以理解,最關鍵的是我們會重寫 require 函數(shù),非常的巧妙,不妨先看:
我們新建一個函數(shù) bundle 來處理 createGraph 函數(shù)得到的結果。
function bundle(graph) { let moduleStr = ''; graph.forEach(module => { moduleStr += ` ${module.id}: [ // require,module,exports 作為參數(shù)傳進來 // 在下面我們自己定義了,這里記作【位置 1】 function(require, module, exports) { ${module.code} }, ${JSON.stringify(module.mapping)} ], ` }) const result = ` (function(modules){ function require(id) { const [fn, mapping] = modules[id]; // 這其實就是一個空對象, // 我們導出的那個東西會掛載到這個對象上 const module = { exports: {} } // fn 就是上面【位置 1】 那個函數(shù) fn(localRequire, module, module.exports) // 我們使用 require 是 require(文件名) // 所有這里要做一層映射,轉到 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 就是最終打包的結果,復制整段到控制臺運行,可見成功輸出了 'hello world':
于是基本的功能就完成了,也就是 webpack 最基本的功能。
接下來解決包緩存的問題,目前來說,import 過的文件,會被轉成 require 函數(shù)。每一次都會重新調用 require 函數(shù),現(xiàn)在先辦法把已經(jīng)調用過的緩存起來:
function createGraph(entry) { const modules = []; const visitedAssert = {}; // 增加了這個對象 createGraphImpl( path.resolve(__dirname, entry), ); function createGraphImpl(absoluteFilePath) { // 如果已經(jīng)訪問過了,那就直接返回 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){ // 增加對已訪問模塊的緩存 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)問題
這個問題比較經(jīng)典,如下所示,這個例子來自于 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 語句引用的文件加到依賴項里,還不夠,再支持一下 require。做的也很簡單,就是 解析 AST 的時候再加入 require 語法的解析就好:
traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); }, CallExpression ({ node }) { if (node.callee.name === 'require') { dependencies.push(node.arguments[0].value) } } })
然后,如果這樣,我們直接運行,按照現(xiàn)在的寫法處理不了這種情況,會報錯棧溢出:
但是我們需要改的也特別少。先看官網(wǎng)對這種情況的解釋:
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』。也就是,提前返回一個未完成的結果出來。我們需要做到也很簡單,只需要把緩存的結果提前就好了。
之前我們是這么寫的:
fn(localRequire, module, module.exports) cache[id] = module;
接著改為:
cache[id] = module; fn(localRequire, module, module.exports)
這樣就解決了這個問題:
到現(xiàn)在我們就基本了解了它的實現(xiàn)原理,實現(xiàn)了一個初版的 webpack,撒花~
明白了它的實現(xiàn)原理,我才知道為什么網(wǎng)上說 webpack 慢是因為要把所有的依賴都先收集一遍,且看我們的 createGraph 。它確實是做了這件事。
但是寫完發(fā)現(xiàn),這個題材不適合寫文章,比較適合視頻或者直接看代碼,你覺得呢??_?
所有的代碼在這個倉庫
以上就是mini webpack打包基礎解決包緩存和環(huán)依賴的詳細內容,更多關于mini webpack包緩存環(huán)依賴的資料請關注腳本之家其它相關文章!
相關文章
JS創(chuàng)建對象常用設計模式工廠構造函數(shù)及原型
本篇帶來你一定熟知的、用于創(chuàng)建對象的三種設計模式:工廠模式、構造函數(shù)模式、原型模式,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-07-07