webpack / 前端 / 小程序 / 小程序架构 · 2020-10-15 0

webpack的打包流程

初始化

// webpack.js
const webpack = (options, callback) => {
  let compiler

  // 补全默认配置
  options = new WebpackOptionsDefaulter().process(options)

  // 创建 compiler 对象
  compiler = new Compiler(options.context)
  compiler.options = options

  // 应用用户通过 webpack.config.js 配置或命令行参数传递的插件
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler)
    }
  }

  // 根据配置,应用 webpack 内置插件
  compiler.options = new WebpackOptionsApply().process(options, compiler)

  // compiler 启动
  // 这个方法调用了 compile 方法,而 compile 触发了 make 这个事件,控制权转移到 compilation
  compiler.run(callback)
  return compiler
}
  • npx webpack//npx为npm5.x自带
  • webpack 会解析 webpack.config.js 文件,以及命令行参数,将其中的配置和参数合成一个 options 对象
  • 创建了 compiler 对象,并将完整的配置参数 options 保存到 compiler 对象中 — 这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin
  • 执行plugins的apply
    • 用户配置的 plugin 先于内置的 plugin 被应用。
  • 最后调用了 compiler 的 run 方法

关于 WebpackOptionsApply.process

// WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
  process(options, compiler) {
    new EntryOptionPlugin().apply(compiler)
    compiler.hooks.entryOption.call(options.context, options.entry)
  }
}
  • WebpackOptionsApply (.process)应用了 EntryOptionPlugin 插件并立即触发了 compiler 的 entryOption 事件钩子 (entryOption:钩子事件在new EntryOptionPlugin().apply(compiler)执行后绑定)
// EntryOptionPlugin.js
const itemToPlugin = (context, item, name) => {
  if (Array.isArray(item)) {
    return new MultiEntryPlugin(context, item, name)
  }
  return new SingleEntryPlugin(context, item, name)
}

module.exports = class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      if (typeof entry === 'string' || Array.isArray(entry)) {
        // 如果没有指定入口的名字,那么默认为 main
        itemToPlugin(context, entry, 'main').apply(compiler)
      } else if (typeof entry === 'object') {
        for (const name of Object.keys(entry)) {
          itemToPlugin(context, entry[name], name).apply(compiler)
        }
      } else if (typeof entry === 'function') {
        new DynamicEntryPlugin(context, entry).apply(compiler)
      }
      // 注意这里返回了 true,
      return true
    })
  }
}
  • 而 EntryOptionPlugin 内部则注册了对 entryOption 事件钩子的监听
  • entryOption 是个 SyncBailHook, 意味着只要有一个插件返回了 true, 注册在这个钩子上的后续插件代码,将不会被调用。我们在编写小程序插件时,用到了这个特性
    • SyncBailHook同步熔断保险钩子,即return一个非undefined的值,则不再继续执行后面的监听函数
// SingleEntryPlugin.js
class SingleEntryPlugin {
  constructor(context, entry, name) {
    this.context = context
    this.entry = entry
    this.name = name
  }

  apply(compiler) {
    //触发时机: compiler 在 run 方法中调用了 compile 方法,在该方法中创建了 compilation 对象
    compiler.hooks.compilation.tap('SingleEntryPlugin', (compilation, { normalModuleFactory }) => {
      // 设置 dependency 和 module 工厂之间的映射关系
      compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
    })

    // 触发时机:compiler 创建 compilation 对象后,触发 make 事件
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const { entry, name, context } = this

      // 根据入口文件和名称创建 Dependency 对象
      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
  }
}
  • compiler.hooks.compilation.tap
    • 触发时机: compiler 在 run 方法中调用了 compile 方法,在该方法中创建了 compilation 对象时触发(参考Compiler.js)
  • compiler.hooks.make.tapAsync
    • compiler 创建 compilation 对象后,触发 make 事件
