Webpack & Vite 面试题整理

856 阅读46分钟

Webpack

对 webpack 的理解

webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。我们可以使用 webpack 管理模块。因为在 webpack 看来,项目中的所有资源皆为模块,通过分析模块间的依赖关系,在其内部构建出一个依赖图,最终编绎输出模块为 HTML、JavaScript、CSS 以及各种静态文件(图片、字体等),让我们的开发过程更加高效。

webpack的主要作用:

  • 模块打包: 可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容: 通过 webpack 的 loader 机制,可以对代码做 polyfill,还可以编译转换诸如.less.vue.jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展: 通过 webpack 的 plugin 机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

loader 和 plugin 的区别是什么?

  • loader 直译为"加载器"。webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js/json 文件,如果想将其他文件也打包的话,就会用到 loader。所以 loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力。
  • plugin 直译为"插件"。plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性,例如打包优化、资源管理、环境变量注入等。在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果,目的在于解决 loader 无法实现的其他事。
  • 在运行时机上,loader 运行在打包文件之前;plugin 则是在整个编译周期都起作用
  • 在配置上,loader 在module.rules中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options(参数) 等属性;plugin 在 plugins 中单独配置,类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

常见的 loader 有哪些?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件(处理图片和字体)
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码(处理图片和字体)
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • less-loader:加载并编译 LESS 文件
  • sass-loader:加载并编译 SASS/SCSS 文件
  • postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • vue-loader:加载并编译 Vue 组件

常见的 plugin 有哪些?

  • html-webpack-plugin:默认会创建一个空的HTML文件,自动引入打包输出的所有资源(js/css)
  • define-plugin:定义环境变量
  • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
  • mini-css-extract-plugin: 将 CSS 代码抽离为独立文件,支持按需加载,配合 css-minimize-webpack-plugin使用
  • clean-webpack-plugin: 每次打包时删除上次打包的产物,保证打包目录下的文件都是最新的

有没有用过好用的工具/plugin ? // TODO

  • splitChunkPlugin:用于代码分割
  • webpack-merge: 提取公共配置,用于分别编写不同环境的配置文件
  • hot-module-replacement-plugin:支持模块热替换
  • ignore-plugin: 忽略指定文件,可以加快构建速度
  • speed-measure-webpack-plugin: 分析出 webpack 打包过程中的 loader 和 plugin 的耗时,用于性能分析
  • terser-webpack-plugin: 实现更精细的代码压缩功能
  • source-map-devtool-plugin:精细度配置 SourceMap,不能和 devtool 选项同时使用
  • unused-webpack-plugin:反向查找项目中没被用到的文件,日常工作经常用到,可在重构或者性能分析时使用
  • webpack-dashboard:一个命令行可视化工具,能够在编译过程中实时展示编译进度、模块分布、产物信息等相关信息,性能分析时很有用
  • Webpack Analysis:webpack 官方提供的可视化分析工具
  • bundle-analyzer-plugin:性能分析插件,可以在运行后查看是否包含重复模块/不必要模块等

Webpack 中 SplitChunksPlugin 是如何定义代码分割的?

  • 基本原理:SplitChunksPlugin 主要是基于模块之间的依赖关系和一些配置规则来进行代码分割。它会分析模块的重复使用情况、模块的大小等因素,将符合条件的模块提取到单独的文件中,以达到优化加载性能的目的。
  • 默认配置下的行为
    • 重复模块提取:在默认配置下,SplitChunksPlugin 会自动提取在多个入口(entry)点之间共享的模块。例如,如果你有两个入口文件 entry1.js 和 entry2.js,它们都依赖于同一个 lodash 库,那么 SplitChunksPlugin 会将 lodash 提取到一个单独的文件中。这样,当浏览器加载这两个入口文件对应的页面时,lodash 库只需要被加载一次,而不是在每个入口文件对应的脚本中都加载一次,从而减少了总的代码体积和网络请求次数。
    • 动态导入模块的处理:对于动态导入import()的模块,SplitChunksPlugin 也会根据一定的规则进行处理。当动态导入的模块在多个地方被使用或者模块体积达到一定大小等情况时,它会被提取到单独的文件中。例如,在一个单页应用中,不同的路由组件可能会动态导入同一个工具模块,SplitChunksPlugin 会将这个工具模块提取出来,使得不同路由切换时可以更高效地加载所需模块。
  • 配置参数与代码分割规则
    • chunks 参数:用于指定要进行代码分割的代码块(chunks)类型。它有三个可选值:async(只对异步代码块进行分割)、initial(只对初始的同步代码块进行分割)和 all(对所有类型的代码块进行分割,包括同步和异步)。
    • minSize 参数:用于设定被提取的模块的最小体积(以字节为单位)。默认值是 30000(约 30KB)。只有模块体积大于这个值时,才有可能被提取。这可以避免提取过小的模块,因为过小的模块单独提取可能会导致更多的 HTTP 请求,反而降低性能。
    • minChunks 参数:表示一个模块至少被多少个代码块(chunks)引用才会被提取。默认值是 1。例如,如果设置为 2,那么只有一个模块被至少两个不同的代码块引用时,它才会被提取到单独的文件中。这有助于提取真正被多个地方复用的模块。
    • maxAsyncRequests 和 maxInitialRequests 参数:maxAsyncRequests 用于限制异步加载时的最大并行请求数,maxInitialRequests 用于限制初始页面加载时的最大并行请求数。这两个参数的默认值都是 6。合理设置这些参数可以避免一次请求过多的文件,防止浏览器性能下降。
    • 缓存组(cacheGroups):
      • 缓存组是 SplitChunksPlugin 中一个重要的概念,它允许你定义不同的规则集来对模块进行分组和提取。每个缓存组都有自己的一套配置参数,这些参数可以继承自全局的 splitChunks 配置,也可以进行单独的设置。
      • 在下面这个例子中,vendors 缓存组通过 test 正则表达式来匹配模块路径,只要模块路径包含 node_modules 目录,就会被考虑放入这个缓存组。priority 参数用于设置缓存组的优先级,值越高优先级越高。当一个模块同时符合多个缓存组的条件时,优先级高的缓存组会优先处理这个模块。default 缓存组是一个备用的缓存组,当模块不符合其他缓存组的条件时,会根据 default 缓存组的规则来处理。
    optimization: {
      splitChunks: {
        chunks: 'async',
        minSize: 50000,
        minChunks: 2,
        maxAsyncRequests: 4,
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
          }
        }
      }
    }
    

