Jest 代码转换(Code Transformation)完全指南:从 babel-jest 默认配置到自定义 Transformer 实战
Jest 代码转换Code Transformation完全指南从 babel-jest 默认配置到自定义 Transformer 实战【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jestJest 默认以纯 JavaScript 执行项目代码当你的源码或测试使用了 Node 原生不支持的语法如 JSX、TypeScript、Vue 模板时就需要通过代码转换Code Transformation将其转译为普通 JavaScript。本文以 docs/CodeTransformation.md 为骨架结合 packages/babel-jest、packages/jest-transform、packages/babel-preset-jest 等仓库源码系统讲解transform配置、babel-jest默认行为、Transformer 完整 API、同步/异步转换机制与缓存失效原理并给出可复制的自定义 Transformer 实战代码帮助你为任意文件类型图片、CSS、模板引擎等编写转换器并接入 Jest。为什么需要代码转换Jest 本身并不理解 JSX、TypeScript 类型注解、Vue 模板等语法——它的运行时环境是 Node 的 JavaScript 引擎。因此与为浏览器打包类似你需要在测试运行前把这些语法转换成 Node 可以直接执行的纯 JavaScript。Jest 通过transform配置项支持这一机制。一个 transformer转换器就是提供了转换源码文件方法的模块。例如当你想在模块或测试中使用 Node 尚未支持的新语言特性时可以接入一个代码预处理器把未来版本的 JavaScript 转译成当前版本。值得强调的是Jest 会缓存转换结果并根据多种因素尝试使缓存失效例如被转换文件的源码内容、Jest 配置的变化等。理解缓存机制见下文缓存与缓存键一节对于编写高性能转换器至关重要。默认行为开箱即用的 babel-jestJest 自带一个开箱即用的转换器 ——babel-jest源码位于 packages/babel-jest/src/index.ts。它的行为如下加载项目自身的 Babel 配置babel-jest会读取你项目根目录下的 Babel 配置文件如babel.config.js、.babelrc并使用这些配置转译代码匹配文件范围转换所有匹配/\.[jt]sx?$/正则的文件即.js、.jsx、.ts、.tsx四种扩展名注入 mock 提升hoisting插件babel-jest会自动注入使jest.mock提升生效的 Babel 插件具体机制参见 ES Module mocking。transform配置项的默认值是{\\.[jt]sx?$: babel-jest}见 docs/Configuration.md。这意味着只要安装了jest-cli并执行yarn add --dev babel-jest babel/coreJS/TS 代码就会自动经过 Babel 转换参见 packages/babel-jest/README.md。关于 babel-preset-jest默认情况下babel-jest会额外注入babel-preset-jest。从 packages/babel-preset-jest/index.js 可以看到这个 preset 由两部分组成const jestPreset { plugins: [require.resolve(babel-plugin-jest-hoist)], presets: [require.resolve(babel-preset-current-node-syntax)], };即babel-plugin-jest-hoist负责jest.mock等调用的提升和babel-preset-current-node-syntax让 Babel 输出的代码语法与当前 Node 版本能力对齐。在 packages/babel-jest/src/index.ts 的createTransformer中可以看到注入逻辑const {excludeJestPreset, ...inputOptions} transformerConfig ?? {}; // ... presets: [ ...(inputOptions.presets ?? []), ...(excludeJestPreset true ? [] : [jestPresetPath]), ],也就是说excludeJestPreset: true会阻止注入该 preset。但请注意禁用它将同时停止jest.mock的提升hoisting可能导致你的测试失效。配置方式如下transform: { \\.[jt]sx?$: [babel-jest, { excludeJestPreset: true }] }与其他预处理器并存必须显式声明 babel-jest如果你要同时使用多个代码预处理器务必显式把默认的babel-jest也写在transform中否则.js/.ts文件将不再被转换transform: { \\.[jt]sx?$: babel-jest, \\.css$: some-css-transformer }babel-jest还支持传入任意 Babel 选项例如{\\.js$: [babel-jest, {rootMode: upward}]}或者像官方 README 示例那样指定extends与额外插件transform: { \\.[jt]sx?$: [babel-jest, { extends: ./babel.config.js, plugins: [babel-plugin-transform-import-meta] }] }编写自定义 Transformer完整 API你可以编写自己的转换器。Jest 的 Transformer 接口定义在 packages/jest-transform/src/types.ts其核心类型如下为便于阅读做了精简完整定义请查阅仓库源码interface TransformOptionsTransformerConfig unknown { supportsDynamicImport: boolean; supportsExportNamespaceFrom: boolean; /** * 取值为 * - 若 Jest 未以 Node ESM 标志 --experimental-vm-modules 运行则为 false * - 若文件扩展名定义在 [extensionsToTreatAsEsm](https://link.gitcode.com/i/96937fa2ceda18dba0a1ae29b9959f19) 中 * 且 Jest 以 --experimental-vm-modules 运行则为 true */ supportsStaticESM: boolean; supportsTopLevelAwait: boolean; instrument: boolean; /** 由 jest-runtime 使用的缓存文件系统用于提升性能。 */ cacheFS: Mapstring, string; /** 当前运行项目的 Jest 配置。 */ config: ProjectConfig; /** config 的字符串化版本——常用于缓存失效cache busting。 */ configString: string; /** 用户通过 transform 选项传入的转换器配置。 */ transformerConfig: TransformerConfig; } type TransformedSource { code: string; map?: RawSourceMap | string | null; }; interface SyncTransformerTransformerConfig unknown { canInstrument?: boolean; getCacheKey?: (sourceText, sourcePath, options) string; getCacheKeyAsync?: (sourceText, sourcePath, options) Promisestring; process: (sourceText, sourcePath, options) TransformedSource; processAsync?: (sourceText, sourcePath, options) PromiseTransformedSource; } interface AsyncTransformerTransformerConfig unknown { canInstrument?: boolean; getCacheKey?: (sourceText, sourcePath, options) string; getCacheKeyAsync?: (sourceText, sourcePath, options) Promisestring; process?: (sourceText, sourcePath, options) TransformedSource; processAsync: (sourceText, sourcePath, options) PromiseTransformedSource; } type TransformerTransformerConfig unknown SyncTransformerTransformerConfig | AsyncTransformerTransformerConfig; type TransformerCreatorX extends Transformer, TransformerConfig unknown (transformerConfig?: TransformerConfig) X; type TransformerFactoryX extends Transformer { createTransformer: TransformerCreatorX; };从源码结构看TransformOptions中的supports*系列标志来自CallerTransformOptions在 packages/jest-transform/src/types.ts 中与Options一起被引用它们用于告知转换器应当输出 ESM 还是 CJS与同步/异步本身没有直接关系。同步与异步转换理解 process / processAsyncJest 按需on demand对文件进行代码转换当require或import被求值时才触发。这一过程也叫 transpilation可能是同步的发生在require时异步的发生在import或import()时后者也能在 CommonJS 模块中使用。因此接口提供了成对的方法process{Async}与getCacheKey{Async}。后者用于判断是否真的需要调用process{Async}——即先计算缓存键命中磁盘缓存则直接复用避免重复转译。两个重要的兼容性规则异步可以回退到同步如果processAsync未实现异步转译会回退到同步的process同步不能调用异步同步转译永远无法使用processAsync。因此如果你的代码库只使用 ESM实现异步变体即可但只要有任何代码通过require加载包括 ESM 内部通过createRequire加载就必须实现同步的process变体。Transformer类型定义中的注释也印证了这一点见 packages/jest-transform/src/types.ts。与转换方式密切相关的supports*标志含义如下参见 ECMAScriptModules 文档supportsDynamicImport: true转换器可以返回import()表达式ESM 与 CJS 都支持supportsStaticESM: true顶层import语句被支持返回的代码将按 ESM而非 CJS解释执行。缓存与缓存键为什么要实现 getCacheKey虽然不强制但官方强烈建议实现getCacheKey否则每次都要重新转译而不是从磁盘读取上一次的结果白白浪费资源。在 packages/jest-transform/src/ScriptTransformer.ts 中ScriptTransformer负责协调整个转换流程先按文件匹配transform正则加载对应的 transformer支持直接导出对象或导出带createTransformer的工厂见loadTransformers然后计算缓存键、调用process、把结果写入磁盘缓存目录jest-transform-cache-id按缓存键前两位分子目录存放。缓存键的默认计算_buildCacheKeyFromFileInfo会综合以下要素做 SHA-1 摘要文件内容fileData序列化后的 Jest 配置configString是否开启插桩instrumentcallerSupport信息supports*标志文件路径与内部CACHE_VERSION。babel-jest的getCacheKey实现则进一步加入了 Babel 配置本身见 packages/babel-jest/src/index.ts它会 SHA-1 摘要babel-jest自身的代码、序列化后的 Babel options、源码文本、相对路径、Jest 配置字符串、Babel 配置文件路径、是否插桩、NODE_ENV、BABEL_ENV以及 Node 版本号。这意味着修改 Babel 配置或环境变量都会导致缓存失效行为完全可预期。官方提供了一个辅助包jest/create-cache-key-function帮助实现getCacheKey仓库源码见 packages/jest-create-cache-key-function。开发期间的调试建议在开发转换器的过程中可以配合--no-cache运行 Jest 来绕过缓存并在需要时手动清空缓存目录参见 缓存问题排查。工厂模式createTransformer 与转换器配置除了直接实现Transformer接口你还可以选择导出createTransformer—— 一个用于动态创建转换器的工厂函数。这样做的好处是可以在 Jest 配置中传入转换器配置当transform配置的值为[path-to-transformer, {options}]这种元组形式时ScriptTransformer.loadTransformers会检测到工厂并调用createTransformer(transformerConfig)见 packages/jest-transform/src/ScriptTransformer.ts。babel-jest本身就是这一模式的范例它在 packages/babel-jest/src/index.ts 导出createTransformer接受{excludeJestPreset, ...}等配置同时为了兼容 Jest 的requireOrImportModule加载方式把工厂挂到了default导出上const transformerFactory { createTransformer, }; export default transformerFactory;关于 transformIgnorePatternsnode_modules 默认不转换请注意默认配置下node_modules不会被转译如需转译必须修改transformIgnorePatterns配置项默认值为[/node_modules/, \\.pnp\\.[^\\/\\\\]$]见 docs/Configuration.md。该配置项是一个正则字符串数组在转换前与所有源文件路径匹配只要匹配其中任意一个模式该文件就不会被转换。常见的实战场景是某些第三方模块尤其是 React Native、TypeScript 项目以未转译的 ES 代码发布此时需要显式放行// jest.config.js module.exports { transformIgnorePatterns: [ /node_modules/(?!(foo|bar)/), ], };上例会转译node_modules/foo/与node_modules/bar/下的文件。注意相互重叠的多个模式可能导致你以为会转换、实际却没转换的文件——例如再加一个/bar/模式node_modules/bar/会因匹配第二个模式而仍然不被转换。模式字符串匹配的是完整路径建议使用rootDir令牌锚定项目根目录避免在不同环境中误伤所有文件例如rootDir/bower_components/、rootDir/node_modules/。另外如果使用pnpmnode_modules下的包是通过符号链接指向.pnpm目录的直接写rootDir/node_modules/(?!(package-a)/)不会生效需要改用rootDir/node_modules/.pnpm/(?!(package-a|scope\\pkg-b))之类的模式。源码映射与覆盖率process 返回值的关键细节官方特别提醒务必让process{Async}返回 source map与转译后的代码一起这样代码覆盖率和测试错误报告才能准确定位到源码行号。内联 source map 也可以工作但性能更慢。从babel-jest的实现看它在构造 Babel 选项时固定设置了sourceMaps: both同时产出内联与外置 map见 packages/babel-jest/src/index.ts并在process中把{code, map}一并返回。当启用覆盖率插桩时transformOptions.instrument为真它还会注入babel-plugin-istanbul插件完成插桩addIstanbulInstrumentation。如果你的转换器声明了canInstrument: trueJest 会认为返回的代码已自行插桩若为false或未声明Jest 会在转换后用 Babel 对返回代码执行 Istanbul 插桩参见 packages/jest-transform/src/types.ts 与 packages/jest-transform/src/ScriptTransformer.ts 中的_instrumentFile。实战示例一TypeScript 类型检查ts-jestbabel-jest虽然能转译 TypeScript 文件但 Babel 的转译是语法剥离不会做类型检查。如果你希望在测试时同步进行类型校验可以使用ts-jest作为转换器配置方式为将transform中 TS 文件的正则指向ts-jest。它把转译与类型检查结合在一条流水线里。实战示例二把图片转换为路径fileTransformer导入图片是浏览器打包中的常见做法但图片本身不是合法 JavaScript。在 Jest 中一种优雅的处理方式是写一个转换器把导入的值替换为图片文件名。官方文档给出如下实现const path require(path); module.exports { process(sourceText, sourcePath, options) { return { code: module.exports ${JSON.stringify(path.basename(sourcePath))};, }; }, };module.exports { transform: { \\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$: rootDir/fileTransformer.js, }, };这个例子展示了转换器最核心的形态接收sourceText、sourcePath、options三个参数返回{code}必要时附带map。module.exports xxx.jpg这样的输出让require(./logo.png)在测试中直接得到文件名。注意由于默认transformIgnorePatterns排除node_modules如果你后续想把这个转换器用于node_modules内的资源需要相应调整该配置。实战示例三模板引擎预编译仓库内真实用例仓库的端到端测试 e2e/coverage-handlebars/transform-handlebars.js 提供了一个真实的自定义转换器它把 Handlebars 模板预编译为 JavaScript并返回带 source map 的代码从而让覆盖率能正确映射回.hbs源文件const {build} require(jridgewell/build-mapping); const Handlebars require(handlebars/dist/cjs/handlebars.js); const dedent require(string-dedent); exports.process (code, filename) { const pc Handlebars.precompile(code, {srcName: filename}); return dedent(build) const Handlebars require(handlebars/dist/cjs/handlebars.runtime.js); module.exports Handlebars.template(${pc}); ; };配合该目录的 jest 配置\\.hbs$会命中这个转换器。这说明只要返回合法的 JavaScript 与可选的 source map任何文件格式都能接入 Jest。相关的端到端测试如 e2e/tests/coverageHandlebars.test.ts验证了转换后的覆盖率映射行为。总结代码转换是 Jest 支持非 JavaScript 语法与自定义资源类型的核心机制脉络清晰默认即用babel-jest开箱即用地转换.js/.jsx/.ts/.tsx加载项目 Babel 配置并注入 mock 提升所需的 preset可扩展通过transform配置可以叠加任意转换器但别忘了显式保留babel-jest接口明确实现process/processAsync按需选配getCacheKey系列同步/异步的回退规则决定了你的代码库必须支持哪种形态性能关键实现getCacheKey才能充分利用磁盘缓存避免重复转译边界清晰transformIgnorePatterns默认排除node_modules需要转译第三方未编译代码时按需放行质量保障返回 source map 是覆盖率与错误定位准确性的前提canInstrument则决定了是否由 Jest 代为插桩。从图片路径替换到 Handlebars 预编译再到仓库端到端测试中的真实用例掌握了 Transformer 接口你就能让 Jest 理解项目中任何一种文件格式。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Cherry Studio ai-sdk-provider 修复实录:Content-Type 头泄漏导致 CherryIN 图像编辑 multipart 请求失败

