webpack核心功能實(shí)現(xiàn)思路

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í)別requireexports

先觀察一下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ù)中,將自定義的requireexports作為實(shí)參傳遞給這個(gè)函數(shù)局服,從而達(dá)到替換原有requireexports的作用钓瞭。

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è)東西)中最核心的代碼就是modulesexec函數(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,再把modulesexec函數(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)思路,歡迎交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末涡拘,一起剝皮案震驚了整個(gè)濱河市玲躯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鳄乏,老刑警劉巖跷车,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異橱野,居然都是意外死亡朽缴,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)水援,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)密强,“玉大人,你說(shuō)我怎么就攤上這事蜗元∈某猓” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵许帐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我毕谴,道長(zhǎng)成畦,這世上最難降的妖魔是什么距芬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮循帐,結(jié)果婚禮上框仔,老公的妹妹穿的比我還像新娘。我一直安慰自己拄养,他們只是感情好离斩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瘪匿,像睡著了一般跛梗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上棋弥,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天核偿,我揣著相機(jī)與錄音,去河邊找鬼顽染。 笑死漾岳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的粉寞。 我是一名探鬼主播尼荆,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼唧垦!你這毒婦竟也來(lái)了捅儒?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤业崖,失蹤者是張志新(化名)和其女友劉穎野芒,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體双炕,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狞悲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了妇斤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摇锋。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖站超,靈堂內(nèi)的尸體忽然破棺而出荸恕,到底是詐尸還是另有隱情,我是刑警寧澤死相,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布融求,位于F島的核電站,受9級(jí)特大地震影響算撮,放射性物質(zhì)發(fā)生泄漏生宛。R本人自食惡果不足惜县昂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陷舅。 院中可真熱鬧倒彰,春花似錦、人聲如沸莱睁。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)仰剿。三九已至创淡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間酥馍,已是汗流浹背辩昆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旨袒,地道東北人汁针。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像砚尽,于是被迫代替她去往敵國(guó)和親施无。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容