如何提高 webpack 的构建速度?

  • 优化 loader 配置
    • 使用 include 和 exclude 配置,缩小 loader 的搜索范围
    • 并行处理文件,使用 thread-loaderhappypack 插件
    • 缓存 loader 的编译结果,使用 cache-loaderhard-source-webpack-plugin
  • 优化 Output 配置
    • 使用 split-chunks-plugin 分割代码,减少重复打包
    • 使用 mini-css-extract-plugin 提取 CSS 到单独文件,避免 CSS 阻塞渲染
    • 开启 Tree Shaking 功能,移除未使用的代码
  • 优化 Resolve 配置
    • 减少目录搜索深度,使用 alias 和 modules 配置
    • 使用 noParse 配置,忽略无需解析的第三方库
  • 优化 Entry 配置
    • 使用 dll-plugindll-reference-plugin 提取稳定的第三方库
    • 使用 hot-module-replacement-plugin 开启热更新,减少重新构建的时间
  • 使用构建分析工具
    • 使用 webpack-bundle-analyzer 可视化分析包内容和大小
    • 使用 speed-measure-webpack-plugin 分析各阶段构建耗时
  • 其他优化方法
    • 使用 source map 的 cheap-module-eval-source-map 模式,提升 source map 的生成速度
    • 关闭不需要的插件和功能

转义出的文件过大怎么办?

  • 提取通用模块
  • 动态加载(按需加载)
  • 压缩代码 删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件,利用 cssnano(css-loader?minimize)来压缩 css。
  • 利用CDN加速 在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径。
  • 提取公共代码

如何按需加载代码?

结合 Vue 的异步组件和 Webpack 的代码分割功能。

  //首先,可以将异步组件定义为返回一个 Promise 的工厂函数
  //该函数返回的 Promise 应该 resolve 组件本身
  const Foo = () => Promise.resolve({
    /* 组件定义对象 */
  })
  
  //第二,在 Webpack 2 中,我们可以使用动态 import 语法来定义代码分块点
  import('./Foo.vue') // 返回 Promise

  //结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件
  const Foo = () => import('./Foo.vue')

webpack 的构建流程是什么?

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

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

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR,通过设置devServer: {hot: true}开启,在不需要刷新整个页面的同时更新模块,能够提升开发的效率和体验。热更新时只会局部刷新页面上发生了变化的模块,同时可以保留当前页面的状态,比如复选框的选中状态等。

原理:

  1. webSocket 通信建立: 当开启 HMR 功能时,webpack 会启动一个 webSocket 服务,用于与客户端进行通信。这个 webSocket 连接用于传输更新的模块信息。
  2. 监听文件变化: webpack 会监听源文件的变化,一旦发现变化,就会重新编译这些变化的模块。
  3. 模块更新信息推送: webpack 完成编译后,会通过 webSocket 将更新的模块信息推送到客户端。这些信息包括新模块的 hash 值、被更新的模块 ID 等。
  4. 客户端更新处理: 客户端收到更新信息后,会使用 webpack 提供的 HMR runtime 模块来处理这些更新。主要包括:
    • 确认需要更新的模块
    • 获取新的模块代码
    • 使用新的模块替换旧的模块
    • 触发 HMR 钩子函数,允许应用程序代码处理更新
  5. 页面状态保留: HMR 的一个重要特性是能够保留页面的状态,不需要整个页面刷新。这是通过 webpack 提供的module.hot.accept()方法实现的,允许开发者编写更新逻辑,使页面状态得以保留。

Tree shaking 的原理 // TODO

Tree Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。

在 Webpack 中,启动 Tree Shaking 功能必须同时满足三个条件:

  • 使用 ESM 规范编写模块代码
  • 配置 optimization.usedExports 为 true,启动标记功能
  • 启动代码优化功能,可以通过如下方式实现:
    • 配置 mode = production
    • 配置 optimization.minimize = true
    • 提供 optimization.minimizer 数组
    // webpack.config.js
    module.exports = {
      entry: "./src/index",
      mode: "production",
      devtool: false,
      optimization: {
        usedExports: true
      }
    }
    