// Compiler.js
class Compiler extends Tapable {
  run(callback) {
    const onCompiled = (err, compilation) => {
      // ...
    }
    // 调用 compile 方法
    this.compile(onCompiled)
  }

  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.compile.call(params)
    // 创建 compilation 对象
    const compilation = this.newCompilation(params)
    // 触发 make 事件钩子,控制权转移到 compilation,开始编译流程
    this.hooks.make.callAsync(compilation, err => {
      // ...
    })
  }

  newCompilation(params) {
    const compilation = this.createCompilation()
    // compilation 对象创建后,触发 compilation 事件钩子
    // 如果想要监听 compilation 中的事件,这是个好时机
    this.hooks.compilation.call(compilation, params)
    return compilation
  }
}
  • webpack->
  • compiler 对象代表了完整的 webpack 环境配置
  • 而 compilatoin 对象则负责整个打包过程 , 它存储着打包过程的中间产物
  • compiler 对象触发 make 事件后,控制权就会转移到 compilation,compilation 通过调用 addEntry 方法,开始了编译与构建主流程

自定义一个插件

class MinaWebpackPlugin {
  constructor() {
    this.entries = []
  }

  // apply 是每一个插件的入口
  apply(compiler) {
    const { context, entry } = compiler.options
    // 找到所有的入口文件,存放在 entries 里面

    // 这里订阅了 compiler 的 entryOption 事件,当事件发生时,就会执行回调里的代码
    compiler.hooks.entryOption.tap('MinaWebpackPlugin', () => {
      
       xxxxx 执行操作
   
      // 返回 true 告诉 webpack 内置插件就不要处理入口文件了,因为这里已经处理了
      return true
    })
  }

module 构建阶段–loader的使用

class Compilation extends Tapable {
  // 如果有留意 SingleEntryPlugin 的源码,应该知道这里的 entry 不是字符串,而是 Dependency 对象
  // 根据entry(依赖dependency)获取对应模块工厂(moduleFactory),
  //通过模块工厂(moduleFactory.create())创建对一个模块
  addEntry(context, entry, name, callback) {
    
    this._addModuleChain(context, entry, onModule, callbak)
  }

  _addModuleChain(context, dependency, onModule, callback) {
    const Dep = dependency.constructor
    // 获取模块对应的工厂,这个映射关系在 SingleEntryPlugin 中有设置
    const moduleFactory = this.dependencyFactories.get(Dep)
    // 通过工厂创建模块
    moduleFactory.create(/*
      在这个方法的回调中,
      调用 this.buildModule 来构建模块。
      构建完成后,调用 this.processModuleDependencies 来处理模块的依赖
      这是一个循环和递归过程,通过依赖获得它对应的模块工厂来构建子模块,直到把所有的子模块都构建完成
    */)
  }

  buildModule(module, optional, origin, dependencies, thisCallback) {
    // 构建模块
    module.build(/*
    而构建模块作为最耗时的一步,又可细化为三步:
    1. 调用各 loader 处理模块之间的依赖
    2. 解析经 loader 处理后的源文件生成抽象语法树 AST
    3. 遍历 AST,获取 module 的依赖,结果会存放在 module 的 dependencies 属性中
    */)
  }
}
  • SingleEntryPlugin.apply中定义 compiler.hooks.compilation.tap 的钩子(run时调用)设置了dependency 和 module 工厂之间的映射关系
    • compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
  • SingleEntryPlugin.apply中定义 compiler.hooks.make.tapAsync的钩子(run时调用)设置了 compilation.addEntry(context, dep, name, callback) 随着这个方法被调用,将会开启编译流程 (addEntry步骤)
    • 根据entry(依赖dependency)获取对应模块工厂(const moduleFactory = this.dependencyFactories.get(Dep) ),//映射关系如上 dependencyFactories
    • 通过模块工厂(moduleFactory.create())创建对一个模块
      • 在这个方法( create )的回调中 调用 this.buildModule 来构建模块。( 步骤:this.buildModule )
        • 调用 module.build() // 构建模块 (步骤:module.build())而构建模块作为最耗时的一步,又可细化为三步 ()
          • 调用各 loader 处理模块之间的依赖
          • 解析经 loader 处理后的源文件生成抽象语法树 AST
          • 遍历 AST,获取 module 的依赖,结果会存放在 module 的 dependencies 属性中
      • 构建完成后,调用 this.processModuleDependencies 来处理模块的依赖
      • 总的来说:以上这是一个循环和递归过程,通过依赖获得它对应的模块工厂来构建子模块,直到把所有的子模块都构建完成

chunk 生成阶段 — 在所有的模块构建完成后开始生成 chunks

webpack 调用 compilation.seal 方法

class Compiler extends Tapable {
  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.compile.call(params)
    // 创建 compilation 对象
    const compilation = this.newCompilation(params)
    // 触发 make 事件钩子,控制权转移到 compilation,开始编译流程
    this.hooks.make.callAsync(compilation, err => {
      // 编译和构建流程结束后,回到这里,
      compilation.seal(err => {})
    })
  }
}
// https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L1188
class Compilation extends Tapable {
  seal(callback) {
    // _preparedEntrypoints 在 addEntry 方法中被填充,
    //它存放着 entry 名称和对应的 entry module
    // 将 entry 中对应的 module 都生成一个新的 chunk
    for (const preparedEntrypoint of this._preparedEntrypoints) {
      const module = preparedEntrypoint.module
      const name = preparedEntrypoint.name
      // 创建 chunk
      const chunk = this.addChunk(name)
      // Entrypoint 继承于 ChunkGroup
      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)

      // 建立 chunk 和 chunkGroup 之间的关系
      GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk)
      // 建立 chunk 和 module 之间的关系
      GraphHelpers.connectChunkAndModule(chunk, module)

