惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
MongoDB | Blog
MongoDB | Blog
U
Unit 42
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
L
LINUX DO - 最新话题
博客园_首页
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
C
Cisco Blogs
博客园 - 【当耐特】
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
罗磊的独立博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
A
About on SuperTechFans
G
GRAHAM CLULEY
Y
Y Combinator Blog
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
D
DataBreaches.Net
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
I
InfoQ
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 叶小钗
Project Zero
Project Zero

kshao-blog-前端知识记录

win or mac clash 无 TUN 让 Antigravity、Chrome 强制 proxy(解决 Antigravity 无法加载选择 model、自动更新无法登录、跳转) 【大杂烩】在 pnpm 中直接修改 node_modules(.pnpm) 中的依赖项,项目中持久化 - pnpm 中的依赖处理、幽灵依赖、寻址规则等 在 html 中直接使用 Esm、Jsx 脚本快速调试和使用 React@19 和 Vue@3 源码,解决 React19 UMD 构建等问题 一键在本地批量检测并升级更新 package.json 中的模块依赖,ncu(npm-check-updates)在 npm、pnpm 或 workspace 项目中的使用教程 解决 Mac Docker Desktop 中启动出现的问题合集 通过阿里云、腾讯云无服务器搭建自定义的企业域名邮箱,实现在 QQ邮箱 收发等功能(附腾讯 SMTP 和 IMAP) 解决使用代理(clash 等)进行 SSH 连接(如 Github ssh key clone/push)出现 kex_exchange_identification 错误 静态文件资源 cdnjs, jsdelivr 抖音字节国内快速 CDN 镜像推荐【2025】- 仍在使用 bootcdn 和 staticfile CDN 请注意验证资源的完整性(SRI) pnpm monorepo 中管理依赖的最佳实践,与 Catalogs(目录)协议的使用(monorepo 中统一版本管理) Web 安全中的 Secure Contexts(安全上下文)- 解决在本地中使用 clipboard 或 Crypto 等 API 限制或关闭上下文限制 使用 serve 配合 openssl 或 mkcert 创建本地自签名可信任的证书 - 创建本地 TLS\SSL https 协议服务 利用 Github Actions 和 Acme 自动申请、更新和部署至阿里云、腾讯云 CDN Lets Encrypt SSl\TLS ECC RSA 双证书 【CSS】解决在 flex 容器中使用 align-content 或 justify-content 属性 center 居中时的溢出滚动和截断问题 - 理解 safe 关键字 在线工具 - 一键获取下载抖音无水印视频、抖音去水印解析工具、下载抖音无水印高清图集【2025 最新】 【React Router】v6 data router 在非组件(或工具方法)中如何优雅的跳转路由 【React】为什么路由跳转时页面滚动高度不会被重置(保留上个页面高度)?理解 history scrollRestoration 的场景与使用,以及如何使用 React Router 重置和跳转前保留滚动高度 【React】在本地 Html 中快速 debug(调试)React 源码 【React】结合源码和 EventLoop 分析 - 为什么 useLayoutEffect 会阻止 DOM 重绘(而 useEffect 闪烁)?为什么其内部 useState 会“同步”执行? React Compiler - 解放在函数中编程时的性能焦虑(React Conf 2024)附 Next 在线演示 在 github actions 中获取时间,并转换为中国标准时间(中国时区) 【npm】npm ci - npm clean install,在 CI、CD 中保持构建的一致性和可重复性 eslint 9.x 升级或使用指南,eslint.config.js 配置,包含 react、typescript、prettier 等常用配置升级迁移 使用 Spicetify 自定义 Spotify - 歌词翻译、全屏展示、主题替换 在 node 中快速代理请求(Proxy),解决跨域或请求转发问题 - http-proxy-middleware 修改请求体和返回 在 Hexo 中使用 AI(Gemini、deepseek、Azure)生成文章摘要,支持自定义模板。hexo-ai-summaries 插件文档(默认适配主题 Butterfly) 解决在 webstorm 或 idea 等 jetbrains 工具中遇到 Git 无法 force push,或 force push 灰色禁用无法点击(protected branches) hexo-seo-submit,Hexo 博客 SEO 优化插件 - 每日定时自动或手动提交链接至百度、Bing、Google,支持 Github Actions 和 Coding Jenkins 等CI(Hexo 插件编写) 一文吃透 pnpm 如何使用 workspace 构建 monorepo,与 npm、yarn 的用法对比(pnpm 9.x 内部安装依赖问题 link-workspace-packages) 【Node】Corepack - 解决 pnpm 或 yarn 的多版本管理、解决本地版本与 packageManager 中的版本一致性问题 解决 npm、cnpm 或 pnpm install 遇到 certificate has expired (证书已过期) npm、yarn、pnpm 设置最新国内镜像源(附官方镜像源和最新阿里源),以及 nrm 的使用教程【2025】 解决从 docker desktop 内镜像 linux 创建的容器,启动就停止、无法启动等问题(无进程容器) docker(docker desktop)中设置国内镜像源加速(阿里云、中科大),以及代理和容器代理设置,解决桌面端无法登陆就退出问题 【CSS】解决外边距重叠(重合)引起的 margin 垂直方向(top、bottom)不生效,无作用的问题 【CSS】CSS-Nesting:CSS 嵌套写法 —— 有望替代 less sass 的原生嵌套 【CSS】解决移动端(高清方案)下在谷歌浏览器中出现 字体大小布局异常,和设置的 font-size 不符(Text Autosizer、Font Boosting) 【CSS】主流 UI 库都在用的逻辑伪类选择器 not、where、is、has 【windows 11】使用 wePe 纯净 制作pe启动盘 安装\重装\升级 windows 11(windows 10)详细教程,附 windows 11 跳过联网、分区等教程 【Windows】一句话(一键、一分钟、一段代码)清除 Windows 11(10) 快捷方式角标(小箭头) 【Git】cherry-pick 使用场景介绍,如何在 WebStorm 中使用 cherry-pick;(从分支中提取 commit 功能) 【message-channel】了解频繁出现在框架的 Message Channel,及在事件循环(Event Loop)中的表现 【web-worker】浅析 useWorker 库如何只需函数方法即可在 worker 内运行;如何区分 Web Socket、Web Worker和Service Worker? 【React】理解学习 React 17中的批处理 与 18 批处理 为何用谷歌(Chrome)浏览器下载PDF时有时预览有时下载? 【less】Parent Selectors & 和 &&(多个父选择器的用法及回顾) 【React Router】在非组件(或工具方法)中如何优雅的跳转路由 - 理解 HashHistory 和 BrowserHistory 处理谷歌浏览器(google Chrome)https 站点将 http 资源自动转成 https - 附全站资源强制转 https vscode 国内直链接下载,提升(加速)下载速度 【css-tricks】从 flex 子元素超出父级(容器)宽度,探其原理 flex:1 时 width 的作用 linux,mac 终端(Terminal)上使用代理(http/https/socks5)提升速度,给 git 一键设置代理提升速度 修复 Win 11或10 Xbox Game Bar 录屏截屏打不开灰色等 ios,Windows 网页皆可平滑、惯性滚动(Smooth-scroll.js)- 博客、网站、web、JavaScript 【2022年】apple 苹果(ios)注册美服区(日区)ID教程(附免税地区及身份信息生成器)-时效:2022年2月 javaScript 文件上传 - input 上传 和 webAPi 上传(解决上传对 input 的依赖) Git 解决 pull 操作出现:不建议在没有为偏离分支指定合并策略时执行pull操作 一行代码解决谷歌浏览器出现:"您的连接不是私密连接" Nginx 中使用 cookie 灰度或分流 css 指定方向模糊 - 鸿蒙开机效果(animation 事件) PE 升级 windows11 解决不满足限制(绕开 TPM),顺带 surface pro 一键升级 windows11(无需 PE) 在 Hexo 中一键部署 github page 并同步 push 变更文件到指定分支(hexo-deployer-git) z390 + i79700 + rx560(讯景转蓝宝石) 双显卡双系统 - openCore 0.6x 黑苹果 BigSur 11.4(20F71) Progressive JPEG - Web 前端 渐进(模糊)加载图片 z390+英伟达1060+i7 9700 安装黑苹果教程(避坑)- MAC OS 10.13.6 饿了么外卖红包-百亿补贴(每天都可免费领),来自内部的优惠! Google AdSense 添加付款方式-使用银行外汇(招行外汇) ECMAScript2021 可用新特性 客户端或服务端 cookie 的设置、读取、携带问题 css 文字渐入效果,css 变量的进一步使用 setProperty Css 模拟赛博朋克 故障风格 按钮 Fetch、Ajax 请求遇到301或302时 记录前后端不同域名 携带 cookie,及如何处理 Chrome SameSite Mac下 Nginx、openssl,配置本地 Localhost Https 测试环境 在线生成短链接,新浪短链接 t.cn webStorm 开发,配置 ts(TypeScript)的 FileWatchers 微信小程序 - typeScript 的配置与热加载 win10 重装系统,电脑出现:"由于最近更改电脑硬件而无法正常启动",或重装卡在:"海内存知己"等问题解决方法 算法- js 二叉树,二叉查找树的基本实现及递归与循环的版本 异或运算符 ^ 理解与应用 面试 - js 在指定数组中找出不重复的两数之和为 target
【webpack】Externals(外部扩展)浅析 - webpack 5
kshao · 2023-05-25 · via kshao-blog-前端知识记录