原理:

  • 简要版 Tree shaking 的工作流程可以分为:

    1. 标记哪些导出值没有被使用,标记的流程如下:
    2. Make 阶段:收集模块导出变量并记录到模块依赖关系图中
    3. Seal 阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
    4. 构建阶段:利用 Terser 插件将没有被用到的导出语句删除
    5. 使用 Terser 插件将这些没用到的导出语句删除
  • 详细版

    1. 收集模块导出 首先,Webpack 需要弄清楚每个模块分别有什么导出值,这一过程发生在 make 阶段,大体流程:
      • 将模块的所有 ESM 导出语句转换为 Dependency 对象,并记录到 module 对象的 dependencies 集合,具名导出转换为 HarmonyExportSpecifierDependency 对象,default 导出转换为 HarmonyExportExpressionDependency 对象
      • 所有模块都编译完毕后,触发 compilation.hooks.finishModules 钩子,开始执行 FlagDependencyExportsPlugin 插件回调,经过 FlagDependencyExportsPlugin 插件处理后,所有 ESM 风格的 export 语句都会记录在 ModuleGraph 体系内,后续操作就可以从 ModuleGraph 中直接读取出模块的导出值
      • FlagDependencyExportsPlugin 插件从 entry 开始读取 ModuleGraph 中存储的模块信息,遍历所有 module 对象
      • 遍历 module 对象的 dependencies 数组,找到所有 HarmonyExportXXXDependency 类型的依赖对象,将其转换为 ExportInfo 对象并记录到 ModuleGraph 体系中
    2. 标记模块导出 模块导出信息收集完毕后,Webpack 需要标记出各个模块的导出列表中,哪些导出值有被其它模块用到,哪些没有,这一过程发生在 Seal 阶段,主流程:
      • 触发 compilation.hooks.optimizeDependencies 钩子,开始执行 FlagDependencyUsagePlugin 插件逻辑
      • 在 FlagDependencyUsagePlugin 插件中,从 entry 开始逐步遍历 ModuleGraph 存储的所有 module 对象
      • 遍历 module 对象对应的 exportInfo 数组
      • 为每一个 exportInfo 对象执行 compilation.getDependencyReferencedExports 方法,确定其对应的 dependency 对象有否被其它模块使用
      • 被任意模块使用到的导出值,调用 exportInfo.setUsedConditionally 方法将其标记为已被使用。
      • exportInfo.setUsedConditionally 内部修改 exportInfo._usedInRuntime 属性,记录该导出被如何使用
    3. 生成代码 经过前面的收集与标记步骤后,Webpack 已经在 ModuleGraph 体系中清楚地记录了每个模块都导出了哪些值,每个导出值又没那块模块所使用。接下来,Webpack 会根据导出值的使用情况生成不同的代码,这一段生成逻辑均由导出语句对应的 HarmonyExportXXXDependency 类实现,大体的流程:
      • 打包阶段,调用 HarmonyExportXXXDependency.Template.apply 方法生成代码
      • 在 apply 方法内,读取 ModuleGraph 中存储的 exportsInfo 信息,判断哪些导出值被使用,哪些未被使用
      • 对已经被使用及未被使用的导出值,分别创建对应的 HarmonyExportInitFragment 对象,保存到 initFragments 数组
      • 遍历 initFragments 数组,生成最终结果
    4. 删除 Dead Code 经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在 __webpack_exports__ 对象中,形成一段不可能被执行的 Dead Code 效果,在此之后,将由 Terser、UglifyJS 等 DCE 工具摇掉这部分无效代码,构成完整的 Tree Shaking 操作。

Tree shaking 的最佳实践

  • 避免无意义的赋值: 使用 Webpack 时,需要有意识规避一些不必要的赋值操作,当导出模块被赋值给了变量,但实际上并没有被使用,理应被删除,但 Webpack 的 Tree Shaking 操作并没有生效,产物中依然保留了该模块导出。这是因为 Tree Shaking 逻辑停留在代码静态分析层面,只是浅显地判断模块导出变量是否被其它模块引用,以及引用模块的主体代码中有没有出现这个变量,没有进一步从语义上分析模块导出值是不是真的被有效使用。 更深层次的原因则是 JavaScript 的赋值语句并不纯,视具体场景有可能产生意料之外的副作用,很难在确保正确副作用的前提下,完美地 Shaking 掉所有无用的代码枝叶。

  • 使用#pure标注纯函数调用: JavaScript 中的函数调用语句也可能产生副作用,因此默认情况下 Webpack 并不会对函数调用做 Tree Shaking 操作。不过可以在调用语句前添加/*#__PURE__*/备注,明确告诉 Webpack 该次函数调用并不会对上下文环境产生副作用。

  • 禁止 Babel 转译模块导入导出语句: Babel 提供的部分功能特性会致使 Tree Shaking 功能失效,例如 Babel 可以将 import/export 风格的 ESM 语句等价转译为 CommonJS 风格的模块化语句,但该功能却导致 Webpack 无法对转译后的模块导入导出内容做静态分析。所以,在 Webpack 中使用 babel-loader 时,建议将 babel-preset-env 的 modules 配置项设置为 false,关闭模块导入导出语句的转译。

  • 优化导出值的粒度(细化导出模块的颗粒度)

  • 使用支持 Tree Shaking 的包: 如果可以的话,应尽量使用支持 Tree Shaking 的 npm 包,例如使用 lodash-es 替代 lodash,或者使用 babel-plugin-lodash 实现类似效果

如何编写 loader?

loader 的本质为函数,函数中的 this 作为上下文会被 webpack 填充,函数接受一个参数,为 webpack 传递给 loader 的文件源内容。函数中 this 是由 webpack 提供的对象,能够获取当前 loader 所需要的各种信息。函数中有异步操作或同步操作,异步操作通过 this.callback 返回,返回值要求为 string 或者 Buffer。

// 导出一个函数,source 为 webpack 传递给 loader 的文件源内容
module.exports = function(source) {
  const content = doSomeThing2JsString(source)
  // 如果 loader 配置了 options 对象,那么 this.query 将指向 options
  const options = this.query
  // 可以用作解析其他模块路径的上下文
  console.log('this.context')
  /*
    * this.callback 参数:
    * error:Error | null,当 loader 出错时向外抛出一个 error
    * content:String | Buffer,经过 loader 编译后需要导出的内容
    * sourceMap:为方便调试生成的编译后内容的 source map
    * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
    */
  this.callback(null, content) // 异步
  return content // 同步
}

一般在编写 loader 的过程中,保持功能单一,避免做多种功能。

如何编写 plugin?

webpack 编译会创建两个核心对象:

  • compiler:包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子
  • compilation:作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 compilation 将被创建
