只為你能懂webpack

hello 大家好移必,????♀?????♀?????♀?

我是一個(gè)熱愛(ài)知識(shí)傳遞,正在學(xué)習(xí)寫(xiě)作的作者,ClyingDeng 凳凳逸尖!

好久不見(jiàn)哈!

今天我要給大家?guī)?lái)一個(gè)手寫(xiě)webpack的打包功能!

????前方硬核娇跟,請(qǐng)注意岩齿!

簡(jiǎn)易webpack使用

webpack想必大家都挺熟悉的吧,我們先來(lái)完成webpack打包的一個(gè)簡(jiǎn)單例子苞俘。

初始化一個(gè)空項(xiàng)目npm init -y盹沈,安裝依賴webpackwebpack-cli吃谣。

創(chuàng)建一個(gè)src目錄乞封,存放自己代碼的文件夾。內(nèi)部新建一個(gè)index.js文件作為入口文件岗憋。在src目錄下肃晚,index.js引入了add(兩數(shù)相加)和minus(兩數(shù)相減)兩個(gè)方法。入口文件內(nèi)容:

import add from './add.js'
import minus from './minus.js'

console.log(add(1, 2))
console.log(minus(1, 2))

創(chuàng)建一個(gè)webpack.config.js文件仔戈,作為webpack配置关串。目前webpack 5可以只需要指定它的環(huán)境模式就可以進(jìn)行打包。默認(rèn)入口就是src下的index.js文件监徘,出口就是dist目錄下的main.js文件晋修。配置如下:

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, './dist'),
  },
  mode: 'production',
}

具體目錄結(jié)構(gòu)如下:


52fe51d94e9241d085f8031a2e57f290_tplv-k3u1fbpfcp-watermark.png

最后執(zhí)行webopack命令,進(jìn)行webpack打包凰盔。

index.html文件中引入打包后的文件飞蚓,在瀏覽器運(yùn)行,控制臺(tái)如果可以正常輸出打包后的結(jié)果(1和2加減的結(jié)果)廊蜒,那么這就證明使用webpack打包成功趴拧。

6ab4275e384744a2b418041272029d9d_tplv-k3u1fbpfcp-watermark.png

webpack工作流程

  • 初始化Compiler:new Compiler(config) 得到Compiler對(duì)象
  • 開(kāi)始編譯:調(diào)用Compiler 對(duì)象 run 方法開(kāi)始執(zhí)行編譯
  • 確定入口: 根據(jù)配置中的 entry 找出所有的入口文件
  • 編譯模塊:從入口文件出發(fā),調(diào)用所有配置的Loader 對(duì)模塊進(jìn)行編譯山叮,再找出該模塊依賴的模塊著榴,遞歸知道所有模塊被加載進(jìn)來(lái)
  • 完成模塊編譯:在經(jīng)過(guò)第4步使用Loader 編譯完成所有模塊后,得到了每個(gè)模塊被編譯后的最終內(nèi)容以及它們之間的依賴關(guān)系
  • 輸出資源:根據(jù)入口模塊之間的依賴關(guān)系屁倔,組裝成一個(gè)個(gè)包含多個(gè)模塊的Chunk脑又。再把每個(gè)Chunk 轉(zhuǎn)換成一個(gè)單獨(dú)的文件加入到輸出列表。(注意:這步是可以修改輸出內(nèi)容的最后機(jī)會(huì))
  • 輸出完成:在確定好的輸出內(nèi)容后锐借,根據(jù)配置確定的路徑和文件名问麸,把文件內(nèi)容寫(xiě)入到文件系統(tǒng)

手寫(xiě)webpack

根據(jù)webpack的工作流程,這次我們涉及到的沒(méi)有l(wèi)oader和plugin钞翔。但是存在文件的依賴严卖,至此我們需要在編譯文件模塊的時(shí)候需要進(jìn)行一個(gè)模塊的遞歸遍歷收集的過(guò)程。

很好布轿,上面就是我們今天要實(shí)現(xiàn)的打包功能:獲取webpack配置哮笆,執(zhí)行Compiler.run()方法来颤,

