Webpack: 剖析插件基本形态与架构逻辑

news2024/12/22 23:18:48

概述

Webpack 对外提供了 Loader 与 Plugin 两种扩展方式,其中 Loader 职责比较单一,开发方法比较简单容易理解;Plugin 则功能强大,借助 Webpack 数量庞大的 Hook,我们几乎能改写 Webpack 所有特性,但也伴随着巨大的开发复杂度。

学习如何开发 Webpack 插件并不是一件简单的事情,所以我打算用 3 个连续的章节,力求足够全面地剖析如何开发一款成熟、稳定的插件。本文将聚焦在插件代码形态、插件架构、Hook 与上下文参数等内容,同时深入剖析若干常用插件的实现原理,帮你构建起关于 Webpack 插件开发的基本认知。

插件简介

从形态上看,插件通常是一个带有 apply 函数的类,如:

class SomePlugin {
    apply(compiler) {
    }
}

Webpack 在启动时会调用插件对象的 apply 函数,并以参数方式传递核心对象 compiler ,以此为起点,插件内可以注册 compiler 对象及其子对象的钩子(Hook)回调,例如:

class SomePlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("SomePlugin", (compilation) => {
      compilation.addModule(/* ... */);
    });
  }
}

示例中的 compiler 为 Hook 挂载的对象;thisCompilation 为 Hook 名称;后面调用的 tap 为调用方式,支持 tap/tapAsync/tapPromise 等,后面章节会展开细讲。

在 Webpack 运行过程中,随着构建流程的推进会触发各个钩子回调,并传入上下文参数(例如上例回调函数中的 compilation 对象),插件可以通过调用上下文接口、修改上下文状态等方式「篡改」构建逻辑,从而将扩展代码「勾入」到 Webpack 构建流程中。

  • 提示:网上不少资料将 Webpack 的插件架构归类为“事件/订阅”模式,我认为这种归纳有失偏颇。订阅模式是一种松散耦合结构,发布器只是在特定时机发布事件消息,订阅者并不或者很少与事件源直接发生交互。

基于 Hook 这一设计,开发插件时我们需要重点关注两个问题:

  1. 针对插件需求,我们应该使用什么钩子?
  2. 选定钩子后,我怎么跟上下文参数交互?

什么时候会触发什么钩子:

Webpack5 暴露了多达 200+ 个 Hook,基本上覆盖了整个构建流程的所有环节 —— 这也就意味着通过编写插件,我们几乎可以改写 Webpack 的所有执行逻辑。问题是,我们在什么情况下该用什么钩子?这就需要了解 Webpack 内部几个核心对象,以及各对象下 Hook 的触发时机,例如:

  • Compiler:全局构建管理器,Webpack 启动后会首先创建 compiler 对象,负责管理配置信息、Loader、Plugin 等。从启动构建到结束,compiler 大致上会触发如下钩子:
  • Compilation:单次构建过程的管理器,负责遍历模块,执行编译操作;当 watch = true 时,每次文件变更触发重新编译,都会创建一个新的 compilation 对象;compilation 生命周期中主要触发如下钩子:

请添加图片描述

  • 此外,还有 Module、Resolver、Parser、Generator 等关键类型,也都相应暴露了许多 Hook。

由此可见,Webpack Hook 与构建流程强相关,使用时你结合上面流程图分析 Hook 对应的流程环节,以及这个环节主要完成了什么工作,可以借助 Hook 做出哪些修改,等等。

使用 Hook 上下文接口:

Webpack Hook 有两个重点,一是上面介绍的触发时机;二是触发时传递的上下文参数。例如:

  • compiler.hooks.compilation

    • 时机:Webpack 刚启动完,创建出 compilation 对象后触发;
    • 参数:当前编译的 compilation 对象。
  • compiler.hooks.make

    • 时机:正式开始构建时触发;
    • 参数:同样是当前编译的 compilation 对象。
  • compilation.hooks.optimizeChunks

    • 时机: seal 函数中,chunk 集合构建完毕后触发;
    • 参数:chunks 集合与 chunkGroups 集合。
  • compiler.hooks.done

    • 时机:编译完成后触发;
    • 参数: stats 对象,包含编译过程中的各类统计信息。

