honghu

webpack

webpack

  功能强大: 代码分割, 文件压缩与合并, 模块合并, 编译兼容, 按需加载, 代码校验, 自动刷新, 模块热替换, Tree Shaking...

webpack 中 plugin 和 loader 的区别, 它们的执行时机, 以及常用的 plugin 和 loader

区别:

概念:

  • loader 是文件加载器, 实质是一个转换器, 本质是一个函数. 能够加载资源文件,并对这些文件进行一些处理,诸如编译(将A文件进行编译形成B文件, 比如将A.scssA.less转变为B.css)、压缩等,最终一起打包到指定的文件中,
  • plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事; 由于webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,plugins 通过监听这些事件,就可以在特定的阶段执行自己的插件任务

运行时机:

  • loader 运行在打包文件之前
  • plugins 在整个编译周期都起作用

webpack 运行时机

loader(加载器) --> 模块代码转换器, 本质是一个函数

  • babel-loader --> 把 ES6 转换成 ES5
  • ts-loader --> 打包编译 Typescript
  • postcss-loader --> 是一个允许使用 JS 插件转换样式的工具, 可以利用 autoprefixer 补充 css 样式各种浏览器内核前缀, 做到浏览器兼容; 可以用 postcss-pxtorem 将 px 转换为 rem, 还可以用 postcss-px-to-viewport 实现 vw 适配;
  • style-loader --> 通过注入 <style> 标签将 CSS 插入到 DOM 中 -> 为解决闪屏问题将 style-loader 换成 mini-css-extract-plugin MiniCssExtractPlugin.loader, 将 CSS 提取为一个文件(即不要将 CSS 存储在 JS 模块中)
  • css-loader --> 仅处理 css 的各种加载语法, 支持模块化等特性
  • image-loader --> 加载并且压缩图片文件
  • eslint-loader --> 通过 ESLint 检查 js 代码
  • html-minify-loader --> 压缩 HTML

webpack 5 之后可以用 Asset Modules 替换以下 loader

  • file-loader --> 用于处理文件类型资源, 如 jpg, png 等图片
  • url-loader --> 处理图片的, url-loader 可以根据图片大小进行不同的操作, 如果图片过大, 则将图片进行打包, 否则将图片转换为 base64 字符串合并到 js 文件里
// asset/resource: 发出一个单独的文件并导出URL, 替换 file-loader
// asset/inline: 导出资产的数据URI, 替换 url-loader
// asset/source: 导出资产的源代码, 替换 raw-loader
 module: {
   rules: [
     {
       test: /\.png/,
       type: 'asset/resource'
     }
   ]
 },

plugins(扩展插件) --> 解决 loader 无法实现的其他事, 打包优化, 资源管理, 注入环境变量等

  • html-webpack-plugin --> 生成 html 文件, 依赖于 html-loader

  • mini-css-extract-plugin --> 将 CSS 提取到单独的文件中, 为每个包含 CSS 的 JS 文件创建一个 CSS 文件, 解决屏闪问题

  • clean-webpack-plugin --> 删除 webpack 出口路径的所有文件

  • define-plugin --> 定义环境变量

  • commons-chunk-plugin --> 提取公共代码

  • uglifyjs-webpack-plugin --> 通过 UglifyES 压缩 ES6 代码

    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
        drop_console: true,
        pure_funcs: ['console.log'] // 移除 console.log
      },
      sourceMap: false
    })
    
  • happypack --> 实现多线程加速编译(作者已经不维护) -> thread-loader

    module.exports = {
      module: {
        rules: [
          {
            test: /.js$/,
            include: path.resolve('src'),
            use: [
              "thread-loader",
              // 耗时的 loader (例如 babel-loader)
            ],
          },
        ],
      },
    };
    
    
  • cache-loader 使用缓存,提升打包速度(webpack5 内置了 cache 模块, 可弃用 cache-loader)

    module.exports = {
      cache: {
        type: 'filesystem',
      },
    };
    

webpack 5 大核心概念

  1. entry 入口 --> 指示 webpack 从哪个文件开始打包
  2. output 输出 --> 指示打包后的文件的命名和输出位置
  3. loader 加载器 --> webpack 本身只能处理 js 和 json 等资源, 其他资源需要借助 loader 才能解析
  4. plugins 插件 --> 扩展 webpack 的功能
  5. mode 模式 --> 开发模式 development / 生产模式 production

webpack 如何处理样式资源(基础处理)

  • css-loader
  • less-loader
  • sass-loader
  • stylus-loader

webpack 处理 css

  1. 解决闪屏现象 --> mini-css-extract-plugin css 文件目前被打包到 js 文件中, 所以要等到 js 文件加载完成才能出现对应的样式, html 先加载出来, 短暂延迟后样式才被加载 mini-css-extract-plugin 如何解决这个问题的:单独处理 css 文件, 样式引入方式从 style 改成 link 引入
  2. css 兼容性处理 --> postcss-loader 将代码中的 css 文件进行兼容性处理
  3. css 压缩 --> css-minimizer-webpack-plugin webpack 生产模式下已经对 html 和 js 文件进行了压缩, 我们只需要处理 css 压缩

