Webpack: Loader开发 (1)

概述

  • 如何扩展 Webpack?有两种主流方式,一是 Loader —— 主要负责将资源内容翻译成 Webpack 能够理解、处理的 JavaScript 代码;二是 Plugin —— 深度介入 Webpack 构建过程,重塑 构建逻辑。

  • 相对而言,Loader 的职责更单一,入门成本相对较低。

  • 我们关注以下的基础

    • Loader 的基本形态与输入输出
    • 如何使用 Loader Context 上下文接口,并结合一些知名开源项目展开介绍部分常用接口
    • 如何为 Loader 编写自动测试代码
    • 深入剖析 Loader 链式调用模型

为什么需要 Loader?

为什么 Webpack 需要设计出 Loader 这一扩展方式?本质上是因为计算机世界中的文件资源格式实在太多,不可能一一穷举, 那何不将"解析"资源这部分任务开放出去,由第三方实现呢?Loader 正是为了将文件资源的“读”与“处理”逻辑解耦,Webpack 内部只需实现对标准 JavaScript 代码解析/处理能力,由第三方开发者以 Loader 方式补充对特定资源的解析逻辑。

  • 提示:Webpack5 之后增加了 Parser 对象,事实上已经内置支持图片、JSON 等格式的内容,不过这并不影响我们对 Loader 这一概念的理解。

  • 实现上,Loader 通常是一种 mapping 函数形式,接收原始代码内容,返回翻译结果,如:

    module.exports = function(source) {// 执行各种代码计算return modifySource;
    };
    

在 Webpack 进入构建阶段后,首先会通过 IO 接口读取文件内容,之后调用 LoaderRunner 并将文件内容以 source 参数形式传递到 Loader 数组,source 数据在 Loader 数组内可能会经过若干次形态转换,最终以标准 JavaScript 代码提交给 Webpack 主流程,以此实现内容翻译功能。

Loader 函数签名如下:

module.exports = function(source, sourceMap?, data?) {return source;
};

Loader 接收三个参数,分别为:

  • source:资源输入,对于第一个执行的 Loader 为资源文件的内容;后续执行的 Loader 则为前一个 Loader 的执行结果,可能是字符串,也可能是代码的 AST 结构;
  • sourceMap: 可选参数,代码的 sourcemap 结构;
  • data: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递额外的 AST 对象。

其中 source 是最重要的参数,大多数 Loader 要做的事情就是将 source 转译为另一种形式的 output ,比如 webpack-contrib/raw-loader 的核心源码:

//... 
export default function rawLoader(source) {// ...const json = JSON.stringify(source).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');const esModule =typeof options.esModule !== 'undefined' ? options.esModule : true;return `${esModule ? 'export default' : 'module.exports ='} ${json};`;
}

这段代码的作用是将文本内容包裹成 JavaScript 模块,例如:

// source
I am 范文杰// output
module.exports = "I am 范文杰"

经过模块化包装之后,这段文本内容变成 Webpack 可以理解的 JavaScript,其它 Module 也就能引用、使用它了。

需要注意,Loader 中执行的各种资源内容转译操作通常都是 CPU 密集型 —— 这放在 JavaScript 单线程架构下可能导致性能问题;又或者异步 Loader 会挂起后续的加载器队列直到异步 Loader 触发回调,稍微不注意就可能导致整个加载器链条的执行时间过长。

为此,Webpack 默认会缓存 Loader 的执行结果直到资源或资源依赖发生变化,开发者需要对此有个基本的理解,必要时可以通过 this.cachable 显式声明不作缓存:

module.exports = function(source) {this.cacheable(false);// ...return output;
};

Loader 简单示例

接下来我们尝试编写一个简单的 Loader Demo,理解如何开发、调试、使用自定义 Loader 组件。示例代码结构如下:

loader-custom
├─ src
│  ├─ cjs.js
│  ├─ index.js
│  └─ options.json
├─ package.json
└─ babel.config.js

核心代码 src/index.js 内容如下:

