webpack源碼分析3

2一、定位webpack打包入口node

01 cmd 文件核心的做用就組裝了 node */webpack/bin/webpack.jswebpack

02 webpack.js 中核心的操做就是 require 了 node_modules/webpack-cli/bin/cli.jsweb

03 cli.js
01 當前文件通常有二個操做,處理參數,將參數交給不一樣的邏輯(分發業務)
02 options
03 complier
04 complier.run( 至於run 裏面作了什麼,後續再看,當前只關注代碼入口點 )數組

wepack的一個流程併發

合併配置 compilers.beforerunapp

實例化compiler compilers.runasync

設置node文件讀寫的能力 compilers.beforecompile函數

經過循環掛載plugins compilers.compileui

處理webpack內部默認的插件(入口文件) compilers.makethis

2二、webpack手寫實現

./webpack.js

const Compiler = require('./Compiler')

const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')



const webpack = function (options) {

// 01 實例化 compiler 對象
let compiler = new Compiler(options.context)

compiler.options = options



// 02 初始化 NodeEnvironmentPlugin(讓compiler具體文件讀寫能力)
new NodeEnvironmentPlugin().apply(compiler)



// 03 掛載全部 plugins 插件至 compiler 對象身上
if (options.plugins && Array.isArray(options.plugins)) {

for (const plugin of options.plugins) {

plugin.apply(compiler)
}
}


// 04 掛載全部 webpack 內置的插件(入口)
// compiler.options = new WebpackOptionsApply().process(options, compiler);


// 05 返回 compiler 對象便可
return compiler

}


module.exports = webpack




const {

Tapable,
AsyncSeriesHook
} = require('tapable')



class Compiler extends Tapable {

constructor(context) {
super()
this.context = context

this.hooks = {

done: new AsyncSeriesHook(["stats"]),//

}
}
run(callback) {
callback(null, {
toJson() {
return {

entries: [], // 當前次打包的入口信息

chunks: [], // 當前次打包的 chunk 信息

modules: [], // 模塊信息

assets: [], // 當前次打包最終生成的資源

}
}
})
}
}


module.exports = Compiler

2三、entryOptionPlugin

WebpackOptionsApply
process(options, compiler)
EntryOptionPlugin

entryOption 是一個鉤子實例,
entryOption 在 EntryOptionPlugin 內部的 apply 方法中調用了 tap (註冊了事件監聽)
上述的事件監聽在 new 完了 EntryOptionPlugin 以後就調用了
itemToPlugin, 它是一個函數,接收三個參數( context item ‘main)

SingleEntryPlugin
在調用 itemToPlugin, 的時候又返回了一個 實例對象
有一個構造函數,負責接收上文中的 context entry name
compilation 鉤子監聽
make 鉤子監聽

./EntryOptionPlugin.js

const SingleEntryPlugin = require("./SingleEntryPlugin")


const itemToPlugin = function (context, item, name) {
  return new SingleEntryPlugin(context, item, name)
}


class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      itemToPlugin(context, entry, "main").apply(compiler)
    })
  }
}


module.exports = EntryOptionPlugin



./WebpackOptionsApply.js

const EntryOptionPlugin = require("./EntryOptionPlugin")

class WebpackOptionsApply {
  process(options, compiler) {
    new EntryOptionPlugin().apply(compiler)


    compiler.hooks.entryOption.call(options.context, options.entry)
  }
}


module.exports = WebpackOptionsApply





./SingleEntryPlugin.js

class SingleEntryPlugin {
  constructor(context, entry, name) {
    this.context = context
    this.entry = entry
    this.name = name
  }


  apply(compiler) {
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const { context, entry, name } = this
      console.log("make 鉤子監聽執行了~~~~~~")
      // compilation.addEntry(context, entry, name, callback)
    })
  }
}


module.exports = SingleEntryPlugin

2四、實現run方法

run(callback) {
 console.log('run 方法執行了~~~~')


 const finalCallback = function (err, stats) {

  callback(err, stats)

 }


  const onCompiled = function (err, compilation) {

  console.log('onCompiled~~~~')
  finalCallback(err, {
  toJson() {
  return {

   entries: [],

   chunks: [],

   modules: [],

   assets: []

  }
  }
   })
}


    this.hooks.beforeRun.callAsync(this, (err) => {

    this.hooks.run.callAsync(this, (err) => {

    this.compile(onCompiled)
  })
})
}

2五、實現compile方法