第一層入口文件處理:根據(jù)傳入配置的入口路徑,讀取文件讀取資源轉(zhuǎn)成utf-8格式稠肘,通過(guò)@babel/parser將資源轉(zhuǎn)成ast抽象語(yǔ)法樹(shù)福铅;利用@babel/traverse去遍歷ast中program.body節(jié)點(diǎn),獲取依賴路徑项阴,并將其收集到deps中滑黔;通過(guò)@babel/core將ast編譯成瀏覽器能夠識(shí)別的js。
經(jīng)過(guò)一系列處理环揽,然后輸出到配置指定的目錄和文件夾中略荡。

第一層處理好之后,定義一個(gè)modules總的依賴收集器薯演,將第一層文件依賴push到modules中撞芍。遍歷modules秧了,去處理入口文件的deps依賴跨扮,將其每個(gè)依賴都轉(zhuǎn)成和第一層入口文件的格式。再push到modules验毡,如果依賴中還存在依賴衡创,那么就需要繼續(xù)遞歸遍歷,直到每個(gè)deps中為空晶通。

處理modules璃氢,將其變成一個(gè)關(guān)系圖,類似于:

 {
      'index.js':{
        code:'xx',
        deps:{
          'add.js':"xxx"
        },
        'add.js':{
          code:'xxx',
          deps:{}
        }
      }
  }

最后生成bundle狮辽,輸出資源一也。

初始環(huán)境搭建

在上述環(huán)境的基礎(chǔ)上,我們已經(jīng)有了webpack的配置喉脖,那么我們就需要寫(xiě)一個(gè)我們自己的webpack方法椰苟,去接收配置信息。

首先我們知道會(huì)有一個(gè)Compiler類树叽,內(nèi)部存在run方法去執(zhí)行打包舆蝴。新建webpack功能處理文件:lib文件夾下Compiler.js文件。

class Compiler{
    constructor(options){
        this.options = options
    }
    run(){
        console.log('執(zhí)行run方法',this.options)
    }
}
module.exports = Compiler

新建webpack功能處理文件:lib文件夾下index.js文件题诵。接收config配置洁仗,通過(guò)new Compiler將配置傳入Compiler類中。

const Compiler = require('./Compiler')

const myWebpack = (config) => {
  return new Compiler(config)
}
module.exports = myWebpack

新建一個(gè)打包的腳本文件:scripts文件夾--build.js文件性锭。根據(jù)上面的原理闡述赠潦,不難發(fā)現(xiàn)webpack打包是執(zhí)行其中的run方法。

const config = require('../webpack.config')
const myWebpack = require('../lib/index')
myWebpack(config).run()

在package.json文件中配置打包命令:"build": "node ./scripts/build.js"草冈。執(zhí)行npm run build可以看到run方法中已經(jīng)獲取到相關(guān)配置信息祭椰。

處理第一層入口文件

接下來(lái)的步驟主要就是去完善Compiler類臭家。

處理第一層入口文件,就需要讀取文件資源將其轉(zhuǎn)成ast方淤,并對(duì)ast進(jìn)行遍歷收集第一層文件依賴文件的信息钉赁,最后還需要根據(jù)ast編譯成瀏覽器可識(shí)別的js語(yǔ)言。

獲取ast

根據(jù)配置拿到入口文件路徑携茂,通過(guò)fs.readFileSync讀取文件資源你踩,再通過(guò)@babel/parser將其轉(zhuǎn)成ast。

const fs = require('fs')
const path = require('path')
const babelPaser = require('@babel/parser')

 const filePath = this.options.entry
 const file = fs.readFileSync(filePath, 'utf-8')
 // 轉(zhuǎn)成ast
 const ast = babelPaser.parse(file, {
   sourceType: 'module',
 }) 
 return ast

瀏覽器調(diào)試方法