每个钩子传递的上下文参数不同,但主要包含如下几种类型(以 Webpack5 为例):

  • complation

    对象:构建管理器,使用率非常高,主要提供了一系列与单次构建相关的接口,包括:

    • addModule:用于添加模块,例如 Module 遍历出依赖之后,就会调用该接口将新模块添加到构建需求中;
    • addEntry:添加新的入口模块,效果与直接定义 entry 配置相似;
    • emitAsset:用于添加产物文件,效果与 Loader Context 的 emitAsset 相同;
    • getDependencyReference:从给定模块返回对依赖项的引用,常用于计算模块引用关系;
    • 等等。
  • compiler

    对象:全局构建管理器,提供如下接口:

    • createChildCompiler:创建子 compiler 对象,子对象将继承原始 Compiler 对象的所有配置数据;
    • createCompilation:创建 compilation 对象,可以借此实现并行编译;
    • close:结束编译;
    • getCache:获取缓存接口,可借此复用 Webpack5 的缓存功能;
    • getInfrastructureLogger:获取日志对象;
    • 等等。
  • module 对象:资源模块,有诸如 NormalModule/RawModule/ContextModule 等子类型,其中 NormalModule 使用频率较高,提供如下接口:

    • identifier:读取模块的唯一标识符;
    • getCurrentLoader:获取当前正在执行的 Loader 对象;
    • originalSource:读取模块原始内容;
    • serialize/deserialize:模块序列化与反序列化函数,用于实现持久化缓存,一般不需要调用;
    • issuer:模块的引用者;
    • isEntryModule:用于判断该模块是否为入口文件;
    • 等等。
  • chunk 对象:模块封装容器,提供如下接口:

    • addModule:添加模块,之后该模块会与 Chunk 中其它模块一起打包,生成最终产物;
    • removeModule:删除模块;
    • containsModule:判断是否包含某个特定模块;
    • size:推断最终构建出的产物大小;
    • hasRuntime:判断 Chunk 中是否包含运行时代码;
    • updateHash:计算 Hash 值。
  • stats 对象:构建过程收集到的统计信息,包括模块构建耗时、模块依赖关系、产物文件列表等。

  • 提示:无论官网还是社区,我都没有找到完整介绍这些对象的,足够好、足够完备的文档,且 Webpack 本身还在不断升级迭代,许多内部对象的接口并不稳定,建议读者使用时直接翻阅相关版本源码。

篇幅关系,我们只对部分重要接口做了简单介绍,后面我还会讲解各种常用插件源码,展开介绍部分常见接口的使用方法。

总结一下,Webpack 的插件体系与平常所见的 订阅/发布 模式差别很大,是一种非常强耦合的设计,Hook 回调由 Webpack 决定何时,以何种方式执行;而在 Hook 回调内部可以通过调用上下文 API 、修改上下文状态等方式,对 Webpack 原定流程产生 Side Effect。

所以想熟练编写插件,需要深入理解常见 Hook 调用时机,以及各类上下文参数的用法,这方面没有太多学习资料,我建议直接翻阅相关开源插件源码,下面我会抽几个比较经典、逻辑简单、容易理解的插件,剖析如何灵活使用 Hook。

实例剖析:imagemin-webpack-plugin

如何遍历、修改最终产物文件

imagemin-webpack-plugin 是一个用于实现图像压缩的插件,它会在 Webpack 完成前置的代码分析构建,提交(emit)产物时,找出所有图片资源并调用 imagemin 压缩图像。核心逻辑:

export default class ImageminPlugin {
  constructor(options = {}) {
    // init options
  }

  apply(compiler) {
    // ...
    const onEmit = async (compilation, callback) => {
      // ...
      await Promise.all([
        ...this.optimizeWebpackImages(throttle, compilation),
        ...this.optimizeExternalImages(throttle),
      ]);
    };

    compiler.hooks.emit.tapAsync(this.constructor.name, onEmit);
  }

  optimizeWebpackImages(throttle, compilation) {}

  optimizeExternalImages(throttle) {}
}

上述代码主要用到 compiler.hooks.emit 钩子,该钩子在 Webpack 完成代码构建与打包操作,准备将产物发送到输出目录之前执行,我们可以在此修改产物内容,如上例 optimizeWebpackImages 函数:

export default class ImageminPlugin {
  optimizeWebpackImages(throttle, compilation) {
    const {
        // 用于判断是否对特定文件做图像压缩操作
        testFunction,
        // 缓存目录
        cacheFolder
      } = this.options
  
    // 遍历 `assets` 产物数组
      return map(compilation.assets, (asset, filename) => throttle(async () => {
        // 读取产物内容
        const assetSource = asset.source()
        if (testFunction(filename, assetSource)) {
          // 尝试从缓存中读取
          let optimizedImageBuffer = await getFromCacheIfPossible(cacheFolder, assetSource, () => {
            // 调用 `imagemin` 压缩图片
            return optimizeImage(assetSource, this.options)
          })
  
          // 之后,使用优化版本替换原始文件
          compilation.assets[filename] = new RawSource(optimizedImageBuffer)
        }
      }))
  }
}

这里面的关键逻辑是:

  1. 遍历 compilation.assets 产物列表,调用 asset.source() 方法读取产物内容;
  2. 调用 imagemin 压缩图片;
  3. 修改 compilation.assets,使用优化后的图片 RawSource 对象替换原始 asset 对象。

至此完成文件压缩操作。

  • 提示:Source 是 Webpack 内代表资源内容的类,由 webpack-source 库实现,支持 RawSource/ConcatSource 等子类型,用于实现文件读写、合并、修改、Sourcemap 等操作。

实例剖析:eslint-webpack-plugin

如何提交错误日志

eslint-webpack-plugin 是一个基于 ESLint 实现的代码风格检查插件,它的实现比较巧妙,一是使用多个 Hook,在不同时间点执行 Lint 检查;二是复用 Webpack 内置的 error/warn 方法提交代码风格问题。核心逻辑:

class ESLintWebpackPlugin {
  constructor(options = {}) {
    // ...
  }

  apply(compiler) {
    compiler.hooks.run.tapPromise(this.key, (c) =>
      this.run(c, options, wanted, exclude)
    );
  }

  async run(compiler, options, wanted, exclude) {
    compiler.hooks.compilation.tap(this.key, (compilation) => {
      ({ lint, report, threads } = linter(this.key, options, compilation));

      const files = [];

      // 单个模块成功编译后触发
      compilation.hooks.succeedModule.tap(this.key, ({ resource }) => {
        // 判断是否需要检查该文件
        if (
          isMatch(file, wanted, { dot: true }) &&
          !isMatch(file, exclude, { dot: true })
        ) {
          lint(file);
        }
      });

      // 所有模块构建完毕后触发
      compilation.hooks.finishModules.tap(this.key, () => {
        if (files.length > 0 && threads <= 1) {
          lint(files);
        }
      });

      // 等待检查结果
      compilation.hooks.additionalAssets.tapPromise(this.key, processResults);

      async function processResults() {}
    });
  }
}

代码用到如下 Hook:

  • compiler.hooks.compilation:Compiler 环境初始化完毕,创建出 compilation 对象,准备开始执行构建前触发;
  • compilation.hooks.succeedModule:Webpack 完成单个「模块」的读入、运行 Loader、AST 分析、依赖分析等操作后触发;
  • compilation.hooks.finishModules:Webpack 完成「所有」模块的读入、运行 Loader、依赖分析等操作后触发;
  • compilation.hooks.additionalAssets:构建、打包完毕后触发,通常用于为编译创建附加资产。

其中,比较重要的是借助 compilation.hooks.succeedModule 钩子,在每个模块处理完毕之后立即通过 lint 函数添加非阻塞代码检查任务,相比于过去的 eslint-loader 的阻塞式执行,这种方式能够提高 ESLint 的并发度,效率更高。

其次,借助 compilation.hooks.additionalAssets 钩子,在所有模块处理完毕后读取检查结果 —— 即 processResults 函数,核心代码:

async function processResults() {
  const { errors, warnings } = await report();

  if (warnings && !options.failOnWarning) {
    compilation.warnings.push(warnings);
  } else if (warnings && options.failOnWarning) {
    compilation.errors.push(warnings);
  }

  if (errors && options.failOnError) {
    compilation.errors.push(errors);
  } else if (errors && !options.failOnError) {
    compilation.warnings.push(errors);
  }
}

代码读取 ESLint 执行结果(report 函数),并使用 compilationerrorswarnings 数组提交错误/警告信息,这种方式只会输出错误信息,不会中断编译流程,运行效果如:
在这里插入图片描述

实例剖析:DefinePlugin

学习在插件中如何与 AST 结构交互

