Published on

webpack相关总结

Authors
  • avatar
    Name
    noodles
    每个人的花期不同,不必在乎别人比你提前拥有

webpack基础

  • 模块打包 将不同模块的文件整合在一起,保证引用顺序正确
  • 编译兼容 通过loader可以实现polyfill
  • 能力扩展 通过plugin扩展模块打包和编译兼容的能力

webpack打包流程

初始化阶段

  • 读取合并配置参数
  • 创建Compiler对象/加载插件,准备开始编译

编译阶段

  • 确定入口 根据entry配置找到入口文件,构建依赖图谱
  • 模块路径解析 解析模块路径
  • 构建模块 通过loader加载文件解析模块内容并且收集模块依赖

生成阶段

  • 确定输出内容 根据依赖图提取模块- 根据entry 和 optimization配置决定分组 分割策略
  • 优化 代码分割 压缩混淆等等
  • 生成资源 生成最终的chunk source map等
  • 构建资源映射表 包含所有资源的路径清单 用于异步加载

Loader

将各种类型的文件转换成 Webpack 可以处理的模块

Loader的执行顺序

Loader是通过数组进行配置的,loader-runner会从配置的末尾依次执行对应loader的处理逻辑(compose) [a,b,c]

normalPitch
[a,b,c] 如果b loader的pitch返回了结果
stopPitch

Loader开发

  • this.callback 一个可以同步或者异步调用的可以返回多个结果的函数
      this.callback(
        err: Error | null,
        content: string | Buffer,
        sourceMap?: SourceMap,
        meta?: any
      );
  • this.async 告知loader-runner这个loader将会异步回调返回this.callback

同步loader

    // content 
    module.exports = function(content, map, meta) { 
      return someDealFunc(content) // 直接返回同步执行逻辑 同步loader会阻塞webpack编译
    }
    module.exports = function(content, map, meta) {
      // 通过callback触发
      this.callback(null, someDealFunc(content), map, meta)
      // 需要这个return 显示的返回undefined 
      return 
    }

异步loader

    module.exports = function(content, map, meta) {
    // 告知loader-runner是一个异步loader 异步loader不会阻塞webpack编译
      const callback = this.async();
      someAsyncOperation(content, function(err, result) {
        if (err) return callback(err);
        callback(null, result, map, meta);
      });
    };

Plugin

基于webpack构建的hooks来增强构建能力

开发plugin

  • 插件必须是一个函数或者一个包含apply方法的对象
  • 传递给插件的compiler和compilation对象是同一个引用,修改对应的对象会对后面的插件有影响
  • 异步事件需要插件调用回调函数通知webpack进入下一个流程(同步钩子会阻塞编译,异步钩子不会阻塞编译)
      // 同步钩子
      class MyPlugin {
        apply (compiler) {
          // 找到合适的事件钩子,实现自己的插件功能
          compiler.hooks.emit.tap('MyPlugin', compilation => {
              // compilation: 当前打包构建流程的上下文
              console.log(compilation);
              // do something...
          })
        }
      }
      // 异步钩子
      class MyPlugin {
        apply (compiler) {
          // 找到合适的事件钩子,实现自己的插件功能
          compiler.hooks.run.tapAsync('MyPlugin', (compilation, callback) => {
              // compilation: 当前打包构建流程的上下文
              console.log(compilation);
              // 这里调用这个callback 表示这个异步操作结束了
              callback()
          })
          compiler.hooks.run.tapPromise('MyPlugin', (compilation) => {
          // compilation: 当前打包构建流程的上下文
          return new Promise((resolve, reject) => {
            resolve(11)
          })
      })
        }
      }

webpack性能优化

优化构建速度

缓存机制:利用 cache 选项或 cache-loader 插件来缓存构建过程中生成的中间文件,加快后续构建速度。 并行编译:使用 thread-loader 或者 Webpack 自身的多线程能力加速资源处理。
文件查找优化: resolve.modules(指定模块解析路径)/resolve.alias(别名)/resolve.extensions(扩展名尝试)/noParse(不解析特定的文件)

打包体积性能优化

Tree Shaking: 去除无用的代码
按需加载Polyfills: 按需加载polyfills
三方库: 单独拆分或者CDN引入
代码分割: optimization.splitChunks
环境变量: definePlugin

webpack相关概念/配置项

配置项

配置项说明
output.publicPath用户配置打包结果资源引用的位置
output.path影响生成文件存放的目录和引用
devServer.publicPath开发模式下的资源访问路径配置,默认 '/'
output.filename输出bundle的文件名
output.chunkFilename配置非入口 chunk 的文件名,主要有异步加载模块/代码分割模块

相关概念

配置项说明
module需要编译处理的每个文件,模块中包含代码、依赖关系等
chunk由多个模块组成的代码块,比如Entry chunk,Split chunk拆分出的代码等
bundle最终生成的输出文件
sourceMap浏览器会通过sourceURL获取映射文件,通过解析器解析后实现源码和混淆代码之间的映射.通过会做域名的限制来防止外网访问
hash每次构建都会生成一个hash, 只要项目文件有修改整个项目的hash值就会有变化
contenthash跟单个文件内容相关,内容改变就会改变hash,通常在抽取单独的css文件的时候使用contenthash
chunkhash跟生成的chunk有关,不同的chunk文件会有不同的hash,js文件会使用chunkhash
webpack热更新在浏览器和与Webpack Dev Server之间维护了一个Websocket,在监听模式下,资源变化Dev Server会向浏览器推送更新(包括构建的hash值),浏览器完成更新资源的替换
compilerwebpack在启动后会创建一个单例的Compiler对象,它负责存储webpack所有配置信息、插件以及声明周期钩子函数
compilationcompilation对象是每一次构建的上下文对象,包含当次构建的所有信息