大多情況下讳苦,我們都習(xí)慣于直接在終端輸出結(jié)果带膜,但是遇到輸出像ast這樣的對(duì)象結(jié)構(gòu),通常會(huì)展示很長(zhǎng)一段鸳谜,層級(jí)嵌套過(guò)深膝藕,很難分清子屬性屬于哪個(gè)父屬性。
在此咐扭,我們使用瀏覽器調(diào)試方式芭挽,就可以很清楚的看到ast的層級(jí)結(jié)構(gòu)。
在我們獲取ast的地方打上debugger蝗肪,并在package.json配置命令:"debug": "node --inspect-brk ./scripts/build.js"袜爪。執(zhí)行命令。
在瀏覽器端任意頁(yè)面打開(kāi)控制臺(tái)薛闪,會(huì)發(fā)現(xiàn)多出一個(gè)node.js圖標(biāo):

1.png

點(diǎn)擊圖標(biāo)辛馆,會(huì)彈出一個(gè)新的控制臺(tái)窗口。


2.png

點(diǎn)擊下一個(gè)斷點(diǎn)就會(huì)跳轉(zhuǎn)到我們打斷點(diǎn)的代碼文件中豁延。

3.png

在控制臺(tái)中監(jiān)聽(tīng)ast昙篙,我們就可以很清楚的看到ast中的結(jié)構(gòu)。當(dāng)然也可以使用ast工具網(wǎng)站:https://astexplorer.net/

編譯ast

拿到ast后我們需要將ast語(yǔ)法樹(shù)編譯成瀏覽器能夠識(shí)別的js诱咏。直接通過(guò)@babel/core中的transformFromAst方法即可苔可。js如果存在高級(jí)語(yǔ)法,就加一個(gè)@babel/preset-env去解析js胰苏。

const {transformFromAst} = require('@babel/core')
getCode(ast){
   const { code } = transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
   })
   return code
 }

遍歷ast收集依賴

我們需要收集入口文件的依賴硕蛹,就需要去遍歷ast的program.body中的節(jié)點(diǎn),找到類型為ImportDeclaration的引入聲明節(jié)點(diǎn)硕并。

4.png

再找到節(jié)點(diǎn)下的source.vlaue(入口文件引入依賴的相對(duì)路徑)法焰,根據(jù)相對(duì)路徑生成絕對(duì)路徑。

const traverse = require('@babel/traverse').default
  // 獲取依賴
getDeps(ast, filePath) {
  // 獲取文件夾路徑
  const dirname = path.dirname(filePath)
  // 收集ast語(yǔ)法樹(shù)依賴: 通過(guò)ast上的program.body.source.value收集依賴
  //  定義收集存儲(chǔ)依賴的容器
  const deps = {}
  traverse(ast, {
    // 內(nèi)部遍歷ast中的program.body 判斷里面語(yǔ)句類型
    // 如果 type:ImportDeclaration 就會(huì)觸發(fā)當(dāng)前函數(shù)
    ImportDeclaration({ node }) {
      // 引入聲明
      // code.node.source.value 引入文件的相對(duì)路徑  './add.js'
      const relativePath = node.source.value
      // 生成基于入口文件的絕對(duì)路徑
      const absolutePath = path.resolve(dirname, relativePath)
      // 添加依賴
      deps[relativePath] = absolutePath
    },
  })
  return deps
}

收集相對(duì)路徑與絕對(duì)路徑有映射關(guān)系的依賴deps倔毙。

5.png

遞歸收集所有依賴

收集依賴

將上面對(duì)第一層入口文件處理的三大方法提取封裝成公共方法埃仪。作為構(gòu)建基礎(chǔ)方法build。

  // 開(kāi)始構(gòu)建
  build(filePath) {
    // 生成ast語(yǔ)法樹(shù)
    const ast = getAst(filePath)
    // 收集依賴
    const deps = getDeps(ast, filePath)
    // 編譯
    const code = getCode(ast)
    return {
      // 文件路徑
      filePath,
      // 當(dāng)前依賴
      deps,
      // 解析后的代碼
      code,
    }
  }

在Compiler類中陕赃,創(chuàng)建一個(gè)收集多層文件總的依賴modules數(shù)組卵蛉。
在執(zhí)行方法run:

需要收集全部文件的依賴颁股,我們就需要去遞歸遍歷入口文件中的deps,根據(jù)deps中的依賴找到相應(yīng)文件傻丝,再對(duì)相應(yīng)文件進(jìn)行轉(zhuǎn)ast甘有,編譯,收集依賴葡缰。

   // 1.讀取入口文件內(nèi)容
    const filePath = this.options.entry
    const fileInfo = this.build(filePath)

    this.modules.push(fileInfo)
    // 遍歷所有依賴
    this.modules.forEach((_) => {
      const deps = _.deps
      for (const path in deps) {
        // 獲取絕對(duì)路徑亏掀,然后收集依賴
        const absolutePath = deps[path]
        // 對(duì)依賴文件進(jìn)行處理,并添加到modules中
        this.modules.push(this.build(absolutePath))
      }
    })

整理依賴

進(jìn)一步處理依賴泛释,每一個(gè)引入的相對(duì)路徑文件下存在自己的依賴文件路徑滤愕、編譯code。將依賴整理成如下的依賴關(guān)系圖:

 {
   'index.js':{
     code:'xx',
     deps:{
       'add.js':"xxx"
     },
     'add.js':{
       code:'xxx',
       deps:{}
     }
   }
 }

此時(shí)怜校,我們就需要一個(gè)空對(duì)象來(lái)收集處理后的依賴關(guān)系间影。遍歷modules依賴,根據(jù)上述結(jié)構(gòu)進(jìn)行組織茄茁。

const depsGraph = this.modules.reduce((graph, module) => {
     return {
        ...graph,
       [module.filePath]: {
         code: module.code,
         deps: module.deps,
       },
     }
   }, {})

打包輸出資源

拿到處理后的依賴關(guān)系圖魂贬,我們就可以生成bundle并打包輸出資源。

生成bundle

webpack打包后的bundle是一個(gè)立即執(zhí)行函數(shù)胰丁,內(nèi)部是通過(guò)執(zhí)行依賴關(guān)系圖中的code随橘,再次對(duì)發(fā)起相應(yīng)依賴文件的請(qǐng)求喂分。bundle內(nèi)容如下:

(function (depsGraph) {
function require(modulePath){
  const module ={
    id:modulePath,
    exports:{}
  }
  // 依賴文件的require 
  // relativePath:./add.js
  function localRequire(relativePath){
const absolutePath = depsGraph[modulePath].deps[relativePath]
return require(absolutePath)
  }
// 執(zhí)行第一層code
(function (exports,code,require) {
eval(code)
})(module.exports,depsGraph[modulePath].code,localRequire)

  return module.exports
}
require('${this.options.entry}')
})(${JSON.stringify(depsGraph)})

立即執(zhí)行函數(shù)傳入關(guān)系圖锦庸,require第一層入口文件。在第一個(gè)入口文件中我們需要執(zhí)行內(nèi)部的code蒲祈,就需要再加一個(gè)立即執(zhí)行函數(shù)去執(zhí)行code甘萧。在第一個(gè)入口文件執(zhí)行時(shí),會(huì)遇到依賴的add.js文件梆掸。執(zhí)行的時(shí)候會(huì)將其轉(zhuǎn)成require請(qǐng)求扬卷,所以我們還需要寫(xiě)一個(gè)請(qǐng)求內(nèi)部文件依賴的方法localRequire,遞歸調(diào)用require酸钦。

可能會(huì)有人不理解在eval(code)這個(gè)函數(shù)中參數(shù)意義怪得,是這樣的:

  • exports:對(duì)外暴露的模塊
  • code:需要執(zhí)行的文件內(nèi)容
  • require:依賴再次請(qǐng)求時(shí),會(huì)觸發(fā)require函數(shù)卑硫,所以入?yún)⒌膔equire名稱不能改變徒恋。否則不會(huì)觸發(fā)立即執(zhí)行函數(shù)中的localRequire方法,會(huì)直接執(zhí)行外層的require函數(shù)欢伏,這樣會(huì)導(dǎo)致請(qǐng)求時(shí)入挣,傳入的是依賴文件的相對(duì)路徑,獲取不到相關(guān)文件內(nèi)容硝拧,執(zhí)行錯(cuò)誤径筏。

輸出資源

通過(guò)options獲取輸出文件的絕對(duì)路徑:

const filePath = path.resolve(this.options.output.path, this.options.output.filename)