class MyPlugin {
  // Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation: 当前打包构建流程的上下文
      console.log(compilation)
      // do something...
    })
  }
}

在 emit 事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。

Webpack 中自定义 plugin 时,在 compile 中有哪些钩子?

  • beforeRun:compiler.hooks.beforeRun.tap
    • 含义:在开始一次新的编译之前触发。
    • 用途示例:可以用于检查一些前置条件是否满足,比如检查某些配置文件是否存在,或者验证一些环境变量。例如,如果你的项目依赖于特定的环境变量来决定是否进行某种优化,就可以在这个钩子中检查环境变量的值。
  • run:compiler.hooks.run.tap
    • 含义:在读取完配置后,开始执行编译时触发。
    • 用途示例:可以用于记录编译开始的时间,或者在编译开始时初始化一些资源。比如,你可以在这里初始化一个日志记录器,用于记录编译过程中的各种信息。
  • watchRun:compiler.hooks.watchRun.tap
    • 含义:在监听模式下,当一个文件变化导致重新编译时触发。
    • 用途示例:可以用于处理文件变化相关的逻辑。例如,当某个特定类型的文件(如样式文件)发生变化时,你可以在这里执行额外的操作,如通知相关的开发工具更新样式预览。
  • normalModuleFactory:compiler.hooks.normalModuleFactory.tap
    • 含义:在创建普通模块(normal module)工厂时触发。普通模块是指除了一些特殊模块(如入口模块)之外的模块,比如 JavaScript 模块、CSS 模块等。
    • 用途示例:可以用于修改模块的创建规则。例如,你可以在这里对模块的加载路径进行调整,或者对模块添加一些自定义的属性。
  • contextModuleFactory:compiler.hooks.contextModuleFactory.tap
    • 含义:在创建上下文模块(context module)工厂时触发。上下文模块通常用于处理一些具有上下文相关的资源,比如处理目录下的一组文件。
    • 用途示例:可以用于自定义上下文模块的生成逻辑。例如,在处理一个包含多个组件的目录时,你可以在这里对组件的加载方式进行优化,或者对组件添加一些公共的依赖。
  • beforeCompilation:compiler.hooks.beforeCompilation.tap
    • 含义:在编译(compilation)创建之前触发,此时已经完成了模块工厂的创建等准备工作。
    • 用途示例:可以用于在编译真正开始之前进行最后的检查和准备工作。例如,你可以在这里清理一些临时文件或者缓存,以确保编译的准确性。
  • compilation:compiler.hooks.compilation.tap
    • 含义:在每次编译(compilation)创建时触发,这是一个非常重要的钩子,可以用于访问和修改编译过程中的各种模块、资源等。
    • 用途示例:可以用于添加自定义的模块构建规则,或者对现有的模块进行转换。例如,你可以在这里添加一个自定义的加载器,用于处理特定类型的文件。
  • thisCompilation:compiler.hooks.thisCompilation.tap
    • 含义:在当前编译(compilation)对象上触发,它与compilation钩子类似,但this - compilation钩子更侧重于当前正在进行的编译。
    • 用途示例:可以用于在当前编译过程中添加一些特定的优化逻辑。例如,在处理代码压缩时,你可以在这个钩子中根据当前编译的目标环境(如生产环境还是开发环境)来决定是否应用更激进的压缩策略。
  • compilationFinish:compiler.hooks.compilationFinish.tap
    • 含义:在编译(compilation)完成后触发,此时所有的模块都已经构建完成,资源也已经处理完毕。
    • 用途示例:可以用于对编译结果进行最后的处理。例如,你可以在这里生成一个编译报告,记录编译过程中的各种统计信息,如模块数量、文件大小等。
  • shouldEmit:compiler.hooks.shouldEmit.tap
    • 含义:在决定是否输出(emit)编译后的文件之前触发。可以用于根据某些条件来控制文件是否输出。
    • 用途示例:例如,你可以在开发环境下根据一个配置选项来决定是否输出带有调试信息的文件,或者在测试环境下决定是否输出测试相关的文件。
  • emit:compiler.hooks.emit.tap
    • 含义:在输出(emit)编译后的文件时触发。可以用于对输出的文件进行最后的修改或者添加额外的文件。
    • 用途示例:例如,你可以在这里添加一个版权声明文件,或者对输出的 JavaScript 文件进行代码混淆等操作。
  • afterEmit:compiler.hooks.afterEmit.tap
    • 含义:在所有文件已经输出(emit)完成后触发。可以用于清理一些在输出过程中使用的临时资源。
    • 用途示例:例如,如果你在emit钩子中使用了一些临时的文件缓存,就可以在这个钩子中清理这些缓存。

什么是 Source map?

sourceMap 是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中 debug 会带来非常糟糕的体验,sourceMap 可以帮助我们快速定位到源代码的位置,提高我们的开发效率。

既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾。有了这份映射文件,我们只需要在压缩代码的最末端加上//# sourceURL=/path/to/file.js.map这句注释,即可让 sourceMap 生效。有了这段注释后,浏览器就会通过 sourceURL 去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。