本篇主要介绍 webpack 中的 externals 如何使用,顺带浅析一下 webpack 内部实现。

对于 Externals 就不过多描述了,蹭下热度,把这个问题交给 AI 小助手。

使用 Externals

项目目录

在 codesandbox 中预览项目,run build or build-externals

|--pages/
|   |--A.js
|--App.js
|--index.js
|--webpack.config.js
// Pages/A.js
import React from "react";
import { get } from "lodash";
import { Button } from "antd";

const A = () => {
  const obj = { a: 1 };

  return (
    <div>
      <Button
        onClick={() => {
          console.log(get(obj, "a"), "use loadsh-es");
        }}
      >
        click
      </Button>
    </div>
  );
};

export default A;
// webpack.config.js
{
	externals: {
    lodash: "_",
  },
}
<!-- public/index.html -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

产物对比

机器配置

产物分析

[{"url":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl58mc6j30zk0kcnic.webp?imageMogr2/thumbnail/!50p","dataset":{"originPic":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl58mc6j30zk0kcnic.webp","thumbnail":""}},{"url":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl3e932j30zk0kb7q5.webp?imageMogr2/thumbnail/!50p","dataset":{"originPic":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl3e932j30zk0kb7q5.webp","thumbnail":""}}]

速度对比

[{"url":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl45hc3j30q4160qmy.webp?imageMogr2/thumbnail/!50p","dataset":{"originPic":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl45hc3j30q4160qmy.webp","thumbnail":""}},{"url":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl0fzd6j30rg19g4pw.webp?imageMogr2/thumbnail/!50p","dataset":{"originPic":"https://static.ksh7.com/post/webpack-externals/0085UwQ9gy1heagl0fzd6j30rg19g4pw.webp","thumbnail":""}}]

API 介绍

// webpack.config.js
{
  externalsType: 'var'
  externals: {
	// key is the module name, value is the global variable name
    "module-name": "global-variable-name"
  }
}

externalsType - 模块类型

模块类型,例如 amd、module(esm)。默认 var,从 全局 当中取变量,具体看模块资源 export 的方式,其他类型可在 externals 中自定义。

// pseudocode
// 编译后 - externalsType: 'var'
const jq = $;
jq('.my-element').animate(/* ... */);

externals

externals 支持多种类型,这里简单介绍对象方式的配置

externals: {
  // key is the module name, value is the global variable name
  "module-name": "global-variable-name",
  "lodash": "_",
}

key => module-name

webpack 编译时会替换 引入模块 路径中匹配的 module-name,将其替换为 global-variable-name

value => global-variable-name

模块资源中暴露的变量名称,一般资源文件在结尾处都会导出,例如 lodash 编译后的 lodash.min.js 中(或 lodash.js,此类文件未经压缩方便查看),正常情况下 变量名称 和 包名一致。

CDN / 资源文件

externals 配置的是全局变量名称,所以需要我们在 index.html 文件中引入相关资源来确保变量存在,若资源有依赖性需要确保引入的顺序

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

Features:

externalsType = script
写法1:
// webpack.config.js 
externalsType: 'script',
externals: {
  "module-name": ["library", "global-variable-name"],
  react: ['https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js', 'React'],
  'react-dom': ['https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.profiling.min.js', 'ReactDOM'],
  lodash: ['https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js', '_'],
},
写法2:

string 语法类似,你可以使用${externalsType} ${libraryName}语法在数组的第一项中指定外部库类型,例如:

// webpack.config.js 
externals: {
  "module-name": ["${externalsType} ${libraryName}", "global-variable-name"],
  react: ['script https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js', 'React'],
  'react-dom': ['script https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.profiling.min.js', 'ReactDOM'],
  lodash: ['script https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js', '_'],
}

lodash 只会在 visible 为 true 时引入资源。

// lodash in test
const Test = React.lazy(() => import(/* webpackChunkName: "test" */'./Test'));

<React.Suspense fallback={<div>loading...</div>}>
  {this.state.visible && <Test/>}
</React.Suspense>

把玩链接,run start-externals

externalsType = module

当 type 为 module 时,此时我们可以为所欲为了,直接使用 esm,不需要考虑各个模块的变量直接引入资源即可,确保资源为 esm 导出的即可。

// webpack.config.js
externals: {
  react: 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm',
  'react-dom': 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm',
  lodash: "https://cdn.jsdelivr.net/npm/lodash@4.17.21/+esm",
}
externalsType: "module",
experiments: {
  outputModule: true,
},
import * as __WEBPACK_EXTERNAL_MODULE_lodash__ from 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/+esm';

const lodash = __WEBPACK_EXTERNAL_MODULE_jquery__['default'];
lodash.xx

需要注意的是 module 为实验性的 不建议生产使用!

浅析

externals 是如何替换模块的?

在配置了 Externals 时,webpack 会注册 ExternalsPlugin,插件中会在 webpack 提供的钩子内订阅事件(normalModuleFactory.hooks.factorize),并在 callback 中重定义了当前 module 的代码生成逻辑,跳过常规模块的 resolve 和 module create 等流程。

Webpack 大致流程

Externals 大致流程

Source code

ExternalsPlugin

class ExternalsPlugin {
  // ...
  apply(compiler): {
    compiler.hooks.compile.tap("ExternalsPlugin", ({ normalModuleFactory }) => {
			new ExternalModuleFactoryPlugin(this.type, this.externals).apply(
				normalModuleFactory
			);
		});
  }
}

ExternalModuleFactoryPlugin

在 normalModuleFactory.hooks.factorize 钩子中判断当前模块是否为扩展模块

class ExternalModuleFactoryPlugin {
  // ...
  apply(normalModuleFactory) {
    // ...
    /**
     * 订阅 normalModuleFactory.hooks.factorize 后,会将重新定义后的 module callback 透传下去
    */
    normalModuleFactory.hooks.factorize.tapAsync(
      "ExternalModuleFactoryPlugin",
      (resolveData, callback) => {
        const dependency = resolveData.dependencies[0];
        // ...
        
        if (typeof externals === "object") {
         	// ...
          /**
           * resolvedExternals: config 内的 externals 配置
           * request: request 一般为 import 的路径,例:import lodash from "lodash/get"; request: 'lodash/get'
          */
          if (
            Object.prototype.hasOwnProperty.call(
              resolvedExternals,
              dependency.request
            )
          ) {
            callback(
              null,
              new ExternalModule(
                externalConfig,
                type || globalType,
                dependency.request
              )
            );
          } 
        }
      }
    )
  }
}

ExternalModule

重新定义当前 module 的构建和代码生成等(以 ExternalsType = script 为例)

class ExternalModule extends Module {
  // ...
  
  codeGeneration({
    runtimeTemplate,
    moduleGraph,
    chunkGraph,
    runtime,
    concatenationScope
  }) {
		const { request, externalType } = this._getRequestAndExternalType();
		switch (externalType) {
			default: {
        // 根据 ExternalsType 生成对应模版代码
				const sourceData = this._getSourceData(
					request,
					externalType,
					runtimeTemplate,
					moduleGraph,
					chunkGraph,
					runtime
				);

				let sourceString = sourceData.expression;
				// ...
				if (sourceData.init)
					sourceString = `${sourceData.init}\n${sourceString}`;

				let data = undefined;
				if (sourceData.chunkInitFragments) {
					data = new Map();
					data.set("chunkInitFragments", sourceData.chunkInitFragments);
				}

				const sources = new Map();
				if (this.useSourceMap || this.useSimpleSourceMap) {
					sources.set(
						"javascript",
						new OriginalSource(sourceString, this.identifier())
					);
				} else {
					sources.set("javascript", new RawSource(sourceString));
				}

				let runtimeRequirements = sourceData.runtimeRequirements;
				if (!concatenationScope) {
					if (!runtimeRequirements) {
						runtimeRequirements = RUNTIME_REQUIREMENTS;
					} else {
						const set = new Set(runtimeRequirements);
						// "module"
						set.add(RuntimeGlobals.module);
						runtimeRequirements = set;
					}
				}

				return {
					sources,
					runtimeRequirements:
						runtimeRequirements || EMPTY_RUNTIME_REQUIREMENTS,
					data
				};
			}
		}
	}
  
  build(options, compilation, resolver, fs, callback) {
		this.buildMeta = {
			async: false,
			exportsType: undefined
		};
		this.buildInfo = {
			strict: true,
			topLevelDeclarations: new Set(),
			module: compilation.outputOptions.module
		};
		const { request, externalType } = this._getRequestAndExternalType();
		this.buildMeta.exportsType = "dynamic";
		let canMangle = false;
		this.clearDependenciesAndBlocks();
    // ...
		switch (externalType) {
      // ....
			case "script":
			case "promise":
				this.buildMeta.async = true;
				break;
		}
		this.addDependency(new StaticExportsDependency(true, canMangle));
		callback();
	}
  
  // ...
}

在构建时根据 Type 修改对应 module 实例 build 属性,例如 script 为异步加载所以添加 buildMeta.async 为 true,编译时则会对该模块在引入部分代码做修改。
在生成阶段(seal)针对不同 Type 生成不同的运行时代码

type 为 script 时的 Module 代码生成逻辑。

// ExternalModule
const getSourceForScriptExternal = (urlAndGlobal, runtimeTemplate) => {
	if (typeof urlAndGlobal === "string") {
		urlAndGlobal = extractUrlAndGlobal(urlAndGlobal);
	}
	const url = urlAndGlobal[0];
	const globalName = urlAndGlobal[1];
	return {
		init: "var __webpack_error__ = new Error();",
		expression: `new Promise(${runtimeTemplate.basicFunction(
			"resolve, reject",
			[
				`if(typeof ${globalName} !== "undefined") return resolve();`,
				`${RuntimeGlobals.loadScript}(${JSON.stringify(
					url
				)}, ${runtimeTemplate.basicFunction("event", [
					`if(typeof ${globalName} !== "undefined") return resolve();`,
					"var errorType = event && (event.type === 'load' ? 'missing' : event.type);",
					"var realSrc = event && event.target && event.target.src;",
					"__webpack_error__.message = 'Loading script failed.\\n(' + errorType + ': ' + realSrc + ')';",
					"__webpack_error__.name = 'ScriptExternalLoadError';",
					"__webpack_error__.type = errorType;",
					"__webpack_error__.request = realSrc;",
					"reject(__webpack_error__);"
				])}, ${JSON.stringify(globalName)});`
			]
		)}).then(${runtimeTemplate.returningFunction(
			`${globalName}${propertyAccess(urlAndGlobal, 2)}`
		)})`,
    // 在运行时代码内插入
    // RUNTIME_REQUIREMENTS_FOR_SCRIPT = __webpack_require__.l
    // __webpack_require__.l :基于 JSONP 实现的异步模块加载函数
		runtimeRequirements: RUNTIME_REQUIREMENTS_FOR_SCRIPT
	};
};
// 编译后
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
	if(typeof _ !== "undefined") return resolve();
	__webpack_require__.l("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js", (event) => {
		if(typeof _ !== "undefined") return resolve();
		var errorType = event && (event.type === 'load' ? 'missing' : event.type);
		var realSrc = event && event.target && event.target.src;
		__webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
		__webpack_error__.name = 'ScriptExternalLoadError';
		__webpack_error__.type = errorType;
		__webpack_error__.request = realSrc;
		reject(__webpack_error__);
	}, "_");
  // rerutn Module 变量,通过 module.exports 暴露
}).then(() => (_));