打包工具对比

大型项目和企业级应用:选择 Webpack,因为它的高度可配置性和强大的插件生态系统可以满足复杂的需求。
快速原型开发和中小型项目:选择 Parcel,因为它的零配置和自动优化特性可以显著提高开发效率.
库开发和中小型项目:选择 Rollup,因为它的简洁配置和优秀的 Tree Shaking 支持可以有效减小打包体积。

特性WebpackParcelRollup
配置复杂度中等
性能中等
适用场景大型项目、企业级应用快速原型开发、中小型项目库开发、中小型项目
模块化支持支持多种模块格式支持多种模块格式支持多种模块格式
插件系统丰富丰富丰富
HMR支持支持支持
代码分割支持支持支持
Tree Shaking支持支持优秀
多语言支持支持支持支持

webpack tree shaking的原理

Tree Shaking指在打包过程中通过静态分析的方式识别和删除没有使用过的代码,以减少最终文件的打包体积

  • 识别未使用的代码 webpack会遍历整个模块依赖图,标记模块的导出和引用,并将这些信息保存到内存中
  • 标记未使用的代码 通过静态分析的方式确定哪些代码没有被引用,对于没有使用的代码webpack会标记为'未使用'
  • 删除未使用的代码 在打包阶段 webpack会根据标记阶段的结果,将标记为'未使用'的代码从打包的结果中删除

webpack hooks整理

初始化阶段

钩子触发时机类型
environment初始化环境,插件初始化完触发SyncHook
afterEnvironment在environment后触发,相关配置设置完成SyncHook
entryOption开始处理配置文件的entry配置之后触发SyncBailHook
afterPlugins添加完插件之后触发SyncHook
afterResolversresolver设置完成之后触发SyncHook
beforeRun在执行compiler的run方法之前AsyncSeriesHook

编译阶段

钩子触发时机常见用途类型
run在开始编译之后触发AsyncSeriesHook
beforeCompile编译参数创建后AsyncSeriesHook
compile准备编译资源SyncHook
compilationcompilation创建之后执行SyncHook
makecompilation结束之前执行AsyncParallelHook
afterCompilecompilation结束和封印之后执行AsyncSeriesHook

生成阶段

钩子触发时机常见用途类型
shouldEmit在输出 asset 之前调用SyncBailHook
emit输出 asset 到 output 目录之前执行AsyncSeriesHook
afterEmit输出 asset 到 output 目录之后执行AsyncSeriesHook
done在 compilation 完成时执行AsyncSeriesHook

监听钩子

钩子触发时机常见用途类型
watchRun监听模式下,一次编译开始处理监听编译AsyncSeriesHook
watchClose监听模式停止处理监听停止SyncHook

webpackBootstrap 启动文件的格式

          // 立即执行函数,创建独立的作用域
/******/ (() => { // webpackBootstrap
          // 定义所有的模块
/******/   var __webpack_modules__ = ({
/******/     "./src/moduleA.js":
/******/       ((__unused_webpack_module, exports) => {
/******/         exports.foo = function() {
/******/           return 'Hello from moduleA';
/******/         };
/******/       })
/******/   });
/******/   // 模块缓存 
/******/   var __webpack_module_cache__ = {};
           // 指定模块加载逻辑
/******/   function __webpack_require__(moduleId) {
/******/     // 检查模块是否在缓存中
/******/     if(__webpack_module_cache__[moduleId]) {
/******/       return __webpack_module_cache__[moduleId].exports;
/******/     }
/******/     // 如果不在缓存中,创建新的模块并放入缓存
/******/     var module = __webpack_module_cache__[moduleId] = {
/******/       exports: {}
/******/     };
/******/     // 执行模块函数 传入预设的参数
/******/     __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/     // 返回模块的导出内容
/******/     return module.exports;
/******/   }
           // 启动入口模块
/******/   var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/   // ...
/******/ })()

webpack模块联邦

模块联邦用于解决模块的共享和加载的问题。它允许你在不同的webpack构建之间共享JavaScript模块,相比npm包管理的方式它实现更加灵活的代码共享方式,可以实现动态加载并减少的依赖管理等问题

    // 主应用
    const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

    module.exports = {
        // ...
        plugins: [
            new ModuleFederationPlugin({
                name: "mainApp", // 主应用的名字
                remotes: { // 远程模块配置 名称入口文件
                    remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js",
                },
                // 共享模块
                shared: ["lodash"],
            }),
        ],
    };

    // 远程应用
    const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

    module.exports = {
        // ...
        plugins: [
            new ModuleFederationPlugin({
                name: "remoteApp", // 远程应用的名字
                filename: "remoteEntry.js", // 远程应用的入口文件
                exposes: { // 指定要共享出去的模块
                    "./Button": "./src/Button",
                },
                // 共享模块
                shared: ["lodash"],
            }),
        ],
    };

    // 主应用中的代码 动态加载远程模块

    const remoteApp = import("remoteApp/Button");

    remoteApp.then((Button) => {
        // 使用远程模块
        const button = new Button();
        document.body.appendChild(button);
    });

webpack模块联邦的优势

  • 动态加载模块 模块联邦可以在运行时加载模块,从而实现更加灵活的模块加载
  • 模块共享 不需要通过npm管理依赖项,降级了项目依赖关系的复杂性同时提高的构建效率
  • 独立部署和自治 不同应用可独立部署和自治
  • 可细粒度控制 选择性的共享模块

相关文章

webpack源码解读
dive into webpack
当面试官问Webpack的时候他想知道什么
三十分钟掌握Webpack性能优化
吐血整理」再来一打Webpack面试题