Webpack 基本配置

  • mode:有三个值'none' | 'development' | 'production',默认为'production'。Webpack 根据 mode 配置选项使用相应模式的内置优化。
    • development:会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development,为模块和 chunk 启用有效的名称
    • production:会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称
    • none:不使用任何默认优化选项
  • entry:编译打包的配置入口,入参可以为字符串、对象、数组。
  • output:最终打包输出的文件,主要参数配置:
    • filename 输出文件名称
    • path 打包输出文件目录(配置output.path需要取绝对路径,因此可引入 node 自带的 path 库来获取当前打包输出文件目录的绝对路径)
    • clean 打包前会将打包目录删除
  • devtool:控制是否生成,以及如何生成 source-map。
    • source-map:会生成一个bundle.js.map的 source-map 文件,在打包文件bundle.js最下面会有//# sourceMappingURL=bundle.js.map这样一条注释,它会帮我们指向 source-map 文件
    • cheap-source-map:跟 source-map 一样,会生成.map文件,也会在bundle.js最下面生成//# sourceMappingURL=bundle.js.map注释指向.map文件,不同的是这一个低开销的生成方式,它没有列映射,只会定位到某一行
    • cheap-module-source-map:该选择与cheap-source-map的区别是它会对使用 loader 的 source-map 处理更好
    • hidden-source-map:该选项会生成 source-map 文件,但是在bundle.js文件最下面//# sourceMappingURL=bundle.js.map这条注释会被删除,这时就引用不了 source-map 文件,如果在bundle.js下面手动添加上面这条注释,就又会生效
    • inline-source-map:该选项不会生成.map文件,但是它会在打包文件bundle.js最下面以 DataURL 方式添加到文件最底下
    • eval-source-map:该选项不会生成.map文件,但是 source-map 会以 DataURL 的方式添加到 eval 最后面
  • devServer:这是一个本地开发时候用的属性,但需要安装webpack-dev-server这个依赖包。
    • hot:配置热更新(HMR)是否开启,默认启用,开始后自动应用webpack.HotModuleReplacementPlugin
    • host
    • port:设置监听的端口,默认情况下是 8080
    • open:是否打开浏览器,默认 false 不打开,设置 true 就会打开
    • compress:是否为静态文件开启gzip compression,默认 false,开启后可以看到bundle.js与原先对比小了很多,并且可以看到响应头带有gizp标志,表示这是资源已被gizp压缩了
    • proxy:设置代理服务器来解决跨域问题,基本属性:
      • target:表示的是代理到的目标地址
      • pathRewrite:重写路径
      • secure:默认情况下不接收转发到 https 的服务器上,如果希望支持可以设置为 false
      • changeOrigin:表示是否更新代理后请求的 headers 中 host 地址,默认情况值是被代理到localhost: 3000,如果想要让它还是指向被代理地址,那么可以设置 true
  • resolve:配置引用模块该如何解析
    • extensions:引用的文件名如果没有后缀,会自动添加后缀名
    • mainFiles:配置当文件目录时,默认会找目录内的 index 文件
    • alias:设置路径别名
  • module:处理项目中不同类型的模块
    • rules:rules 不同的类型模块对应着不同 loader,这些 loader 为这些模块提供相对应的转换规则
      • test: 用于对资源进行匹配的,通常会设置成正则表达式
      • use: 对应的值时一个数组,每个元素是一个对象,可以通过对象的属性来设置一些其他属性:
        • loader: 必须有一个 loader 属性,对应的值是一个字符串
        • options: 可选的属性,值是一个字符串或者对象,值会被传入到 loader 中
      • loader:Rule.use: [{ loader }]的简写
  • plugins:配置插件用于自定义 Webpack 构建过程
  • optimization:用于配置代码优化的属性
    • minimizer:用于压缩工具
    • splitChunks:通用分块策略,利用 SplitChunksPlugin 将公共代码抽离,可在 SplitChunksPlugin 页面中查看配置其行为的可用选项
    • chunkIds:告知 Webpack 当选择模块 id 时需要使用哪种算法,设置为 false 会告知 Webpack 没有任何内置的算法会被使用
      • natural:按照数字的顺序使用 id
      • named:开发模式下的默认值,一个可读的名称的 id(开发过程推荐)
      • deterministic:确定性的,在不同的编译中不变的短数字 id(打包过程推荐)
    • runtimeChunk:配置runtime相关的代码是否抽取到一个单独的chunk中,runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码
      • true/multiple:针对每个入口打包一个 runtime 文件
      • single:打包一个 runtime 文件
      • 对象:name 属性决定 runtimeChunk 的名称

babel-loader 的原理

babel-loader 是 Webpack 的一个加载器(loader),它允许你在 Webpack 构建过程中使用 Babel 来转译 JavaScript 文件,使得开发者可以在现代 JavaScript 开发中,编写使用最新语言特性的代码,同时确保这些代码能够在当前和旧版本的浏览器和环境中运行。其原理和工作流程可以概括为以下几个步骤:

  1. 解析(Parsing):babel-loader 首先使用 Babel 的解析器(如 Babylon)将 JavaScript 代码转换成抽象语法树(AST)。这个过程包括词法分析和语法分析,将源代码分解成一系列的 tokens,然后根据这些 tokens 构建 AST。
  2. 转换(Transformation):接着,Babel 遍历 AST,使用插件(plugins)对 AST 进行转换。插件可以识别特定的代码模式并对其进行转换,比如将 ES6 代码转换为 ES5 代码,或者将 JSX 转换为 JavaScript 代码。这个阶段是 babel-loader 实现代码转译的核心。
  3. 生成(Code Generation):转换完成后,Babel 将修改后的 AST 再次转换为 JavaScript 代码。这个阶段通常涉及代码的格式化和优化,以确保生成的代码既符合旧版本 JavaScript 的语法,又尽可能高效。
  4. 缓存(Caching):为了提高构建性能,babel-loader 支持缓存机制。通过设置 cacheDirectory 选项,可以将转译结果缓存到文件系统中,这样在后续的构建中,如果源代码没有变化,就可以直接使用缓存的结果,避免重复转译。
  5. 集成(Integration):在 Webpack 的配置中,通过在 module.rules 中添加 babel-loader,可以指定对哪些文件使用 Babel 转译。通常,你会设置一个正则表达式来匹配需要转译的文件,并通过 exclude 选项排除不需要转译的目录,如 node_modules。
  6. 配置(Configuration):babel-loader 允许通过 options 属性传递 Babel 的配置选项,如预设(presets)和插件(plugins)。这些配置定义了转译的具体行为,比如支持哪些 JavaScript 新特性。
  7. 优化(Optimization):为了减少最终打包文件的大小,babel-loader 可以与 Babel 的 transform-runtime 插件结合使用,将常用的辅助函数和 polyfills 集中管理,避免在每个文件中重复包含相同的代码。