import { validate } from "schema-utils";
import schema from "./options.json";export default function loader(source) {const { version, webpack } = this;const options = this.getOptions();validate(schema, options, "Loader");const newSource = `/*** Loader API Version: ${version}* Is this in "webpack mode": ${webpack}*//*** Original Source From Loader*/${source}`;return newSource;
}
  • 提示:也可以在 Loader 代码中插入 debugger 语句,配合 ndb 工具启动调试模式。

代码逻辑很简单,核心功能只是在原来 source 上拼接了一些文本,但该有的东西也都有了:

  1. 通过 this.getOptions 接口获取 Loader 配置对象;
  2. 使用 schema-utils 的 validate 接口校验 Loader 配置是否符合预期,配置 Schema 定义在 src/options.json 文件;
  3. 返回经过修改的内容。

开发完成后,可以通过 module.rules 测试该 Loader,如:

const path = require("path");module.exports = {// ...module: {rules: [{test: /\.js$/,use: [{ // 传入示例 Loader 的绝对路径loader: path.resolve(__dirname, "../dist/index.js") }],},],},
};

也可以将 resolveLoader.modules 配置指向到 Loader 所在目录,Webpack 会在该目录查找加载器,如:

const path = require('path');module.exports = {//...resolveLoader: {modules: ['node_modules', path.resolve(__dirname, 'loaders')],},
};

接下来,我们可以开始使用 Loader 上下文接口实现更丰富的功能。

使用上下文接口

  • 提示:此处主要围绕 Webpack5 展开,Webpack4 稍有差异,不作单独解释。

除了作为内容转换器外,Loader 运行过程还可以通过一些上下文接口,有限制地影响 Webpack 编译过程,从而产生内容转换之外的副作用。上下文接口将在运行 Loader 时以 this 方式注入到 Loader 函数:
在这里插入图片描述

Webpack 官网对 Loader Context 已经有比较详细的说明,这里简单介绍几个比较常用的接口:

  • fs:Compilation 对象的 inputFileSystem 属性,我们可以通过这个对象获取更多资源文件的内容;
  • resource:当前文件路径;
  • resourceQuery:文件请求参数,例如 import "./a?foo=bar"resourceQuery 值为 ?foo=bar
  • callback:可用于返回多个结果;
  • getOptions:用于获取当前 Loader 的配置对象;
  • async:用于声明这是一个异步 Loader,开发者需要通过 async 接口返回的 callback 函数传递处理结果;
  • emitWarning:添加警告;
  • emitError:添加错误信息,注意这不会中断 Webpack 运行;
  • emitFile:用于直接写出一个产物文件,例如 file-loader 依赖该接口写出 Chunk 之外的产物;
  • addDependency:将 dep 文件添加为编译依赖,当 dep 文件内容发生变化时,会触发当前文件的重新构建;

下面我会抽取几个比较关键的接口,结合开源项目的用法展开讲解。

取消 Loader 缓存

需要注意,Loader 中执行的各种资源内容转译操作通常都是 CPU 密集型 —— 这放在 JavaScript 单线程架构下可能导致性能问题;又或者异步 Loader 会挂起后续的加载器队列直到异步 Loader 触发回调,稍微不注意就可能导致整个加载器链条的执行时间过长。

为此,Webpack 默认会缓存 Loader 的执行结果直到模块或模块所依赖的其它资源发生变化,我们也可以通过 this.cacheable 接口显式关闭缓存:

module.exports = function(source) {this.cacheable(false);// ...return output;
};

在 Loader 中返回多个结果

简单的 Loader 可直接 return 语句返回处理结果,复杂场景还可以通过 callback 接口返回更多信息,供下游 Loader 或者 Webpack 本身使用,例如在 webpack-contrib/eslint-loader 中:

export default function loader(content, map) {// ...linter.printOutput(linter.lint(content));this.callback(null, content, map);
}

通过 this.callback(null, content, map) 语句,同时返回转译后的内容与 sourcemap 内容。callback 的完整签名如下:

this.callback(// 异常信息,Loader 正常运行时传递 null 值即可err: Error | null,// 转译结果content: string | Buffer,// 源码的 sourcemap 信息sourceMap?: SourceMap,// 任意需要在 Loader 间传递的值// 经常用来传递 ast 对象,避免重复解析data?: any
);