DefinePlugin 是 Webpack 官方实现的,用于构建时注入预定义常量的插件,先简单回顾一下用法,如:

const { DefinePlugin } = require("webpack");

const baseConfig = {
  // ...
  plugins: [
    new DefinePlugin({
      PROD: true,
      VERSION: JSON.stringify("12.13.0"),
    }),
  ],
};

之后,Webpack 会帮我们替换掉代码中所有 DefinePlugin 声明的属性值,例如:

// 源码:
console.log(PROD, VERSION);

// 构建结果:
console.log(true, "5fa3b9");

DefinePlugin 的 底层实现 比较复杂,需要遍历 AST 找出变量名对应的代码位置之后再做替换,插件核心结构:

class DefinePlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "DefinePlugin",
      (compilation, { normalModuleFactory }) => {
        const handler = (parser) => {
          // 递归处理 `DefinePlugin` 参数
          const walkDefinitions = (definitions, prefix) => {
            Object.keys(definitions).forEach((key) => {
              const code = definitions[key];
              if (isObject /*...*/) {
                // 递归处理对象属性
                walkDefinitions(code, prefix + key + ".");
                applyObjectDefine(prefix + key, code);
                return;
              }
              applyDefineKey(prefix, key);
              applyDefine(prefix + key, code);
            });
          };

          // 替换基本类型的表达式值
          const applyDefine = (key, code) => {
            if (!isTypeof) {
              // 借助 expression 钩子替换内容
              parser.hooks.expression.for(key).tap("DefinePlugin", (expr) => {
                /*...*/
              });
            }
            // 处理 `'typeof window': JSON.stringify('object'),` 场景
            parser.hooks.typeof.for(key).tap("DefinePlugin", (expr) => {
              /*...*/
            });
          };

          // 替换引用类型的表达式值
          const applyObjectDefine = (key, obj) => {
            // ...
            parser.hooks.expression.for(key).tap("DefinePlugin", (expr) => {
              /*...*/
            });
          };

          walkDefinitions(definitions, "");
        };

        // 监听 `parser` 钩子
        normalModuleFactory.hooks.parser
          .for("javascript/auto")
          .tap("DefinePlugin", handler);
        normalModuleFactory.hooks.parser
          .for("javascript/dynamic")
          .tap("DefinePlugin", handler);
        normalModuleFactory.hooks.parser
          .for("javascript/esm")
          .tap("DefinePlugin", handler);
      }
    );
  }
}
module.exports = DefinePlugin;
  • 提示:可能有同学注意到,上例代码中出现 xxx.hooks.xxx.for(condition).tap 形式的调用,这里的 for 函数可以理解为 Hook 的过滤条件,仅在满足 condition 时触发,后面章节会详细讲解。

核心逻辑:

  1. 使用 normalModuleFactory.hooks.parser 钩子(上例 48 行),在 Webpack 创建出代码解析器 Parser 对象后执行 handler 函数。注意,此时还没有执行代码转 AST 操作;
  2. walkDefinitions 函数中递归遍历 DefinePlugin 参数对象,为每一个属性注册 parser.hooks.expression 钩子回调,该钩子会在 Webpack 遍历 AST 过程遇到表达式语句时触发;
  3. parser.hooks.expression 回调中创建新的 Dependency 对象,调用 addPresentationalDependency 添加为模块依赖:
const toConstantDependency = (parser, value, runtimeRequirements) => {
  return function constDependency(expr) {
    const dep = new ConstDependency(value, expr.range, runtimeRequirements);
    dep.loc = expr.loc;
    // 创建静态依赖对象,替换 loc 指定位置内容
    parser.state.module.addPresentationalDependency(dep);
    return true;
  };
};

const applyDefine = (key, code) => {
  parser.hooks.expression.for(key).tap("DefinePlugin", (expr) => {
    const strCode = toCode(/*...*/);
    if (/*...*/) {
      /*...*/
    } else {
      return toConstantDependency(parser, strCode)(expr);
    }
  });
};

之后,Webpack 会借助 Template 接口将上述 Dependency 打包进 Chunk 中,替换对应位置(loc)代码:

这是一个功能效果看起来简单,但实现特别复杂的例子,底层需要使用 Parser 钩子遍历 AST 结构,之后借助 Dependency 声明代码依赖,最后借助 Template 替换代码内容,过程中已经涉及到许多 Webpack 底层对象。