      // 表明这个 chunk 是通过 entry 生成的
      chunk.entryModule = module
      chunk.name = name

      this.assignDepth(module)
    }
    // 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
    this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice())

    // 优化
    // SplitChunksPlugin 会监听 optimizeChunksAdvanced 事件,
    //抽取公共模块,形成新的 chunk
    // RuntimeChunkPlugin 会监听 optimizeChunksAdvanced 事件,
    // 抽离 runtime chunk
    while (
      this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
      this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
      this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
    ) {
      /* empty */
    }
    this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups)

    // 哈希
    this.createHash()

    // 通过这个钩子,可以在生成 assets 之前修改 chunks,我们后面会用到
    this.hooks.beforeChunkAssets.call()

    // 通过 chunks 生成 assets
    this.createChunkAssets()
  }
}

assets 渲染阶段

通过 chunks 生成 assets

class Compilation extends Tapable {
  // https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L2373
  createChunkAssets() {
    // 每一个 chunk 会被渲染成一个 asset
    for (let i = 0; i < this.chunks.length; i++) {
      // 如果 chunk 包含 webpack runtime 代码,
      // 就用 mainTemplate 来渲染,否则用 chunkTemplate 来渲染
      // 关于什么是 webpack runtime,后续我们在优化小程序 webpack 插件时会讲到
      const template = chunk.hasRuntime() ? 
      this.mainTemplate : this.chunkTemplate
    }
  }
}
// https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js
class MainTemplate extends Tapable {
  constructor(outputOptions) {
    super()
    this.hooks = {
      bootstrap: new SyncWaterfallHook(['source', 'chunk', 'hash', 'moduleTemplate', 'dependencyTemplates']),
      render: new SyncWaterfallHook(['source', 'chunk', 'hash', 'moduleTemplate', 'dependencyTemplates']),
      renderWithEntry: new SyncWaterfallHook(['source', 'chunk', 'hash']),
    }
    // 自身监听了 render 事件
    this.hooks.render.tap('MainTemplate', (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
      const source = new ConcatSource()
      //  拼接 runtime 源码
      source.add(new PrefixSource('/******/', bootstrapSource))

      // 拼接 module 源码,mainTemplate 把渲染模块代码的职责委托给了
       moduleTemplate,自身只负责生成 runtime 代码
      source.add(this.hooks.modules.call(new RawSource(''), chunk, hash, moduleTemplate, dependencyTemplates))

      return source
    })
  }

  // 这是 Template 的入口方法
  render(hash, chunk, moduleTemplate, dependencyTemplates) {
    // 生成 runtime 代码
    const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates)

    // 触发 render 事件,请注意 MainTemplate 自身在构造函数中监听了这一事件,
       完成了对 runtime 代码和 module 代码的拼接
    let source = this.hooks.render.call(
      // 传入 runtime 代码
      new OriginalSource(Template.prefix(buf, ' \t') + '\n', 'webpack/bootstrap'),
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates,
    )

