【Webpack】前端工程化之Webpack与模块化开发
目 录
- 前言
- 模块化开发
- Stage1 - 文件划分方式
- Stage2 - 命名空间方式
- Stage3 - IIFE(立即调用函数表达式)
- Stage 4 - IIFE 依赖参数
- 模块化的标准规范
- 使用Webpack实现模块化打包
- 安装Webpack
- Webpack基本配置
- Webpack构建流程
- Webpack热更新
- Webpack打包优化
前言
车同轨,书同文,行同伦。 ——《礼记·中庸》
模块化开发
Webpack
是一个JavaScript应用的静态模块化打包工具,它最早的出发点就是去实践前端方向的模块化开发,解决如何在前端项目中更高效地管理和维护项目中的每一个资源问题。在Webpack的理念中,前端项目中的任何资源都可以作为一个模块,任何模块都可以经过Loader机制的处理,最终再被打包到一起。
既如此,说到webpack,就不得不cue一下模块化了。
模块化: 随着前端应用的日益复杂化,我们的项目已经逐渐膨胀到了不得不花大量时间去管理的程度。而模块化就是一种最主流的项目组织方式,它通过把复杂的代码按照功能划分为不同的模块单独维护,从而提高开发效率,降低维护成本。
关于模块化的发展,其实是有几个代表阶段:
Stage1 - 文件划分方式
早期是基于文件划分的方式实现模块化开发,这也是web最原始的模块系统。
具体做法:将每个功能及其相关状态数据各自单独放到不同的JS文件中,约定每个文件是一个独立的模块。如果要使用某个模块,就将这个模块引入到页面中,一个
script
标签对应一个模块,然后直接调用模块中的成员(变量/函数)。
|——module-a.js
|——module-b.js
|——index.html
// module-a.js
function foo() {console.log("moduleA#foo");
}
// module-b.js
let name = "aDiao";
const data = "moduleB#foo";
// index.html<body><script src="moudle-a.js"></script><script src="moudle-b.js"></script><script>foo();console.log(name);console.log(data);name = "aDiao#Ya";console.log("===", name);data = "index#html";console.log("===", data);</script></body>
从上面的demo中可以看到,这样写会出现一些问题:
- 模块直接在全局工作,大量模块成员污染全局作用域;
- 没有私有空间,所有模块内的成员都可以在模块外部被访问或者修改;
- 一旦模块增多,容易产生命名冲突;
- 无法管理模块与模块之间的依赖关系;
- 在维护的过程中,也很难分辨每个成员所属的模块。
Stage2 - 命名空间方式
后来,我们约定每个模块只暴露一个全局对象,所有模块成员都挂载到这个全局对象中。
具体做法是在第一阶段的基础上,通过将每个模块“包裹”为一个全局对象的形式实现,这种方式就好像是为模块内的成员添加了“命名空间”,所以我们又称之为命名空间方式。
|——module-a.js
|——module-b.js
|——index.html
// module-a.js
window.moduleA = {method1: function () {console.log("moduleA#method1");},
};
// module-b.js
window.moduleB = {data: "ImModuleB",method1: function () {console.log("moduleB#method1");},
};
// index.html<body><script src="moudle-a.js"></script><script src="moudle-b.js"></script><script>moduleA.method1();moduleB.method1();moduleA.data = "ImModuleA";console.log(moduleA.data);console.log(moduleB.data);</script></body>
这种方式只是解决了命名冲突的问题,但其他问题仍然存在。
Stage3 - IIFE(立即调用函数表达式)
立即调用函数表达式(IIFE)是一个在定义时就会立即执行的 JavaScript 函数。
它是一种设计模式,也被称为自执行匿名函数,主要包含两部分:
- 第一部分是一个具有词法作用域的匿名函数,并且用圆括号运算符 () 运算符闭合起来。这样不但阻止了外界访问自执行匿名函数中的变量,而且不会污染全局作用域。
- 第二部分创建了一个立即执行函数表达式 (),通过它,JavaScript 引擎将立即执行该函数。
使用IIFE给模块提供私有空间,避免污染全局命名空间。
具体做法是将每个模块成员都放在一个立即执行函数所形成的私有作用域中,对于需要暴露给外部的成员,通过挂到全局对象上的方式实现。
|——module-a.js
|——module-b.js
|——index.html
// module-a.js
(function () {var name = "module-a";function method1() {console.log(name + "#method1");}window.moduleA = {method1: method1,};
})();
// module-b.js
(function () {var name = "module-b";function method1() {console.log(name + "#method1");}window.moduleB = {method1: method1,};
})();
// index.html<body><script src="moudle-a.js"></script><script src="moudle-b.js"></script><script>moduleA.method1();moduleB.method1();console.log(name);</script></body>
这种方式将成员私有化,私有成员只能在模块成员内通过闭包的形式访问,解决了全局作用域污染和命名冲突的问题。
Stage 4 - IIFE 依赖参数
在IIFE的基础上,还可以利用IIFE参数作为依赖声明使用,让每一个模块之间的依赖关系变得更加明显。
// module-a.js
(function ($) {var name = "module-a";function method1() {console.log(name + "#method1");$(".box").animate({ width: "200px" });}window.moduleA = {method1: method1,};
})(jQuery);
// index.html<style>.box {width: 100px;height: 100px;background-color: pink;}</style><body><div class="box"></div><script src="https://unpkg.com/jquery"></script><script src="moudle-a.js"></script><script src="moudle-b.js"></script><script>moduleA.method1();moduleB.method1();</script></body>
模块化的标准规范
以上4个阶段是早期的开发者,在没有工具和规范的情况下对模块化的实现方式,虽然解决了很多在前端领域实现模块化的问题,但仍然存在一些没有解决的问题。
其中,比较明显的问题:
- 模块化的加载。 上面都是通过
script
标签的方式直接在页面中引入这些模块,时间久了维护起来会十分麻烦。比如,某个代码需要用到某个模块,如果html中忘记引入这个模块,或者代码中移除了某个模块,但是html中忘记删除该模块的引用等,都会引起很多问题和麻烦。 - 模块化的规范。 上面几种方式,不同的开发者在实现的过程中都会出现一些细微的差别,为了统一不同开发者、不同项目之间的差异,需要指定一个行业标准来规范模块化的实现方式。
由此,结合上述的问题,现在的需求就是:
- 一个统一的模块化标准规范
- 一个可以自动加载模块的基础库
说到模块化规范,在历史的长河中,出现了前端五大模块化规范(我知道滴有五种~)
-
CommonJS规范:最初提出来是在浏览器以外的地方使用,并且当时命名为
ServerJS
,后来为了体现它的广泛性,更名为CommonJS
,也可以简称为CJS
。- 该规范约定,一个文件就是一个模块,每个模块都有单独的作用域,通过
exports
或者module.exports
导出需要暴露的内容,然后通过require
方法同步加载所依赖的模块。 - CommonJS模块的加载是同步的,需要等模块加载完毕后,后面的逻辑才会执行,这个在服务器不会有什么问题,因为服务器加载的是本地JS文件,速度会很快。但如果在浏览器端加载,需要先从服务端下载下来,然后再加载运行,会造成浏览器线程阻塞。
- 该规范约定,一个文件就是一个模块,每个模块都有单独的作用域,通过
-
AMD规范:即异步模块定义规范,主要是为浏览器环境设计的,推崇依赖前置,也就是提前执行(预执行),在模块使用之前就已经执行完毕。
- 在 AMD 规范中约定每个模块通过
define()
函数定义,这个函数默认可以接收两个参数,第一个参数是一个数组,用来声明这个模块的依赖项;第二个参数是一个函数,参数与前面的依赖项一一对应,每一项分别对应依赖项模块的导出成员,这个函数的作用就是为当前模块提供一个私有空间。如果在当前模块中需要向外部导出成员,可以通过return
的方式实现,然后通过require
语句加载模块。 - 实现
AMD
规范的库主要是require.js
和curl.js
。原生的JavaScript
环境并不支持异步加载的方式,require.js
提供了一种机制来异步加载模块,并且可以在加载完成后执行回调函数。
- 在 AMD 规范中约定每个模块通过
-
CMD规范:应用于浏览器的一种模块化规范,也是通过异步加载模块的,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖、延迟执行,目前也很少使用了。
- 在CMD规范中,通过全局函数
define
定义模块,这个函数接受一个factory
参数,可以是一个函数,也可以是一个对象或字符串;当factory
是函数时,接收三个参数,function(require, exports, module)
,require
函数用来获取其他模块提供的接口require(模块标识ID);exports
对象用来向外提供模块接口;module
对象存储了与当前模块相关联的属性和方法。 - CMD从语法上分析,结合了AMD模块定义的特点,同时又沿用了CommonJs 模块导入和导出的特点
- 在CMD规范中,通过全局函数
-
UMD规范:UMD是AMD和CommonJS的糅合。
- UMD的实现:先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式;再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块;前两个都不存在,则将模块公开到全局(window或global)。
-
ESModule规范:ESM是ECMAScript 2015 (ES6)中才定义的模块系统,存在环境兼容问题。它的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
- 在ESM规范 中,使用
import
引用模块,使用export
导出模块。默认情况下,Node.js
是不支持import
语法的,通过babel
将ES6
模块 编译为ES5
的CommonJS
。因此Babel
实际上是将import/export
翻译成Node.js
支持的require/exports
。 - ESM的解析过程可以划分为三个阶段:(1)构建,根据地址查找JS文件,并且下载,将其解析为模块记录。(2)实例化,对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。 (3)运行,运行代码,计算值,并将值填充到内存地址中。
- 在ESM规范 中,使用
模块化可以帮助我们更好地解决复杂应用开发过程中的代码组织问题,但是也会产生新的问题:
- 模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率。【将散落的模块打包到一起】
- 随着应用日益复杂,在前端应用开发过程中不仅仅只有
JavaScript
代码需要模块化,HTML
和CSS
这些资源文件也会面临需要被模块化的问题。而且从宏观角度来看,这些文件也都应该看作前端应用中的一个模块,只不过这些模块的种类和用途跟JavaScript
不同。【支持不同种类的前端资源模块】
针对这些问题,我们可使用前端模块打包工具来解决。
使用Webpack实现模块化打包
- Webpack 作为一个模块打包工具,可以解决模块化代码打包的问题,将零散的JavaScript代码打包到一个JS文件中。
- 对于有环境兼容问题的代码,Webpack 可以在打包过程中通过 Loader 机制对其实现编译转换,然后再进行打包。
- 对于不同类型的前端模块类型,Webpack 支持在 JavaScript 中以模块化的方式载入任意类型的资源文件,例如,我们可以通过 Webpack 实现在 JavaScript 中加载 CSS 文件,被加载的 CSS 文件将会通过 style 标签的方式工作。
- Webpack 还具备代码拆分的能力,它能够将应用中所有的模块按照我们的需要分块打包。这样一来,就不用担心全部代码打包到一起,产生单个文件过大,导致加载慢的问题。我们可以把应用初次加载所必需的模块打包到一起,其他的模块再单独打包,等到应用工作过程中实际需要用到某个模块,再异步加载该模块,实现增量加载或者叫作渐进式加载,非常适合现代化的大型 Web 应用。
安装Webpack
1、初始化项目
npm init --yes
2、安装Webpack(如果是webpack4.0以上版本,需要安装Webpack-cli)
npm i webpack webpack-cli --save-dev
3、查看Webpack版本信息
npx webpack --version
Webpack基本配置
- 在项目根目录添加Webpack的配置文件
webpack.config.js
module.exports = {};
1. 配置入口文件
入口文件就是应用程序的起点,webpack在解析代码的时候,会先找到入口文件,从入口文件开始递归解析入口文件中所有的依赖项,构建依赖图。
// 单入口
module.exports = {entry: "./src/main.js",
};
// 多入口:当需要创建多个 bundle 时,可以配置多个入口点。
module.exports = {entry: {pageOne: './src/pageOne/index.js',pageTwo: './src/pageTwo/index.js',pageThree: './src/pageThree/index.js',},
};
2. 配置输出
通过配置output属性,告诉webpack在哪里输出它所创建的bundle,以及如何命名这些文件。
const path = require('path');module.exports = {entry: "./src/main.js",output: {path: path.resolve(__dirname, 'dist'),filename: "bundle.js",},
};
3. 配置loader
loader用来转换某些类型的模块,负责完成项目中各种各样资源模块的加载。因为webpack默认只能打包处理JS类型的文件,无法处理其它非JS类型的文件。如果想要处理非JS类型的文件,需要手动安装一些合适的第三方loader加载器。比如将样式表(CSS)、图片、JSON 或 TypeScript 文件转换为 JavaScript 模块。
const path = require('path');module.exports = {entry: "./src/main.js",output: {path: path.resolve(__dirname, 'dist'),filename: "bundle.js",},module: {rules: [{ test: /\.css$/, use: 'css-loader' },{ test: /\.ts$/, use: 'ts-loader' },],},
};
4. 配置plugin
plugin可以用来执行范围更广的任务,增强webpack在项目自动化构建方面的能力。比如:
- 打包之前自动清除上次打包的dist文件;
- 自动生成应用所需要的html文件;
- 自动压缩webpack打包完成后输出的文件;
- 自动发布打包结果到服务器实现自动部署…
const path = require('path');module.exports = {entry: "./src/main.js",output: {path: path.resolve(__dirname, 'dist'),filename: "bundle.js",},module: {rules: [{ test: /\.css$/, use: 'css-loader' },{ test: /\.ts$/, use: 'ts-loader' },],},plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
5. 配置工作模式
通过设置 mode 参数,来选择不同的工作模式:production【启动内置优化插件,自动优化打包结果,打包速度偏慢】、development【自动优化打包速度,添加一些调试过程中的辅助插件】、none【运行最原始的打包,不做任何额外处理】,可以启用 webpack 内置在相应环境下的优化。其默认值为 production。
const path = require('path');module.exports = {mode: "development",entry: "./src/main.js",output: {path: path.resolve(__dirname, 'dist'),filename: "bundle.js",},module: {rules: [{ test: /\.css$/, use: 'css-loader' },{ test: /\.ts$/, use: 'ts-loader' },],},plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
Webpack构建流程
webpack打包的大致过程: 根据配置找到指定的入口文件,从入口文件开始,根据代码中出现的
import/require
解析这个文件所依赖的资源模块,然后再分别解析每个资源模块的依赖,构建依赖关系树,然后递归遍历这个依赖树,找到每个节点对应的资源文件,把不同类型的模块交给对应的Loader 处理,处理完成后打包到一起 (bundle.js) 。注意: 对于依赖模块中无法通过JS代码表示的资源模块,如图片、字体文件等,一般Loader会把它们单独作为资源文件拷贝到输出目录中,然后将这个资源文件所对应的访问路径作为这个模块的导出成员,暴露给外部。
浅看一下 webpack@5.92.1
和 webpack-cli@5.1.4
的源码 ~
运行webpack命令时,把通过命令行传入的参数转换为webpack的配置选项对象,根据命令行参数加载指定的配置文件,载入webpack核心模块,传入配置选项,创建Compiler编译器对象。【webpack-cli@5.1.4】
// webpack-cli/bin/cli.js
...
// 初始化并执行runCLI
const runCLI = require("../lib/bootstrap");
...
runCLI(process.argv);
// webpack-cli/lib/bootstrap.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// eslint-disable-next-line @typescript-eslint/no-var-requires
const WebpackCLI = require("./webpack-cli");
// // 创建Webpack CLI的一个新实例,并使用给定的参数运行CLI
const runCLI = async (args) => {const cli = new WebpackCLI();try {await cli.run(args);}catch (error) {cli.logger.error(error);process.exit(2);}
};
module.exports = runCLI;
// webpack-cli/lib/webpack-cli.js
...// 运行 CLI,解析命令行参数并执行相应的命令。async run(args, parseOptions) {...// 如果命令是构建或监听命令,则执行相应的Webpack配置和构建流程。if (isBuildCommandUsed || isWatchCommandUsed) {await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {// 加载Webpack配置。this.webpack = await this.loadWebpack();// 获取内置选项。return this.getBuiltInOptions();}, async (entries, options) => {// 如果有入口文件,则将其合并到Webpack配置的入口选项中。if (entries.length > 0) {options.entry = [...entries, ...(options.entry || [])];}// 运行Webpack构建/监听流程。await this.runWebpack(options, isWatchCommandUsed);});}...}...// 异步执行 Webpack 打包过程的核心方法async runWebpack(options, isWatchCommand) {// 初始化Webpack编译器实例。let compiler;...// 创建Webpack编译器实例。compiler = await this.createCompiler(options, callback);...}...// 配置和初始化Webpack编译器async createCompiler(options, callback) {...// 加载Webpack配置let config = await this.loadConfig(options);// 构建Webpack配置config = await this.buildConfig(config, options);let compiler;try {// 根据传入的配置选项数据,初始化Webpack编译器,如果提供了回调函数,则在编译完成后调用compiler = this.webpack(config.options,callback? (error, stats) => {if (error && this.isValidationError(error)) {this.logger.error(error.message);process.exit(2);}callback(error, stats);}: callback);// @ts-expect-error error type assertion} catch (error) {// 处理初始化Webpack编译器时的错误...}return compiler;}
...
从Webpack模块的入口文件出发,首先会检查options参数是否符合webpack配置要求,然后判断options的类型,创建单个或多个编译器。
// webpack/lib/webpack.js
...
/*** 根据提供的配置选项创建并运行 Webpack 编译器,可以接受单个配置对象或多个配置对象的数组。* 如果提供了回调函数,则会根据配置运行编译器,并在编译完成后执行回调。* 如果配置中设置了监听模式(watch),则会进入监听模式。*/
const webpack = ((options, callback) => {// 用于创建编译器实例并配置监听模式。const create = () => {// // 检查配置选项是否符合 Webpack 配置 schemaif (!asArray(options).every(webpackOptionsSchemaCheck)) {// 如果配置不通过 schema 检查,则报告错误getValidateSchema()(webpackOptionsSchema, options);// 使用 util.deprecate 标记已弃用的功能,并提供错误消息util.deprecate(() => {},"webpack bug: Pre-compiled schema reports error while real schema is happy. This has performance drawbacks.","DEP_WEBPACK_PRE_COMPILED_SCHEMA_INVALID")();}let compiler;let watch = false;let watchOptions;// 根据 options 是否为数组,创建单个或多个编译器if (Array.isArray(options)) {/*** 创建一个处理多个Webpack配置的编译器实例* createMultiCompiler()内部还是通过遍历options,创建单个编译器实例合并成数组做处理。*/compiler = createMultiCompiler(options,options);watch = options.some(options => options.watch);watchOptions = options.map(options => options.watchOptions || {});} else {// 创建一个处理单个Webpack配置的编译器实例const webpackOptions = options;compiler = createCompiler(webpackOptions);watch = webpackOptions.watch;watchOptions = webpackOptions.watchOptions || {};}return { compiler, watch, watchOptions };};if (callback) {...}}
);module.exports = webpack;
在创建单个Compiler对象的时候,webpack会注册配置中的插件。
// webpack/lib/webpack.js
...
/*** 创建单个Webpack编译器实例。* @param {number} [compilerIndex] index of compiler* @returns {Compiler} a compiler*/
const createCompiler = (rawOptions, compilerIndex) => {...const compiler = new Compiler(options.context,options);// 初始化Node环境插件,设置编译器的运行环境。new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);// 遍历并应用配置中的插件列表。if (Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin === "function") {plugin.call(compiler, compiler);} else if (plugin) {plugin.apply(compiler);}}}...// 触发编译器的environment和afterEnvironment钩子,在编译开始前进行环境相关的初始化。compiler.hooks.environment.call();compiler.hooks.afterEnvironment.call();// 这里创建内置插件new WebpackOptionsApply().process(options, compiler);compiler.hooks.initialize.call();return compiler;
};
...
创建完Compiler对象之后,会判断配置选项中是否开启监视模式。
- 如果是监视模式就调用Compiler对象的watch方法,用监视模式启动构建。
- 如果不是监视模式就调用Compiler对象的run方法,开始构建整个应用。
// webpack/lib/webpack.js
...
/*** 根据提供的配置选项创建并运行 Webpack 编译器,可以接受单个配置对象或多个配置对象的数组。* 如果提供了回调函数,则会根据配置运行编译器,并在编译完成后执行回调。* 如果配置中设置了监听模式(watch),则会进入监听模式。*/
const webpack = ((options, callback) => {// 用于创建编译器实例并配置监听模式。const create = () => {... };if (callback) {try {// 创建Webpack编译器和监听配置const { compiler, watch, watchOptions } = create();// 如果设置了监听模式,则直接开始监听if (watch) {compiler.watch(watchOptions, callback);} else {// 在非监听模式下执行编译,开始构建整个应用,在编译完成后关闭编译器compiler.run((err, stats) => {compiler.close(err2 => {callback(err || err2,stats);});});}// 返回webpack编译器实例return compiler;} catch (err) {// 如果在处理过程中出现错误,就延迟调用回调函数,并传递错误process.nextTick(() => callback(err));// 返回null,处理过程中出现错误return null;}} else {// 创建webpack编译器实例,但不执行编译或者监听const { compiler, watch } = create();if (watch) {util.deprecate(() => { },"A 'callback' argument needs to be provided to the 'webpack(options, callback)' function when the 'watch' option is set. There is no way to handle the 'watch' option without a callback.","DEP_WEBPACK_WATCH_WITHOUT_CALLBACK")();}return compiler;}}
);module.exports = webpack;
Compiler内部先触发beforeRun和run两个钩子,然后调用 this.compile(onCompiled);
开始编译整个项目。
// webpack/lib/Compiler.js
...
class Compiler {constructor(context, options = ({})) {...}...// 执行编译过程run(callback) {if (this.running) {return callback(new ConcurrentCompilationError());}const finalCallback = (err, stats) => {...}const startTime = Date.now();this.running = true;const onCompiled = (err, _compilation) => {...}// 执行编译的函数。const run = () => {// 调用beforeRun钩子。this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);// 调用run钩子。this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);// 读取记录。this.readRecords(err => {if (err) return finalCallback(err);// 开始编译。this.compile(onCompiled);});});});};// 如果当前处于空闲状态,则先结束缓存的空闲状态,然后开始运行。if (this.idle) {this.cache.endIdle(err => {if (err) return finalCallback(err);this.idle = false;run();});} else {// 如果不处于空闲状态,直接开始运行。run();}}...
}
module.exports = Compiler;
调用 this.compile(onCompiled);
方法内部主要是创建一个Compilation对象,包含本次构建中全部的资源和信息。
// webpack/lib/Compiler.js
...
/*** 编译器编译方法:负责执行编译的各个阶段,包括预编译、编译、制作、完成制作、完成编译等步骤。*/
compile(callback) {// 初始化编译参数const params = this.newCompilationParams();// 在编译前执行自定义钩子,异步调用。this.hooks.beforeCompile.callAsync(params, err => {// 如果钩子执行出错,直接返回错误回调。if (err) return callback(err);// 执行编译阶段的钩子。this.hooks.compile.call(params);// 创建新的编译实例,里面包含这次构建中全部的资源信息const compilation = this.newCompilation(params);// 获取编译器的日志记录器,用于记录特定于编译器的日志。const logger = compilation.getLogger("webpack.Compiler");// 记录make阶段的开始时间。logger.time("make hook");// 执行make阶段的钩子,异步调用。this.hooks.make.callAsync(compilation, err => {// 记录make阶段的结束时间。logger.timeEnd("make hook");// 如果钩子执行出错,直接返回错误回调。if (err) return callback(err);// 记录完成make阶段的开始时间。logger.time("finish make hook");// 执行完成make阶段的钩子,异步调用。this.hooks.finishMake.callAsync(compilation, err => {...});});
}
...
创建完Compilation之后,触发make钩子【事件触发机制】,根据入口文件配置找到入口模块,开始递归遍历所有的依赖,形成依赖关系树。
make钩子是在编译过程中生成新的模块、依赖关系、chunk等。这个阶段的代码执行是通过事件触发机制,让外部监听这个make事件的地方开始执行的。如果要知道哪些地方会开始执行,就需要找到哪个地方注册了make事件。
// webpack/lib/Compiler.js
...
// 调用 make 钩子,在编译过程中生成新的模块和 chunk
this.hooks.make.callAsync(compilation, (err) => {logger.timeEnd("make hook"); // 结束 "make hook" 计时器if (err) return callback(err); // 如果出现错误,调用回调函数并传递错误logger.time("finish make hook"); // 开始 "finish make hook" 计时器// 调用 finishMake 钩子,在 make 钩子完成后执行的逻辑this.hooks.finishMake.callAsync(compilation, (err) => {logger.timeEnd("finish make hook"); // 结束 "finish make hook" 计时器if (err) return callback(err); // 如果出现错误,调用回调函数并传递错误// 使用 process.nextTick 来确保在事件循环的下一轮执行process.nextTick(() => {logger.time("finish compilation"); // 开始 "finish compilation" 计时器// 调用 compilation.finish,完成编译过程,准备生成最终的输出compilation.finish((err) => {logger.timeEnd("finish compilation"); // 结束 "finish compilation" 计时器if (err) return callback(err); // 如果出现错误,调用回调函数并传递错误logger.time("seal compilation"); // 开始 "seal compilation" 计时器// 调用 compilation.seal,封闭编译结果,使其不可更改compilation.seal((err) => {logger.timeEnd("seal compilation"); // 结束 "seal compilation" 计时器if (err) return callback(err); // 如果出现错误,调用回调函数并传递错误logger.time("afterCompile hook"); // 开始 "afterCompile hook" 计时器// 调用 afterCompile 钩子,用于执行编译完成后的逻辑this.hooks.afterCompile.callAsync(compilation, (err) => {logger.timeEnd("afterCompile hook"); // 结束 "afterCompile hook" 计时器if (err) return callback(err); // 如果出现错误,调用回调函数并传递错误// 如果整个过程成功完成,调用回调函数并传递编译结果return callback(null, compilation);});});});});});
});
...
webpack官方通过自己的Tapable库实现事件注册,可以在VSCode全局搜索 make.tap
来找到事件的注册位置。
// VSCode可能无法搜索node_modules里面的内容,可以在setting.json里添加代码:
{"search.exclude": {"**/node_modules":false},"search.useIgnoreFiles":false
}
然后就搜索到了7个插件中都注册了make事件,这些插件都是前面创建Compiler对象的时候创建的【去看createCompiler代码】。
根据内置插件,找到入口文件的处理插件 EntryPlugin
,内部调用了 compilation.addEntry()
传入上下文、入口依赖和选项等参数,开始解析入口文件。
// webpack/lib/EntryPlugin.js
class EntryPlugin {
...
/*** 此方法主要是在webpack编译器上应用EntryPlugin插件。通过监听编译和make阶段的钩子来设置入口依赖项和添加入口点到编译。*/
apply(compiler) {// 在编译阶段注册一个钩子,用于设置入口依赖的工厂。compiler.hooks.compilation.tap("EntryPlugin",(compilation, { normalModuleFactory }) => {// 将EntryDependency依赖的工厂设置为normalModuleFactory。compilation.dependencyFactories.set(EntryDependency,normalModuleFactory);});const { entry, options, context } = this;// 创建一个入口依赖项。const dep = EntryPlugin.createDependency(entry, options);// 在make阶段注册一个钩子,用于向编译添加入口点。compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {// 添加入口点到编译,传入上下文、入口依赖和选项。compilation.addEntry(context, dep, options, err => {// 回调函数处理错误或完成添加。callback(err);});});
}
...
}
找到 addEntry()
方法,可以发现里面通过addModuleTree()
,将入口模块添加到模块依赖列表中。
// webpack/lib/Compilation.js
class Compilation {...// 向webpack配置中的特定上下文添加一个新的入口项。addEntry(context, entry, optionsOrName, callback) {...// 调用内部方法添加入口项到指定的dependencies中。this._addEntryItem(context, entry, "dependencies", options, callback);}...// 添加入口项。_addEntryItem(context, entry, target, options, callback) {...// // 添加模块树,并处理结果。this.addModuleTree({context,dependency: entry,contextInfo: entryData.options.layer? { issuerLayer: entryData.options.layer }: undefined},(err, module) => {...});}...// 根据传入的依赖信息创建一个新的模块,添加模块树。addModuleTree({ context, dependency, contextInfo }, callback) {// // 验证 dependency 是否为有效对象if (typeof dependency !== "object" || dependency === null || !dependency.constructor) {return callback(new WebpackError("Parameter 'dependency' must be a Dependency"));}// 从 dependency 中获取构造函数const Dep = dependency.constructor;// 尝试从工厂中获取对应的模块创建函数const moduleFactory = this.dependencyFactories.get(Dep);// 如果没有找到对应的工厂,返回错误if (!moduleFactory) {return callback(new WebpackError(`No dependency factory available for this dependency type: ${dependency.constructor.name}`));}// 处理模块的创建过程this.handleModuleCreation({factory: moduleFactory,dependencies: [dependency],originModule: null,contextInfo,context},(err, result) => {...});}
}
在 handleModuleCreation
里调用 _handleModuleBuildAndDependencies
, 其内部通过Compiler对象的buildModule
来进行模块构建。
// webpack/lib/Compilation.js
...
// 模块构建,处理模块的解析、工厂调用、依赖注入和模块图的更新。
handleModuleCreation({factory,dependencies,originModule,contextInfo,context,recursive = true,connectOrigin = recursive,checkCycle = !recursive},callback
) {// 获取模块图实例const moduleGraph = this.moduleGraph;// 根据当前是否启用 profiling 创建对应的模块profileconst currentProfile = this.profile ? new ModuleProfile() : undefined;// 实例化模块的过程,包括解析依赖和执行工厂函数this.factorizeModule({currentProfile,factory,dependencies,factoryResult: true,originModule,contextInfo,context},(err, factoryResult) => {// 处理工厂结果中的依赖信息const applyFactoryResultDependencies = () => {...};...// 获取工厂函数返回的模块实例const newModule = factoryResult.module;...// 添加模块到模块图this.addModule(newModule, (err, _module) => {if (err) {applyFactoryResultDependencies();if (!err.module) {err.module = _module;}this.errors.push(err);return callback(err);}// 处理模块的不安全缓存逻辑const module = _module;if (this._unsafeCache &&factoryResult.cacheable !== false &&module.restoreFromUnsafeCache &&this._unsafeCachePredicate(module)) {...}...// 继续处理模块的构建和依赖this._handleModuleBuildAndDependencies(originModule,module,recursive,checkCycle,callback);});});
}
...
// 处理模块构建及其依赖
_handleModuleBuildAndDependencies(originModule, // 原始模块,触发构建的起点module, // 当前要处理的模块recursive, // 是否递归处理依赖checkCycle, // 是否检查循环依赖callback // 构建完成后的回调函数
) {// 检查在另一个构建过程中是否触发了构建,以避免循环依赖let creatingModuleDuringBuildSet = undefined;...// 构建模块,buildModule方法中执行具体的Loader,处理特殊资源的加载。this.buildModule(module, err => {if (creatingModuleDuringBuildSet !== undefined) {// 如果在构建过程中添加了模块,构建完成后移除creatingModuleDuringBuildSet.delete(module);}if (err) {// 如果构建过程中出现错误,将错误与模块关联并添加到错误列表if (!err.module) {err.module = module;}this.errors.push(err);return callback(err); // 返回错误}if (!recursive) {// 如果不需要递归处理依赖,直接处理当前模块的依赖this.processModuleDependenciesNonRecursive(module);callback(null, module); // 回调函数,无错误,返回模块return;}// 为了避免循环依赖导致的死锁,检查是否已经在处理依赖队列中if (this.processDependenciesQueue.isProcessing(module)) {return callback(null, module); // 如果已经在处理中,直接返回模块}// 递归处理模块的依赖this.processModuleDependencies(module, err => {if (err) {return callback(err); // 如果处理依赖时出现错误,返回错误}callback(null, module); // 否则,回调函数,无错误,返回模块});});
}
然后再回到EntryPlugin类的apply方法里,有一段代码是将 EntryDependency
类与 normalModuleFactory
关联起来。这意味着当遇到 EntryDependency
类型的依赖时,将使用 normalModuleFactory
来创建对应的模块。
compiler.hooks.compilation.tap("EntryPlugin",(compilation, { normalModuleFactory }) => {compilation.dependencyFactories.set(EntryDependency,normalModuleFactory);
});
normalModuleFactory
主要是负责创建处理 JavaScript 模块的 NormalModule
实例。在 Webpack 的构建过程中,normalModuleFactory
用来生成模块对象,这些对象随后会经过一系列的处理步骤,包括解析、编译、优化等。
在 normalModuleFactory
里通过createParser()
创建一个新的解析器实例, getParser()
获取一个特定类型的模块的解析器,来解析模块构成抽象语法树AST。
// webpack/lib/NormalModuleFactory.js
...
getParser(type, parserOptions = EMPTY_PARSER_OPTIONS) {let cache = this.parserCache.get(type);if (cache === undefined) {cache = new WeakMap();this.parserCache.set(type, cache);}let parser = cache.get(parserOptions);if (parser === undefined) {parser = this.createParser(type, parserOptions);cache.set(parserOptions, parser);}return parser;
}/*** @param {string} type type* @param {ParserOptions} parserOptions parser options* @returns {Parser} parser*/
createParser(type, parserOptions = {}) {parserOptions = mergeGlobalOptions(this._globalParserOptions,type,parserOptions);const parser = this.hooks.createParser.for(type).call(parserOptions);if (!parser) {throw new Error(`No parser registered for ${type}`);}this.hooks.parser.for(type).call(parser, parserOptions);return parser;
}
...
根据语法树分析模块是否还有对应的依赖模块,如果有的话就会继续循环构建每个依赖,直到所有的依赖解完成,构建阶段结束。
最后会合并生成需要输出的bundle.js到dist目录。
Webpack热更新
webpack中的模块热替换,就是说我们在程序运行的时候,修改了某个模块内容,如果没有使用模块热替换,就需要刷新整个应用程序来实现更新,并且刷新后页面中的状态信息都会丢失;如果使用模块热替换,就可以只用把变更的模块替换到应用程序里,不用完全刷新整个应用。
在webpack中主要是通过开启 HotModuleReplacementPlugin
这个插件来开启模块热更新。
在HMR运行的时候,通过执行 webpack-dev-server
命令,开启两个服务器 express server
和socket server
。express server
主要负责提供静态资源的服务,打包后的资源直接被浏览器请求和解析;socket server
是一个websocket长连接,主要是监听对应模块发生变化后,生成两个补丁文件,并且推送给浏览器端。
当某个文件或者模块发生变化,webpack通过监听这个文件或者模块对应的唯一hash值的变化,来判断文件是否需要重新编译打包,重新编译之后会再生成文件/模块对应的hash值,作为下次热更新的标识。之后服务端会通过socket server向浏览器端推送变更消息,消息内容主要是两个补丁文件和新的hash值。浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新。
Webpack打包优化
1. 提高打包速度
(1)优化Loader(让webpack拥有了加载和解析非js文件的能力):在使用loader时,可以通过配置include、exclude、test属性来匹配文件,通过include、exclude规定哪些匹配应用loader,优化Loader的文件搜索范围
module.exports = {module:{rules:[{// js文件才使用babeltest:/\.js$/,loader:'babel-loader',// 只在src文件夹下查找include:[resolve('src')],// 不会去查找的路径exclude:/node_modules/}]}
}
(2)HappyPack:因为webpack在打包的过程中是单线程的,在执行过程中可能会遇到很多需要耗时间编译的任务,HappyPack可以将Loader的同步执行转换为并行的。
module:{loaders:[{test:/\.js$/,include:[resolve('src')],exclude:/node_modules/,// id后面的内容对应下面loader:'happypack/loader?id=happybabel'}]
},
plugins:[new HappyPack({id:'happybabel',loaders:['babel-loader?cacheDirectory'],// 开启4个线程threads:4})
]
(3)DllPlugin:DllPlugin可以将特定的类库提前打包然后引入。这种方式可以减少导包类库的次数,只有当类库更新版本才会需要重新打包。
// 打包一个Dll库
module.exports = {entry:{// 想统一打包的类库vendor:['react']},output:{path:path.join(__dirname,'dist'),filename:'[name].dll.js',library:'[name]-[hash]'},plugins:[new webpack.DllPlugin({//name必须和output.library一致name:'[name]-[hash]',path:path.resolve(__dirname,"./dll/[name].mainfest.json")})]
}// 引入Dll库
module.exports = {...plugins:[new webpack.DllReferencePlugin({context:__dirname,// manifest就是之前打包出来的json文件manifest:require('./dist/vendor-manifest.json')})]
}
(4)Code Splitting:代码分割,把项目中的资源模块按照我们设计的规则打包到不同的bundle中。实现方式有两种:多入口打包;动态导入。
// 多入口打包-webpack.config.js
module.exports = {entry: {index: './src/index.js',main: './src/main.js'},output: {filename: '[name].bundle.js'},optimization:{splitChunks:{chunks:'all'}}
};
// 动态导入-在入口文件里写
const update = () => {const hash = window.location.hash || '#posts'const mainElement = document.querySelector('.main')mainElement.innerHTML = ''switch (hash) {case '#posts':import('./components/posts').then(posts => {mainElement.appendChild(posts())})breakcase '#about':import('./components/about').then(about => {mainElement.appendChild(about())})breakdefault:}
}
window.addEventListener('hashchange', update)
update()
2. 减少webpack打包体积
(1)使用插件压缩代码:CSS代码压缩 CssMinimizerPlugin
、HTML代码压缩 HtmlWebpackPlugin
、文件大小压缩 ComepressionPlugin
、图片压缩…
const { HtmlWebpackPlugin } = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');module.exports = {module: {rules: [{test: /\.(png|jpg|gif)$/,use: [{loader: 'file-loader',options: {name: '[name]_[hash].[ext]',outputPath: 'images/',},},{loader: 'image-webpack-loader',options: {mozjpeg: {progressive: true,quality: 65,},optipng: {enabled: false,},pngquant: {quality: '65-90',speed: 4,},gifsicle: {interlaced: false,},webp: {quality: 75,},},},],},],},plugins: [new HtmlWebpackPlugin({// HtmlWebpackPlugin 的配置项minify: {minifyCSS: false,collapseWhitespace: false,removeComments: true,},}),new CompressionPlugin({test: /\.(css|js)$/,threshold: 500,minRatio: 0.7,algorithm: 'gzip',}),],optimization: {minimize: true,minimizer: [new CssMinimizerPlugin({parallel: true,}),],},
};
(2)Scope Hoisting:能分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
module.exports = {optimization:{concatenateModules:true, // 尽可能将所有模块合并到一起输出到一个函数中}
}
(3)Tree Shaking:可以“摇掉”项目中没有被引用的代码。webpack的Tree-shaking特性在生产环境下会自动开启。在其他环境下,通过如下配置:
module.exports = {optimization:{usedExports:true, // 打包结果中指导处外部用到的成员minimize:true, // 压缩打包结果}
}
(4)sideEffects:通过配置标识我们的代码是否有副作用(模块执行的时候除了导出成员是否还做了其他的事情),从而决定是否要完整移除没有用到的模块。在生产环境下会自动开启。
module.exports = {optimization:{sideEffects:true // 判断模块是否有副作用,是否需要被打包。}
}
以上就是我学习Webpack的知识笔记,如有误,请指正!
学习链接
JavaScript模块化七日谈
一文吃透 Webpack核心原理
前端模块化开发那点历史
「前端工程四部曲」模块化的前世今生
相关文章:

【Webpack】前端工程化之Webpack与模块化开发
目 录 前言模块化开发Stage1 - 文件划分方式Stage2 - 命名空间方式Stage3 - IIFE(立即调用函数表达式)Stage 4 - IIFE 依赖参数模块化的标准规范 使用Webpack实现模块化打包安装WebpackWebpack基本配置Webpack构建流程Webpack热更新Webpack打包优化 前言…...

【Android】记录在自己的AMD处理器无法使用Android studio 虚拟机处理过程
文章目录 问题:无法在AMD平台打开Android studio 虚拟机,已解决平台:AMD 5700g系统:win10专业版1、在 amd平台上使用安卓虚拟机需要安装硬件加速器2、关闭win10上的系统服务 问题:无法在AMD平台打开Android studio 虚拟…...

LearnOpenGL - Android OpenGL ES 3.0 使用 FBO 进行离屏渲染
系列文章目录 LearnOpenGL 笔记 - 入门 01 OpenGLLearnOpenGL 笔记 - 入门 02 创建窗口LearnOpenGL 笔记 - 入门 03 你好,窗口LearnOpenGL 笔记 - 入门 04 你好,三角形OpenGL - 如何理解 VAO 与 VBO 之间的关系LearnOpenGL - Android OpenGL ES 3.0 绘制…...

人工智能虚拟仿真系统,解决算法难、编程难、应用场景难三大难题
近年来,人工智能技术迅猛发展,广泛渗透至各行业,市场份额持续扩大,预示着智能化转型的广阔前景。该行业本质上属于知识高度密集型,近年来的迅猛发展进一步加剧了对专业人才的迫切需求。 然而,我国目前在人工…...
CTE(公共表表达式)和视图在查询时的性能影响
在SQL查询优化和数据库设计中,CTE(公共表表达式)和视图都是常用的工具。尽管它们在功能和使用场景上有很多相似之处,但在查询性能方面可能存在显著差异。本文将探讨CTE和视图在查询时的性能影响,帮助您在实际项目中做出…...

新能源行业必会基础知识-----电力市场概论笔记-----绪论
新能源行业知识体系-------主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/139946830 目录 1. 电力市场的定义2. 对传统电力系统理论的挑战 1. 电力市场的定义 1. 我国电力市场的进程 我国新一轮电力体制改革的5大亮点&…...

003 SpringBoot操作ElasticSearch7.x
文章目录 5.SpringBoot集成ElasticSearch7.x1.添加依赖2.yml配置3.创建文档对象4.继承ElasticsearchRepository5.注入ElasticsearchRestTemplate 6.SpringBoot操作ElasticSearch1.ElasticsearchRestTemplate索引操作2.ElasticsearchRepository文档操作3.ElasticsearchRestTempl…...

npm install报错Maximum call stack size exceeded
npm 报错 方案: npm cache clean --force npm install...

第1章 基础知识
第1章 基础知识 1.1 机器语言 机器语言就是机器指令的集合,机器指令展开来讲就是一台机器可以正确执行的命令 1.2 汇编语言的产生 汇编语言的主题是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式。…...
python脚本 限制 外部访问 linux服务器端口
注意:该脚本会清空linux防火墙的filter表的规则和用户自定义链路 脚本的效果是将端口限制为仅服务器内部访问 可以提供ip地址白名单 具体脚本: #!/usr/bin/python3 import argparse, subprocess, sys, redef popen(cmd):global resulttry:result su…...

Redis-哨兵模式-主机宕机-推选新主机的过程
文章目录 1、为哨兵模式准备配置文件2、启动哨兵3、主机6379宕机3.4、查看sentinel控制台日志3.5、查看6380主从信息 4、复活63794.1、再次查看sentinel控制台日志 1、为哨兵模式准备配置文件 [rootlocalhost redis]# ll 总用量 244 drwxr-xr-x. 2 root root 150 12月 6 2…...

游戏工厂:AI(AIGC/ChatGPT)与流程式游戏开发
游戏工厂:AI(AIGC/ChatGPT)与流程式游戏开发 码客 卢益贵 ygluu 关键词:AI(AIGC、ChatGPT、文心一言)、流程式管理、好莱坞电影流程、电影工厂、游戏工厂、游戏开发流程、游戏架构、模块化开发 一、前言…...
每日一练 - OSPF 组播地址
01 真题题目 判断以下陈述是否正确: 224.0.0.6 是 ALL DRouters 监听地址 224.0.0.5 是 ALL SPFRouters 监听地址 A.正确 B.错误 02 真题答案 A 03 答案解析 在OSPF (Open Shortest Path First) 路由协议中,为了实现高效的信息交换和发现邻居&#x…...
AMHS工程师的培养
一、岗位职责主要包括: 1. 负责生产现场设备运行维护及异常处理,确保设备安全操作与保养。 2. 制定并实施AMHS计划和措施,对过程问题进行追踪解决。 3. 监控生产过程中的不良品率,确保生产过程的稳定性。 4. 建立AMHS标准作业程序文件,并定期更新和维护。 5. 负责AMHS…...

如何在前端项目中制定代码注释规范
本文是前端代码规范系列文章,将涵盖前端领域各方面规范整理,其他完整文章可前往主页查阅~ 开始之前,介绍一下最近很火的开源技术,低代码。 作为一种软件开发技术逐渐进入了人们的视角里,它利用自身独特的优势占领市…...
一位苹果手机硬件工程师繁忙的一天
早晨:迎接新的一天 7:00 AM - 起床 早晨七点准时起床。洗漱、吃早餐后,查看手机上的邮件和消息,以便提前了解今天的工作安排和优先事项。 7:30 AM - 前往公司 开车前往位于加州库比蒂诺的苹果总部。在车上习惯性地听一些与电子工程相关的播…...
Python | 使用均值编码(MeanEncoding)处理分类特征
在特征工程中,将分类特征转换为数字特征的任务称为编码。 有多种方法来处理分类特征,如OneHotEncoding和LabelEncoding,FrequencyEncoding或通过其计数替换分类特征。同样,我们可以使用均值编码(MeanEncoding)。 均值编码 均值…...

面试-java异常体系
1.java异常体系 error类是指与jvm相关的问题。如系统崩溃,虚拟机错误,内存空间不足。 非runtime异常不处理,程序就没有办法执行。 一旦遇到异常抛出,后面的异常就不会进行。 (1)常见的error以及exception 2.java异常要点分析…...

Clickhouse 的性能优化实践总结
文章目录 前言性能优化的原则数据结构优化内存优化磁盘优化网络优化CPU优化查询优化数据迁移优化 前言 ClickHouse是一个性能很强的OLAP数据库,性能强是建立在专业运维之上的,需要专业运维人员依据不同的业务需求对ClickHouse进行有针对性的优化。同一批…...

变工况下转子、轴承数据采集及测试
1.固定工况下的数据采集 1.wireshark抓包 通过使用 Wireshark 抓包和 Linux 端口重放技术,可以模拟实际机械设备的运行环境,从而减少实地验证软件和算法的复杂性和麻烦。 打开设备正常运转,当采集器通过网口将数据发送到电脑时,…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...

STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

多模态大语言模型arxiv论文略读(108)
CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题:CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者:Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...