这正是学习开发 Webpack 插件的难点,有时候你不仅仅需要了解每一个 Hook 的时机与作用、如何与上下文参数交互,还需要了解 Webpack 底层许多类型的实现、作用、接口等等,才能写出符合预期的功能,而 Webpack 是一个极度复杂、庞大的工具,这些具体知识点太多太碎,几乎不可能一一枚举。不过,我们可以换一种方式,从更高更抽象的视角审视 Webpack 插件架构,从“道”的角度加深理解。

插件架构综述

前端社区里很多有名的框架都各自有一套插件架构,例如 axios、quill、vscode、webpack、vue、rollup 等等。插件架构灵活性高,扩展性强,但通常架构复杂度更高,学习曲线更陡峭。插件架构至少需要解决三个方面的问题:

  • 接口:需要提供一套逻辑接入方法,让开发者能够将代码插入特定环节,变更原始逻辑;
  • 输入:如何将上下文信息高效传导给插件;
  • 输出:插件内部通过何种方式影响整套运行体系。

针对这些问题,webpack 基于 tapable 实现了:

  1. 编译过程的特定节点以钩子形式,通知插件此刻正在发生什么事情;
  2. 通过 tapable 提供的回调机制,以参数方式传递上下文信息;
  3. 在上下文参数对象中附带了很多存在 Side Effect 的交互接口,插件可以通过这些接口改变。

这一切都离不开 tapable,举例来说:

class Compiler {
  // 在构造函数中,先初始化钩子对象
  constructor() {
    this.hooks = {
      thisCompilation: new SyncHook(["compilation", "params"]),
    };
  }

  compile() {
    // 特定时机触发特定钩子
    const compilation = new Compilation();
    this.hooks.thisCompilation.call(compilation);
  }
}

Compiler 类型内部定义了 thisCompilation 钩子,并在 compilation 创建完毕后发布事件消息,插件开发者就可以基于这个钩子获取到最新创建出的 compilation 对象:

class SomePlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("SomePlugin", (compilation, params) => {
        // 上下文信息: compilation、params
    });
  }
}

钩子回调传递的 compilation/params 参数,就是 Webpack 希望传递给插件的上下文信息,也是插件能拿到的输入。不同钩子会传递不同的上下文对象,这一点在钩子被创建的时候就定下来了,比如:

class Compiler {
    constructor() {
        this.hooks = {
            /** @type {SyncBailHook<Compilation>} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {AsyncSeriesHook<>} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook<Compiler>} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<string, Buffer>} */
            assetEmitted: new AsyncSeriesHook(["file", "content"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),
        };
    }
}
  • shouldEmit 会被传入 compilation 参数;
  • done 会被传入 stats 参数;
  • ……

这一设计贯穿 Webpack 整个执行过程,几乎无处不在,我们可以借此介入 Webpack 的运行逻辑。

插件架构的灵魂就在于,框架自身只负责实现最关键的核心流程,其它具体功能都尽量交给具体插件实现,包括 Webpack 仓库内也会内置非常多插件(如 DefinePlugin/EntryPlugin 等),这就为我们提供了非常充分的学习素材。因此,我的建议是:

  1. 先透彻理解上述 Webpack 插件架构的设计逻辑,捋清楚 Webpack 主流程与 Hook 之间的关系;
  2. 尝试用本文第一节提及的若干常见 Hook 与上下文参数对象编写一些示例,对这些钩子有一个感性认知;
  3. 尝试分析一些常用但不是很复杂的插件源码,例如文中提到的 eslint-webpack-plugin 等,或者:terser-webpack-pluginstylelint-webpack-plugin 等,从中学习一些编写插件的常见方法;
  4. 最后,在实际开发时参考相关插件源码实现,带着问题与明确目标,逐行分析插件实现逻辑。

总结

综上,Webpack 插件在代码形态上是一个带 apply 方法的对象,我们可以在 apply 函数中注册各式各样的 Hook 回调,监听对应事件,之后在回调中修改上下文状态,达到干预 Webpack 构建逻辑的效果。

由此可见,编写插件时大部分工作都围绕 Hook 展开,因此需要理解构建过程中的不同环节会触发什么 Hook、对应传递什么上下文参数、如何与上下文参数对象交互等,而学习这些知识最高效的方式,我认为是阅读、分析各种开源插件源码。例如文章中提及的:

