webpack4主流程源码阅读,以及动手实现一个简单的webpack(一)

简介: webpack4主流程源码阅读,以及动手实现一个简单的webpack

简单概述 Webpack 整体运行流程


  1. 读取参数
  2. 实例化 Compiler
  3. entryOption 阶段,读取入口文件
  4. Loader 编译对应文件,解析成 AST
  5. 找到对应依赖,递归编译处理,生成 chunk
  6. 输出到 dist


webpack 打包主流程源码阅读


通过打断点的方式阅读源码,来看一下命令行输入 webpack 的时候都发生了什么?P.S. 以下的源码流程分析都基于 webpack4

先附上一张自己绘制的执行流程图b386698fedb003742a336b0ed098dc1d_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


初始化阶段


  1. 初始化参数(webpack.config.js+shell options

webpack 的几种启动方式

  • 通过 webpack-cli执行 会走到 ./node_modules/.bin/webpack-cli(执行)
  • 通过 shell 执行webpack ,会走到 ./bin/webpack.js
  • 通过 require("webpack")执行 会走到 ./node_modules/webpack/lib/webpack.js

追加 shell 命令的参数,如-p , -w,通过 yargs 解析命令行参数convert-yargs 把命令行参数转换成 Webpack 的配置选项对象 同时实例化插件 new Plugin()

  1. 实例化 Compiler

阅读完整源码点击这里:webpack.js

// webpack入口
const webpack = (options, callback) => {
  let compiler
  // 实例Compiler
  if (Array.isArray(options)) {
    // ...
    compiler = createMultiCompiler(options)
  } else {
    compiler = createCompiler(options)
  }
  // ...
  // 若options.watch === true && callback 则开启watch线程
  if (watch) {
    compiler.watch(watchOptions, callback)
  } else {
    compiler.run((err, stats) => {
      compiler.close((err2) => {
        callback(err || err2, stats)
      })
    })
  }
  return compiler
}

webpack 的入口文件其实就实例了 Compiler 并调用了 run 方法开启了编译,

  1. 注册 NodeEnvironmentPlugin 插件,挂载 plugin 插件,使用 WebpackOptionsApply 初始化基础插件

在此期间会 apply 所有 webpack 内置的插件,为 webpack 事件流挂上自定义钩子

源码仍然在webpack.js文件

const createCompiler = (rawOptions) => {
  // ...省略代码
  const compiler = new Compiler(options.context)
  compiler.options = options
  //应用Node的文件系统到compiler对象,方便后续的文件查找和读取
  new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging,
  }).apply(compiler)
  // 加载插件
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      //  依次调用插件的apply方法(默认每个插件对象实例都需要提供一个apply)若为函数则直接调用,将compiler实例作为参数传入,方便插件调用此次构建提供的Webpack API并监听后续的所有事件Hook。
      if (typeof plugin === 'function') {
        plugin.call(compiler, compiler)
      } else {
        plugin.apply(compiler)
      }
    }
  }
  // 应用默认的Webpack配置
  applyWebpackOptionsDefaults(options)
  // 随即之后,触发一些Hook
  compiler.hooks.environment.call()
  compiler.hooks.afterEnvironment.call()
  // 内置的Plugin的引入,对webpack options进行初始化
  new WebpackOptionsApply().process(options, compiler)
  compiler.hooks.initialize.call()
  return compiler
}


编译阶段


  1. 启动编译(run/watch 阶段)

这里有个小逻辑区分是否是 watch,如果是非 watch,则会正常执行一次 compiler.run()

如果是监听文件(如:--watch)的模式,则会传递监听的 watchOptions,生成 Watching 实例,每次变化都重新触发回调。

如果不是监视模式就调用 Compiler 对象的 run 方法,befornRun->beforeCompile->compile->thisCompilation->compilation开始构建整个应用。

