webpack是一個(gè)功能豐富且復(fù)雜的打包工具杂靶,使用時(shí)需要掌握Loader传货、Plugin等等概念晃跺,不過(guò)其核心功能就是將瀏覽器看不懂的代碼翻譯成可執(zhí)行代碼,為了快速掌握webpack的實(shí)現(xiàn)思路也糊,讓我們拋開(kāi)那些繁瑣的概念炼蹦,看看打包工具是如何翻譯模塊化代碼的。
webpack核心功能切入點(diǎn)
現(xiàn)有的commonJS
規(guī)范和es6
模塊化方案等瀏覽器并不支持狸剃,也就是說(shuō)我們?cè)?code>node環(huán)境下執(zhí)行的好好的require掐隐、exports
瀏覽器無(wú)法識(shí)別。現(xiàn)在以commonJS
規(guī)范為例钞馁,如果我們使用commonJS
進(jìn)行模塊化虑省,首先要解決的問(wèn)題是如何讓瀏覽器識(shí)別require
和exports
。
先觀察一下require
這個(gè)關(guān)鍵字指攒,我們會(huì)發(fā)現(xiàn)它實(shí)際上就是一個(gè)函數(shù)慷妙,接收的參數(shù)是一個(gè)路徑,只不過(guò)在node
環(huán)境下天然存在這樣一個(gè)函數(shù)供你使用允悦。瀏覽器不認(rèn)識(shí)require
是因?yàn)闉g覽器并沒(méi)有幫你去聲明require
。所以打包工具要做的就是要實(shí)現(xiàn)一個(gè)require
取代代碼中的require
虑啤。那么問(wèn)題又來(lái)了隙弛,怎么才能在不改動(dòng)源碼的情況下代替原有require
呢?其實(shí)答案很明顯了狞山,把我們實(shí)現(xiàn)的require
當(dāng)做參數(shù)傳遞給這個(gè)模塊就好了全闷。
實(shí)際上現(xiàn)在我們的模塊化、以單文件形式進(jìn)行作用域隔離等萍启,在之前都是使用立即執(zhí)行函數(shù)去做的总珠,我們可以借鑒前輩們的方法屏鳍,將模塊中的內(nèi)容放入一個(gè)函數(shù)中,將自定義的require
和exports
作為實(shí)參傳遞給這個(gè)函數(shù)局服,從而達(dá)到替換原有require
和exports
的作用钓瞭。
let what = require('./eat')
let where = require('./run')
exports.name = `mayun eat ${whatObj.what} run to ${whereObj.where}`
//轉(zhuǎn)換為下面的形式
function(require,exports){
let what = require('./eat')
let where = require('./run')
exports.name = `mayun eat ${whatObj.what} run to ${whereObj.where}`
}
modules結(jié)構(gòu)雛形
現(xiàn)在我們需要將這些松散的模塊組織在一起,將他們放入對(duì)象中是一種不錯(cuò)的形式淫奔,我們給這個(gè)對(duì)象起個(gè)名字modules
山涡。同時(shí),每個(gè)模塊都需要一個(gè)名字方便我們找到它唆迁,所以我們給每個(gè)模塊一個(gè)不重復(fù)的id
:
let modules = {
0: function (require, exports) {
let whatObj = require('./eat')
let whereObj = require('./run')
exports.action = `mayun eat ${whatObj.what} run to ${whereObj.where}`
},
1: function (require, exports) {
exports.what = '火鍋'
},
2: function (require, exports) {
exports.where = '北京'
},
}
模塊代碼執(zhí)行函數(shù)exec
接下來(lái)就需要去聲明一個(gè)require
方法和一個(gè)函數(shù)鸭丛,讓這個(gè)函數(shù)去執(zhí)行modules
中的函數(shù),我們給它起名叫exec
唐责。大概像這樣:
function exec(id) {
let fn = modules[id]
let exports = {}
// 模擬 require 語(yǔ)句
function require(path) {
}
// 執(zhí)行存放所有模塊數(shù)組中的第0個(gè)模塊
fn(require,exports)
}
exec(0)
模塊依賴(lài)映射mapping
bundle.js
(對(duì)鳞溉,就是webpack
打包后生成的那個(gè)東西)中最核心的代碼就是modules
和exec
函數(shù),實(shí)際上現(xiàn)在我們已經(jīng)得到了bundle.js
的雛形鼠哥。require
函數(shù)的實(shí)現(xiàn)我們先放在一邊熟菲,現(xiàn)在再來(lái)思考一個(gè)問(wèn)題,打包工具需要通過(guò)原require
中的路徑找到對(duì)應(yīng)的模塊肴盏,但是modules
對(duì)象被整合出來(lái)后科盛,各個(gè)模塊代碼脫離了之前的位置,所以我們很難再通過(guò)這個(gè)相對(duì)路徑去尋找對(duì)應(yīng)的模塊文件了菜皂。既然我們已經(jīng)抽離出需要的模塊代碼贞绵,我們是不是可以直接做一個(gè)映射,將相對(duì)路徑和被抽離出來(lái)的模塊對(duì)應(yīng)起來(lái)呢恍飘?為了讓每個(gè)模塊都可以通過(guò)這個(gè)映射找到依賴(lài)模塊榨崩,我們就給這個(gè)模塊加一個(gè)mapping
,正好現(xiàn)在模塊id和代碼已經(jīng)一一對(duì)應(yīng)了章母,修改一下modules
的結(jié)構(gòu)即可母蛛。我們讓模塊id對(duì)應(yīng)一個(gè)數(shù)組,之前的模塊代碼現(xiàn)在放在數(shù)組第0個(gè)位置乳怎,它的mapping
放在數(shù)組的第1個(gè)位置:
let modules = {
0: [function (require, exports) {
let whatObj = require('./eat')
let whereObj = require('./run')
exports.action = `mayun eat ${whatObj.what} run to ${whereObj.where}`
}, {
'./eat': 1,
'./run': 2,
}
],
1: [function (require, exports) {
exports.what = '火鍋'
}, {}
],
2: [function (require, exports) {
exports.where = '北京'
}, {}
],
}
按照新的數(shù)據(jù)結(jié)構(gòu)彩郊,我們調(diào)整一下exec
函數(shù)的實(shí)現(xiàn):
function exec(id) {
let [fn,mapping] = modules[id]
let exports = {}
fn(require, exports)
function require(path) {
return exec(mapping[path])
}
return exports
}
exec(0)
當(dāng)目前為止我們已經(jīng)做到使用exec
函數(shù)可以順利執(zhí)行轉(zhuǎn)換后的modules
了,所以接下來(lái)的重點(diǎn)就是如何將模塊文件讀取出來(lái)生成modules
蚪缀。先捋清思路秫逝,首先我們需要讀取入口文件,拿到入口文件的依賴(lài)询枚,同時(shí)將入口文件代碼和依賴(lài)組成數(shù)組追加到modules
中违帆;拿到依賴(lài)后,讀取依賴(lài)文件金蜀,重復(fù)上一步操作刷后。很顯然這時(shí)候我們需要用到nodejs
的畴。不管怎么說(shuō),我們先實(shí)現(xiàn)一個(gè)拿到模塊代碼中依賴(lài)項(xiàng)的方法尝胆,可以使用正則去匹配require
中的路徑:
// 獲取模塊依賴(lài)數(shù)組
function getDependencies(str){
let reg = /require\(['"](.+?)['"]\)/g
let result = null
let dependencies = []
// 通過(guò)正則匹配到require括號(hào)中的相對(duì)路徑丧裁,存放在數(shù)組中
while(result = reg.exec(str)){
dependencies.push(result[1])
}
return dependencies
}
此時(shí)將讀取的文件內(nèi)容作為參數(shù)傳遞給getDependencies
即可:
// 獲取入口文件內(nèi)容
let fileContent = fs.readFileSync('./index.js','utf-8')
console.log(getDependencies(fileContent))
[ './people.js' ]
讀取modules中的模塊項(xiàng)
這時(shí)候我們回過(guò)頭看一下modules
的結(jié)構(gòu),既然需要得到關(guān)于這個(gè)模塊的多種信息班巩,我們最好是封裝一個(gè)函數(shù)返回這個(gè)模塊的信息:
// 全局變量 作為模塊的id
let id = 0
// 根據(jù)文件路徑獲取文件信息并生成一個(gè)對(duì)象
function getModule(filename){
let fileContent = fs.readFileSync(filename,'utf-8')
return {
id:id++,
filename:filename,
dependencies:getDependencies(fileContent),
code:`function(require,exports){
${fileContent}
}`,
}
}
生成資源列表Graph
現(xiàn)在我們有了入口文件對(duì)象的信息渣慕,可以將它放在一個(gè)數(shù)組里,接下來(lái)就是根據(jù)這個(gè)入口對(duì)象的依賴(lài)獲取到依賴(lài)模塊對(duì)象信息抱慌,并且push
到對(duì)象數(shù)組中逊桦,生成一個(gè)資源列表。現(xiàn)在我們來(lái)實(shí)現(xiàn)這個(gè)函數(shù):
// 傳入入口文件路徑抑进,生成模塊數(shù)組(資源列表)
function getGraph(filename){
let indexModule = getModule(filename)
let graph = [indexModule]
// tips:這里使用for of非常便利强经,因?yàn)檠h(huán)后數(shù)組項(xiàng)會(huì)動(dòng)態(tài)增加,
// for of語(yǔ)句會(huì)在已經(jīng)循環(huán)過(guò)的基礎(chǔ)上繼續(xù)循環(huán)寺渗,而不會(huì)從頭再循環(huán)一次
for(let value of graph){
value.mapping = {}
value.dependencies.forEach((relativePath)=>{
const absolutePath = path.join(__dirname,relativePath)
let module = getModule(absolutePath)
value.mapping[relativePath] = module.id
graph.push(module)
})
}
return graph
}
// graph
[ { id: 0,
filename: './index.js',
dependencies: [ './people.js' ],
code:
'function(require,exports){\n let todo = require(\'./people.js\')\nconsole.log(todo)\n \n }',
mapping: { './people.js': 1 } },
{ id: 1,
filename: '/Users/fengjixuan/Downloads/webpack-simple/people.js',
dependencies: [ './eat.js', './run.js' ],
code:
'function(require,exports){\n let whatObj = require(\'./eat.js\')\nlet whereObj = require(\'./run.js\')\nexports.action = `mayun eat ${whatObj.what} run to ${whereObj.where}`\n\n\n \n }',
mapping: { './eat.js': 2, './run.js': 3 } },
{ id: 2,
filename: '/Users/fengjixuan/Downloads/webpack-simple/eat.js',
dependencies: [],
code:
'function(require,exports){\n exports.what = \'火鍋\' \n }',
mapping: {} },
{ id: 3,
filename: '/Users/fengjixuan/Downloads/webpack-simple/run.js',
dependencies: [],
code:
'function(require,exports){\n exports.where = \'北京\' \n }',
mapping: {} } ]
生成bundle.js
準(zhǔn)備工作都已經(jīng)做好匿情,現(xiàn)在只需要把上面獲取到的數(shù)據(jù)轉(zhuǎn)換成modules
,再把modules
和exec
函數(shù)拼接成字符串寫(xiě)入到一個(gè)名為bundle.js
的文件中信殊,這個(gè)js文件就可以無(wú)障礙的在瀏覽器中執(zhí)行了:
// 生成瀏覽器可執(zhí)行的代碼并寫(xiě)入bundle.js中
function createBundle(graph) {
let modules = ''
// 生成modules字符串
graph.forEach((module) => {
modules += `${module.id}:[
${module.code},
${JSON.stringify(module.mapping)}
],`
})
// 生成立即執(zhí)行函數(shù)炬称,并且將moudules作為參數(shù)傳遞進(jìn)去
let result = `(function f(modules) {
function exec(id) {
let [fn, mapping] = modules[id]
let exports = {}
fn && fn(require, exports)
function require(path) {
return exec(mapping[path])
}
return exports
}
exec(0)
})({${modules}})`
// 寫(xiě)入到bundle.js中
fs.writeFileSync('./dist/bundle.js',result)
}
以上就是webpack核心功能實(shí)現(xiàn)思路,歡迎交流