webpack4打包流程分析,实现一个简易的webpack打包器

2023-11-12

文章内容输出来源:拉勾教育前端高薪训练营

webpack打包的流程大致可以归纳成:

  1. options:通过config文件传递进来的配置参数
  2. webpack:生成Compiler实例及其他webpack初始化逻辑
  3. compiler:编译的核心桥梁,根据配置文件,通过执行webpack(options),生成的并返回一个Compiler实例compiler(自我理解为一个编译工厂)
    2.1 在生成的Compiler实例上将options配置参数挂载到compiler.options,以方便将来获取配置参数
    2.2 通过new NodeEnvironmentPlugin()让compiler得到读写文件的能力
    2.3 遍历options配置中的plugins,如果有plugins,则通过tapable机制注册一个的监听事件,以便将来某个时刻触发监听来执行插件
    2.4 通过new WebpackOptionsApply()的process方法来注册所有内置插件的监听事件(第4大点)
  4. 通过new WebpackOptionsApply().process(options, compiler)注册内置插件
    4.1 新建一个EntryOptionPlugin实例并注册entryOption监听事件
    4.2 执行entryOption事件来注册make监听事件
  5. 执行compiler的run方法来打包生成代码文件,以及返回最终的打包对象数据
    5.1 生成一个Compilation实例compilation(负责具体的代码编译)
    5.2 执行make事件来读取文件源代码转换成ast语法树,随后替换其中的关键字为webpack关键字,再转换成编译后的源代码
    5.3 遍历已被编译的源代码是否有依赖模块,如果有再重复执行5.2步骤来递归编译
    5.4 通过compilation实例方法将编译后的代码写入模版文件
    5.5 通过compiler实例将代码写入到指定文件夹下的指定文件,并返回打包后的数据信息

话不多说,下面我们直接从webpack的入口文件开始来一步步拆解模拟一个webpack的简易打包器

1. 启动打包程序

start.js

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

// 生成compiler编译工厂
const compiler = webpack(options)

// 执行打包程序
compiler.run((err, stats) => {
  console.log(err)
  console.log(stats)
})

2. 生成Compiler实例及其他webpack初始化逻辑

webpack.js

const Compiler = require('./Compiler')
const NodeEnvironmentPlugin = require('./NodeEnvironmentPlugin')
const WebpackOptionsApply = require('./WebpackOptionsApply')

const webpack = options => {
  // 实例化一个compiler对象(编译工厂)
  const compiler = new Compiler(options.context)

  // 将options配置参数挂载到compiler.options,以方便将来获取配置参数
  compiler.options = options

  // 让compiler获得读写文件的能力
  new NodeEnvironmentPlugin().apply(compiler)

  // 遍历options配置中的plugins注册监听事件,以便将来某个时刻触发监听来执行插件
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler)
    }
  }
  
  // 注册内置插件
  new WebpackOptionsApply().process(options, compiler)

  return compiler
}

module.exports = webpack

3. 生成Compiler实例

// 实例化一个compiler对象(编译工厂)
const compiler = new Compiler(options.context)

通过上文的分析,Compiler必须要具备一个run方法来开始编译,并且需具备一些钩子来让用户注册自定义插件和挂载内置插件。由于Compiler只是一个“工厂”,只负责流程管理,所以我们还需要创造一个工人compilation来负责代码的编译工作,当compilation把代码编译完成后,通知Compiler来写入文件。

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

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']),
      emit: new AsyncSeriesHook(['compilation'])
    }
  }

  emitAssets(compilation, callback) {
    // TODO: 写入打包文件
  }

  run (callback) {
    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        // TODO:this.compile(callback) 执行编译逻辑,callback为编译完成后的回调
      })
    })
  }

}

module.exports = Compiler

接下来将编译逻辑compile方法补全,按源码的逻辑,此时会生成一个compilation工人,同时会调用一些钩子事件,最重要的就是make钩子的调用进入make流程,在make流程完成后执行回调处理onCompiled编译完成之后的流程

compile (callback) {
  // 生成beforeCompile,compile钩子用到的参数
  const params = this.newCompilationParams()
  this.hooks.beforeCompile.callAsync(params, err => {
    this.hooks.compile.call(params);

    // 生成compilation
    const compilation = this.newCompilation(params)
    
    // 执行make钩子
    this.hooks.make.callAsync(compilation, err => {
      // TODO:make钩子执行完成的回调
    })
  })
}

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

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