在 Loader 返回异步结果

涉及到异步或 CPU 密集操作时,Loader 中还可以以异步形式返回处理结果,例如 webpack-contrib/less-loader 的核心逻辑:

import less from "less";async function lessLoader(source) {// 1. 获取异步回调函数const callback = this.async();// ...let result;try {// 2. 调用less 将模块内容转译为 cssresult = await (options.implementation || less).render(data, lessOptions);} catch (error) {// ...}const { css, imports } = result;// ...// 3. 转译结束,返回结果callback(null, css, map);
}export default lessLoader;

在 less-loader 中,包含三个重要逻辑:

  • 调用 this.async 获取异步回调函数,此时 Webpack 会将该 Loader 标记为异步加载器,会挂起当前执行队列直到 callback 被触发;
  • 调用 less 库将 less 资源转译为标准 css;
  • 调用异步回调 callback 返回处理结果。

this.async 返回的异步回调函数签名与上一节介绍的 this.callback 相同,此处不再赘述。

在 Loader 中直接写出文件

Loader Context 的 emitFile 接口可用于直接写出新的产物文件,例如在 file-loader 中:

export default function loader(content) {const options = getOptions(this);validate(schema, options, {name: 'File Loader',baseDataPath: 'options',});// ...if (typeof options.emitFile === 'undefined' || options.emitFile) {// ...this.emitFile(outputPath, content, null, assetInfo);}const esModule =typeof options.esModule !== 'undefined' ? options.esModule : true;return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
}export const raw = true;

借助 emitFile 接口,我们能够在 Webpack 构建主流程之外提交更多产物,这有时候是必要的,除上面提到的 file-loader 外,response-loadermermaid-loader 等也依赖于 emitFile 实现构建功能。

在 Loader 中添加额外依赖