Cherry Studio ai-sdk-provider 修复实录:Content-Type 头泄漏导致 CherryIN 图像编辑 multipart 请求失败

Cherry Studio ai-sdk-provider 修复实录:Content-Type 头泄漏导致 CherryIN 图像编辑 multipart 请求失败 【免费下载链接】cherry-studio 🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端 项目地址: https://gitcode.com/CherryHQ/cherry-s…

2026/9/19 15:10:45 阅读更多 →
LLVM编译器基础设施解析:从IR、Pass到向量化与JIT实践

LLVM编译器基础设施解析:从IR、Pass到向量化与JIT实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 15:10:45 阅读更多 →
Apollo 2.0.0 版本解析:从 Java 17 支持到灰度发布、安全加固与 OpenAPI 重构

Apollo 2.0.0 版本解析:从 Java 17 支持到灰度发布、安全加固与 OpenAPI 重构

Apollo 2.0.0 版本解析:从 Java 17 支持到灰度发布、安全加固与 OpenAPI 重构 【免费下载链接】apollo Apollo is a reliable configuration management system suitable for microservice configuration management scenarios. 项目地址: https://gitcode.com/gh…

2026/9/19 15:10:45 阅读更多 →

最新新闻

MaxKey集成JeecgBoot实现单点登录:OAuth2统一身份认证实战