chunkhash 和 contenthash 的区别?

  • 作用对象不同
    • chunkhash:是根据同一个 chunk(模块集合)的内容计算得出的哈希值。如果一个 chunk 中的任何一个模块发生变化,这个 chunk 的 chunkhash 就会改变。通常用于区分不同的代码块,比如区分业务代码和第三方库代码生成的不同 chunks。
    • contenthash:是根据文件内容计算得出的哈希值。只有当文件自身内容发生变化时,对应的 contenthash 才会改变。一般用于提取的 CSS 文件等,确保只有 CSS 文件内容变化时,其对应的哈希值才会变化,避免因其他文件变化导致 CSS 文件的缓存失效。
  • 用途不同
    • chunkhash:主要用于确保当特定的一组模块发生变化时,对应的 chunk 可以被浏览器正确地识别并更新缓存。例如,当业务逻辑模块发生变化时,只有包含该业务逻辑的 chunk 对应的缓存会被更新,而其他未变化的 chunks 可以继续使用缓存,提高加载速度。
    • contenthash:主要用于保证资源文件的独立性和缓存有效性。比如,当 CSS 文件内容发生变化时,只有该 CSS 文件的 URL 会因为 contenthash 的变化而改变,不会影响到其他资源文件的缓存。

前端打包都做了什么?

  • 合并文件
    • 将多个 JavaScript、CSS 等文件合并为数量较少的文件,减少浏览器的请求次数,提高页面加载速度。例如,把多个模块的 JavaScript 代码合并成一个大的 JS 文件,把多个样式表文件合并成一个 CSS 文件。
  • 压缩代码
    • 对合并后的 JavaScript、CSS 和 HTML 文件进行压缩,去除不必要的空格、换行符和注释等,减小文件体积。这样可以减少网络传输的数据量,加快页面的加载速度。
  • 优化资源路径
    • 确保打包后的资源文件路径正确,以便在不同的部署环境中都能正确加载资源。比如,图片、字体等资源的路径在打包过程中会被调整为相对于打包后输出目录的正确路径。
  • 处理依赖关系
    • 分析项目中的各种模块依赖关系,确保在打包后的文件中正确加载所需的模块。例如,在使用模块化开发时,一个模块可能依赖于其他多个模块,打包工具会将这些依赖关系梳理清楚,并按照正确的顺序加载模块。
  • 静态资源处理
    • 对于图片、字体等静态资源,进行适当的处理和优化。可能会对图片进行压缩,或者将一些小的图标合并成一个雪碧图(sprite sheet),以减少请求次数。
  • 生成输出文件
    • 将打包后的文件输出到指定的目录,通常是一个适合部署的结构,包括 HTML、CSS、JS 文件以及静态资源文件夹等。

打包的预编译做了什么?

  • 语言转译:对于使用新的编程语言特性(如 TypeScript、ES6 + 等)的代码,预编译会将其转译为浏览器能够理解的旧版本 JavaScript 代码。例如,将 TypeScript 代码转译为普通的 JavaScript 代码,把 ES6 的箭头函数、let 和 const 声明等语法转换为 ES5 语法,以便在不支持新特性的浏览器中也能正常运行。
  • 模板处理:如果项目中使用了模板引擎,预编译会将模板文件进行处理,将其转换为可执行的 JavaScript 代码。比如,在 Vue.js 项目中,预编译会把单文件组件中的模板部分转换为渲染函数,提高运行时的性能。
  • 样式预处理:对于使用了 CSS 预处理器(如 Sass、Less、Stylus)的项目,预编译会将这些预处理器的代码转换为普通的 CSS 代码。预处理器通常提供了一些额外的功能,如变量、嵌套、函数等,可以使 CSS 的编写更加高效和可维护。例如,使用 Sass 的变量和嵌套规则编写的样式代码,在预编译阶段会被转换为标准的 CSS 代码。
  • 文件优化:预编译阶段可能会对图片、字体等静态资源进行优化处理。例如,对图片进行压缩,选择合适的图片格式以减小文件体积。同时,也可能会对字体文件进行优化,去除不必要的字符集,减小字体文件的大小。
  • 环境变量注入:根据不同的构建环境(如开发环境、生产环境),在预编译阶段可以注入相应的环境变量。这样在代码中就可以根据不同的环境进行不同的处理,比如在生产环境中关闭调试信息、启用代码压缩等。

待解答的问题

  • Webpack 层面如何性能优化?
  • Webpack 打包中 Babel 插件是如何工作的?
  • 如何保证众多 loader 按照想要的顺序执行?
  • Webpack optimize 有配置过吗?
  • Webpack 和 Rollup 有什么相同点与不同点?
  • Webpack5 更新了哪些新特性?

Vite

