webpack是近幾年前端比較流行的打包工具,基本上是目前所有前端都必須要掌握的開發利器。不過,光停留在使用工具的階段上,是難以得到成長的。因此,這篇文章將帶大家手把手自己實現webpack的核心功能。
其實,webpack的核心功能是分爲如下幾個步驟:
- 解析文件,並提取出各自的模塊依賴
- 根據各個模塊之間的依賴關係,遞歸生成依賴圖
- 最後將所有的依賴文件打包到一個單一的文件中
接下來,我們以一個簡單的例子作爲開始,作爲測試用例。
先建立一個入口main.js
文件:
import moduleB from "./moduleB";
console.log(moduleB());
接着,建立moduleA和moduleB兩個模塊:
export default {
getName: () => {
return "scq000";
}
}
模塊B代碼如下:
import moduleA from "./moduleA";
console.log(moduleA());
其中模塊B引用模塊A,這樣我們就實現了一個簡單的demo例子。
有了這個測試例子,接着我們就可以來實現我們自定義的webpack功能。
先創建一個myWebpack.js
的文件,我們可以通過一個createAsset
函數讀取main.js
入口文件,並生成一個最終可以直接在瀏覽器上運行的代碼。
讀取入口文件
第一步,是要先讀取文件內容,這個實現起來比較簡單:
const fs = require('fs');
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf8');
console.log(content);
}
createAsset('./example/main.js');
可以在命令行中輸入如下命令進行查看:
node myWebpack.js | js-beautify | highlight
解析依賴
接着,我們就需要根據源碼文件,進行解析。這裏,我們可以藉助AST Explorer來生成代碼的AST語法樹,然後找ImportDeclaration
語句,先打印出來看看效果:
const fs = require('fs');
const babylon = require('baylon');
const traverse = require('babel-traverse').default;
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf8');
// 根據源碼內容生成語法書
const ast = babylon.parse(content, {
sourceType: 'module'
});
// traverse方法,用來操作語法樹
traverse(ast, {
ImportDeclaration: ({node}) => {
console.log(node);
}
});
}
接着,我們就要根據找到的import語句,將對應的依賴模塊放入到dependencies
數組中去,並給它賦予id。修改代碼如下:
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
let ID = 0;
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf8');
const ast = babylon.parse(content, {
sourceType: 'module'
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
}
});
const id = ID++;
return {
id,
filename,
dependencies
}
};
const result = createAsset('./main.js');
console.log(result);
打印出來,可以看到如下結果:
遞歸生成依賴圖
有了上面的基礎,我們就能根據入口文件遞歸生成依賴圖了:
function createGraph(entry) {
const mainAsset = createAsset(entry);
const queue = [mainAsset];
for(const asset of queue) {
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath);
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
queue.push(child);
});
}
console.log(queue);
}
createGraph('./main.js');
根據依賴圖,打包生成文件
最後一步,就是需要根據獲得的模塊依賴信息,合併模塊並生成可執行的文件。
爲了保證代碼可在瀏覽器上成功運行,這裏,我們需要藉助babel
工具進行代碼的轉換。
先安裝依賴:
npm install babel-core babel-preset-env
接着,在createAsset
方法中實現如下代碼:
const {code} = babel.transformFromAst(ast, null, {
presets: ['env']
});
然後再將轉換後的代碼放入依賴圖中,便於後續拼接。
最終依賴圖結果如下:
所有準備工作完成後,開始實現bundle方法。在bundle方法裏,主要是根據依賴圖信息,將所有的模塊組裝到一個字符串中去,最後再輸出成文件就可以了。
function bundle(graph) {
let modules = '';
// 遍歷graph, 生成代碼
graph.forEach(mod => {
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
]`
})
//組裝模塊
const result = `(function() {
})({${modules}})`;
return result;
}
接着我們需要自己去實現require語句:
const modMap = JSON.stringify(mod.mapping);
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${modMap}
]`;
將modules作爲參數傳給該立即執行函數,然後直接執行第一個模塊就可以了:
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
const module = { exports: {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
最終,完整代碼如下:
function bundle(graph) {
let modules = '';
// 遍歷graph, 生成代碼
graph.forEach(mod => {
const modMap = JSON.stringify(mod.mapping);
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${modMap}
],`;
});
//組裝模塊
const result = `(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
const module = { exports: {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})`;
return result;
}
至此,我們完成了最基礎的打包工作,可以在命令行中執行一下試試效果:
node myWebpack.js > build.js && node build.js
參考資料
https://www.youtube.com/watch?v=Gc9-7PBqOC8
——--轉載請註明出處--———
最後,歡迎大家關注我的公衆號,一起學習交流。