const { SyncHook, SyncBailHook, AsyncSeriesHook } = require('tapable')
class Compiler {
  constructor() {
    // 1. 定义生命周期钩子
    this.hooks = Object.freeze({
      // ...只列举几个常用的常见钩子,更多hook就不列举了,有兴趣看源码
      done: new AsyncSeriesHook(['stats']), //一次编译完成后执行,回调参数:stats
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']), //在编译器开始读取记录前执行
      emit: new AsyncSeriesHook(['compilation']), //在生成文件到output目录之前执行,回调参数:compilation
      afterEmit: new AsyncSeriesHook(['compilation']), //在生成文件到output目录之后执行
      compilation: new SyncHook(['compilation', 'params']), //在一次compilation创建后执行插件
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']), //在一个新的compilation创建之前执行
      make: new AsyncParallelHook(['compilation']), //完成一次编译之前执行
      afterCompile: new AsyncSeriesHook(['compilation']),
      watchRun: new AsyncSeriesHook(['compiler']),
      failed: new SyncHook(['error']),
      watchClose: new SyncHook([]),
      afterPlugins: new SyncHook(['compiler']),
      entryOption: new SyncBailHook(['context', 'entry']),
    })
    // ...省略代码
  }
  newCompilation() {
    // 创建Compilation对象回调compilation相关钩子
    const compilation = new Compilation(this)
    //...一系列操作
    this.hooks.compilation.call(compilation, params) //compilation对象创建完成
    return compilation
  }
  watch() {
    //如果运行在watch模式则执行watch方法,否则执行run方法
    if (this.running) {
      return handler(new ConcurrentCompilationError())
    }
    this.running = true
    this.watchMode = true
    return new Watching(this, watchOptions, handler)
  }
  run(callback) {
    if (this.running) {
      return callback(new ConcurrentCompilationError())
    }
    this.running = true
    process.nextTick(() => {
      this.emitAssets(compilation, (err) => {
        if (err) {
          // 在编译和输出的流程中遇到异常时,会触发 failed 事件
          this.hooks.failed.call(err)
        }
        if (compilation.hooks.needAdditionalPass.call()) {
          // ...
          // done:完成编译
          this.hooks.done.callAsync(stats, (err) => {
            // 创建compilation对象之前
            this.compile(onCompiled)
          })
        }
        this.emitRecords((err) => {
          this.hooks.done.callAsync(stats, (err) => {})
        })
      })
    })
    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.readRecords((err) => {
          this.compile(onCompiled)
        })
      })
    })
  }
  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, (err) => {
      this.hooks.compile.call(params)
      //已完成complication的实例化
      const compilation = this.newCompilation(params)
      //触发make事件并调用addEntry,找到入口js,进行下一步
      // make:表示一个新的complication创建完毕
      this.hooks.make.callAsync(compilation, (err) => {
        process.nextTick(() => {
          compilation.finish((err) => {
            // 封装构建结果(seal),逐次对每个module和chunk进行整理,每个chunk对应一个入口文件
            compilation.seal((err) => {
              this.hooks.afterCompile.callAsync(compilation, (err) => {
                // 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,
                // 不然运行流程将会一直卡在这不往下执行
                return callback(null, compilation)
              })
            })
          })
        })
      })
    })
  }
  emitAssets() {}
}
  1. 编译模块:(make 阶段)
  • 从 entry 入口配置文件出发, 调用所有配置的 Loader 对模块进行处理,
  • 再找出该模块依赖的模块, 通过 acorn 库生成模块代码的 AST 语法树,形成依赖关系树(每个模块被处理后的最终内容以及它们之间的依赖关系),
  • 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环每个依赖;再递归本步骤直到所有入口依赖的文件都经过了对应的 loader 处理。
  • 解析结束后,webpack 会把所有模块封装在一个函数里,并放入一个名为 modules 的数组里。
  • modules 传入一个自执行函数中,自执行函数包含一个 installedModules 对象,已经执行的代码模块会保存在此对象中。
  • 最后自执行函数中加载函数(webpack__require)载入模块。
class Compilation extends Tapable {
 constructor(compiler) {
  super();
  this.hooks = {};
  // ...
  this.compiler = compiler;
  // ...
  // 构建生成的资源
  this.chunks = [];
  this.chunkGroups = [];
  this.modules = [];
  this.additionalChunkAssets = [];
  this.assets = {};
  this.children = [];
  // ...
 }
 //
 buildModule(module, optional, origin, dependencies, thisCallback) {
  // ...
  // 调用module.build方法进行编译代码,build中 其实是利用acorn编译生成AST
  this.hooks.buildModule.call(module);
  module.build( /**param*/ );
 }
 // 将模块添加到列表中,并编译模块
 _addModuleChain(context, dependency, onModule, callback) {
  // ...
  // moduleFactory.create创建模块,这里会先利用loader处理文件,然后生成模块对象
  moduleFactory.create({
   contextInfo: {
    issuer: "",
    compiler: this.compiler.name
   },
   context: context,
   dependencies: [dependency]
  }, (err, module) = > {
   const addModuleResult = this.addModule(module);
   module = addModuleResult.module;
   onModule(module);
   dependency.module = module;
   // ...
   // 调用buildModule编译模块
   this.buildModule(module, false, null, null, err = > {});
  });
 }
 // 添加入口模块,开始编译&构建
 addEntry(context, entry, name, callback) {
  // ...
  this._addModuleChain( // 调用_addModuleChain添加模块
  context, entry, module = > {
   this.entries.push(module);
  },
  // ...
  );
 }
 seal(callback) {
  this.hooks.seal.call();
  // ...
  //完成了Chunk的构建和依赖、Chunk、module等各方面的优化
  const chunk = this.addChunk(name);
  const entrypoint = new Entrypoint(name);
  entrypoint.setRuntimeChunk(chunk);
  entrypoint.addOrigin(null, name, preparedEntrypoint.request);
  this.namedChunkGroups.set(name, entrypoint);
  this.entrypoints.set(name, entrypoint);
  this.chunkGroups.push(entrypoint);
  GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
  GraphHelpers.connectChunkAndModule(chunk, module);
  chunk.entryModule = module;
  chunk.name = name;
  // ...
  this.hooks.beforeHash.call();
  this.createHash();
  this.hooks.afterHash.call();
  this.hooks.beforeModuleAssets.call();
  this.createModuleAssets();
  if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
   this.hooks.beforeChunkAssets.call();
   this.createChunkAssets();
  }
  // ...
 }
 createHash() {
  // ...
 }
 // 生成 assets 资源并 保存到 Compilation.assets 中 给webpack写插件的时候会用到
 createModuleAssets() {
  for (let i = 0; i < this.modules.length; i++) {
   const module = this.modules[i];
   if (module.buildInfo.assets) {
    for (const assetName of Object.keys(module.buildInfo.assets)) {
     const fileName = this.getPath(assetName);
     this.assets[fileName] = module.buildInfo.assets[assetName];
     this.hooks.moduleAsset.call(module, fileName);
    }
   }
  }
 }
 createChunkAssets() {
  asyncLib.forEach(
  this.chunks, (chunk, callback) = > {
   // manifest是数组结构,每个manifest元素都提供了 `render` 方法,提供后续的源码字符串生成服务。至于render方法何时初始化的,在`./lib/MainTemplate.js`中
   let manifest = this.getRenderManifest()
   asyncLib.forEach(
   manifest, (fileManifest, callback) = > {...
    source = fileManifest.render()
    this.emitAsset(file, source, assetInfo)
   }, callback)
  }, callback)
 }
}
class SingleEntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      'SingleEntryPlugin',
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          SingleEntryDependency,
          normalModuleFactory
        )
      }
    )
    compiler.hooks.make.tapAsync(
      'SingleEntryPlugin',
      (compilation, callback) => {
        const { entry, name, context } = this
        const dep = SingleEntryPlugin.createDependency(entry, name)
        compilation.addEntry(context, dep, name, callback)
      }
    )
  }
  static createDependency(entry, name) {
    const dep = new SingleEntryDependency(entry)
    dep.loc = { name }
    return dep
  }
}