此时的Compile.js代码

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']),
      emit: new AsyncSeriesHook(['compilation'])
    }
  }

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

  compile (callback) {
    // 生成beforeCompile,compile钩子用到的参数
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params);
  
      // 生成compilation
      const compilation = this.newCompilation(params)
      
      // 执行make钩子
      this.hooks.make.callAsync(compilation, err => {
        // TODO:make钩子执行完成的回调, 开始处理chunk
      })
    })
  }
  
  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params
  }
  
  newCompilation (params) {
    const compilation = new Compilation(this)
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }

}

module.exports = Compiler

4. NodeEnvironmentPlugin,赋予compiler实例读写能力

// 让compiler获得读写文件的能力
new NodeEnvironmentPlugin().apply(compiler)

我们让NodeEnvironmentPlugin类继承fs模块的能力,由于属于插件,根据webpack的思想,需要有一个apply方法来调用实现

NodeEnvironmentPlugin.js

const fs = require('fs')

class NodeEnvironmentPlugin {
  constructor(options) {
    this.options = options || {}
  }
  
  apply(compiler) {
    complier.inputFileSystem = fs
    complier.outputFileSystem = fs
  }
}

module.exports = NodeEnvironmentPlugin

5. 注册自定义插件apply方法,注册钩子事件

// 遍历options配置中的plugins注册监听事件,以便将来某个时刻触发监听来执行插件
if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    plugin.apply(compiler)
  }
}

6. 注册webpack内置插件

// 注册内置插件
new WebpackOptionsApply().process(options, compiler)

通过注册内置插件来注册compiler实例上的entryOption,make钩子监听事件。

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

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) => {
      // 注册make钩子
      itemToPlugin(context, entry, "main").apply(compiler)
    })
  }
}

module.exports = EntryOptionPlugin

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
      compilation.addEntry(context, entry, name, callback)
    })
  }
}

module.exports = SingleEntryPlugin

7. compilation编译工实现

首先实现一个ast解析器,将来用来把代码转换成ast语法树
Parser.js

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

通过make钩子,我们得知compilation实例上需要有一个addEntry方法作为入口

const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树

const parser = new Parser()