MaxKey集成JeecgBoot实现单点登录:OAuth2统一身份认证实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 17:50:00 阅读更多 →
Nacos启动报错No DataSource set?三招解决standalone模式数据源配置

Nacos启动报错No DataSource set?三招解决standalone模式数据源配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 17:50:00 阅读更多 →
LLVM项目深度解析:编译器基础设施架构与实战构建指南

LLVM项目深度解析:编译器基础设施架构与实战构建指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 17:50:00 阅读更多 →
ROS与Anaconda Python环境冲突解决:隔离配置与共存指南

ROS与Anaconda Python环境冲突解决:隔离配置与共存指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 17:50:00 阅读更多 →
通达信龙抬头指标源码拆解:低位放量选股模型

通达信龙抬头指标源码拆解:低位放量选股模型

简介:这份《自用多年的龙抬头通达信指标公式源码.doc》适合通达信软件用户、股票技术分析爱好者以及希望自主编写或改良选股公式的投资者。文档完整给出指标源码,并逐条拆解其核心条件:成交量放大配合均线关系、收盘价触及阶段高位、涨幅过滤…

2026/9/19 17:50:00 阅读更多 →
分布式与微服务的区别:从一次订单超时雪崩复盘架构难点

分布式与微服务的区别:从一次订单超时雪崩复盘架构难点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 17:48:59 阅读更多 →