概括一下 make 阶段单入口打包的流程,大致为 4 步骤

  1. 执行 SingleEntryPlugin(单入口调用 SingleEntryPlugin,多入口调用 MultiEntryPlugin,异步调用 DynamicEntryPlugin),EntryPlugin 方法中调用了 Compilation.addEntry 方法,添加入口模块,开始编译&构建
  2. addEntry 中调用 _addModuleChain,将模块添加到依赖列表中,并编译模块
  3. 然后在 buildModule 方法中,调用了 NormalModule.build,创建模块之时,会调用 runLoaders,执行 Loader,利用 acorn 编译生成 AST
  4. 分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的 require 语法替换成 webpack_require 来模拟模块化操作。


从源码的角度,思考一下, loader 为什么是自右向左执行的,loader 中有 pitch 也会从右到左执行的么?


runLoaders 方法调用 iteratePitchingLoaders 去递归查找执行有 pich 属性的 loader ;若存在多个 pitch 属性的 loader 则依次执行所有带 pitch 属性的 loader ,执行完后逆向执行所有带 pitch 属性的 normalnormal loader 后返回 result,没有 pitch 属性的 loader 就不会再执行;若 loaders 中没有 pitch 属性的 loader 则逆向执行 loader;执行正常 loader 是在 iterateNormalLoaders 方法完成的,处理完所有 loader 后返回 result

出自文章你真的掌握了 loader 么?- loader 十问(https://juejin.im/post/5bc1a73df265da0a8d36b74f)


目录
相关文章
|
缓存 资源调度 编译器
原来是这样啊!浅谈webpack4和webpack5的区别
相对于webpack4,webpack5内置了很多plugin插件,比如、打包、压缩、缓存
772 1
|
JavaScript
webpack4主流程源码阅读,以及动手实现一个简单的webpack(二)
webpack4主流程源码阅读,以及动手实现一个简单的webpack
132 0
|
1月前
|
缓存 前端开发 JavaScript
Webpack 4 和 Webpack 5 区别?
【10月更文挑战第23天】随着时间的推移,Webpack 可能会继续发展和演进,未来的版本可能会带来更多的新特性和改进。保持对技术发展的关注和学习,将有助于我们更好地应对不断变化的前端开发环境。
|
7月前
|
前端开发 JavaScript 安全
【网络安全】WebPack源码(前端源码)泄露 + jsmap文件还原
【网络安全】WebPack源码(前端源码)泄露 + jsmap文件还原
1134 0
|
JavaScript 前端开发 API
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(二)
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader
124 0
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(二)
|
缓存 前端开发 JavaScript
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(一)
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader
122 0
|
缓存
webpack原理篇(五十二):webpack-cli源码阅读
webpack原理篇(五十二):webpack-cli源码阅读
151 0
webpack原理篇(五十二):webpack-cli源码阅读
|
开发工具 git Windows
webpack 4升级到 webpack 5 (node 14.6 升级到 node16 引发的问题)
第一次启动项目报错,报错内容是 Node Sass does not yet support your current environment: Windows 64-bit, 这个问题相信大家都清楚, node-sass 出问题了
webpack 4升级到 webpack 5 (node 14.6 升级到 node16 引发的问题)
|
移动开发 前端开发 JavaScript
阅读 webpack 源码简单了解打包过程
通过阅读 webpack 源码简单了解打包过程~
288 0
|
3月前
|
JavaScript
webpack打包TS
webpack打包TS
138 60