class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
    this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
    this.entries = []  // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = []  // 存放当前次打包过程中所产出的 chunk
    this.assets = []
    this.files = []
    this.hooks = {
      succeedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  /**
   * 完成模块编译操作
   * @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,
      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) {
    // 代码工厂,解析ast语法树,编译最终代码形态
    const module = normalModuleFactory.create(data)
    const afterBuild = () => {
      // TODO
    }
    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)
    })
  }
}

module.exports = Compilation

compilation createModule需要实现一个模块代码工厂类,将Parser后的ast代码替换关键字后,编译成源代码
NormalModuleFactory.js

const NormalModule = require("./NormalModule")

class NormalModuleFactory {
  create (data) {
    return new NormalModule(data)
  }
}

module.exports = NormalModuleFactory

NormalModule.js

const path = require('path')
const types = require('@babel/types')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default

class NormalModule {
  constructor (data) {
    this.context = data.context
    this.name = data.name
    this.moduleId = data.moduleId
    this.rawRequest = data.rawRequest
    this.parser = data.parser
    this.resource = data.resource
    this._source  // 存放某个模块的源代码
    this._ast // 存放某个模板源代码对应的 ast 
    this.dependencies = [] // 定义一个空数组用于保存被依赖加载的模块信息
  }

  build(compilation, callback) {
    /**
     * 01 从文件中读取到将来需要被加载的 module 内容
     * 02 如果当前不是 js 模块则需要 Loader 进行处理,最终返回 js 模块 
     * 03 上述的操作完成之后就可以将 js 代码转为 ast 语法树
     * 04 当前 js 模块内部可能又引用了很多其它的模块,因此我们需要递归完成 
     * 05 前面的完成之后,我们只需要重复执行即可
     */
    this.doBuild(compilation, err => {
      // 将源代码转成ast语法树
      this._ast = this.parser.parse(this._source)

      // 对语法树进行修改,最后再将 ast 转回成 code 代码 
      traverse(this._ast, {
        CallExpression: nodePath => {
          let node = nodePath.node

          // 定位 require 所在的节点
          if (node.callee.name === 'require') {
            // 获取原始请求路径
            let modulePath = node.arguments[0].value
            // 取出当前被加载的模块名称
            let moduleName = modulePath.split(path.posix.sep).pop()
            // 当前我们的打包器只处理 js
            let extName = moduleName.indexOf('.') == -1 ? '.js' : ''
            moduleName += extName
            // 最终我们想要读取当前js里的内容,所以我们需要个绝对路径
            let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
            // 将当前模块的id定义为./src/**形式
            let depModuleId = './' + path.posix.relative(this.context, depResource)

            // 记录当前被依赖模块的信息,方便后面递归加载
            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 转回成 code 
      const { code } = generator(this._ast)
      this._source = code
      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

接下来回到Compilation中完成afterBuild之后的工作,判读module中是否有依赖,如果有继续递归执行createModule,直到没有依赖为止,最后执行addEntry的回调函数(即make钩子执行完成的回调)

const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树

const parser = new Parser()

class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
    this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
    this.entries = []  // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = []  // 存放当前次打包过程中所产出的 chunk
    this.assets = []
    this.files = []
    this.hooks = {
      succeedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  /**
   * 完成模块编译操作
   * @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,
      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) {
    // 代码工厂,解析ast语法树,编译最终代码形态
    const module = normalModuleFactory.create(data)
    const afterBuild = (err, module) => {
      // 判断当前次module加载完成之后是否需要处理依赖加载
      if (module.dependencies.length > 0) {
        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)
    })
  }
}

module.exports = Compilation

8. make钩子执行完成后的操作

在Compilation的afterBuild执行完成后,会触发make钩子的回调函数,通过Compilation开始处理chunk,将刚才被编译过的code写入模板文件中,最后执行写入目录操作,到此Compiler的任务就完成了。现在先回到Compiler里面,补全Compiler代码

Compiler.js

const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')
const path = require('path')
const mkdirp = require('mkdirp')
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']),
      emit: new AsyncSeriesHook(['compilation'])
    }
  }

  // 创建打包目录,在目录创建完成之后执行文件的写操作
  emitAssets(compilation, callback) {
    // 01 定义一个工具方法用于执行文件的生成操作
    const emitFlies = (err) => {
      const assets = compilation.assets
      let outputPath = this.options.output.path

      for (let file in assets) {
        let source = assets[file]
        let targetPath = path.posix.join(outputPath, file)
        this.outputFileSystem.writeFileSync(targetPath, source, 'utf8')
      }

      callback(err)
    }

    // 创建目录之后启动文件写入
    this.hooks.emit.callAsync(compilation, (err) => {
      mkdirp.sync(this.options.output.path)
      emitFlies()
    })

  }

  run (callback) {
    
    // 输出stats打包后的数据信息
    const finalCallback = function (err, stats) {
      callback(err, stats)
    }

    const onCompiled = (err, compilation) => {

      // 调用emitAssets将处理好的 chunk 写入到指定的文件然后输出至打包目录 
      this.emitAssets(compilation, (err) => {
        let stats = new Stats(compilation)
        finalCallback(err, stats)
      })
    }

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

  compile (callback) {
    // 生成beforeCompile,compile钩子用到的参数
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params);
  
      // 生成compilation
      const compilation = this.newCompilation(params)
      
      // 执行make钩子
      this.hooks.make.callAsync(compilation, err => {

        // 开始处理 chunk 
        compilation.seal((err) => {
          this.hooks.afterCompile.callAsync(compilation, (err) => {
            callback(err, compilation)
          })
        })
      })
    })
  }
  
  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params
  }
  
  newCompilation (params) {
    const compilation = new Compilation(this)
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }

}

module.exports = Compiler

Stats.js

class Stats {
  constructor (compilation) {
    this.entries = compilation.entries
    this.modules = compilation.modules
    this.chunks = compilation.chunks
    this.files = compilation.files
  }

  toJson() {
    return this
  }
}

module.exports = Stats

9. 补全Compilation中处理chunk的逻辑

处理chunk指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并

Chunk.js

class Chunk {
  constructor (entryModule) {
    this.entryModule = entryModule
    this.name = entryModule.name
    this.files = []  // 记录每个 chunk的文件信息
    this.modules = [] // 记录每个 chunk 里的所包含的 module
  }
}

module.exports = Chunk

Compilation.js

const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树
const ejs = require('ejs')
const Chunk = require('./Chunk')
const path = require('path')
const async = require('neo-async')

const parser = new Parser()
const normalModuleFactory = new NormalModuleFactory()

class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
    this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
    this.entries = []  // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = []  // 存放当前次打包过程中所产出的 chunk
    this.assets = []
    this.files = []
    this.hooks = {
      succeedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  /**
   * 完成模块编译操作
   * @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,
      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) {
    // 代码工厂,解析ast语法树,编译最终代码形态
    const module = normalModuleFactory.create(data)
    const afterBuild = (err, module) => {
      // 判断当前次module加载完成之后是否需要处理依赖加载
      if (module.dependencies.length > 0) {
        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 】
    const 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)
  }

  // 处理Chunk
  seal (callback) {
    this.hooks.seal.call()
    this.hooks.beforeChunks.call()

    // 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
    for (const entryModule of this.entries) {
      // 核心:创建模块加载已有模块的内容,同时记录模块信息 
      const chunk = new Chunk(entryModule)
      // 保存 chunk 信息
      this.chunks.push(chunk)
      // 给 chunk 属性赋值 
      chunk.modules = this.modules.filter(module => module.name === chunk.name)
    }

    this.hooks.afterChunks.call(this.chunks)
    // 根据模板渲染代码
    this.createChunkAssets()
    // 执行回调写入文件
    callback()
  }

  createChunkAssets () {
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i]
      const fileName = chunk.name + '.js'
      chunk.files.push(fileName)

      // 获取模板文件的路径
      const tempPath = path.posix.join(__dirname, 'temp/main.ejs')
      // 读取模块文件中的内容
      const tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
      // 获取渲染函数
      const tempRender = ejs.compile(tempCode)
      // 按ejs的语法渲染数据
      const source = tempRender({
        entryModuleId: chunk.entryModule.moduleId,
        modules: chunk.modules
      })

      // 输出文件
      this.emitAssets(fileName, source)
    }
  }

  emitAssets (fileName, source) {
    this.assets[fileName] = source
    this.files.push(fileName)
  }
}

module.exports = Compilation

至此webpack的简要打包流程及一个简易的webpack打包器就完成了,一句话总结起来就是:
根据配置文件,递归拿到文件的源代码,替换源代码中的关键字,然后合并代码,最后根据模板输出到指定目录

附文结构

  • Chunk.js
  • Compilation.js
  • Compiler.js
  • EntryOptionPlugin.js
  • NodeEnvironmentPlugin.js
  • NormalModule.js
  • NormalModuleFactory.js
  • Parser.js
  • SingleEntryPlugin.js
  • Stats.js
  • webpack.js
  • WebpackOptionsApply.js
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

webpack4打包流程分析,实现一个简易的webpack打包器 的相关文章

  • 使用 JavaScript 以编程方式编辑 Google 文档

    我想做的是运行一些 JavaScript 代码 将文本输入到 Google 文档中 到目前为止 我所做的是在我的个人网页上创建一个嵌入 Google 文档的 iframe 元素 目前我想做的是使用 Google 源代码中的函数来输入文本 当
  • 循环选项在 youtube js api 中不起作用

    我想知道为什么我的代码不循环播放视频 除了循环选项之外 一切正常 我真的需要它 多谢 div You need Flash player 8 and JavaScript enabled to view this video div
  • 为什么我会收到此 Javascript 错误“连接未定义”?

    我不确定为什么会收到此错误 connection is not defined document getElementById flashTest sendValFromHtml connection value 这是我的代码 functi
  • 如何在 的每四个循环项之后添加

    我想在循环中的每第四个数字项之后退出循环 我想创建一个二十人的名单 在每一个tr应该是4个人 So I want to break from the loop after every 4th number of loop My one tr
  • 使用 lambda 更新 amazon s3 对象元数据而不执行对象复制?

    是否可以使用 lambda 函数添 加或更新 s3 对象元数据而不复制对象 这篇 2 年前的帖子说我们确实需要复制一份 https stackoverflow com questions 32646646 how do i update m
  • 在 JSON 数组中按属性查找对象

    我在获取 JSON 数据中的字符串时遇到问题 格式如下 name Alice age 20 id David last 25 id John last 30 有时它会一起改变位置 John从第三名到第二名 name Alice age 20
  • 用于自由形式绘图的 javascript 库

    是否有一个 JavaScript 库可以让我在网页上绘图 然后保存该绘图的状态 我想使用鼠标绘制 2D 图像 然后如何存储和加载该绘图 使用 HTML5 画布 绘制图像的简单示例如下 http jsfiddle net ghostoy wT
  • 如何使用 RSpec 测试 javascript 重定向?

    我正在使用 xhr post 与控制器交互 并且我期待重定向 在 js erb 中 我有 window location href address 手动测试 浏览器会正确重定向 我如何使用 RSpec 测试它 response should
  • React:未捕获的引用错误:未定义需求

    我正在阅读 React 教程 http facebook github io react docs animation html http facebook github io react docs animation html 并且我无法
  • 为什么 Bootstrap 需要 jQuery? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我已经多次用谷歌搜索这个问题 但从未找到满意的答案 大多数答案似乎只是说 是的 Bootstrap 插件确实需要 jQuery https st
  • .then(functionReference) 和 .then(function(value){return functionReference(value)}) 之间有区别吗?

    给定一个用于处理的命名函数Promise value function handlePromise data do stuff with data return data a 传递命名函数handlePromise作为参考 then pro
  • setTimeout() 的问题

    这是我的代码 我想要它做的是写 0 等待一秒 写 1 等待一秒 写 2 等待一秒 等等 而是写 5 5 5 5 5 for i 0 i lt 5 i setTimeout document write i 1000 http jsfiddl
  • 在javascript中通过window.location传递数据

    我试图通过 window location 传递数据 数据在 del id img album 中可用 我想通过 window location 发送多个值 window location save php type deldownload
  • 使用 ngx-translate 时更改 URL

    当有人使用 ngx translate 单击所选语言时 我尝试更改 URL 我想我应该通过订阅语言更改事件然后修改当前的 url 以反映所选的语言来做到这一点 因为我是新手 所以我不确定是否需要服务来做到这一点 或者可能是另一种解决方法 我
  • 添加元数据到快速路线

    有什么方法可以将元数据添加到 Express 的路线中吗 例如 app get some route function req res some meta data 我正在寻找一种针对我的节点应用程序的 AOP 方法 因此我想通过身份验证和
  • js中将div旋转到一定高度

    How to rotate a div to certain height suppose 10px I can rotate a div otherwise around 360 degrees I need the angle by w
  • 同源政策目的可疑

    正如我所读到的 同源策略是防止源自 邪恶 域 A 的脚本向 良好 域 B 发出请求 换句话说 跨站点请求伪造 玩了一下我了解到的Access Control Allow Origin标头和CORS据我了解 它允许从好域 B 指定服务器 域
  • 检测浏览器是否支持 contentEditable?

    There s 这个问题 https stackoverflow com questions 3497942 browser detect contenteditable features 但发布的解决方案是浏览器嗅探 我试图避免这种情况
  • 如何找出javascript中加载了哪些javascript?

    继另一个问题的评论之后 我问自己是否有办法获取页面上加载的所有 js 代码的列表 就像 Firebug 或 chrome Inspector 所做的那样 有没有一种纯javascript的方法 一种方法是抓取脚本标签 但这样你可能会错过动态
  • Page_ClientValidate 正在验证多次。

    我的问题是 验证摘要消息 警报 显示两次 我无法弄清楚原因 请帮忙 这是代码 function validate javascript function if typeof Page ClientValidate function var

随机推荐

  • Java多线程读取本地照片为二进制流,并根据系统核数动态确定线程数

    Java多线程读取图片内容并返回 1 ExecutorService线程池 2 效率截图 3 源码 1 ExecutorService线程池 ExecutorService线程池 并可根据系统核数动态确定线程池最大数 最大 最小线程数一致
  • vue打包上线如此简单

    大家好 我是大帅子 最近好多人私信我 要我出一期vue的打包上线的文章 那么今天他来了 废话不多说 我们直接开始吧 我们顺便给大家提一下vue项目中的优化 项目打包 1 打开终端 直接在终端输入 我把npm 跟 yarn的打包命令都放在这里
  • CMake增加版本号

    为工程设置版本号 当然可以在源文件中增加版本号变量 但也可以使用CMakeLists txt设置可变的版本号 提供更多的便利性 1 修改CMakeLists txt 用set命令设置版本号 设置最大版本号和最小版本号 set Calcula
  • python 历史版本下载大全

    历史版本下载地址 https www python org ftp python
  • java 对接OmniLayer钱包

    上代码 如果帮助到了你 请点点关注 谢谢 Data public class BtcApi Logger logger Logger getLogger BtcApi class private String rpcUrl private
  • 详解八大排序算法-附动图和源码(插入,希尔,选择,堆排序,冒泡,快速,归并,计数)

    目录 一 排序的概念及应用 1 排序的概念 2 排序的应用 3 常用的排序算法 二 排序算法的实现 1 插入排序 1 1直接插入排序 1 2希尔排序 缩小增量排序 2 选择排序 2 1直接选择排序 2 2堆排序 3 比较排序 3 1冒泡排序
  • Java接口幂等性设计场景解决方案v1.0

    Java接口幂等性设计场景解决方案v1 0 1 面试 实际开发场景 1 1面试场景题目 分布式服务接口的幂等性如何设计 比如不能重复扣款 1 2 题目分析 一个分布式系统中的某个接口 要保证幂等性 如何保证 这个事 其实是你做分布式系统的时
  • JSP session的生命周期简介说明

    转自 JSP session的生命周期简介说明 下文笔者将讲述session生命周期的相关简介说明 如下所示 Session存储在服务器端 当客户端关闭浏览器 并不意味着Session对象的销毁 如果不是显式调用invalidate 去销毁
  • [39题] 牛客深度学习专项题

    1 卷积核大小 提升卷积核 convolutional kernel 的大小会显著提升卷积神经网络的性能 这种说法是 正确的 错误的 这种说法是错误的 提升卷积核的大小并不一定会显著提升卷积神经网络的性能 卷积核的大小会影响网络的感受野 r
  • Java时间处理(UTC时间和本地时间转换)

    文章内容引用来源 http blog csdn net top code article details 50462922 前言 本文主要对UTC GMT CST等时间概念做简单的介绍 比较实用的在于本文最后一个小知识点 带时区格式的时间和
  • python编程题-基本编程题 --python

    1 让Python帮你随机选一个饮品吧 import random listC 加多宝 雪碧 可乐 勇闯天涯 椰子汁 print random choices listC type random choices listC choices函
  • hbuilder如何设置图片居中显示_啊哦!WORD设置格式后,我插入的图片显示不全怎么办?...

    每天分享一个小技巧 不如各位在日常办公中 有没有这样的烦恼 一个编辑好的文档 已经到了最后一步 Ctrl A 全选 设置格式 然后 发现文档里的图片 它 它 它 它 它显示不全了 就像这样 其实导致这种问题发生的原因 很简单 就是因为我们批
  • LeetCode算法题 - 两整数相加(简单)

    题目 func sum num1 int num2 int int return num1 num2
  • SpringBoot通过Excel文件导入用户信息,找出Excel(ArrayList)中重复的元素和个数

    Excel文件内容如下 其中userCode不能重复 怎么返回重复的userCode和个数呢 因为Map是存储键值对这样的双列数据的集合 其中存储的数据是无序的 它的键是不允许重复的 值是允许重复的 也就是只保留一项数据 不记录重复数据 所
  • 2021年南京大学842考研-软件工程部分代码设计题

    题干 1 以下代码是否有问题 有什么影响 2 给出改进 按钮构件 Class Button private Label label private List list public void change list update label
  • 启动hadoop集群

    1 配置core site xml 每个节点上都要配置 路径 usr local hadoop 2 7 3 etc hadoop core site xml 配置项1 name fs defaultFS value hdfs master的
  • 敏感性和特异性

    敏感性 sensitivity 在测验的阳性结果中 有多少是真阳性 就是在生病的病例中 能检测出来多少 是病例中 你的诊断方法对疾病的敏感程度 识别程度 敏感性越高 识别疾病 阳性 的概率越高 不漏诊概率 特异性 Specificity 在
  • 使用yolov8进行字符检测

    最近使用yolov8进行字符检测任务 因为场景数据是摆正后的证件数据 所以没有使用DB进行模型训练 直接选用了yolov8n进行文本检测 但是长条字符区域检测效果一直不太好 检出不全 通过检测和分割等算法的调试 发现算法本身不太适合作文本检
  • Qt 之进程间通信(TCP/IP)

    Qt 之进程间通信 TCP IP 原创 一去丶二三里 发布于2016 02 04 10 19 46 阅读数 15428 收藏 更新于2018 05 30 10 35 06 分类专栏 Qt Qt 实战一二三
  • webpack4打包流程分析,实现一个简易的webpack打包器

    文章内容输出来源 拉勾教育前端高薪训练营 webpack打包的流程大致可以归纳成 options 通过config文件传递进来的配置参数 webpack 生成Compiler实例及其他webpack初始化逻辑 compiler 编译的核心桥