当为 var 时

const getSourceForDefaultCase = (optional, request, runtimeTemplate) => {
	// ...
	const variableName = request[0]; // 模块全局变量,例如 lodash 为 _
	const objectLookup = propertyAccess(request, 1);
	return {
		init: optional
			? checkExternalVariable(variableName, request.join("."), runtimeTemplate)
			: undefined,
    // _
		expression: `${variableName}${objectLookup}`
	};
};
/* 1 */ Module id
/*!********************!*\
  !*** external "_" ***!
  \********************/
/*! dynamic exports */
/*! exports [maybe provided (runtime-defined)] [no usage info] */
/*! runtime requirements: module */
/***/ ((module) => {
// type 为 var 时,Externals 只关注全局变量,所以需要确保变量的存在
module.exports = _;

/***/ })

NormalModule

构建流程

  1. 调用 handleModuleCreate,根据文件类型构建module子类
  2. 调用 loader-runner 仓库的runLoaders转译module内容,通常是从各类资源类型转译为 JavaScript 文本
  3. 调用 acorn 将 JS 文本解析为AST
  4. 遍历 AST,触发各种钩子
    a. 在HarmonyExportDependencyParserPlugin插件监听exportImportSpecifier钩子,解读 JS 文本对应的资源依赖
    b. 调用module对象的addDependency将依赖对象加入到module依赖列表中
  5. AST 遍历完毕后,调用module.handleParseResult处理模块依赖
  6. 对于module新增的依赖,调用handleModuleCreate,控制流回到第一步
  7. 所有依赖都解析完毕后,构建阶段结束