  • imagemin-webpack-plugin 学习:如何借助 assets 数组修改最终产物内容;
  • eslint-webpack-plugin 学习:如何提交错误信息;
  • DefinePlugin 学习:如何与 AST 结构交互。

相信通过阅读这些内容,已经对 Webpack 插件的形态与开发方式有了一个基本理解,思考如何继续抽象一些常见的开发用例,包括:校验插件参数、提交日志、搭建自动化测试环境等,帮助自己进一步掌握插件的开发方法

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

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

相关文章

改变图像中特定区域的颜色

背景与原理 再训练施工即系模型中&#xff0c;发现大量施工机械都是黄色的&#xff0c;我需要把它们换成蓝色的&#xff0c;以增强模型使用可靠性。 目前没有尝试深度学习算法&#xff0c;只是简单的进行了处理。 核心目的&#xff1a;通过人工标注与opencv的结合&#xff0…

Cybervadis认证是什么?

Cybervadis认证是一种全面且深入的网络安全评估和认证服务&#xff0c;旨在帮助组织提高其网络安全实践的成熟度&#xff0c;并有效应对不断变化的网络威胁和攻击。以下是关于Cybervadis认证的一些关键信息&#xff1a; 认证目的&#xff1a; 评估和验证组织在网络安全方面的能…

使用目标检测模型YOLO V10 OBB进行旋转目标的检测:训练自己的数据集(基于卫星和无人机的农业大棚数据集)

这个是在YOLO V10源码的基础上实现的。我只是在源码的基础上做了些许的改动。 YOLO V10源码&#xff1a;YOLO V10源码 YOLOv10是清华大学的研究人员在Ultralytics Python包的基础上&#xff0c;引入了一种新的实时目标检测方法&#xff0c;解决了YOLO 以前版本在后处理和模型架…

计组_虚拟存储器

2024.06.09&#xff1a;计算机组成原理学习笔记 第11节 虚拟存储器* 1.1 分页思想1.1.1 虚拟地址/逻辑地址1.1.2 主存地址/物理地址1.1.3 页表1.1.4 分页方式的缺陷 1.2 分段思想1.2.1 虚拟地址/逻辑地址1.2.2 主存地址/物理地址1.2.3 段表 1.3 段页式思想1.3.1 虚拟地址/逻辑地…

分布式技术专题 | TCP在分布式网络中的通信机制与底层实现

深入解析分布式网络中的TCP通信协议实现 跨地域通信与资源共享网络节点与主机的定义网络技术通信机制TCP/IP协议模型TCP/IP分层机制TCP的Socket链接处理控制TCP的优势和特性自动差错控制正确性和有序性 TCP的Socket使用端口在应用程序间通信TCP的Socket使用端口套接字操作 跨地…

5G(NR) NTN 卫星组网架构

5G(NR) NTN 卫星组网架构 参考 3GPP TR 38.821 5G NTN 技术适用于高轨、低轨等多种星座部署场景&#xff0c;是实现星地网络融合发展的可行技术路线。5G NTN 网络分为用户段、空间段和地面段三部分。其中用户段由各种用户终端组成&#xff0c;包括手持、便携站、嵌入式终端、车…

鸿蒙应用开发-时间屏幕

点击下载源码&#xff1a; https://download.csdn.net/download/liuhaikang/89509449 做一个时间屏幕&#xff0c;可以点击切换白色和黑色&#xff0c;有渐变效果&#xff0c;使用到了鸿蒙的动画效果。 在这个设计中&#xff0c;我们首先引入了通用能力包&#xff0c;以实现功…

uniapp做小程序内打开地图展示位置信息

使用场景&#xff1a;项目中需要通过位置信息打开地图查看当前位置信息在地图那个位置&#xff0c;每个酒店有自己的经纬度和详细地址&#xff0c;点击地图按钮打开内置地图如图 方法如下&#xff1a; <view class"dttu" click"openMap(info.locationY,info.…

24/07/02数据结构(1.1201)算法效率顺序表

数据结构基本内容:1.时间复杂度 空间复杂度2.顺序表链表3.栈 队列4.二叉树5.排序 数据结构是存储,组织数据的方式.指相互之间存在一种或多种特定关系的数据元素的集合 算法是定义良好的计算过程.取一个或一组值为输入并产生一个或一组值为输出. 需要知道虽然选择题有20-30个…

什么是Java泛型擦除?

JDK1.5之后引入泛型擦除的概念。 目录 验证逻辑 实际编译后的结果&#xff1a; 验证例子&#xff1a; 两个类型相同&#xff0c;表面泛型都被擦出了&#xff0c;都是Arraylist。 List<String> list1 new Arraylist<>(); List<Integer> list2 new Arrayli…

自动化一些操作

下拉选择框 from selenium import webdriver from time import sleep # 导包 from selenium.webdriver.support.select import Select driver webdriver.Edge() driver.get(r"D:\WORK\ww\web自动化_day01_课件笔记资料代码\web自动化_day01_课件笔记资料代码\02_其他资料…

配置windows环境下独立浏览器爬虫方案【不依赖系统环境与chrome】

引言 由于部署浏览器爬虫的机器浏览器版本不同&#xff0c;同时也不想因为部署了爬虫导致影响系统浏览器数据&#xff0c;以及避免爬虫过程中遇到的chrome与webdriver版本冲突。我决定将特定版本的chrome浏览器与webdriver下载到项目目录内&#xff0c;同时chrome_driver在初始…

【车载开发系列】SPI总线通信技术

【车载开发系列】SPI总线通信技术 【车载开发系列】SPI总线通信技术 【车载开发系列】SPI总线通信技术一. 什么是SPI二. 应用范围三. 协议特点1&#xff09;传输速率2&#xff09;主要功能3&#xff09;拓扑结构4&#xff09;接口配置 四. SPI总线原理五. 信号列表六. SPI传输时…

攻防世界-WEB-catcat-new

前言 .................. 开干 正文 信息收集 有意思 估计是权限不够导致无法访问 我们点击几只小猫看看有什么东西 好的,?File 试试看是否存在任意文件读取 思路 成功,接下来我们尝试获取历史记录 这里补充一下知识点 /proc/self proc是一个伪文件系统&#xff0c;它提…

PDF文档如何统计字数,统计PDF文档字数的方法有哪些?

在平时使用pdf阅读或者是处理文档的时候&#xff0c;常常需要统计文档的字数。pdf在查看文字时其实很简单。 PDF文档是一种常见的电子文档格式&#xff0c;如果需要对PDF文档中的字数进行统计&#xff0c;可以使用以下方法&#xff1a; Adobe Acrobat DC&#xff1a;Adobe Ac…

Linux 防火墙开放端口

启动防火墙服务&#xff1a;systemctl start firewalld 查看防火墙开放端口 &#xff1a;firewall-cmd --list-ports 开放3306端口&#xff1a;firewall-cmd --zonepublic --add-port2375/tcp --permanent 防火墙重启&#xff1a;firewall-cmd --reload

html+css+js气球消除小游戏

气球消除小游戏 消除15个就成功 源码在图片后 点赞加关注&#xff0c;谢谢 左上角的数字显示消除气球的数量 定时随机生成气球 &#x1f388;&#x1f388;&#x1f388; 图片 源代码 <!DOCTYPE html> <html lang"en"> <head> <meta charset&…

2.3.2 主程序和外部IO交互 (文件映射方式)----IO Client实现

2.3.2 主程序和外部IO交互 &#xff08;文件映射方式&#xff09;----IO Client C实现 和IOServer主要差别&#xff1a; 1 使用Open_Client 连接 2 一定要先打开IOServer&#xff0c;再打开IO_Client 效果显示 1 C 代码实现 1.1 shareddataClient.h 头文件中引用 和sharedd…

0 TMS320F28379D 开坑

开坑原因 最近开始做实验&#xff0c;实验室的主控采用的是F2812FPGA&#xff0c;属于够用但不好用的状态。FPGA用于生成调制信号&#xff0c;DSP完成采样和控制。师兄师姐研究拓扑及调制策略&#xff0c;对驱动数量以及驱动逻辑有比较高的要求&#xff0c;因此不好脱离FPGA&a…

机器学习原理之 -- 支持向量机分类:由来及原理详解

支持向量机&#xff08;Support Vector Machine, SVM&#xff09;是统计学习理论的一个重要成果&#xff0c;广泛应用于分类和回归问题。SVM以其高效的分类性能和良好的泛化能力在机器学习领域中占据重要地位。本文将详细介绍支持向量机的由来、基本原理、构建过程及其优缺点。…