newCompilationParams 方法調用,返回params,normalModuleFactory

上述操做是爲了獲取params

接着調用beforeCompile鉤子監聽,在它的回調中又觸發了compile監聽

調用newCompilation 方法,傳入上面的params,返回一個compilation

調用了createCompilation

上述操做以後出發make鉤子監聽

const {

Tapable,
SyncHook,
SyncBailHook,
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable')



const NormalModuleFactory = require('./NormalModuleFactory')

const Compilation = require('./Compilation')



class Compiler extends Tapable {

constructor(context) {
super()
this.context = context

this.hooks = {

done: new AsyncSeriesHook(["stats"]),

entryOption: new SyncBailHook(["context", "entry"]),



beforeRun: new AsyncSeriesHook(["compiler"]),

run: new AsyncSeriesHook(["compiler"]),



thisCompilation: new SyncHook(["compilation", "params"]),

compilation: new SyncHook(["compilation", "params"]),



beforeCompile: new AsyncSeriesHook(["params"]),

compile: new SyncHook(["params"]),

make: new AsyncParallelHook(["compilation"]),

afterCompile: new AsyncSeriesHook(["compilation"]),

}
}
run(callback) {
console.log('run 方法執行了~~~~')


const finalCallback = function (err, stats) {

callback(err, stats)

}


const onCompiled = function (err, compilation) {

console.log('onCompiled~~~~')
finalCallback(err, {
toJson() {
return {

entries: [],

chunks: [],

modules: [],

assets: []

}
}
})
}


this.hooks.beforeRun.callAsync(this, (err) => {

this.hooks.run.callAsync(this, (err) => {

this.compile(onCompiled)
})
})
}


compile(callback) {
const params = this.newCompilationParams()



this.hooks.beforeRun.callAsync(params, (err) => {

this.hooks.compile.call(params)
const compilation = this.newCompilation(params)



this.hooks.make.callAsync(compilation, (err) => {

console.log('make鉤子監聽觸發了~~~~~')
callback()
})
})
}


newCompilationParams() {
const params = {

normalModuleFactory: new NormalModuleFactory()

}


return params

}


newCompilation(params) {
const compilation = this.createCompilation()

}


createCompilation() {
return new Compilation(this)

}
}


module.exports = Compiler

2六、make流程實現

1、步驟
01 實例化 compiler 對象( 它會貫穿整個webpack工做的過程 )
02 由 compiler 調用 run 方法

2、compiler 實例化操做
01 compiler 繼承 tapable,所以它具有鉤子的操做能力(監聽事件,觸發事件,webpack是一個事件流)

02 在實例化了 compiler 對象以後就往它的身上掛載不少屬性,其中 NodeEnvironmentPlugin 這個操做就讓它具有了
文件讀寫的能力(咱們的模擬時採用的是 node 自帶的 fs )

03 具有了 fs 操做能力以後又將 plugins 中的插件都掛載到了 compiler 對象身上

04 將內部默認的插件與 compiler 創建關係,其中 EntryOptionPlugin 處理了入口模塊的 id

05 在實例化 compiler 的時候只是監聽了 make 鉤子(SingleEntryPlugin)

5-1 在 SingleEntryPlugin 模塊的 apply 方法中有二個鉤子監聽
5-2 其中 compilation 鉤子就是讓 compilation 具有了利用 normalModuleFactory 工廠建立一個普通模塊的能力
5-3 由於它就是利用一個本身建立的模塊來加載須要被打包的模塊
5-4 其中 make 鉤子 在 compiler.run 的時候會被觸發,走到這裏就意味着某個模塊執行打包以前的全部準備工做就完成了
5-5 addEntry 方法調用()

3、run 方法執行( 當前想看的是何時觸發了 make 鉤子 )

01 run 方法裏就是一堆鉤子按着順序觸發(beforeRun run compile)

02 compile 方法執行

1 準備參數(其中 normalModuleFactory 是咱們後續用於建立模塊的)
2 觸發beforeCompile
3 將第一步的參數傳給一個函數,開始建立一個 compilation (newCompilation)
4 在調用 newCompilation 的內部
  - 調用了 createCompilation
  - 觸發了 this.compilation 鉤子 和 compilation 鉤子的監聽

03 當建立了 compilation 對象以後就觸發了 make 鉤子

04 當咱們觸發 make 鉤子監聽的時候,將 compilation 對象傳遞了過去

4、總結

1 實例化 compiler
2 調用 compile 方法
3 newCompilation
4 實例化了一個compilation 對象(它和 compiler 是有關係)
5 觸發 make 監聽
6 addEntry 方法(這個時候就帶着 context name entry 一堆的東西) 就奔着編譯去了.....

WebpackOptionsApply
process(options, compiler)
EntryOptionPlugin
entryOption 是一個鉤子實例,
entryOption 在 EntryOptionPlugin 內部的 apply 方法中調用了 tap (註冊了事件監聽)
上述的事件監聽在 new 完了 EntryOptionPlugin 以後就調用了
itemToPlugin, 它是一個函數,接收三個參數( context item 'main)
SingleEntryPlugin
在調用 itemToPlugin, 的時候又返回了一個 實例對象
有一個構造函數,負責接收上文中的 context entry name
compilation 鉤子監聽
make 鉤子監聽

2七、addEntry流程分析

01 make 鉤子在被觸發的時候,接收到了 compilation 對象實現,它的身上掛載了不少內容

02 從 compilation 當中解構了三個值
entry : 當前須要被打包的模塊的相對路徑(./src/index.js)
name: main
context: 當前項目的根路徑

03 dep 是對當前的入口模塊中的依賴關係進行處理

04 調用了 addEntry 方法

05 在 compilation實例的身上有一個 addEntry 方法,而後內部調用了 _addModuleChain 方法,去處理依賴

06 在 compilation 當中咱們能夠經過 NormalModuleFactory 工廠來建立一個普通的模塊對象

07 在 webpack 內部默認啓了一個 100 併發量的打包操做,當前咱們看到的是 normalModule.create()

08 在 beforeResolve 裏面會觸發一個 factory 鉤子監聽【 這個部分的操做實際上是處理 loader, 當前咱們重點去看 】

09 上述操做完成以後獲取到了一個函數被存在 factory 裏,而後對它進行了調用

10 在這個函數調用裏又觸發了一個叫 resolver 的鉤子( 處理 loader的,拿到了 resolver方法就意味着全部的Loader 處理完畢 )

11 調用 resolver() 方法以後,就會進入到 afterResolve 這個鉤子裏,而後就會觸發 new NormalModule

12 在完成上述操做以後就將module 進行了保存和一些其它屬性的添加

13 調用 buildModule 方法開始編譯---》 調用 build ---》doBuild

2八、addEntry方法實現

const path = require('path')

const Parser = require('./Parser')

const NormalModuleFactory = require('./NormalModuleFactory')

const { Tapable, SyncHook } = require('tapable')



// 實例化一個 normalModuleFactory parser
const normalModuleFactory = new NormalModuleFactory()

const parser = new Parser()



class Compilation extends Tapable {

constructor(compiler) {
super()
this.compiler = compiler

this.context = compiler.context

this.options = compiler.options

// 讓 compilation 具有文件的讀寫能力
this.inputFileSystem = compiler.inputFileSystem

this.outputFileSystem = compiler.outputFileSystem

this.entries = [] // 存入全部入口模塊的數組

this.modules = [] // 存放全部模塊的數據

this.hooks = {

succeedModule: new SyncHook(['module'])

}
}


/**
* 完成模塊編譯操做
* @param {*} context 當前項目的根

* @param {*} entry 當前的入口的相對路徑

* @param {*} name chunkName main

* @param {*} callback 回調

*/
addEntry(context, entry, name, callback) {

this._addModuleChain(context, entry, name, (err, module) => {

callback(err, module)

})
}


_addModuleChain(context, entry, name, callback) {

let entryModule = normalModuleFactory.create({

name,
context,
rawRequest: entry,

resource: path.posix.join(context, entry), // 當前操做的核心做用就是返回 entry 入口的絕對路徑

parser
})


const afterBuild = function (err) {

callback(err, entryModule)

}


this.buildModule(entryModule, afterBuild)



// 當咱們完成了本次的 build 操做以後將 module 進行保存
this.entries.push(entryModule)
this.modules.push(entryModule)
}


/**
* 完成具體的 build 行爲
* @param {*} module 當前須要被編譯的模塊

* @param {*} callback

*/
buildModule(module, callback) {

module.build(this, (err) => {

// 若是代碼走到這裏就意味着當前 Module 的編譯完成了
this.hooks.succeedModule.call(module)
callback(err)
})
}
}


module.exports = Compilation




NormalModuleFactory

const NormalModule = require("./NormalModule")


class NormalModuleFactory {

create(data) {
return new NormalModule(data)

}
}


module.exports = NormalModuleFactory





./NormalModule


class NormalModule {

constructor(data) {
this.name = data.name

this.entry = data.entry

this.rawRequest = data.rawRequest

this.parser = data.parser // TODO: 等待完成

this.resource = data.resource

this._source // 存放某個模塊的源代碼

this._ast // 存放某個模板源代碼對應的 ast

}


build(compilation, callback) {

/**
* 01 從文件中讀取到未來須要被加載的 module 內容,這個
* 02 若是當前不是 js 模塊則須要 Loader 進行處理,最終返回 js 模塊
* 03 上述的操做完成以後就能夠將 js 代碼轉爲 ast 語法樹
* 04 當前 js 模塊內部可能又引用了不少其它的模塊,所以咱們須要遞歸完成
* 05 前面的完成以後,咱們只須要重複執行便可
*/
this.doBuild(compilation, (err) => {

this._ast = this.parser.parse(this._source)

callback(err)
})
}


doBuild(compilation, callback) {

this.getSource(compilation, (err, source) => {

this._source = source

callback()
})
}


getSource(compilation, callback) {

compilation.inputFileSystem.readFile(this.resource, 'utf8', callback)

}
}


module.exports = NormalModule





./parser

const babylon = require('babylon')
const { Tapable } = require('tapable')


class Parser extends Tapable {
  parse(source) {
    return babylon.parse(source, {
      sourceType: 'module',
      plugins: ['dynamicImport']  // 當前插件能夠支持 import() 動態導入的語法
    })
  }
}


module.exports = Parser



const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')


const Stats = require('./Stats')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')


class Compiler extends Tapable {
  constructor(context) {
    super()
    this.context = context
    this.hooks = {
      done: new AsyncSeriesHook(["stats"]),
      entryOption: new SyncBailHook(["context", "entry"]),


      beforeRun: new AsyncSeriesHook(["compiler"]),
      run: new AsyncSeriesHook(["compiler"]),


      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),


      beforeCompile: new AsyncSeriesHook(["params"]),
      compile: new SyncHook(["params"]),
      make: new AsyncParallelHook(["compilation"]),
      afterCompile: new AsyncSeriesHook(["compilation"]),
    }
  }
  run(callback) {
    console.log('run 方法執行了~~~~')


    const finalCallback = function (err, stats) {
      callback(err, stats)
    }


    const onCompiled = function (err, compilation) {
      console.log('onCompiled~~~~')
      finalCallback(err, new Stats(compilation))
    }


    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled)
      })
    })
  }


  compile(callback) {
    const params = this.newCompilationParams()


    this.hooks.beforeRun.callAsync(params, (err) => {
      this.hooks.compile.call(params)
      const compilation = this.newCompilation(params)


      this.hooks.make.callAsync(compilation, (err) => {
        console.log('make鉤子監聽觸發了~~~~~')
        callback(err, compilation)
      })
    })
  }


  newCompilationParams() {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }


    return params
  }


  newCompilation(params) {
    const compilation = this.createCompilation()
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }


  createCompilation() {
    return new Compilation(this)
  }
}