class NormalModule extends Module {
  _doBuild(..., callback) {
    // ...
    runLoaders(...);
  }
  
  // 在build时,会根据该文件匹配到的loader,执行runLoaders转换文件内容。
  // 根据 resolve 解析文件
  build(options, compilation, resolver, fs, callback) {  
    // ...
    return this._doBuild(...)
  }
}

默认情况下,模块会在 NormalModuleFactory 中的 factorize.tapAsync 来创建,由于其定义了 stage: 100,总会在其他事件完成后执行,若其他订阅事件的 callback 带参返回则会终止发布事件(tapable.tapAsync),跳过常规模块创建流程(NormalModule)。

// NormalModuleFactory.js
class NormalModuleFactory extends ModuleFactory {
  // ...
  constructor() {
    // ...
    this.hooks.factorize.tapAsync(
      {
        name: "NormalModuleFactory",
        // 执行权重,优先执行其他 factorize
				stage: 100
      },
      () => {
        // ...
        this.hooks.resolve.callAsync(..., () => {
          createdModule = new NormalModule(
            /** @type {NormalModuleCreateData} */ (createData)
          );
        })
      }
    )
  }
  
  // ...
  
  create() {
    // ...
    this.hooks.factorize.callAsync(
      ...,
      (err, module) => {
      // ...
      const factoryResult = {
        module,
        // ...
      };
      callback(null, factoryResult);
    })
  }
}
// Compilation.js
class Compilation {
  // ...
  _factorizeModule({ factory }) {
    // ...
    
    factory.create({...}, (err, result) => {
      // ...
      callback(null, factoryResult ? result : result.module);
    })
    
    // ...
  }
}