日新闻

BP神经网络时序预测:滑窗长度与多窗口平均策略

BP神经网络时序预测:滑窗长度与多窗口平均策略

简介:面向机器学习、深度学习与数据建模学习者的一份完整研究文献,聚焦BP神经网络在农业产量预测中的应用。文档以1980—2018年全国棉花产量为样本,系统讲解数据归一化处理、激活函数原理、多层神经网络结构搭建及训练流程,展示敏…

2026/9/19 0:00:30 阅读更多 →
Transformer训练实时监控实战:基于MindSpore的损失曲线可视化方案

Transformer训练实时监控实战:基于MindSpore的损失曲线可视化方案

上个月调一个Deformable DETR模型,在单卡上要跑将近两天。第二天早上我下意识打开终端翻日志,发现loss从凌晨两点就开始往上爬,一路从0.8涨到1.35,整整六个小时没人发现。那六个小时的训练不仅白跑,还霸占着卡——等于…

2026/9/19 0:00:30 阅读更多 →
OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南

OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南

OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南 【免费下载链接】opencloud 🌤️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign. 项目地址: htt…

2026/9/19 0:00:30 阅读更多 →

周新闻

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and ag…

2026/9/19 3:59:36 阅读更多 →
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitH…

2026/9/19 3:53:08 阅读更多 →
Flutter应用改名全指南:从Android到iOS的配置与工具实践

Flutter应用改名全指南:从Android到iOS的配置与工具实践

刚接一个外包项目时,甲方要求把工程里临时用的应用名改成正式产品名。我本来觉得“改名”这种小事,打开配置文件改一行不就完了?结果真动手才发现,Flutter项目里“应用名称”根本不是一处配置,而是一整套散落在 Androi…

2026/9/19 4:02:43 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/16 22:31:27 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/15 21:39:18 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/16 22:32:59 阅读更多 →