webpack源码分析3

Posted zhuanglog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack源码分析3相关的知识,希望对你有一定的参考价值。

21、定位webpack打包入口

01 cmd 文件核心的作用就组装了 node */webpack/bin/webpack.js

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

03 cli.js
01 当前文件一般有二个操作,处理参数,将参数交给不同的逻辑(分发业务)
02 options
03 complier
04 complier.run( 至于run 里面做了什么,后续再看,当前只关注代码入口点 )

wepack的一个流程

合并配置 compilers.beforerun

实例化compiler compilers.run

设置node文件读写的能力 compilers.beforecompile

通过循环挂载plugins compilers.compile

处理webpack内部默认的插件(入口文件) compilers.make

22、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

23、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

24、实现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)
  })
})
}

25、实现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

26、make流程实现

一、步骤
01 实例化 compiler 对象( 它会贯穿整个webpack工作的过程 )
02 由 compiler 调用 run 方法

二、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 方法调用()

三、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 对象传递了过去

四、总结

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 钩子监听

27、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

28、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

29、模块依赖

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流程分析

以上是关于webpack源码分析3的主要内容,如果未能解决你的问题,请参考以下文章

[webpack]源码解读:命令行输入webpack的时候都发生了什么?

Webpack-源码三,从源码分析如何写一个plugin

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段

来了来了,最新vite源码分析,vite到底为什么比webpack快

webpack源码分析1