    // 对于每一个入口 module, 即通过 compilation.addEntry 添加的模块
    if (chunk.hasEntryModule()) {
      // 触发 renderWithEntry 事件,让我们有机会修改生成后的代码
      source = this.hooks.renderWithEntry.call(source, chunk, hash)
    }
    return new ConcatSource(source, ';')
  }

  renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates) {
    const buf = []
    // 通过 bootstrap 这个钩子,用户可以添加自己的 runtime 代码
    buf.push(this.hooks.bootstrap.call('', chunk, hash, moduleTemplate, dependencyTemplates))
    return buf
  }
}
  • 所谓渲染就是生成代码的过程 , 渲染就是拼接和替换字符串的过程
  • 最终渲染好的代码会存放在 compilation 的 assets 属性中。

输出文件阶段

// https://github.com/webpack/webpack/blob/master/lib/Compiler.js
class Compiler extends Tapable {
  run(callback) {
    const onCompiled = (err, compilation) => {
      // 输出文件
      this.emitAssets(compilation, err => {})
    }
    // 调用 compile 方法
    this.compile(onCompiled)
  }

  emitAssets(compilation, callback) {
    const emitFiles = err => {}
    // 在输入文件之前,触发 emit 事件,这是最后可以修改 assets 的机会了
    this.hooks.emit.callAsync(compilation, err => {
      outputPath = compilation.getPath(this.outputPath)
      this.outputFileSystem.mkdirp(outputPath, emitFiles)
    })
  }

  compile(onCompiled) {
    const params = this.newCompilationParams()
    this.hooks.compile.call(params)
    // 创建 compilation 对象
    const compilation = this.newCompilation(params)
    // 触发 make 事件钩子,控制权转移到 compilation,开始编译 module
    this.hooks.make.callAsync(compilation, err => {
      // 模块编译和构建完成后,开始生成 chunks 和 assets
      compilation.seal(err => {
        // chunks 和 assets 生成后,调用 emitAssets
        return onCompiled(null, compilation)
      })
    })
  }
}

总结流程: http://note.youdao.com/s/I7zTZzEx

1.npx webpack
2.options = new WebpackOptionsDefaulter().process(options)  // 补全默认配置 
3.compiler = new Compiler(options.context)  // 创建 compiler 对象  
4.compiler.options = options 
// 应用用户通过 webpack.config.js 配置或命令行参数传递的插件          
5.if (options.plugins && Array.isArray(options.plugins)) { 
    for (const plugin of options.plugins) {            
        plugin.apply(compiler)   
   }   
} 
6. WebpackOptionsApply.process()
   6.1 entryOptionsPlugin.apply() //
       6.1.1 apply:compiler.hooks.entryOption.tap//绑定entryOption事件
             6.1.1.1 compiler.hooks.entryOption.tap :执行如下两种方法
                    DynamicEntryPlugin.apply()
                    itemToPlugin.apply(){
                        if (Array.isArray(item)) { 
                        return new MultiEntryPlugin(context, item, name) 
                        }else{
                        return new SingleEntryPlugin(context, item, name)
                        }
                    }    
                    SingleEntryPlugin.apply(){
                       绑定了:compiler.hooks.compilation.tap() 
                       绑定了:compiler.hooks.make.tapAsync()
                    }  //参考如上SingleEntryPlugin.js  
                       
   6.2 compiler.hooks.entryOption.call()//触发entryOption事件