webpack 处理 js 资源

  1. eslint --> eslint-webpack-plugin js、jsx 检查工具
  2. babel --> babel-loader 将 Es6 语法编写的代码转换为向后兼容的 js 语法, 实现兼容旧版浏览器

webpack 处理 html 资源

  1. html-webpack-plugin 以指定 html 文件为模版, 在 dist 文件下创建新的 html 文件, 并自动通过 script 引入所有 webpack 编译后的 js 文件

webpack 热更新(HMR), 搭建开发服务器

  HMR: Hot Module Replacement
  1. webpack-dev-server 在此之前 每一次更改代码都需要执行 npx webpack 命令重新打包修改后的文件 webpack-dev-server 为我们在本地搭建开发服务器, 通过 npx webpack server 命令启动, 此后每次更新都会在内存中编译打包, 将改动的模块发送到浏览器端, 浏览器用新的模块替换掉旧的模块, 去实现局部更新页面而非整体刷新页面, 能够实时重新加载(实时局部热更新)

webpack 生产模式和开发模式的区别

  1. 生产模式 优点:

    • 优化代码运行性能
    • 优化代码打包速度

    注意:

    • 生产不需要配置 webpack-dev-server 开发服务器
    • 启动命令过长可以在 package.json 中配置命令

webpack 优化前端性能

  1. 压缩代码 --> 删除多余的代码、注释、简化代码的写法等等方式。 用 UglifyJsPluginParallelUglifyPlugin 压缩 JS 文件 用 mini-css-extract-plugin 压缩 CSS
  2. 利用 CDN 加速 --> 构建过程中, 将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  3. 删除死代码 --> JS 用 Tree Shaking, CSS 需要使用 Purify-CSS
  4. 提取公共代码 --> 用 CommonsChunkPlugin 插件

提高 webpack 构建速度

  1. 提取公共代码 --> 多入口情况下, 使用 CommonsChunkPlugin 来提取公共代码
  2. 提取常用库 --> 通过 externals 配置来提取常用库
  3. 预编译 --> 利用 DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译, 再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使用 Happypack 实现多线程加速编译
  5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
  6. 使用 Tree-shakingScope Hoisting 来剔除多余代码

什么是打包

将我们写的浏览器无法解析的代码, 交给构建工具进行编译处理的过程就叫做打包, 打包完成后会生成一个浏览器可以解析的文件;

webpack 编译打包的流程

  1. 初始化阶段 - webpack

    • 合并配置项(webpack.config.js)

    • 创建编译器(compiler)实例对象 -> 记录我们传入的配置参数,以及一些串联插件进行工作的 hooks API。同时,还提供了 run 方法启动打包构建,emitAssets 对打包产物进行输出磁盘写入

      // Compiler 构造函数基础结构如下
      // core/compiler.js
      const fs = require('fs');
      const path = require('path');
      const { SyncHook } = require('tapable'); // 串联 compiler 打包流程的订阅与通知钩子
      const Compilation = require('./compilation'); // 编译构造函数
      
      class Compiler {
        constructor(options) {
          this.options = options;
          this.context = this.options.context || process.cwd().replace(/\\/g, '/');
          this.hooks = {
            // 开始编译时的钩子
            run: new SyncHook(),
            // 模块解析完成,在向磁盘写入输出文件时执行
            emit: new SyncHook(),
            // 在输出文件写入完成后执行
            done: new SyncHook(),
          };
        }
        
        run(callback) {
          ...
        }
        
        emitAssets(compilation, callback) {
          ...
        }
      }
      
      module.exports = Compiler;
      
      
    • 插件注册

      有 compiler 实例对象后,就可以注册配置文件中的一个个插件,在合适的时机来干预打包构建。

      插件需要接收 compiler 对象作为参数,以此来对打包过程及产物产生 side effect

      插件的格式可以是函数或对象,如果为对象,需要自定义提供一个 apply 方法

      // 注册插件逻辑如下
      // lib/webpack.js
      function webpack(options) {
        // 1、合并配置项
        const mergeOptions = _mergeOptions(options);
        // 2、创建 compiler
        const compiler = new Compiler(mergeOptions);
        // 3、注册插件,让插件去影响打包结果
        if (Array.isArray(options.plugins)) {
          for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
              plugin.call(compiler, compiler); // 当插件为函数时
            } else {
              plugin.apply(compiler); // 如果插件是一个对象,需要提供 apply 方法。
            }
          }
        }
        return compiler;
      }
      
      
  2. 编译阶段 - build(打包)

    • 创建 compilation 编译对象 -> 负责模块的打包(build)和 代码生成(seal), compilation 实例上记录了构建过程中的 entriesmodulechunksassets 等编译信息,同时提供 buildseal 方法进行代码构建和代码生成
    • 读取 entry 入口文件
    • 编译 entry 入口文件 ->
      1. 通过 fs 模块读取 entry 入口文件内容;
      2. 调用 loader 来转换(更改)文件内容;
      3. 为模块创建 module 对象,借助 babel 解析为 AST 语法树, 收集依赖模块,并改写依赖模块的路径;
      4. 如果存在依赖模块,递归进行上述三步操作;
  3. 生成阶段 - seal(代码生成)

    • 创建 chunk 对象 -> 会根据 entry 创建对应 chunk 并从 this.modules 中查找被 entry 所依赖的 module 集合
    • 生成 assets 对象
  4. 写入阶段 - emit (将最终打包好的代码写入到本地磁盘之中)