externals 和 alias 之间有影响吗?

‒ 不会,从执行顺序上看 externals 在 normalModuleFactory.hooks.factorize 中执行,时机会早于 resolve,此时 alias 后的路径在 resolveData.creatData(afterResolve) 中,将在创建 module 时将其带入。
‒ 在创建 module 之前的阶段(resolve 等)中对 模块 路径(request)的判断应遵循代码,之后(after afterResolve)遵循模块真实路径。

Resolve 阶段

import lodash from "我被Alias了";

console.log(lodash);
// webpack.config.js
resolve: {
    alias: {
      我被Alias了: 'lodash',
    },
 },

Resolve 后

// in AliasPlugin.js
class AliasTest {
	apply(compiler) {
			normalModuleFactory.hooks.afterResolve.tap('AliasTest-afterResolve', (resolveData) => {
				if (resolveData.request.includes('Alias')) {
					console.log(`${resolveData.request}(request) | -- afterResolve.resolveData -- | ${resolveData.context}(context)`);
					console.log(resolveData.createData?.request);
					console.log('----- afterResolve --- \n');
				}
			})
			
			normalModuleFactory.hooks.createModule.tap('AliasTest-createModule', (createData, resolveData) => {
				if (resolveData.request.includes('Alias')) {
					console.log(`${createData.request}(request) | -- createModule.createData -- | ${createData.context}(context)`);
					console.log(`${resolveData.request}(request) | -- createModule.resolveData -- | ${resolveData.context}(context)`);
					console.log('----- createModule ---- \n');
				}
			})
		});
		
	}
}