Vite 的构建流程

  • 开发环境
    • 启动服务器:在开发环境中运行 Vite 时,它首先会启动一个开发服务器。这个服务器会快速地响应你的请求,并且只在需要的时候加载模块。
    • 模块解析:解析项目中的模块依赖关系。它使用 ES modules 的 import 和 export 语句来确定哪些模块是相互依赖的。
    • 按需加载:在开发过程中,Vite 采用按需加载的方式。这意味着只有当你实际访问一个模块时,它才会被加载。
    • 热更新:Vite 支持热更新,当修改了项目中的代码时,服务器会自动检测到这些变化,并实时更新应用程序。
  • 生产环境
    • 解析项目:分析项目中的文件结构和模块依赖关系,确定哪些文件需要被打包。
    • 构建优化:进行构建优化,对代码进行压缩、合并和优化,以减小文件大小和提高加载速度。
    • 静态资源处理:处理项目中的静态资源,如图像、字体和 CSS 文件。它会将这些资源进行优化,并生成相应的哈希值,以确保在浏览器中正确加载。
    • 生成构建产物:最后生成构建产物,包括 HTML、JavaScript 和 CSS 文件。这些文件可以直接部署到服务器上,供用户访问。

在生产环境中,Vite 基于 Rollup 进行构建。Rollup 是一个高效的模块打包器,它可以将你的应用程序打包成高效的、优化的代码。

Rollup 的构建流程

  1. 确定入口点:Rollup 构建的第一步是确定项目的入口点。入口点通常是一个或多个 JavaScript 文件,它们是应用程序的起点。(可以在 Rollup 的配置文件中指定入口点,或者通过命令行参数传递入口点)
  2. 解析模块依赖关系:确定了入口点之后,Rollup 会开始解析入口点文件及其依赖的模块。它会遍历项目中的所有模块,构建一个模块依赖图。(Rollup 使用静态分析技术来解析模块依赖关系,这意味着它不需要实际执行代码就可以确定模块之间的依赖关系)
  3. 加载模块:Rollup 会根据模块依赖图依次加载每个模块的代码。它可以处理不同类型的模块格式,如 ES Modules、CommonJS 和 AMD。对于不同的模块格式,Rollup 会使用相应的加载器来将其转换为 Rollup 内部使用的格式。
  4. 应用插件:Rollup 支持使用插件来扩展其功能。插件可以在构建过程的不同阶段进行干预,例如代码转换、优化、生成代码等。(可以根据项目的需求选择和配置适当的插件)
  5. 代码优化:Rollup 会对加载的代码进行优化。它可以执行一些常见的优化操作,如去除未使用的代码、合并重复的代码、压缩变量名等。这些优化操作可以减小生成的代码体积,提高应用程序的性能。
  6. 确定输出格式:在构建过程的最后阶段,Rollup 需要确定生成的输出格式。可以选择将代码打包为不同的格式,如 CommonJS、ES Modules、UMD 等。
  7. 生成输出文件:根据确定的输出格式,Rollup 会将优化后的代码生成相应的输出文件。输出文件可以包含多个模块的合并代码,也可以是单个模块的独立文件,还可以生成一些额外的文件,如 source maps,用于调试和错误定位。

Vite 和 Webpack 的区别

  • 构建原理:Webpack 是一个静态模块打包器,通过对项目中的 JavaScript、CSS、图片等文件进行分析,生成对应的静态资源,并且可以通过一些插件和加载器来实现各种功能;Vite 则是一种基于浏览器原生 ES 模块解析的构建工具。
  • 打包速度:Webpack 的打包速度相对较慢,Vite 的打包速度非常快。
  • 配置难度:Webpack 的配置比较复杂,因为它需要通过各种插件和加载器来实现各种功能;Vite 的配置相对简单,它可以根据不同的开发场景自动配置相应的环境变量和配置选项。
  • 插件和加载器:Webpack 有大量的插件和加载器可以使用,可以实现各种复杂的构建场景,例如代码分割、按需加载、CSS 预处理器等;Vite 的插件和加载器相对较少。
  • Vite 是按需加载,Webpack 是全部加载:在 HMR(热更新)方面,当改动了一个模块后,Vite 仅需让浏览器重新请求该模块即可,Webpack 则需要把该模块的相关依赖模块全部编译一次。
  • Webpack 是先打包再启动开发服务器,Vite 是直接启动开发服务器,然后按需编译依赖文件:由于 Vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译,这种按需动态编译的方式,极大的缩减了编译时间。

为什么说 Vite 比 Webpack 要快?

  • Vite 不需要做全量的打包,这是比 Webpack 要快的最主要的原因。
  • Vite 在解析模块依赖关系时,利用了 esbuild,所以更快(esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍)
  • 按需加载:在 HMR(热更新)方面,当改动了一个模块后,Vite 仅需让浏览器重新请求该模块即可,不像 Webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高。
  • 由于现代浏览器本身就支持 ES Module,会自动向依赖的 Module 发出请求。Vite 充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 Webpack 那样进行打包合并。
  • 按需编译:当浏览器请求某个模块时,再根据需要对模块内容进行编译,这种按需动态编译的方式,极大的缩减了编译时间。
  • Webpack 是先打包再启动开发服务器,Vite 是直接启动开发服务器,然后按需编译依赖文件。由于 Vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。
  • 利用缓存:Vite 利用了 HTTP 头来加速整个页面的重新加载,源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

Vite 对比 Webpack,优缺点在哪?

  • 优点
    • 更快的冷启动:Vite 借助了浏览器对 ESM 规范的支持,采取了与 Webpack 完全不同的 unbundle 机制
    • 更快的热更新:Vite 采用 unbundle 机制,所以dev server在监听到文件发生变化以后,只需要通过 Websocket 连接通知浏览器去重新加载变化的文件,剩下的工作就交给浏览器去做了。
  • 缺点
    • 开发环境下首屏加载变慢:由于 unbundle 机制, Vite 首屏期间需要额外做其它工作。不过首屏性能差只发生在dev server启动以后第一次加载页面时发生。之后再 reload 页面时,首屏性能会好很多。原因是dev server会将之前已经完成转换的内容缓存起来。
    • 开发环境下懒加载变慢:跟首屏加载变慢的原因一样。Vite 在懒加载方面的性能也比 Webpack 差。由于 unbundle 机制,动态加载的文件,需要做 resolve、load、transform、parse 操作,并且还有大量的 http 请求,导致懒加载性能也受到影响。
    • Webpack 支持的更广:由于 Vite 基于 ES Module,所以代码中不可以使用 CommonJs;Webpack 更多的关注兼容性,而 Vite 关注浏览器端的开发体验。Vite 目前生态还不如 Webpack。