Loader Context 的 addDependency 接口用于添加额外的文件依赖,当这些依赖发生变化时,也会触发重新构建,例如在 less-loader 中包含这样一段代码:

  try {result = await (options.implementation || less).render(data, lessOptions);} catch (error) {// ...}const { css, imports } = result;imports.forEach((item) => {// ...this.addDependency(path.normalize(item));});

代码中首先调用 less 库编译文件内容,之后遍历所有 @import 语句(result.imports 数组),调用 this.addDependency 函数将 import 到的文件都注册为依赖,此后这些资源文件发生变化时都会触发重新编译。

为什么 less-loader 需要这么处理?因为 less 工具本身已经会递归所有 Less 文件树,一次性将所有 .less 文件打包在一起,例如在 a.less@import (less) './b.less' ,a、b 文件会被 less 打包在一起。这里面的文件依赖对 Webpack 来说是无感知的,如果不用 addDependency 显式声明依赖,后续 b.less 文件的变化不会触发 a.less 重新构建,不符合预期啊。

所以,addDependency 接口适用于那些 Webpack 无法理解隐式文件依赖的场景。除上例 less-loaderbabel-loader 也是一个特别经典的案例。在 babel-loader 内部会添加对 Babel 配置文件如 .babelrc 的依赖,当 .babelrc 内容发生变化时,也会触发 babel-loader 重新运行。

此外,Loader Context 还提供了下面几个与依赖处理相关的接口:

  • addContextDependency(directory: String):添加文件目录依赖,目录下内容变更时会触发文件变更;
  • addMissingDependency(file: String):用于添加文件依赖,效果与 addDependency 类似;
  • clearDependencies():清除所有文件依赖。

处理二进制资源

有时候我们期望以二进制方式读入资源文件,例如在 file-loaderimage-loader 等场景中,此时只需要添加 export const raw = true 语句即可,如:

export default function loader(source) {/* ... */}export const raw = true;

之后,loader 函数中获取到的第一个参数 source 将会是 Buffer 对象形式的二进制内容。

在 Loader 中正确处理日志

Webpack 内置了一套 infrastructureLogging 接口,专门用于处理 Webpack 内部及各种第三方组件的日志需求,与 log4js、winston 等日志工具类似,infrastructureLogging 也提供了根据日志分级筛选展示功能,从而将日志的写逻辑与输出逻辑解耦。

  • 提示:作为对比,假如我们使用 console.log 等硬编码方式输出日志信息,用户无法过滤这部分输出,可能会造成较大打扰,体感很不好。

因此,在编写 Loader 时也应该尽可能复用 Webpack 内置的这套 Logging 规则,方法很简单,只需使用 Loader Context 的 getLogger 接口,如:

export default function loader(source) {const logger = this.getLogger("xxx-loader");// 使用适当的 logging 接口// 支持:verbose/log/info/warn/errorlogger.info("information");return source;
}

getLogger 返回的 logger 对象支持 verbose/log/info/warn/error 五种级别的日志,最终用户可以通过 infrastructureLogging.level 配置项筛选不同日志内容,例如:

module.exports = {// ...infrastructureLogging: {level: 'warn',},// ...
};

在 Loader 中正确上报异常

Webpack Loader 中有多种上报异常信息的方式:

  • 使用 logger.error,仅输出错误日志,不会打断编译流程,效果:
    在这里插入图片描述

  • 使用 this.emitError 接口,同样不会打断编译流程,效果:
    在这里插入图片描述

logger.error 相比,emitError 不受 infragstrustureLogging 规则控制,必然会强干扰到最终用户;其次,emitError 会抛出异常的 Loader 文件、代码行、对应模块,更容易帮助定位问题。

  • 使用 this.callback 接口提交错误信息,但注意导致当前模块编译失败,效果与直接使用 throw 相同,用法:
export default function loader(source) {this.callback(new Error("发生了一些异常"));return source;
}

之后,Webpack 会将 callback 传递过来的错误信息当做模块内容,打包进产物文件:

在这里插入图片描述

总的来说,这些方式各自有适用场景,我个人会按如下规则择优选用:

  • 一般应尽量使用 logger.error,减少对用户的打扰;
  • 对于需要明确警示用户的错误,优先使用 this.emitError
  • 对于已经严重到不能继续往下编译的错误,使用 callback

为 Loader 编写单元测试

在 Loader 中编写单元测试收益非常高,一方面对开发者来说,不用重复手动测试各种特性;一方面对于最终用户来说,带有一定测试覆盖率的项目通常意味着更高、更稳定的质量。常规的 Webpack Loader 单元测试流程大致如下:

  1. 创建在 Webpack 实例,并运行 Loader;
  2. 获取 Loader 执行结果,比对、分析判断是否符合预期;
  3. 判断执行过程中是否出错。

下面我们逐一展开讲解。 如何运行 Loader?

有两种办法,一是在 node 环境下运行调用 Webpack 接口,用代码而非命令行执行编译,很多框架都会采用这种方式,例如 vue-loader、stylus-loader、babel-loader 等,优点是运行效果最接近最终用户,缺点是运行效率相对较低(可以忽略)。

以 posthtml/posthtml-loader 为例,它会在启动测试之前创建并运行 Webpack 实例:

// posthtml-loader/test/helpers/compiler.js 文件
module.exports = function (fixture, config, options) {config = { /*...*/ }options = Object.assign({ output: false }, options)// 创建 Webpack 实例const compiler = webpack(config)// 以 MemoryFS 方式输出构建结果,避免写磁盘if (!options.output) compiler.outputFileSystem = new MemoryFS()// 执行,并以 promise 方式返回结果return new Promise((resolve, reject) => compiler.run((err, stats) => {if (err) reject(err)// 异步返回执行结果resolve(stats)}))
}
  • 提示:上面的示例中用到 compiler.outputFileSystem = new MemoryFS() 语句将 Webpack 设定成输出到内存,能避免写盘操作,提升编译速度。

另外一种方法是编写一系列 mock 方法,搭建起一个模拟的 Webpack 运行环境,例如 emaphp/underscore-template-loader ,优点是运行速度更快,缺点是开发工作量大通用性低,了解即可。

  • 如何校验 Loader 执行结果?

上例运行结束之后会以 resolve(stats) 方式返回执行结果,stats 对象中几乎包含了编译过程所有信息,包括:耗时、产物、模块、chunks、errors、warnings 等等,我们可以从 stats 对象中读取编译最终输出的产物,例如 style-loader

// style-loader/src/test/helpers/readAsset.js 文件
function readAsset(compiler, stats, assets) => {const usedFs = compiler.outputFileSystemconst outputPath = stats.compilation.outputOptions.pathconst queryStringIdx = targetFile.indexOf('?')if (queryStringIdx >= 0) {// 解析出输出文件路径asset = asset.substr(0, queryStringIdx)}// 读文件内容return usedFs.readFileSync(path.join(outputPath, targetFile)).toString()
}

解释一下,这段代码首先计算 asset 输出的文件路径,之后调用 outputFileSystem 的 readFile 方法读取文件内容。

接下来,有两种分析内容的方法:

  • 调用 Jest 的 expect(xxx).toMatchSnapshot() 断言,判断当前运行结果是否与之前的运行结果一致,从而确保多次修改的结果一致性,很多框架都大量用了这种方法;
  • 解读资源内容,判断是否符合预期,例如 less-loader 的单元测试中会对同一份代码跑两次 less 编译,一次由 Webpack 执行,一次直接调用 less 库,之后分析两次运行结果是否相同。

对此有兴趣的同学,强烈建议看看 less-loader 的 test 目录。

  • 如何判断执行过程是否触发异常?

最后,还需要判断编译过程是否出现异常,同样可以从 stats 对象解析:

export default getErrors = (stats) => {const errors = stats.compilation.errors.sort()return errors.map(e => e.toString())
}

大多数情况下都希望编译没有错误,此时只要判断结果数组是否为空即可。某些情况下可能需要判断是否抛出特定异常,此时可以 expect(xxx).toMatchSnapshot() 断言,用快照对比更新前后的结果。

链式调用模型详解

举个例子,为了读取 less 文件,我们通常需要同时配置多个加载器:

module.exports = {module: {rules: [{test: /\.less$/i,use: ["style-loader", "css-loader", "less-loader"],},],},
};

示例针对 .less 后缀的文件设定了 less、css、style 三个 Loader,Webpack 启动后会以一种所谓“链式调用”的方式按 use 数组顺序从后到前调用 Loader:

  • 首先调用 less-loader 将 Less 代码转译为 CSS 代码;
  • less-loader 结果传入 css-loader,进一步将 CSS 内容包装成类似 module.exports = "${css}" 的 JavaScript 代码片段;
  • css-loader 结果传入 style-loader,在运行时调用 injectStyle 等函数,将内容注入到页面的 <style> 标签

在这里插入图片描述

三个 Loader 分别完成内容转化工作的一部分,形成从右到左的执行链条。链式调用这种设计有两个好处,一是保持单个 Loader 的单一职责,一定程度上降低代码的复杂度;二是细粒度的功能能够被组装成复杂而灵活的处理链条,提升单个 Loader 的可复用性。

不过,这只是链式调用的一部分,这里面有两个问题:

  • Loader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常;
  • 某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行。

为了解决这两个问题,Webpack 在 Loader 基础上叠加了 pitch 的概念。

  • Q: 什么是 pitch

Webpack 允许在 Loader 函数上挂载名为 pitch 的函数,运行时 pitch 会比 Loader 本身更早执行,例如:

const loader = function (source){console.log('后执行')return source;
}loader.pitch = function(requestString) {console.log('先执行')
}module.exports = loader

Pitch 函数的完整签名:

function pitch(remainingRequest: string, previousRequest: string, data = {}
): void {
}

包含三个参数:

  • remainingRequest : 当前 loader 之后的资源请求字符串;
  • previousRequest : 在执行当前 loader 之前经历过的 loader 列表;
  • data : 与 Loader 函数的 data 相同,用于传递需要在 Loader 传播的信息。

这些参数不复杂,但与 requestString 紧密相关,我们看个例子加深了解:

module.exports = {module: {rules: [{test: /\.less$/i,use: ["style-loader", "css-loader", "less-loader"],},],},
};

css-loader.pitch 中拿到的参数依次为:

// css-loader 之后的 loader 列表及资源路径
remainingRequest = less-loader!./xxx.less
// css-loader 之前的 loader 列表
previousRequest = style-loader
// 默认值
data = {}
  • pitch 函数调度逻辑

Pitch 翻译成中文是_抛、球场、力度、事物最高点_等,它背后折射的是一整套 Loader 被执行的生命周期概念。

实现上,Loader 链条执行过程分三个阶段:pitch、解析资源、执行,设计上与 DOM 的事件模型非常相似,pitch 对应到捕获阶段;执行对应到冒泡阶段;而两个阶段之间 Webpack 会执行资源内容的读取、解析操作,对应 DOM 事件模型的 AT_TARGET 阶段:

在这里插入图片描述

pitch 阶段按配置顺序从左到右逐个执行 loader.pitch 函数(如果有的话),开发者可以在 pitch 返回任意值中断后续的链路的执行:

在这里插入图片描述

那么为什么要设计 pitch 这一特性呢?

在分析了 style-loader、vue-loader、to-string-loader 等开源项目之后,我个人总结出两个字:阻断

回顾一下前面提到过的 less 加载链条:

  • less-loader :将 less 规格的内容转换为标准 css;
  • css-loader :将 css 内容包裹为 JavaScript 模块;
  • style-loader :将 JavaScript 模块的导出结果以 linkstyle 标签等方式挂载到 html 中,让 css 代码能够正确运行在浏览器上。

实际上, style-loader 只是负责让 CSS 在浏览器环境下跑起来,并不需要关心具体内容,很适合用 pitch 来处理,核心代码:

// ...
// Loader 本身不作任何处理
const loaderApi = () => {};// pitch 中根据参数拼接模块代码
loaderApi.pitch = function loader(remainingRequest) {//...switch (injectType) {case 'linkTag': {return `${esModule? `...`// 引入 runtime 模块: `var api = require(${loaderUtils.stringifyRequest(this,`!${path.join(__dirname, 'runtime/injectStylesIntoLinkTag.js')}`)});// 引入 css 模块var content = require(${loaderUtils.stringifyRequest(this,`!!${remainingRequest}`)});content = content.__esModule ? content.default : content;`} // ...`;}case 'lazyStyleTag':case 'lazySingletonStyleTag': {//...}case 'styleTag':case 'singletonStyleTag':default: {// ...}}
};export default loaderApi;

关键点:

  • loaderApi 为空函数,不做任何处理;
  • loaderApi.pitch 中拼接结果,导出的代码包含:
    • 引入运行时模块 runtime/injectStylesIntoLinkTag.js
    • 复用 remainingRequest 参数,重新引入 css 文件。

运行后,关键结果大致如:

var api = require('xxx/style-loader/lib/runtime/injectStylesIntoLinkTag.js')
var content = require('!!css-loader!less-loader!./xxx.less');

注意了,到这里 style-loader 的 pitch 函数返回这一段内容,后续的 Loader 就不会继续执行,当前调用链条中断了:
在这里插入图片描述

之后,Webpack 继续解析、构建 style-loader 返回的结果,遇到 inline loader 语句:

var content = require('!!css-loader!less-loader!./xxx.less');

所以从 Webpack 的角度看,对同一个文件实际调用了两次 loader 链,第一次在 style-loader 的 pitch 中断,第二次根据 inline loader 的内容跳过了 style-loader。

总结

  • Loader 主要负责将资源内容转换为 Webpack 能够理解的 JavaScript 代码形式,开发时我们可以借助 Loader Context 提供的丰富接口实现各种各样的诉求。此外,也需要结合 Loader 的链式调用模型,尽可能设计出复用性更强,更简洁的资源加载器
  • 可以先思考下如何使用 loader-utilsschema-utils 等辅助工具,以及作为实践案例,深度剖析 vue-loader 的实现逻辑
  • 可以进一步参考一些知名 Loader 的源码,包括:file-loaderurl-loaderstyle-loaderless-loaderbabel-loader 等,学习它们的开发模式,巩固对上述各项接口的认识

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/42284.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Zookeeper分布式锁原理说明【简单易理解】

Zookeeper 非公平锁/公平锁/共享锁 。 1.zookeeper分布式锁加锁原理 如上实现方式在并发问题比较严重的情况下&#xff0c;性能会下降的比较厉害&#xff0c;主要原因是&#xff0c;所有的连接都在对同一个节点进行监听&#xff0c;当服务器检测到删除事件时&#xff0c;要通知…

人脸识别课堂签到系统【PyQt5实现】

人脸识别签到系统 1、运用场景 课堂签到,上班打卡,进出门身份验证。 2、功能类别 人脸录入,打卡签到,声音提醒,打卡信息导出,打包成exe可执行文件 3、技术栈 python3.8,sqlite3,opencv,face_recognition,PyQt5,csv 4、流程图 1、导入库 2、编写UI界面 3、打…

HackTheBox----Editorial

Editorial 测试过程 1 信息收集 NMAP端口扫描 nmap -sC -sV 10.10.11.20服务器开启了 22、80 端口 80 端口测试 服务器只开启了 22 和 80 端口&#xff0c;先从 80 端口开始进行测试 echo "10.10.11.20 editorial.htb" | sudo tee -a /etc/hostspublish with us…

SwiftData 模型对象的多个实例在 SwiftUI 中不能及时同步的解决

概览 我们已经知道,用 CoreData 在背后默默支持的 SwiftUI 视图在使用 @FetchRequest 来查询托管对象集合时,若查询结果中的托管对象在别处被改变将不会在 FetchedResults 中得到及时的刷新。 那么这一“囧境”在 SwiftData 里是否也会“卷土重来”呢?空说无益,就让我们在…

RK3588编译rkmpp,拉取海康威视网络摄像头264码流并运行yolo

硬件&#xff1a;EVB评估版 SOC&#xff1a;Rockchip RK3588 背景&#xff1a; 由于项目需要&#xff0c;需要拉取264码流&#xff0c;并通过将yolov5s.pt将模型转化为rknn模型&#xff0c;获取模型分析结果。取流可以通过软件解码或者硬件解码&#xff0c;硬件解码速度更快&…

Java版Flink使用指南——安装Flink和使用IntelliJ制作任务包

大纲 安装Flink操作系统安装JDK安装Flink修改配置启动Flink测试 使用IntelliJ制作任务包新建工程Archetype 编写测试代码打包测试 参考资料 在《0基础学习PyFlink》专题中&#xff0c;我们熟悉了Flink的相关知识以及Python编码方案。这个系列我们将使用相对主流的Java语言&…

LabVIEW在自动化测试项目中的推荐架构

在自动化测试项目中&#xff0c;推荐使用LabVIEW的生产者-消费者&#xff08;Producer-Consumer&#xff09;架构。这种架构利用队列实现数据的异步传输和处理&#xff0c;提供了高效、稳定和可扩展的解决方案。其主要优点包括&#xff1a;实现数据采集与处理的解耦、提高系统响…

中国IDC圈探访北京•光子1号金融算力中心

今天&#xff0c;“AI”、“大模型”是最炙手可热的话题&#xff0c;全球有海量人群在工作生活中使用大模型&#xff0c;大模型产品涉及多模态&#xff0c;应用范围已涵盖电商、传媒、金融、短视频、制造等众多行业。 而回看2003年的互联网记忆&#xff0c; “上网”“在线”是…

【LVGL-SquareLine Studio】

LVGL-SquareLine Studio ■ SquareLine Studio-官网下载地址■ SquareLine Studio-参考博客■ SquareLine Studio-安装■ SquareLine Studio-汉化■ SquareLine Studio-■ SquareLine Studio-■ SquareLine Studio-■ SquareLine Studio-■ SquareLine Studio- ■ SquareLine S…

C++基础(十一):STL简介

从今天开始&#xff0c;我们正式步入STL的学习&#xff0c;STL&#xff08;标准模板库&#xff0c;Standard Template Library&#xff09;是C标准库的重要组成部分&#xff0c;提供了一系列通用的类和函数模板&#xff0c;包括容器、算法、迭代器等。它的设计极大地提高了代码…

[leetcode hot 150]第一百三十题,被围绕的区域

题目&#xff1a; 给你一个 m x n 的矩阵 board &#xff0c;由若干字符 X 和 O 组成&#xff0c;捕获 所有 被围绕的区域&#xff1a; 连接&#xff1a;一个单元格与水平或垂直方向上相邻的单元格连接。区域&#xff1a;连接所有 0 的单元格来形成一个区域。围绕&#xff1a…

C++第一弹 -- C++基础语法上(命名空间 输入输出 缺省参数 函数重载 引用)

目录 前言一. C关键字(C98)二. 命名空间1.命名空间的定义2.命名空间的使用3.其它部分 三. C输入&输出四. 缺省参数1. 缺省参数的概念2.缺省参数的分类 五. 函数重载1.函数重载的概念2. 为什么C支持函数重载, 而C语言不支持重载呢? 六. 引用1.引用的概念2.引用的特性3.常引…

通过AIS实现船舶追踪与照射

前些天突然接到个紧急的项目&#xff1a;某处需要实现对夜航船只进行追踪并用激光灯照射以保障夜航安全。这个项目紧急到什么程度呢&#xff1f;&#xff01;现场激光灯都安装好了&#xff0c;还有三个星期就要验收了&#xff0c;但上家没搞定就甩给我们了:( 从技术上看&#…

Java应用系统设计与实现--学生信息管理系统(附解决方案源码)

一、实验目的及要求 1.1实验目的 掌握Java GUI编程技术&#xff0c;了解Swing框架的使用。 掌握MySQL数据库的基本操作&#xff0c;了解如何在Java中连接和操作数据库。 掌握用户权限管理的基本概念和实现方法。 提升综合运用所学知识设计和实现一个完整应用系统的能力…

windows机器免密登录linux主机

1. 正常连接需要输入密码 ssh root1.1.1.1 2. 在Windows上生成SSH密钥对&#xff08;如果你还没有的话&#xff09;&#xff1a; ssh-keygen 3. scp将id_rsa.pub传输到对应的主机 4.对应机器上查看 5.从windows上免密登录

ESP32 蓝牙网关实践:BLE 设备数据采集与 MQTT 云平台发布(附代码示例)

摘要: 本文详细介绍了如何使用 ESP32 构建强大的蓝牙网关&#xff0c;实现蓝牙设备与 Wi-Fi/互联网之间的无缝连接和数据桥接。文章涵盖了连接和桥接功能、数据处理和分析能力&#xff0c;并提供了详细的代码示例和 Mermaid 生成的图表&#xff0c;助您轻松构建自己的蓝牙网关解…

树(相关知识点)

目录 结点的度&#xff1a;某一个结点所含有字数的个数 叶节点&#xff1a;最后一个结点 非终端节点:不是叶结点 兄弟结点&#xff1a;亲兄弟结点 树的度&#xff1a;最大节点的度 层次&#xff1a;根为第一层&#xff0c;根的子结点为第二层&#xff0c;以此类推 森林&am…

微软拼音输入法不显示选字框问题

问题展示&#xff1a;不显示选字框 解决方式 打开兼容性即可&#xff08;估计是升级带来的bug&#xff09;

Android使用http加载自建服务器静态网页

最终效果如下图&#xff0c;成功加载了电脑端的静态网页内容&#xff0c;这是一个xml文件。 电脑端搭建http服务器 使用“Apache Http Server”&#xff0c;下载地址是&#xff1a;https://httpd.apache.org/download.cgi。具体操作步骤&#xff0c;参考&#xff1a;Apache …

深度学习与CV入门

文章目录 前言历史 前言 历史 tensorflow可以安装Tensorboard第三方库用于展示效果 TensorFlow工作流程&#xff1a;p6-4:20 使用tf.data加载数据。使用tf.data实例化读取训练数据和测试数据模型的建立与调试:使用动态图模式Eager Execution和著名的神经网络高层API框架Ker…