7.compiler.run()
  7.1  const onCompiled = (err, compilation) => {
        // 输出文件
        this.emitAssets(compilation, err => {})
      }
  }
        7.1.1 emitAssets(compilation, callback) {
         const emitFiles = err => {}
        // 在输入文件之前,触发 emit 事件,这是最后可以修改 assets 的机会了
          this.hooks.emit.callAsync(compilation, err => {
          outputPath = compilation.getPath(this.outputPath)
          this.outputFileSystem.mkdirp(outputPath, emitFiles)
       })
  7.2 this.compile(onCompiled)// 调用 compile方法 
      7.2.1 const params = this.newCompilationParams()
      7.2.2 this.hooks.compile.call(params)
      7.2.3 // 创建 compilation 对象  //此时触发如上定义compilation钩子事件
            const compilation = this.newCompilation(params) 
      

钩子总结

钩子执行对应构建结果
compilation钩子 // 设置 dependency 和 module 工厂之间的映射关系             compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
make钩子 // 根据入口文件和名称创建 Dependency 对象                      const dep = SingleEntryPlugin.createDependency(entry, name)

// 随着这个方法被调用,将会开启编译流程                           compilation.addEntry(context, dep, name, callback) {
this._addModuleChain(context, entry, onModule, callbak)//compliation对象方法


}
//最终生成module
compilation.seal //make后回调 — // 模块编译和构建完成后,开始生成 chunks 和 assets 
this.createChunkAssets() // 通过 chunks 生成 assets
MainTemplate // 如果 chunk 包含 webpack runtime 代码,就用 mainTemplate 来渲染,否则用 chunkTemplate 来渲染

//代码拼接,生成assets代码 最终渲染好的代码会存放在 compilation 的 assets 属性中。
调用 Compiler 的 emitAssets 方法 //seal回调后–// chunks 和 assets 生成后,调用 emitAssets

//输出文件 this.emitAssets(compilation, err => {})

分离 Runtime

我们不希望每个入口文件都生成 runtime 代码,而是希望将其抽离到一个单独的文件中,以减少 app 的体积。我们通过配置 runtimeChunk 来达到这一目的。

watch 模式

我们每修改一次代码,便执行一次 npx webpack,这有些麻烦,能不能让 webpack 检测文件的变化,自动刷新呢?答案是有的。

webpack 可以以 run 或 watchRun 的方式运行

// https://github.com/webpack/webpack/blob/master/lib/webpack.js#L62
const webpack = (options, callback) => {
  if (options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
    const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {}
    // 如果执行了 watch 就不会执行 run
    return compiler.watch(watchOptions, callback)
  }
  compiler.run(callback)
  return compiler
}

webpack配置优化

提取公共代码

 splitChunks: {
     chunks: 'all',
     name: 'common',
     minChunks: 2,
     minSize: 0,
   },

可以看到 dist 目录下生成了一个 common.js 文件

Tree Shaking — 去掉deadCode

多环境配置

配置方式:–使用 webpack.EnvironmentPlugin

 const webpack = require('webpack');
 const debuggable = process.env.BUILD_TYPE !== 'release'
 module.exports = {
  plugins: [    
        new webpack.EnvironmentPlugin({
       NODE_ENV: JSON.stringify(process.env.NODE_ENV) || 'development',
       BUILD_TYPE: JSON.stringify(process.env.BUILD_TYPE) || 'debug',
     }),
  ],
  mode: debuggable ? 'none' : 'production',
}

默认情况下,webpack 会帮我们把 process.env.NODE_ENV 的值设置成 mode 的值

使用 NODE_ENV 来区分环境类型是约定俗成的(node中)–有如下区别:

Development

  • More logs come out
  • No views are cached
  • More verbose error messages are generated
  • Front end stuff like javascript, css, etc. aren’t minized and cached

Production

Below are common, regardless of frameworks:

  • Middleware and other dependencies switch to use efficient code path
  • Only packages in dependencies are installed. Dependencies in devDependencies and peerDependencies are ignored.

代码中可以通过以下方式读取这些环境变量

 console.log(`环境:${process.env.NODE_ENV} 构建类型:${process.env.BUILD_TYPE}`) 

关于process.env的变量注入–npm script

关于less的使用

{
        test: /\.(less)$/,
        include: /src/,
        use: [
          {
            loader: 'file-loader',
            options: {
              useRelativePath: true,
              name: '[path][name].wxss',
              context: resolve('src'),
            },
          },
          {
            loader: 'less-loader',
            options: {
              lessOptions:{
                javascriptEnabled: false,
                includePaths: [resolve('src', 'styles'), resolve('src')],
              }
            },
          },
        ]
       },
     ]

参考资料:subPackage打包webpack打包流程分析&小程序工程化