前言
大家好,我是林三心,標題騰訊面試官:同學,你說你會Webpack,那說說他的原理?
,是本菜鳥在面試騰訊時,面試官說的問的原話
,一字不差,哈哈。本菜鳥當時肯定是回答不上來,最後也掛了。今天就簡單實現一下webpack的打包原理
,並分享給大家吧。由於webpack原理是非常複雜
的,所以今天我們們只是簡單實現
哦。
原理圖解
- 1、首先肯定是要先解析入口檔案
entry
,將其轉為AST(抽象語法樹)
,使用@babel/parser
- 2、然後使用
@babel/traverse
去找出入口檔案所有依賴模組
- 3、然後使用
@babel/core+@babel/preset-env
將入口檔案的AST轉為Code - 4、將
2
中找到的入口檔案的依賴模組
,進行遍歷遞迴
,重複執行1,2,3
- 5。重寫
require
函式,並與4
中生成的遞迴關係圖
一起,輸出到bundle
中
程式碼實現
webpack具體實現原理是很複雜的,這裡只是簡單實現
一下,讓大家粗略瞭解一下,webpack是怎麼運作的。在程式碼實現過程中,大家可以自己console.log
一下,看看ast,dependcies,code
這些具體長什麼樣,我這裡就不展示了,自己去看會比較有成就感
,嘿嘿!!
目錄
config.js
這個檔案中模擬webpack的配置
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}
入口檔案
src/index.js
是入口檔案
// src/index
import { age } from './aa.js'
import { name } from './hh.js'
console.log(`${name}今年${age}歲了`)
// src/aa.js
export const age = 18
// src/hh.js
console.log('我來了')
export const name = '林三心'
1. 定義Compiler類
// index.js
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {}
// 重寫 require函式,輸出bundle
generate() {}
}
2. 解析入口檔案,獲取 AST
我們這裡使用@babel/parser
,這是babel7
的工具,來幫助我們分析內部的語法,包括 es6,返回一個 AST 抽象語法樹
const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
const ast = Parser.getAst(this.entry)
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
3. 找出所有依賴模組
Babel
提供了@babel/traverse(遍歷)
方法維護這 AST 樹
的整體狀態,我們這裡使用它來幫我們找出依賴模組
。
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
4. AST 轉換為 code
將 AST 語法樹
轉換為瀏覽器可執行程式碼,我們這裡使用@babel/core 和 @babel/preset-env
。
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST轉換為code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
5. 遞迴解析所有依賴項,生成依賴關係圖
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST轉換為code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
// 解析入口檔案
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判斷有依賴物件,遞迴解析所有依賴項
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依賴關係圖
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用檔案路徑作為每個模組的唯一識別符號,儲存對應模組的依賴物件和檔案內容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 檔案路徑,可以作為每個模組的唯一識別符號
filename,
// 依賴物件,儲存著依賴模組路徑
dependecies,
// 檔案內容
code
}
}
// 重寫 require函式,輸出bundle
generate() {}
}
new Compiler(options).run()
6. 重寫 require 函式,輸出 bundle
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 讀取入口檔案
const content = fs.readFileSync(path, 'utf-8')
// 將檔案內容轉為AST抽象語法樹
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍歷所有的 import 模組,存入dependecies
traverse(ast, {
// 型別為 ImportDeclaration 的 AST 節點 (即為import 語句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 儲存依賴模組路徑,之後生成依賴關係圖需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST轉換為code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模組
this.modules = []
}
// 構建啟動
run() {
// 解析入口檔案
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判斷有依賴物件,遞迴解析所有依賴項
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依賴關係圖
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用檔案路徑作為每個模組的唯一識別符號,儲存對應模組的依賴物件和檔案內容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
this.generate(dependencyGraph)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 檔案路徑,可以作為每個模組的唯一識別符號
filename,
// 依賴物件,儲存著依賴模組路徑
dependecies,
// 檔案內容
code
}
}
// 重寫 require函式 (瀏覽器不能識別commonjs語法),輸出bundle
generate(code) {
// 輸出檔案路徑
const filePath = path.join(this.output.path, this.output.filename)
// 懵逼了嗎? 沒事,下一節我們捋一捋
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`
// 把檔案內容寫入到檔案系統
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}
new Compiler(options).run()
7. 看看main裡的程式碼
實現了上面的程式碼,也就實現了把打包後的程式碼寫到main.js檔案裡,我們們來看看那main.js檔案裡的程式碼吧:
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./aa.js": "./src\\aa.js",
"./hh.js": "./src\\hh.js"
},
"code": "\"use strict\";\n\nvar _aa = require(\"./aa.js\");\n\nvar _hh = require(\"./hh.js\");\n\nconsole.log(\"\".concat(_hh.name, \"\\u4ECA\\u5E74\").concat(_aa.age, \"\\u5C81\\u4E86\"));"
},
"./src\\aa.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.age = void 0;\nvar age = 18;\nexports.age = age;"
},
"./src\\hh.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nconsole.log('我來了');\nvar name = '林三心';\nexports.name = name;"
}
})
大家可以執行一下main.js的程式碼,輸出結果是:
我來了
林三心今年18歲了
結語
我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】