創(chuàng)建打包輸出后的文件夾和文件資源葛假。由于我們只是實(shí)現(xiàn)簡(jiǎn)單版本的webpack,所以只考慮了打包文件夾下的一個(gè)輸出文件滋恬。

// 判斷是否存在相同文件夾名稱聊训,存在刪除后再寫(xiě)入
if (fs.existsSync(this.options.output.path)) {
  fs.unlinkSync(filePath);
  fs.rmdirSync(this.options.output.path)
}
fs.mkdirSync(this.options.output.path)
fs.writeFileSync(filePath, bundle, 'utf-8')

驗(yàn)證功能

執(zhí)行打包命令后,在html文件中引入恢氯。運(yùn)行html文件:


7.png

可以看到與使用webpack打包輸出結(jié)果一致魔眨。

這樣昆淡,一個(gè)簡(jiǎn)單的webpack打包流程功能就算完成啦??????

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載请垛,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末衷模,一起剝皮案震驚了整個(gè)濱河市指黎,隨后出現(xiàn)的幾起案子朋凉,更是在濱河造成了極大的恐慌,老刑警劉巖醋安,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杂彭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡吓揪,警方通過(guò)查閱死者的電腦和手機(jī)亲怠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柠辞,“玉大人团秽,你說(shuō)我怎么就攤上這事“仁祝” “怎么了习勤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)焙格。 經(jīng)常有香客問(wèn)我图毕,道長(zhǎng),這世上最難降的妖魔是什么眷唉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任予颤,我火速辦了婚禮,結(jié)果婚禮上冬阳,老公的妹妹穿的比我還像新娘蛤虐。我一直安慰自己,他們只是感情好摩泪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布笆焰。 她就那樣靜靜地躺著,像睡著了一般见坑。 火紅的嫁衣襯著肌膚如雪嚷掠。 梳的紋絲不亂的頭發(fā)上捏检,一...
    開(kāi)封第一講書(shū)人閱讀 51,198評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音不皆,去河邊找鬼贯城。 笑死,一個(gè)胖子當(dāng)著我的面吹牛霹娄,可吹牛的內(nèi)容都是我干的能犯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼犬耻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼踩晶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起枕磁,我...
    開(kāi)封第一講書(shū)人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤渡蜻,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后计济,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體茸苇,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年沦寂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了学密。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡传藏,死狀恐怖腻暮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漩氨,我是刑警寧澤西壮,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布遗增,位于F島的核電站叫惊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏做修。R本人自食惡果不足惜霍狰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饰及。 院中可真熱鬧蔗坯,春花似錦、人聲如沸燎含。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)屏箍。三九已至绘梦,卻和暖如春橘忱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卸奉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工钝诚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人榄棵。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓凝颇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親疹鳄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拧略,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

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

  • 背景 說(shuō)到構(gòu)建工具,我往往會(huì)在前面加「自動(dòng)化」三個(gè)字瘪弓,因?yàn)闃?gòu)建工具就是用來(lái)讓我們不再做機(jī)械重復(fù)的事情辑鲤,解放我們的雙...
    天明丶胡金斌閱讀 421評(píng)論 0 0
  • 上一節(jié)中,我們了解了 webpack 對(duì)于不同模塊化標(biāo)準(zhǔn)的打包結(jié)果杠茬,分析了其自身的模塊化解決方案月褥。但是 webpa...
    ikonan閱讀 615評(píng)論 0 0
  • 引言 前一段時(shí)間我把webpack源碼大概讀了一遍宁赤,webpack 到4.x版本后,其源碼已經(jīng)比較龐大栓票,對(duì)各種開(kāi)發(fā)...
    涅槃快樂(lè)是金閱讀 749評(píng)論 0 1
  • 0.有哪些常見(jiàn)的Loader决左?你用過(guò)哪些Loader? (我開(kāi)始熟悉的報(bào)起了菜名) raw-loader:加載文件...
    何逸軒gg閱讀 4,550評(píng)論 0 1
  • 1. 入門(mén)(一起來(lái)用這些小例子讓你熟悉webpack的配置) 1.1 初始化項(xiàng)目 新建一個(gè)目錄走贪,初始化npm we...
    kkgo_閱讀 2,015評(píng)論 0 40