module.exports = Compiler

2九、模塊依賴

01 須要將 Index.js 裏的 require 方法替換成 webpack_require
02 還有將 ./title 替換成 ./src/title.js

03 實現遞歸的操做 ,因此要將依賴的模塊信息保存好,方便交給下一次 create

./NormalModule.js
 
 build(compilation, callback) {
    /**
     * 01 從文件中讀取到未來須要被加載的 module 內容,這個
     * 02 若是當前不是 js 模塊則須要 Loader 進行處理,最終返回 js 模塊
     * 03 上述的操做完成以後就能夠將 js 代碼轉爲 ast 語法樹
     * 04 當前 js 模塊內部可能又引用了不少其它的模塊,所以咱們須要遞歸完成
     * 05 前面的完成以後,咱們只須要重複執行便可
     */
    this.doBuild(compilation, (err) => {
      this._ast = this.parser.parse(this._source)


      // 這裏的 _ast 就是當前 module 的語法樹,咱們能夠對它進行修改,最後再將 ast 轉回成 code 代碼
      traverse(this._ast, {
        CallExpression: (nodePath) => {
          let node = nodePath.node


          // 定位 require 所在的節點
          if (node.callee.name === 'require') {
            // 獲取原始請求路徑
            let modulePath = node.arguments[0].value  // './title'  
            // 取出當前被加載的模塊名稱
            let moduleName = modulePath.split(path.posix.sep).pop()  // title
            // [當前咱們的打包器只處理 js ]
            let extName = moduleName.indexOf('.') == -1 ? '.js' : ''
            moduleName += extName  // title.js
            // 【最終咱們想要讀取當前js裏的內容】 因此咱們須要個絕對路徑
            let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
            // 【將當前模塊的 id 定義OK】
            let depModuleId = './' + path.posix.relative(this.context, depResource)  // ./src/title.js


            // 記錄當前被依賴模塊的信息,方便後面遞歸加載
            this.dependencies.push({
              name: this.name, // TODO: 未來須要修改
              context: this.context,
              rawRequest: moduleName,
              moduleId: depModuleId,
              resource: depResource
            })


            // 替換內容
            node.callee.name = '__webpack_require__'
            node.arguments = [types.stringLiteral(depModuleId)]
          }
        }
      })


      // 上述的操做是利用ast 按要求作了代碼修改,下面的內容就是利用 .... 將修改後的 ast 轉回成 code
      let { code } = generator(this._ast)
      this._source = code
      callback(err)
    })



./compilation

const path = require('path')
const async = require('neo-async')
const Parser = require('./Parser')
const NormalModuleFactory = require('./NormalModuleFactory')
const { Tapable, SyncHook } = require('tapable')


// 實例化一個 normalModuleFactory parser
const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()


class Compilation extends Tapable {
  constructor(compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    // 讓 compilation 具有文件的讀寫能力
    this.inputFileSystem = compiler.inputFileSystem
    this.outputFileSystem = compiler.outputFileSystem
    this.entries = []  // 存入全部入口模塊的數組
    this.modules = [] // 存放全部模塊的數據
    this.hooks = {
      succeedModule: new SyncHook(['module'])
    }
  }


  /**
   * 完成模塊編譯操做
   * @param {*} context 當前項目的根
   * @param {*} entry 當前的入口的相對路徑
   * @param {*} name chunkName main
   * @param {*} callback 回調
   */
  addEntry(context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module)
    })
  }


  _addModuleChain(context, entry, name, callback) {
    this.createModule({
      parser,
      name: name,
      context: context,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule)
    }, callback)
  }


  /**
   * 定義一個建立模塊的方法,達到複用的目的
   * @param {*} data 建立模塊時所須要的一些屬性值
   * @param {*} doAddEntry 可選參數,在加載入口模塊的時候,將入口模塊的id 寫入 this.entries
   * @param {*} callback
   */
  createModule(data, doAddEntry, callback) {
    let module = normalModuleFactory.create(data)


    const afterBuild = (err, module) => {
      // 使用箭頭函數能夠保證this指向在定義時就肯定
      // 在 afterBuild 當中咱們就須要判斷一下,當前次module 加載完成以後是否須要處理依賴加載
      if (module.dependencies.length > 0) {
        // 當前邏輯就表示module 有須要依賴加載的模塊,所以咱們能夠再單獨定義一個方法來實現
        this.processDependencies(module, (err) => {
          callback(err, module)
        })
      } else {
        callback(err, module)
      }
    }


    this.buildModule(module, afterBuild)


    // 當咱們完成了本次的 build 操做以後將 module 進行保存
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }


  /**
   * 完成具體的 build 行爲
   * @param {*} module 當前須要被編譯的模塊
   * @param {*} callback
   */
  buildModule(module, callback) {
    module.build(this, (err) => {
      // 若是代碼走到這裏就意味着當前 Module 的編譯完成了
      this.hooks.succeedModule.call(module)
      callback(err, module)
    })
  }


  processDependencies(module, callback) {
    // 01 當前的函數核心功能就是實現一個被依賴模塊的遞歸加載
    // 02 加載模塊的思想都是建立一個模塊,而後想辦法將被加載模塊的內容拿進來?
    // 03 當前咱們不知道 module 須要依賴幾個模塊, 此時咱們須要想辦法讓全部的被依賴的模塊都加載完成以後再執行 callback?【 neo-async 】
    let dependencies = module.dependencies


    async.forEach(dependencies, (dependency, done) => {
      this.createModule({
        parser,
        name: dependency.name,
        context: dependency.context,
        rawRequest: dependency.rawRequest,
        moduleId: dependency.moduleId,
        resource: dependency.resource
      }, null, done)
    }, callback)
  }
}


module.exports = Compilation

30、chunk流程分析

相關文章
相關標籤/搜索