webpack的定位是一個bundler,最基本的要解決的是將多個JS模塊打包成可以在瀏覽器上運行的代碼。接下來我們將實現一個簡易的miniWebpack也就是一個bundler:由入口文件對代碼進行打包,打包成可以在瀏覽器運行的代碼。
被打包項目介紹
整個演示項目的目錄結構如下所示,其中src下的文件是bundler.js需要打包的代碼。
├── bundler.js
├── package-lock.json
└── src
├── index.js
├── msg.js
└── word.js
src下各文件內容如下:
word.js
const word = 'miniWebpack';
export default word;
msg.js
import word from './word.js'
const msg = `hello ${word}`;
export default msg;
index.js
import msg from './msg.js'
console.log(msg)
export default index;
實現bundler.js
要實現bundler,我們需要實現3部分功能:
moduleAnalyser:模塊分析。分析模塊,得到模塊的依賴、代碼等信息。
makeDependenciesGraph:生成依賴圖譜。遍歷打包項目,得到所有需要的模塊的分析結果 。
generateCode:生成可執行代碼。提供require()函數和exports對象,生成可以在瀏覽器執行的代碼。
模塊分析
使用fs模塊讀取module的內容;使用@babel/parser將文件內容轉換成抽象語法樹AST;使用@babel/traverse遍歷了AST ,對每個ImportDeclaration節點(保存的相對於module的路徑信息)做映射,把依賴關系拼裝在 dependencies對象里;使用@babel/core結合@babel/preset-env預設,將AST轉換成了瀏覽器可以執行的代碼。
const moduleAnalyser = (fileName) => {
// 1.fs模塊根據路徑讀取到了module的內容
const content = fs.readFileSync(fileName, 'utf-8');
// 2.使用@babel/parser將文件內容轉換成抽象語法樹AST
const ast = parser.parse(content, {
sourceType: 'module'
})
// 3.使用@babel/traverse遍歷了AST ,對每個ImportDeclaration節點做映射,把依賴關系拼裝在 dependencies對象里
let dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirName = path.dirname(fileName);
const newFile = './' + path.join(dirName, node.source.value);
// key是相對於當前模塊的路徑,value為相對於bundler.js的路徑。
dependencies[node.source.value] = newFile;
}
})
// 4.使用@babel/core結合@babel/preset-env預設,將AST轉換成了瀏覽器可以執行的代碼
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return {
fileName,
dependencies,
code
}
}
模塊分析流程圖如下:
調用console.log(moduleAnalyser('./src/index.js')),可以在控制台打印出以下內容:
{ fileName: './src/index.js',
dependencies: { './msg.js': './src/msg.js' },
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _msg = _interopRequireDefault(require("./msg.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]);\nvar _default = index;\nexports["default"] = _default;' }
執行moduleAnalyser(module),可以返回module的fileName、dependencies、code信息。注意在code里,import語法已經變成一個require函數了,export語法,也變成了在給一個exports變量賦值。
依賴圖譜生成
調用moduleAnalyser('./src/index.js')拿到入口文件的dependencies映射,接下來再把入口文件的依賴路徑再一次做模塊分析,再把依賴模板的依賴路徑再一次做模塊分析...... 其實就是廣度優先遍歷,可以很輕松得到這次打包所有需要的模塊的分析結果。
//生成依賴圖譜
const makeDependenciesGraph = (entry) => {
//entryModule:入口文件的dependencies映射
const entryModule = moduleAnalyser(entry);
//graphArray:圖譜動態數組,初始只有一個元素entryModule
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
//dependencies:當前模塊的dependencies映射
const { dependencies } = item;
//如果當前模塊有依賴文件,則遍歷dependencies,調用moduleAnalyser,對依賴文件進行模板分析
if (dependencies) {
for (let j in dependencies) {
graphArray.push(moduleAnalyser(dependencies[j]))
}
}
}
//graph:遍歷graphArray生成更利於打包使用的graph。其中key為fileName,value為dependencies和code
const graph = {};
graphArray.forEach(item => {
graph[item.fileName] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}
生成依賴圖譜流程圖如下:
調用console.log(makeDependenciesGraph('./src/index.js')),可以在控制台打印出以下內容:
{ './src/index.js':
{ dependencies: { './msg.js': './src/msg.js' },
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _msg = _interopRequireDefault(require("./msg.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]);\nvar _default = index;\nexports["default"] = _default;' },
'./src/msg.js':
{ dependencies: { './word.js': './src/word.js' },
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _word = _interopRequireDefault(require("./word.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar msg = "hello ".concat(_word["default"]);\nvar _default = msg;\nexports["default"] = _default;' },
'./src/word.js':
{ dependencies: {},
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\nvar word = \'miniWebpack\';\nvar _default = word;\nexports["default"] = _default;' } }
生成代碼
我們需要開始生成最終可運行的代碼了。在上文”模塊分析“部分,我們知道利用@babel/core結合@babel/preset-env生成的瀏覽器可執行代碼里,import語法已經變成一個require函數了,export語法,也變成了在給一個exports變量賦值。所以我們的”生成代碼“部分,需要提供一個require函數了和exports對象。
//generateCode 根據依賴圖譜生成瀏覽器可執行代碼
const generateCode = (entry) => {
//根據entry,調用makeDependenciesGraph生成依賴圖譜graph
const graph = JSON.stringify(makeDependenciesGraph(entry))
//根據依賴圖譜生成瀏覽器可執行代碼
return `
可運行代碼...
`
}
為了不污染全局作用域,我們使用立即執行函數來包裝我們的代碼,將依賴圖譜graph作為參數傳入:
(function(graph){
// todo
})(${graph})
在graph中找到入口文件的code,並運行它:
return `
(function(graph){
function require(module){
eval(graph[module].code)
}
require('${entry}')
})(${graph})
在入口文件的code,我們同樣需要調用require去獲取依賴模塊模塊導出的對象exports,所以require函數必須有導出對象,還要支持內部調用require函數。但是注意!!此require並非現在聲明的require函數,定義code內部使用的require函數 -> localRequire。因為我們觀察之前編譯出的代碼,可以知道在code中,require函數傳的參數是相對於當前module的相對路徑,但是我們打包生成可運行代碼時,需要的是相對於bundler.js的相對路徑。這時候,我們之前給每個module存的dependencies映射再次派上了用場,localRequire() 傳入依賴相對於module的相對路徑,根據graph對象,返回依賴相對於bundler.js的相對路徑。
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports={};
eval(graph[module].code)
return exports;
}
require('${entry}')
})(${graph})
為了防止模塊內部變量污染其它模塊,我們在eval外面包一層立即執行函數,將localRequire、exports和code作為參數傳入。
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('${entry}')
})(${graph})
由此一個bundler就寫完了,最終生成的代碼,也是可以直接在瀏覽器中運行的。
調用console.log(generateCode('./src/index.js')),可以在控制台打印出以下內容:
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./msg.js":"./src/msg.js"},"code":"\"use strict\";\n\nvar _msg = _interopRequireDefault(require(\"./msg.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_msg[\"default\"]); // export default index;"},"./src/msg.js":{"dependencies":{"./word.js":"./src/word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = _interopRequireDefault(require(\"./word.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar msg = \"hello \".concat(_word[\"default\"]);\nvar _default = msg;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\nvar word = 'miniWebpack';\nvar _default = word;\nexports[\"default\"] = _default;"}})
將這段代碼賦值到瀏覽器控制台,可以看到代碼執行情況:
完整的bundle.js代碼如下:
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
//moduleAnalyser:分析一個模塊的文件依賴
const moduleAnalyser = (fileName) => {
// 1.fs模塊根據路徑讀取到了入口文件的內容
const content = fs.readFileSync(fileName, 'utf-8');
// 2.使用@babel/parser將文件內容轉換成抽象語法樹AST
const ast = parser.parse(content, {
sourceType: 'module'
})
// 3.使用@babel/traverse遍歷了AST ,對每個ImportDeclaration節點(保存的相對於入口文件的路徑)做映射,把依賴關系拼裝在 dependencies對象里
let dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirName = path.dirname(fileName);
//newFile 相對於bundler.js的相對路徑,打包的時候用這個。
const newFile = './' + path.join(dirName, node.source.value);
// key是相對於當前模塊的路徑,value為相對於bundler.js的路徑。
dependencies[node.source.value] = newFile;
}
})
// 4.使用@babel/core結合@babel/preset-env預設,將AST轉換成了瀏覽器可以執行的代碼
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return {
fileName,
dependencies,
code
}
}
//生成依賴圖譜
const makeDependenciesGraph = (entry) => {
//entryModule:入口文件的dependencies映射
const entryModule = moduleAnalyser(entry);
//graphArray:圖譜動態數組,初始只有一個元素entryModule
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
//dependencies:當前模塊的dependencies映射
const { dependencies } = item;
//如果當前模塊有依賴文件,則遍歷dependencies,調用moduleAnalyser,對依賴文件進行模板分析
if (dependencies) {
for (let j in dependencies) {
graphArray.push(moduleAnalyser(dependencies[j]))
}
}
}
//graph:遍歷graphArray生成更利於打包使用的graph。其中key為fileName,value為dependencies和code
const graph = {};
graphArray.forEach(item => {
graph[item.fileName] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}
//generateCode 根據依賴圖譜生成瀏覽器可執行代碼
const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry))
//大的閉包,防止打包生成的代碼污染全局環境
//瀏覽器可執行的代碼里有require方法,有exports對象,bundler.js打包后的代碼需要提供一個require方法和exports對象。
//小的閉包,防止模塊內部變量污染其它模塊
//localRequire 傳入依賴相對於module的相對路徑,根據graph對象,返回依賴相對於bundler.js的相對路徑
return `
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports={};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('${entry}')
})(${graph})
`
}
const code = generateCode('./src/index.js');
console.log(code)