对 Tree-shaking 的了解, 对 CommonJS 和 ESM 都可以用 tree-shaking 吗

Tree-shaking 是对 DCE(dead code elimination) 的新的实现

  用于 webpack 性能优化, 通过工具将JS文件中用不到的代码消除掉;

在 webpack 项目中, 有一个入口文件, 相当于一棵树的主干, 入口文件有很多依赖的模块, 相当于树枝。实际情况中, 虽然依赖了某个模块, 但其实只使用其中的某些功能。通过 tree-shaking, 将没有使用的模块摇掉, 这样来达到删除无用代码的目的

  • 消除原理: 是依赖于 ES6 的模块特性 ES6 模块依赖关系是确定的(编译时加载:), 和运行时的状态无关, 所以可以进行可靠的静态分析, 这是 tree-shaking 的基础。静态分析就是不执行代码, 从字面量上对代码进行分析;ES6 之前的模块化, 比如我们可以动态 require 一个模块, 只有执行后才知道引用的什么模块, 这个就不能通过静态分析去做优化。这是 ES6 modules 在设计时的一个重要考量, 也是为什么没有直接采用 CommonJS.

CommonJS 和 ESM 的区别:

  1. CommonJS: 特性:

    1. 运行时加载: require() 本质上就是一个可执行方法, 在执行该方法时才会去读取并执行模块内的代码, 然后挂载要导出的属性, 并导出;
    2. 缓存机制: 在执行 require() 方法时, 会先去判断是否已经加载过相同模块的, 如果已经加载将直接返回缓存内容的;
    3. 导出的是值: 在 Commonjs 中, 导出操作其实就是 JS 对象的属性赋值操作;
    4. this 指向当前模块: 在执行包装函数时通过 call 等方式修改了函数的 this 执行;
    5. 同步执行: require() 的执行过程是同步的;
    6. 动态加载: 可以特定条件下动态加载模块;
  2. ESM(EcmaScript Module):

    1. 编译时加载: ESM 在 构造 和 实例化 阶段就会去加载并解析模块了, 而这时模块内部代码其实并不会去执行, ESM 的设计思想是尽量的静态化, 使得编译时就能确定模块的依赖关系, 以及输入和输出的变量;
    2. 缓存机制: 在加载模块时会有一个全局的映射表, 对于相同 URL 的模块只会加载一次;
    3. 导出的是引用地址: 包括基本类型数据, 所以模块内部变量如果改变那么该值在其他地方引用到的话也是会同步变化的;
    4. this 指向: 在 ESM 模块中, this 等于 undefined;
  3. 区别:

    1. CommonJS 模块输出的是值的拷贝,ESM 模块输出的是值的引用

    2. CommonJS 模块是运行时加载,ESM 模块是编译时输出接口。

    3. CommonJS 是单个对象导出,多次导出会覆盖之前的结果;ESM 可以导出多个。

    4. CommonJS 模块是同步加载,ESM 支持异步加载

    5. CommonJS 的 this 是当前模块,ESM 的 this 是 undefined。

husky 工具

husky 是 git 提交到仓库之前的钩子函数

// 全量检查:
// 1. 安装 husky(安装后会生成一个 .husky/pre-commit 文件)
pnpm dlx husky-init && pnpm install
// 2. package.json 中配置 lint 命令(安装后会默认生成)
{
  "prepare": "husky install", // 确保团队其他人在运行时自动安装husky
   "script": {
      // ...
      "lint": "eslint . --ext .js,.jsx,.vue --fix --ignore-path .gitignore"
   }
}
// 3. 修改.husky/pre-commit 文件中的执行命令
pnpm lint
// 暂存区 eslint 校验:
// 1. 安装 lint-staged
npm i lint-staged -D
// 2. package.json 中配置 lint-staged 命令
{
   //...
   "lint-staged": {
      "*.{js,ts,tsx}": {
         "eslint --fix"
      }
   }
}

{
   "script": {
      // ...
      "lint-staged": "lint-staged"
   }
}
// 3. 修改 .husky/pre-commit 文件
pnpm lint-staged