• webpack的编译流程


    Question 2: webpack的编译流程是啥?

    应该会有面试官这样问过你:

    • webpack了解多少?
    • 对webpack的编译原理了解吗?
    • 写过webpack插件吗?
    • 列举webpack编译流程中的hook节点

    这些问题其实都可以被看作是同一个问题,那就是面试官在问你:你对webpack的编译流程了解多少?

    来总结一下我听到过的答案,尽量完全复原候选人面试的时候说的原话。

    答案1: webpack就是通过loader来加载资源,通过插件进行修改,最后打包生成bundles.js

    答案2: Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

        1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
        2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
        3. 确定入口: 根据配置中的 entry 找出所有的入口文件;
        4. 编译模块: 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模
          块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
        5. 完成模块编译: 在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内
          容以及它们之间的依赖关系;
        6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个
        Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
        7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系
        统。
        
        
    复制代码

    哇哦! 这说起来一套一套的,有备而来啊! 这是从哪背的书?这时候,敢这么回答,那一定是作死现场。因为面试官一定会衍生出一系列问题,把你问哭。例如:

    • 你刚说的输出列表是什么? - 其实是在问chunks是挂在哪个对象上。
    • 入口模块是怎么处理的? - 其实在问如何生成入口模块的
    • 最后是怎么把文件内容写入到文件系统的? - 其实是在问compiler是如何具备文件读写能力的
    • 什么时候构建阶段开始? - 其实是在问webpack开始构建之前做了哪些构建准备
    • 你说的调用Loader对模块进行翻译是如何做到的? - 其实是在问在哪个阶段处理loader的
    • ……

    所以,你感觉快哭了没?我给你讲一个我的答案可好?

    面试其实是门很纠结的艺术。webpack东西那么多,不是三两句就能讲明白的。 所以如何花最少的时间尽量多的讲清楚内容,就是需要你认真琢磨的事情了。

    答案3: 先把关键点说清楚,带源码那种,而且是越直观越好。以下1-4点花30秒说完吧,毕竟是说给面试官听。

    1. 初始化参数,webpack.config.jsmodule.export ,结合默认参数,merge出最终的参数。
    2. 开始编译,通过初始化参数来实例化 Compiler对象,加载所有配置的插件,执行对象的run方法。
    3. 确认入口文件。
    4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行加载,再找出该模块依赖的模块。通过递归这个过程直到所有入口文件都经过处理,得到一条依赖线。
    5. webpack.js 中核心的操作就是 requirenode_modules/webpack-cli/bin/cli.js
      • cli.js
        • 01 当前文件一般有二个操作,处理参数,将参数交给不同的逻辑(分发业务)
        • 02 options (初始化参数)
        • 03 complier (实例化 Compiler对象)
        • 04 complier.run( 那 run 里面做了什么,后续再看 )
      • 实例化complier对象,complier会贯穿整个webpack工作流的过程。
        • complier继承Tabable,所以complier具有操作钩子的能力。例如监听、触发事件,而webpack是个事件流。
        • 实例话complier对象时,会把很多属性挂载上去。其中NodeEnvironmentPlugin让complier具备文件读写的能力。
        • plugins中的插件都挂载到了cpmplier身上
        • 将内部默认的pluginscomplier建立联系,其中有个EntryOptionsPlugin处理了入口模块的id
        • webpack/lib/SingleEntryPlugin.js里,compiler监听了make钩子
          • singleEntryPlugin.js 模块的apply方法中有两个钩子监听
          • dep = SingleEntryPlugin.createDependency(entry,name)
          • 其中compilation钩子就是让compilation具备了利用normalModuleFactory工厂创建一个普通模块的能力。因为compilation就是利用自己创建出来的模块来加载需要被打包的模块。
          • 其中make钩子在Compiler.run 的时候会被调用,到这里就意味着某个模块执行打包之前的所有准备工作就做完了。
          • 然后Compilation调用addEntry就标志着make构建阶段开始了。
        • run方法的执行
          • 刚说了Compiler.run方法执行会调用make钩子,那run方法里就是有一堆钩子按着顺序触发,例如 beforeRunruncompile
          • compile 方法的执行
            • 先准备些参数,例如刚才提到的 normalModuleFactory,用于后续创建模块。
            • 触发 beforeCompile
            • 将准备参数传入一个方法(newCompilation),用于创建一个compilation。在 newCompilation 内部,先调用 createCompilation,然后触发this.compilation钩子和compilation 钩子的监听
          • 创建了compilation对象之后就触发了make钩子。当触发 make 钩子监听的时候,会将 compilation 对象传入。 compilation.addEntry 就意味着make构建阶段开始。
          • make 钩子被触发,接收到 compilation 对象,那么从 compilation 可以解构出三个值。entry:当前被打包模块的相对路径,namecontext:当前项目的跟路径
          • processDependencies 处理模块间的依赖关系。函数内部通过async.forEach来递归创建每个被加载进来的模块。
          • compilation 调用 addEntry 方法,内部调用_addModuleChain方法去处理依赖。
          • compilation 当中可以通过 normalModuleFactory 工厂来创建一个普通的模块对象。 webpack 内部默认开启来一个100并发量的打包操作. 源码里看到的是normalModuleFactory.create这样一个方法。
          • 然后在 beforeResolve 方法里会触发一个 factory 钩子监听。上述操作完成后,factory 获取到一个函数并对其进行调用。函数中又有一个resolver钩子被触发,resolver其实是处理loader当触发resolver钩子,就意味着所有的Loader处理完毕。
          • 接下里就会触发 afterResolve 这个钩子,调用 new NormalModule
          • 最后就是调用 buildModule 方法开始编译 -> 调用build -> 调用doBuild。bulid过程中会将js代码转化成ast语法树,如果当前js模块引用了其它模块,那就需要递归重复 bulid。当前所有入口模块都被存放在 compilation 对象的 entries 数组里。
          • 那我还需要对当前模块的ast语法树进行一些修改,再转化回js代码。例如将require转化成__webpack_require__
          • 最后compile方法最后调用compilation.seal方法去处理chunk。 生成代码内容,最终输出文件到指定打包路径下。

    唉。 哭了没。 面试的时候千万别讲这么细,因为面试官也细节不到这个程度。所以,你要讲的话,就把你认为重要话术整理出来吧。

    接下来直接上代码,我们自己来实现webpack编译流程

    • run.js
    let webpack = require('./youWebpack')
    let options = require('./webpack.config')
    
    let compiler = webpack(options); // webpack 初始化 webpack.config.js 的 module.exports 
    
    // 执行run方法
    compiler.run((err, stats) => {
      console.log(err)
      console.log(stats)
    })
    复制代码
    • 既然run方法require了 youWebpack,那就得写出来不是。
    /*
    * youWebpack.js
    */ 
    
    const Compiler = require('./Compiler'); 
    const NodeEnvironmentPlugin = require('./NodeEnvironmentPlugin')
    const WebpackOptionsApply = require('./WebpackOptionsApply')
    
    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 内置的插件(入口)
      new WebpackOptionsApply().process(options, compiler);
    
      // 05 返回 compiler 对象即可
      return compiler
    }
    
    module.exports = webpack
    复制代码
    • 你细品,youWebpack里有require了 Compiler、NodeEnvironmentPlugin、WebpackOptionsApply。 没办法,继续得写出来。
    /*
    * 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
    
    复制代码
    /*
    * NodeEnvironmentPlugin.js
    */
    const fs = require('fs'); // webpack为提升文件读写性能, 源码里是对 node 的 fs 模块进行了二次封装的。我们这勉强够用,就不封装了。 /捂脸
    
    class NodeEnvironmentPlugin {
      constructor(options) {
        this.options = options || {}
      }
    
      apply(complier) {
        complier.inputFileSystem = fs
        complier.outputFileSystem = fs
      }
    }
    
    module.exports = NodeEnvironmentPlugin
    
    复制代码

    Compiler,webpack 核心之一的Compiler来了。

    /*
    * Compiler
    */
    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')
    const { emit } = require('process')
    
    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 创建dist  02 在目录创建完成之后执行文件的写操作
    
        // 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) {
        console.log('run 方法执行了~~~~')
    
        const finalCallback = function (err, stats) {
          callback(err, stats)
        }
    
        const onCompiled = (err, compilation) => {
    
          // 最终在这里将处理好的 chunk 写入到指定的文件然后输出至 dist 
          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) {
        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)
    
            // 在这里我们开始处理 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 = this.createCompilation()
        this.hooks.thisCompilation.call(compilation, params)
        this.hooks.compilation.call(compilation, params)
        return compilation
      }
    
      createCompilation() {
        return new Compilation(this)
      }
    }
    
    module.exports = Compiler
    
    复制代码
    • You look. 你还得自己实现 NormalModuleFactoryCompilationStats
    /*
    * Stats 其实看代码就能明白,Stats只是将compilation身上挂载的 入口模块、模块内容、chunks、文件目录等拿了出来。 这里可以回头看看run 方法
    */
    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
    
    复制代码
    /*
    * NormalModuleFactory
    */
    
    const NormalModule = require("./NormalModule"); 
    
    class NormalModuleFactory {
      create(data) {
        return new NormalModule(data)
      }
      // 源码里头还实现了其它方法,所以这里不要嫌弃为什么又要单独require一个 NormalModule
    }
    
    module.exports = NormalModuleFactory
    
    复制代码

    webpack 核心之一 Compilation,至于它干嘛的?请你回头看看Stats就明白了。

    
    const ejs = require('ejs')
    const Chunk = require('./Chunk')
    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.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: 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) => {
          // 在 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) {
        // 1 当前的函数核心功能就是实现一个被依赖模块的递归加载
        // 2 加载模块的思想都是创建一个模块,然后想办法将被加载模块的内容拿进来?
        // 3 当前我们不知道 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)
      }
    
      seal(callback) {
        this.hooks.seal.call()
        this.hooks.beforeChunks.call()
    
        // 1 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
        // 2 所谓封装 chunk 指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并
    
        for (const entryModule of this.) {
          // 核心: 创建模块加载已有模块的内容,同时记录模块信息 
          const chunk = new Chunk(entryModule)
    
          // 保存 chunk 信息
          this.chunks.push(chunk)
    
          // 给 chunk 属性赋值 
          chunk.modules = this.modules.filter(module => module.name === chunk.name)
    
        }
    
        // chunk 流程梳理之后就进入到 chunk 代码处理环节(模板文件 + 模块中的源代码==》chunk.js)
        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)
    
          // 1 获取模板文件的路径
          let tempPath = path.posix.join(__dirname, 'temp/main.ejs')
          // 2 读取模块文件中的内容
          let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
          // 3 获取渲染函数
          let tempRender = ejs.compile(tempCode)
          // 4 按ejs的语法渲染数据
          let 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
    
    
    复制代码

    还有部分就不写了。 像 Parser、 Chunk。

    其实,主要是为懒,写得太累了, 我得去觅食了。好了,开玩笑。主要webpack整个编译过程到这应该就完全明白了。。

    小结一下

    webpack编译过程是啥? 代码里应该体现得非常清楚了。

    step1: 实例化compiler

    1. 实例化 compiler 对象
    2. 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
    3. 挂载所有 plugins 插件至 compiler 对象身上
    4. 挂载所有 webpack 内置的插件(入口)

    step2: compiler.run

    1. this.hooks.beforeRun.callAsync -> this.hooks.run.callAsync -> this.compile
      • this.compile 接收 onCompiled

      • onCompiled 内容是: 最终在这里将处理好的 chunk 写入到指定的文件然后输出至 dist (文件输出路径,不一定是dist)

    step3: compile方法做的事情

    1. newCompilationParams,实例化Compilation对象之前先初始化其所需参数
    2. 调用this.hooks.beforeRun.callAsync
      • this.newCompilation(params) 实例化Compilation对象
      • this.hooks.make.callAsync 触发make钩子监听
      • compilation.seal 开始处理 chunk
        • this.hooks.afterCompile.callAsync(compilation,...)
        • 流程进入compilation了。。。

    step4: 完成模块编译操作

    1. addEntry
      • _addModuleChain
        • createModule:定义一个创建模块的方法,达到复用的目的
          • module = normalModuleFactory.create(data) : 创建普通模块,目的是用来加载js模块
          • afterBuild
            • this.processDependencies : 找到模块与模块之间的依赖关系
            • this.buildModule(module, afterBuild)
              • module.build : 到这里就意味着当前 Module 的编译完成了
    2. seal: 生成代码内容,输出文件

    Over.


    作者:在剥我的壳
    链接:https://juejin.cn/post/6972378623281987621
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    虚心学习、丰富自己
  • 相关阅读:
    js检测对象是否是数组的三种方法
    mongdb查询数据并且返回数据条数
    mongdb数据库的操作
    NodeJs运行服务器-day01
    html5新增的定时器requestAnimationFrame
    vue 中scroll事件不触发问题
    Node.js快速生成26个字母
    Node.js fs文件系统模块
    Node.js 创建server服务器
    JavaScript exec()方法
  • 原文地址:https://www.cnblogs.com/tkqq000/p/14876174.html
Copyright © 2020-2023  润新知