关于webpack:webpack源码分析3

5次阅读

共计 15020 个字符,预计需要花费 38 分钟才能阅读完成。

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

正文完
 0