注意: 当需要打包到生产环境时,Vite 使用传统的 Rollup 进行打包,所以 Vite 的优势是体现在开发阶段,缺点也只是在开发阶段存在。

如何选择 Webpack 和 Vite?

  • 项目需求
    • 项目规模:
      • 对于小型项目或个人项目,Vite 可能是一个更好的选择。它具有快速的启动时间和热更新速度,可以提高开发效率。
      • 对于大型项目,Webpack 可能更适合。它具有强大的功能和丰富的插件生态系统,可以处理复杂的项目结构和需求。
    • 技术栈
      • 如果项目使用了特定的技术栈,例如 React、Vue 或 Angular,那么可以考虑使用与该技术栈配套的构建工具。例如,Vue 项目可以使用 Vite,而 React 和 Angular 项目可以使用 Webpack。
      • 如果需要使用一些特定的插件或功能,也可以根据插件的兼容性来选择构建工具。
    • 开发环境和生产环境需求:考虑项目在开发环境和生产环境中的需求
      • Vite 在开发环境中表现出色,具有快速的热更新和实时重载功能。
      • Webpack 在生产环境中可以进行更多的优化和配置,以提高性能和减小文件大小。
  • 性能方面
    • 启动时间
      • Vite 在开发环境中的启动时间非常快,因为它利用了浏览器的原生 ES Modules 支持,只在需要时加载模块。这对于快速迭代开发非常有帮助。
      • Webpack 在启动时需要进行更多的配置和构建过程,因此启动时间可能会较长。但是,Webpack 可以通过一些优化措施来减少启动时间。
    • 热更新速度
      • Vite 的热更新速度非常快,可以在毫秒级别内更新代码。这使得开发过程更加流畅,减少了等待时间。
      • Webpack 的热更新速度也在不断改进,但通常可能会比 Vite 稍慢一些。
    • 生产环境性能
      • 在生产环境中,Webpack 可以进行更多的优化,如代码分割、压缩和缓存等,以提高性能和减小文件大小。
      • Vite 也可以在生产环境中进行一些优化,但可能不如 Webpack 那么强大。

如何指定 Vite 插件的执行顺序?

可以使用enforce修饰符来强制插件的位置:

  • pre:在 Vite 核心插件之前调用该插件
  • post:在 Vite 构建插件之后调用该插件
  • 默认:在 Vite 核心插件之后调用该插件

Vite 是否支持 Commonjs 写法?

纯业务代码,一般建议采用 ESM 写法。如果引入的三方组件或者三方库采用了 CJS 写法,Vite 在预构建的时候就会将 CJS 模块转化为 ESM 模块。

如果非要在业务代码中采用 CJS 模块,那可以提供一个 Vite 插件,定义 load hook,在 hook 内部识别是 CJS 模块还是 ESM 模块。如果是 CJS 模块,利用 esbuild 的 transform 功能,将 CJS 模块转化为 ESM 模块。

Vite 常见的配置

  • base:开发或生产环境服务的公共基础路径
  • build.outDir:指定打包文件的输出目录,默认值为 dist
  • build.assetsDir:指定生成静态资源的存放目录,默认值为 assets
  • build.assetsInlineLimit:图片转 base64 编码的阈值。为防止过多的 http 请求,Vite 会将小于此阈值的图片转为 base64 格式,可根据实际需求进行调整
  • plugins:插件
  • css.preprocessorOptions:传递给 CSS 预处理器的配置选项,例如可以定义一个全局变量文件,然后再引入这个文件
  • css.postcss:postcss 也是用来处理 CSS 的,只不过它更像是一个工具箱,可以添加各种插件来处理 CSS(解决浏览器样式兼容问题、浏览器适配等问题)。例如移动端使用postcss-px-to-viewport对不同设备进行布局适配
  • resolve.alias:定义路径别名,通常会给 scr 定义一个路径别名
  • resolve.extensions:导入时想要省略的扩展名列表。默认值为['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json']
  • optimizeDeps.force:是否开启强制依赖预构建。node_modules 中的依赖模块构建过一次就会缓存在node_modules/.vite/deps文件夹下,下一次会直接使用缓存的文件。而有时候我们想要修改依赖模块的代码,做一些测试或者打个补丁,这时候就要用到强制依赖预构建
  • server.host:指定服务器监听哪个 IP 地址。默认值为 localhost ,只会监听本地的 127.0.0.1
  • server.proxy:反向代理也是我们经常会用到的一个功能,通常用它来解决跨域问题

Vite 插件常见的 hook 有哪些?

Vite 会在生命周期的不同阶段中去调用不同的插件以达到不同的目的。

  • config:可用于修改Vite config,用户可以通过这个 hook 修改 config;例如vite-aliases这个插件可以帮助我们自动生成别名。它利用的就是这个钩子
  • configResolved:在解析 Vite 配置后调用,用于获取解析完毕的 config,在这个 hook 中不建议修改 config
  • configureServer:用于给dev server添加自定义 middleware;例如vite-plugin-mock插件就是在这个生命周期调用的
  • configurePreviewServer:与 configureServer 相同但是作为预览服务器。vite-preview插件就是利用这个钩子
  • transformIndexHtml:注入变量,用来转换 HTML 的内容。vite-plugin-html插件可以在 HTML 里注入变量,就是利用这个钩子
  • handleHotUpdate:执行自定义 HMR 更新处理