模块解析
resolver 是一个帮助寻找模块绝对路径的库。一个模块可以作为另一个模块的依赖模块,然后被后者引用。
resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。
- 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。
webpack 中的解析规则
- 使用 enhanced-resolve,webpack 能解析三种文件路径:
- 绝对路径:import ‘/home/me/file’;
- 相对路径:import ‘../src/file1’;
- 模块路径:import ‘module/lib/file’;
- 在 resolve.modules 中指定的所有目录中检索模块。可以通过配置别名的方式来替换初始模块路径。
- 如果 package 中包含 package.json 文件,那么在 resolve.exportsFields 配置选项中指定的字段会被依次查找,package.json 中的第一个字段会根据 package 导出指南确定 package 中可用的 export。
- 如果路径指向文件:
- 如果文件具有扩展名,则直接将文件打包。
- 否则,将使用 resolve.extensions 选项作为文件扩展名来解析。
- 如果路径指向文件夹,则寻找具有正确扩展名的文件:
- 如果文件夹中包含 package.json 文件,则会根据 resolve.mainFields 配置中的字段顺序查找,并根据 package.json 中的符合配置要求的第一个字段来确定文件路径。
- 如果不存在 package.json 文件或 resolve.mainFields 没有返回有效路径,则会根据 resolve.mainFiles 配置选项中指定的文件名顺序查找,看是否能在 import/require 的目录下匹配到一个存在的文件名。
- 然后使用 resolve.extensions 选项,以类似的方式解析文件扩展名。
- 如果路径指向文件:
解析loader
- resolveLoader 配置项可以为 loader 设置独立的解析规则。
缓存
- 每次文件系统访问文件都会被缓存,以便于更快触发对同一文件的多个并行或串行请求。在 watch 模式下,只有修改过的文件会被从缓存中移出。如果关闭 watch 模式,则会在每次编译前清理缓存
Module Federation
可以实现多个独立 Weppack 构建之间的共享模块和代码。它通过让每个构建的应用程序能够使用其他应用程序中的模块来提高代码共享和复用的效率。
多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。这通常被称作微前端。
原理
- 基于webpack的远程容器特性,允许将一个应用程序的某些模块打包为一个独立的、可远程加载的bundle,并在运行时动态加载这些模块。
主要优势
- 多个应用程序之间可以共享代码和模块,从而减少重复代码量
- 应用程序可以更加灵活地划分为更小的子应用程序,从而降低应用程序的复杂度
- 可以避免在应用程序之间传递大量数据,从而提高应用程序的性能和效率
- 可以支持应用程序的动态加载和升级,从而实现更好的版本管理和迭代
底层概念
- 区分本地模块和远程模块。
- 本地模块即为普通模块,是当前构建的一部分。
- 远程模块不属于当前构建,并在运行时从所谓的容器加载。
- 加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个 chunk的加载操作中。如果没有 chunk 加载操作,就不能使用远程模块 。
- chunk 的加载操作通常是通过调用 import() 实现的,但也支持像 require.ensure 或 require([…]) 之类的旧语法。
- 容器是由容器入口创建的,该入口暴露了对特定模块的异步访问。
- 容器可以嵌套使用
- 容器可以使用来自其他容器的模块。
- 容器之间也可以循环依赖。
- 暴露的访问分为两个步骤:
- 加载模块(异步的):将在 chunk 加载期间完成
- 执行模块(同步的):将在与其他(本地和远程)的模块交错执行期间完成
高级概念
- 每个构建都充当一个容器,也可将其他构建作为容器。
- 共享模块是指既可重写的又可作为向嵌套容器提供重写的模块。它们通常指向每个构建中的相同模块。
- packageName 选项允许通过设置包名来查找所需的版本。默认情况下,它会自动推断模块请求,当想禁用自动推断时,请将 requiredVersion 设置为 false 。
构建块
- ContainerPlugin:使用指定的公开模块来创建一个额外的容器入口。
- ContainerReferencePlugin:将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。它还会调用这些容器的 override API 来为它们提供重载。本地的重载(当构建也是一个容器时,通过 webpack_override 或 override API)和指定的重载被提供给所有引用的容器。
- ModuleFederationPlugin:组合了ContainerPlugin和ContainerReferencePlugin
概念目标
- 既可以暴露,又可以使用 webpack 支持的任何模块类型
- 代码块加载应该并行加载所需的所有内容(web:到服务器的单次往返)
- 从使用者到容器的控制
- 重写模块是一种单向操作
- 同级容器不能重写彼此的模块。
- 概念适用于独立于环境
- 共享中的相对和绝对请求
- 会一直提供,即使不使用
- 会将相对路径解析到 config.context
- 默认不会使用 requiredVersion
- 共享中尾部带有 / 的模块请求将匹配所有具有这个前缀的模块请求
- 共享中的模块请求
- 只在使用时提供,并且提供所有匹配模块
- 会匹配构建中所有使用的相等模块请求
- 将从图中这个位置的 package.json 提取 requiredVersion
- 当你有嵌套的 node_modules 时,可以提供和使用多个不同的版本
用例
- 每个页面单独构建
- 可以单独部署每个页面。
- 在更新路由或添加新路由时部署主体应用程序。
- 单页应用的每个页面都是在单独的构建中从容器暴露出来的。
- 主体应用程序将常用库定义为共享模块,以避免在页面构建中出现重复。
- 主体应用程序(application shell)也是独立构建,会将所有页面作为远程模块来引用。
- 将组件库作为容器
- 可以单独部署对组件库的更改。
- 应用程序自动使用组件库的最新版本。
- 每个应用程序使用来自组件库容器的组件。
- 许多应用程序共享一个通用的组件库,将其构建成暴露所有组件的容器。
动态远程容器
- 支持 get 和 init 方法。
- init 是一个兼容 async 的方法,调用时,只含有一个参数:共享作用域对象(shared scope object)。此对象在远程容器中用作共享作用域,并由 host 提供的模块填充。可以利用它在运行时动态地将远程容器连接到 host 容器。
- 容器尝试提供共享模块,但是如果共享模块已经被使用,则会发出警告,并忽略所提供的共享模块。容器仍能将其作为降级模块。
- 在尝试动态连接远程容器之前,确保已加载容器。
- 通过动态加载的方式,提供一个共享模块的不同版本,从而实现 A/B 测试。
- 基于Promise的动态Remote
- 向 remote 传递一个 promise,其会在运行时被调用。
- 当使用该 API 时,必须 resolve 一个包含 get/init API 的对象。
动态Public Path
- 提供一个 host api 来设置 publicPath
- 场景:
在 https://my-host.com/app/* 上有一个 host 应用,并且在 https://foo-app.com 上有一个子应用。子应用程序也挂载在 host 域上, 因此, https://foo-app.com 可以通过 https://my-host.com/app/foo-app 访问,并且可以通过代理重定向到 https://foo-app.com/*。 - 从脚本推断公共路径并在运行时使用
__webpack_public_path__module
变量对其进行设置。
故障排除
- Uncaught Error: Shared module is not available for eager consumption
- 问题:应用程序正急切地执行一个作为全局主机运行的应用程序
- 解决:在模块联邦的高级 API 中将依赖设置为即时依赖
- Uncaught Error: Module “./Button” does not exist in container.
- 问题:将 webpack beta.16 升级到 webpack beta.17 中
- 解决:在 ModuleFederationPlugin 里,更改 exposes,在Button加上./‘./Button’:’./src/Button’
- Uncaught TypeError: fn is not a function
- 问题:丢失了远程容器,或加载了容器
- 解决:使用前添加容器,或需将主机容器的远程容器文件也添加到 HTML 中
- 来自多个 remote 的模块之间的冲突
- 解决:加载多个模块,建议为你的远程构建设置output.uniqueName
依赖图
- 每当一个文件依赖另一个文件时,webpack 都会将文件视为直接存在依赖关系。
- 当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。
- 从入口开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle通常只有一个可由浏览器加载。
target
- 用法:在 webpack 配置中设置 target 字段
- 每个target 都包含各种 deployment(部署)/environment(环境)特定的附加项,以满足其需求
- 多target:通过设置两个独立配置,来构建对 library 进行同构
manifest
当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 “manifest”,当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。
- runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。
- 在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
模块热替换(hot module replacement)
在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。
提高开发速度的方式:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
工作原理:
- 在应用程序中
- 应用程序要求 HMR runtime 检查更新。
- HMR runtime 异步地下载更新,然后通知应用程序。
- 应用程序要求 HMR runtime 应用更新。
- HMR runtime 同步地应用更新。
- 设置 HMR,使此进程自动触发更新,或者选择要求在用户交互时进行更新
- 在 compiler 中
- 除了普通资源,compiler 需要发出 “update”,将之前的版本更新到新的版本。”update” 由两部分组成:
- 更新后的 manifest (JSON)
- 一个或多个 updated chunk (JavaScript)
- 除了普通资源,compiler 需要发出 “update”,将之前的版本更新到新的版本。”update” 由两部分组成:
- 在模块中
- HMR 是可选功能,只会影响包含 HMR 代码的模块。
- 如果一个模块没有 HMR 处理函数,更新就会冒泡。这意味着某个单独处理函数能够更新整个模块树。
- 如果在模块树的一个单独模块被更新,那么整组依赖模块都会被重新加载。
- 在runtime中
- 对于模块系统运行时,会发出额外代码,来跟踪模块parents和children关系。在管理方面,runtime 支持两个方法 check 和 apply。
- check:发送一个 HTTP 请求来更新 manifest。如果请求失败,说明没有可用更新。如果请求成功,会将 updated chunk 列表与当前的 loaded chunk 列表进行比较。每个 loaded chunk 都会下载相应的 updated chunk。当所有更新 chunk 完成下载,runtime 就会切换到 ready 状态。
- apply:将所有 updated module 标记为无效。对于每个无效 module,都需要在模块中有一个 update handler,或者在此模块的父级模块中有 update handler。否则,会进行无效标记冒泡,并且父级也会被标记为无效。继续每个冒泡,直到到达应用程序入口起点,或者到达带有 update handler 的 module。如果它从入口起点开始冒泡,则此过程失败。所有无效 module 都会被处理和解除加载。然后更新当前 hash,并且调用所有 accept handler。runtime 切换回 idle 状态,一切照常继续。
- 对于模块系统运行时,会发出额外代码,来跟踪模块parents和children关系。在管理方面,runtime 支持两个方法 check 和 apply。
- 在开发环境中
- 可以将 HMR 作为 LiveReload 的替代。webpack-dev-server 支持 hot 模式,在试图重新加载整个页面之前,hot 模式会尝试使用 HMR 来更新。