AliasPlugin.js

Resolve

class NormalModuleFactory extends ModuleFactory {
  constructor() {
    // ...
    this.hooks.resolve.tapAsync(..., (resolveData) => {
      //...
      // createData 包含 alias 解析后路径
      Object.assign(resolveData.createData, {
        // ...
        request: stringifyLoadersAndResource(
          allLoaders,
          resourceData.resource
        ),
        userRequest,
        rawRequest: request,
      });
    })
  }
}

Module

class NormalModuleFactory extends ModuleFactory {
  constructor() {
    // ...
    this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
      // ...

      const createData = resolveData.createData;

      this.hooks.createModule.callAsync(
        createData,
        resolveData,
        (err, createdModule) => {
          if (!createdModule) {
            // ...
						// 创建 module
            createdModule = new NormalModule(createData);
          }

          createdModule = this.hooks.module.call(
            createdModule,
            createData,
            resolveData
          );

          return callback(null, createdModule);
        }
      );
    });
  }
}

基本上获取路径都会去取 request,例如 IgnorePlugin 插件,在 beforeResolve 阶段处理。此时的路径为代码内路径(例如:”我被Alias了”)。
怎么判断插件是拿什么匹配的?一般涉及路径都接收多种类型,例如函数,console 即可。

class IgnorePlugin {
  checkIgnore(resolveData) {
    if (
			this.options.checkResource(resolveData.request, resolveData.context)
		) {
			return false;
		}
  }
  
  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap("IgnorePlugin", nmf => {
      nmf.hooks.beforeResolve.tap("IgnorePlugin", this.checkIgnore);
    });
		// ...
  }
}
// webpack.config.js plugins
new webpack.IgnorePlugin({
  resourceRegExp: /我被Alias了/,
})

https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw
https://mp.weixin.qq.com/s?__biz=Mzg3OTYwMjcxMA==&mid=2247484088&idx=1&sn=41bf509a72f2cbcca1521747bf5e28f4&chksm=cf00bfc1f87736d76681e1e1db39deb1fa3121686bcd45a42709fa58aaba6d7ffba6d7fb4975&scene=178&cur_album_id=1856066636768722949#rd
https://mp.weixin.qq.com/s/tXkGx6Ckt9ucT2o8tNM-8w