小程序分包异步化实战:解决跨分包组件复用与主包体积难题
1. 项目概述分包异步化小程序性能与架构的“分水岭”做小程序开发久了尤其是项目体积膨胀到几兆甚至十几兆之后有两个问题会像幽灵一样缠着你一个是首次启动的白屏时间越来越长用户流失率肉眼可见地上升另一个是随着业务模块增加代码耦合越来越严重想从分包A里用一下分包B的某个精致组件或者工具函数却发现无从下手强行打包进去又会导致主包超限。我最近在重构一个电商类小程序时就深度踩进了这个坑。项目里有独立的商品详情分包和用户中心分包用户中心的“我的订单”列表想复用商品详情分包里那个已经打磨得非常完善的商品卡片组件按传统思路要么复制一份代码后期维护噩梦要么把这个组件提成公共组件放到主包主包体积告急。直到我系统性地实践了小程序的分包异步化方案才真正找到了优雅的解法。这不仅仅是解决一个“跨分包引用”的具体问题更是对小程序代码架构和加载性能的一次重塑。简单说它允许你在需要的时候动态、异步地去加载另一个分包里的资源而不是在启动时就全部打包下载。下面我就结合这次实战把其中的门道、实操步骤以及我踩过的那些“坑”彻底讲透。2. 核心需求与问题场景深度解析2.1 传统分包模式的瓶颈在哪里在分包异步化出现之前小程序的分包是相对静态和隔离的。假设我们有两个分包packageA商品模块包含商品详情页、商品卡片组件ProductCard和一系列商品相关的工具函数。packageB用户模块包含“我的订单”页面里面需要展示订单中的商品信息。在packageB的订单页order.wxml中你很想直接这样写!-- order.wxml -- view wx:for{{orderList}} !-- 理想情况直接使用另一个分包里的组件 -- product-card product{{item.productInfo}}/product-card /view但在传统模式下这是行不通的。因为小程序打包构建时每个分包是独立的。packageB在编译阶段根本不知道ProductCard组件在哪里也无法将其打包进自己的资源里。常见的“野路子”解法有两种代码复制把ProductCard组件复制一份到packageB。后果是同一份逻辑维护两遍任何修改都要同步极易出错违背了DRY原则。提升至主包将ProductCard移到主包。后果是无论用户是否访问商品模块这个组件都会在首次启动时被下载直接增加主包体积拖慢启动速度。主包大小限制目前微信小程序主包上限2M也变得岌岌可危。这两种方案在项目初期或简单场景下或许能应付但对于追求用户体验和长期可维护性的项目来说都是饮鸩止渴。2.2 分包异步化带来的范式转变分包异步化功能的核心思想是“按需加载”和“运行时依赖”。它打破了分包之间那堵隐形的墙允许你在一个分包我们称为“引用方分包”的代码逻辑中声明对另一个分包“被引用方分包”中特定资源的依赖。关键点在于声明而非打包引用方只声明“我需要什么”而不在编译时将被引用资源打包进来。异步加载当代码执行到需要该资源的地方时小程序运行时会动态发起网络请求去下载被引用方分包的对应资源文件。依赖前置为了更好的用户体验开发者可以预加载这些异步资源比如在页面onLoad时提前下载等到真正渲染时可能已经缓存好了实现“无感”使用。这样一来packageB的订单页就可以在运行时异步加载并使用packageA里的ProductCard组件完美解决了代码复用和主包体积的矛盾。这不仅仅是解决一个组件引用问题对于跨分包的工具函数、甚至页面跳转逻辑都提供了全新的解耦思路。3. 完整配置与实操流程拆解让我们回到那个电商小程序的例子一步步实现从packageB用户中心异步引用packageA商品模块的ProductCard组件。3.1 项目结构与基础配置首先明确我们的项目目录结构miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── packageA/ # 商品模块分包 │ ├── product-detail/ # 商品详情页 │ └── components/ # 商品相关组件 │ └── product-card/ # 目标组件 │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxml ├── packageB/ # 用户模块分包 │ ├── order/ # 我的订单页 │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ └── utils/ └── pages/ # 主包页面接下来是核心配置在app.json中声明分包和异步化配置{ pages: [ pages/index/index ], subpackages: [ { root: packageA, name: goods, // 为分包指定一个名称便于引用 pages: [ product-detail/index ] }, { root: packageB, name: user, pages: [ order/index ], independent: false // 通常设为false允许异步化 } ], preloadRule: { // 可以配置预加载规则非必需但能提升体验 packageB/order/index: { network: all, packages: [goods] // 当进入订单页时预加载goods分包 } } }注意subpackages中的name字段非常关键它是后续异步引用时用来定位分包的标识符。preloadRule不是异步化的必选项但强烈建议配置。它能在用户进入订单页时在后台悄悄加载goods分包的资源这样当真正需要渲染ProductCard时资源可能已经就绪避免了明显的加载等待。3.2 关键步骤使用require.async异步引入现在进入packageB/order/index.js我们需要在页面的逻辑层动态加载组件。步骤一在页面JSON中声明异步组件首先在packageB/order/index.json中我们不再像传统方式那样在usingComponents里直接写组件路径而是声明一个“占位符”。{ usingComponents: { // 传统方式 product-card: /packageA/components/product-card/index // 异步方式使用一个特殊的路径格式其中 goods 是分包名 product-card: async:./../../packageA/components/product-card/index } }这个async:前缀是告诉小程序框架这个组件需要异步加载。但请注意仅这样配置是不够的框架还不知道goods分包在哪里。步骤二在页面JS中动态加载并注册组件这是最核心的一步。我们需要在页面的生命周期如onLoad中使用require.async来动态加载组件定义并手动将其注册为当前页面的自定义组件。// packageB/order/index.js Page({ data: { orderList: [], // 订单数据 isComponentReady: false // 控制组件渲染的标记 }, onLoad: function(options) { this.loadAsyncComponent(); // ... 其他初始化逻辑如获取订单数据 }, loadAsyncComponent: function() { // 使用 require.async 异步加载组件所在的JS模块 // 路径规则分包名/组件在分包内的相对路径 require.async(goods/components/product-card/index, (productCardComponent) { // 回调函数参数 productCardComponent 就是组件定义 // 手动调用 Page 的 selectOwnerComponent 方法获取当前页面实例 // 然后调用其 registerComponent 方法注册组件 const pageInstance this.selectOwnerComponent(); if (pageInstance) { pageInstance.registerComponent(product-card, productCardComponent); // 注册成功后更新状态触发视图渲染 this.setData({ isComponentReady: true }); } else { console.error(无法获取页面实例组件注册失败); } }, (error) { // 加载失败处理 console.error(异步加载商品卡片组件失败:, error); // 可以在这里展示一个错误占位视图 }); } });步骤三在WXML中条件渲染由于组件是异步加载的在加载完成前WXML中对应的自定义组件标签是无法正确渲染的。因此我们需要用条件渲染来控制。!-- packageB/order/index.wxml -- view wx:for{{orderList}} wx:keyorderId view classorder-item !-- 其他订单信息... -- view classproduct-info !-- 只有当组件准备就绪时才渲染 -- block wx:if{{isComponentReady}} product-card product{{item.productInfo}}/product-card /block block wx:else !-- 加载中的占位符提升用户体验 -- view classloading-placeholder商品信息加载中.../view /block /view /view /view3.3 异步引用工具函数的场景除了组件跨分包引用工具函数也是常见需求。比如packageA里有一个计算商品价格的复杂函数calcFinalPricepackageB的订单页也想用。方法类似但更简单因为不需要注册组件。在packageA/utils/price.js中// packageA/utils/price.js function calcFinalPrice(basePrice, discount, coupon) { // 复杂的价格计算逻辑 return finalPrice; } module.exports { calcFinalPrice };在packageB/order/index.js中// packageB/order/index.js Page({ onLoad: function(options) { this.loadAsyncUtility(); }, loadAsyncUtility: function() { require.async(goods/utils/price, (priceUtil) { // 加载成功后priceUtil 就是 module.exports 的对象 const finalPrice priceUtil.calcFinalPrice(100, 0.8, 10); console.log(计算后的价格:, finalPrice); // 可以将工具函数挂载到页面实例上方便其他方法使用 this.calcFinalPrice priceUtil.calcFinalPrice; }, (error) { console.error(加载价格计算工具失败:, error); }); }, someMethod: function() { // 现在可以安全地使用了 if (this.calcFinalPrice) { const price this.calcFinalPrice(...arguments); } } });4. 原理剖析与性能影响分析4.1 运行时机制解析理解其原理能帮你更好地驾驭和排查问题。当你在packageB中调用require.async(goods/...)时背后发生了以下几步路径解析小程序运行时会解析这个异步路径。goods对应app.json中name为goods的分包。资源定位框架根据分包配置找到goods分包即packageA在服务器上的存储位置。网络请求运行时向CDN发起一个独立的HTTP请求获取目标JS文件例如组件或工具函数的定义文件。这是一个关键的耗时点受网络状况影响。代码执行与注册下载的JS代码在一个隔离的、安全的上下文中被执行。对于组件执行结果是一个组件构造器对于模块是module.exports的对象。然后通过registerComponent将其注入到当前页面的组件系统中。视图更新组件注册成功后由于isComponentReady变为trueWXML的条件渲染触发异步组件被创建并渲染到页面上。整个过程是异步非阻塞的不会影响页面主线程的其他操作如数据请求、用户交互响应。4.2 对小程序性能的双刃剑效应分包异步化是一把双刃剑用得好大幅提升体验用不好则适得其反。积极影响优势主包体积瘦身这是最直接的好处。所有非启动必需的、可被异步加载的组件/模块都可以移出主包确保主包轻量化极大优化首次启动速度和打开率。代码组织与复用真正实现了高内聚、低耦合的模块化架构。业务模块可以独立成包并对外提供清晰的异步接口便于大型团队协作和代码复用。按需加载减少流量用户只会在实际需要时才下载特定分包的特定资源避免了“一刀切”的全量下载节省了用户流量。潜在风险与挑战劣势首次渲染延迟这是最明显的代价。异步组件在第一次渲染前需要经历“网络请求 代码执行 组件注册”的过程这必然会导致该组件区域出现短暂的空白或占位状态。如果网络差或资源大延迟感会很明显。复杂度提升代码中需要加入异步加载的逻辑、状态管理、错误处理并配合条件渲染相比直接同步引入代码结构更复杂。预加载策略考验preloadRule配置需要精心设计。预加载过早如首页就预加载所有分包会浪费流量和内存预加载过晚又起不到“无感”使用的效果。需要根据用户行为路径进行精细化配置。5. 高级技巧、避坑指南与实战心得在实际项目中摸爬滚打我总结了一些教科书里不会写的经验和坑点。5.1 预加载策略的精细化设计不要盲目预加载整个分包。利用preloadRule的packages字段可以指定只预加载分包的某个子路径甚至结合独立分包特性。preloadRule: { packageB/order/index: { network: wifi, // 仅在WIFI下预加载为移动网络用户省流量 packages: [ goods/components/product-card // 只预加载这个组件路径下的资源 ] }, pages/index/index: { packages: [_APP_] // 预加载主包对某些场景有用 } }心得通过数据分析工具了解用户从首页到订单页的转化路径和停留时间。如果路径短、停留时间短预加载要更积极如果路径长可以在中间页面如购物车页再触发预加载平衡体验与流量。5.2 错误处理与降级方案网络是不稳定的异步加载可能失败。一个健壮的程序必须有降级方案。loadAsyncComponent: function() { const loadTask require.async(goods/components/product-card/index, (comp) { // ... 成功逻辑 }, (error) { console.error(组件加载失败, error); this.setData({ componentLoadFailed: true, failError: error.errMsg || 网络异常 }); // 降级方案1展示一个简单的静态视图 // 降级方案2引导用户刷新或检查网络 // 降级方案3记录错误日志上报监控平台 }); // 可以设置超时 setTimeout(() { if (!this.data.isComponentReady !this.data.componentLoadFailed) { loadTask.abort(); // 取消未完成的加载任务 this.setData({ componentLoadFailed: true, failError: 加载超时 }); } }, 5000); // 5秒超时 }在WXML中需要增加对应的失败状态渲染block wx:if{{isComponentReady}} product-card product{{item.productInfo}}/product-card /block block wx:elif{{componentLoadFailed}} view classerror-placeholder bindtapretryLoadComponent 组件加载失败点击重试 ({{failError}}) /view /block block wx:else view classloading-placeholder加载中.../view /block5.3 常见问题排查实录问题1控制台报错[require.async] package not found或component is not found原因Aapp.json中分包的name字段未配置或配置错误。require.async的第一个参数路径开头必须是在app.json中定义的name。检查确认app.json的subpackages里对应分包的root和name都正确且require.async的路径以name开头。原因B组件路径拼写错误。路径是相对于分包根目录的。检查假设分包goods的root是packageA组件在packageA/components/my-comp/index那么异步路径应为goods/components/my-comp/index。问题2组件注册成功了但渲染不出来或样式错乱原因A组件的WXML或WXSS中使用了绝对路径的图片或字体。这些资源在异步加载时路径基准可能发生变化。解决尽量使用网络图片URL或将静态资源放在分包内并使用相对路径。对于背景图片可以将其转为base64内联需注意体积。原因B组件依赖了其所在分包独有的全局样式或行为。解决确保组件是自包含的。如果组件依赖了分包内的某些公共样式需要将这些样式文件也通过异步方式引入或者将组件设计为不依赖外部样式。问题3异步加载的组件内部方法调用失败原因在组件尚未加载完成isComponentReady为false时就尝试通过selectComponent去获取组件实例并调用其方法。解决所有与异步组件实例交互的操作都必须放在组件加载成功的回调函数之后或者用wx:if确保组件存在后再进行。// 错误示例 onShow: function() { if (this.data.isComponentReady) { // 即使ready了组件实例也可能还未挂载到节点树 const comp this.selectComponent(#my-async-comp); comp.someMethod(); // 可能失败因为selectComponent可能返回null } } // 正确做法在注册成功的回调里获取实例或使用nextTick require.async(goods/components/my-comp/index, (compDef) { this.selectOwnerComponent().registerComponent(my-async-comp, compDef); this.setData({ isComponentReady: true }, () { // 在setData回调或使用wx.nextTick确保视图更新完成 wx.nextTick(() { const compInstance this.selectComponent(#my-async-comp); if (compInstance) { compInstance.someMethod(); } }); }); });5.4 性能监控与优化建议将异步加载纳入你的性能监控体系。关键指标记录从调用require.async到组件成功渲染或失败的总耗时。可以在成功/失败回调中打点。网络状态关联将加载耗时与wx.getNetworkType获取的网络类型关联分析了解在不同网络下的体验差异。资源大小监控监控异步加载的JS文件大小。如果某个被异步引用的组件文件过大例如超过100KB就要考虑对其进行进一步拆分比如将其依赖的大的工具库单独抽离。缓存策略理解小程序框架会对下载的分包资源进行缓存。理解缓存机制避免频繁发布导致缓存失效增加用户流量消耗。在版本更新时可以通过更新app.json中的分包版本号或使用独立的CDN策略来管理缓存。分包异步化不是银弹它是一个需要权衡的工具。对于高频使用、对首屏速度至关重要的核心组件仍然应该放在主包。而对于那些低频、非核心、体积较大的功能模块异步化是绝佳的解决方案。它要求开发者从“打包时思维”转向“运行时思维”更精细地管理你的代码和资源最终换来的是用户手中那个启动更快、运行更流畅的小程序。

相关新闻

如何让Windows性能飙升45%:AtlasOS终极优化方案完整指南

如何让Windows性能飙升45%:AtlasOS终极优化方案完整指南

如何让Windows性能飙升45%:AtlasOS终极优化方案完整指南 【免费下载链接】Atlas 🚀 An open and lightweight modification to Windows, designed to optimize performance, privacy and usability. 项目地址: https://gitcode.com/GitHub_Trending/at…

2026/10/19 5:21:25 阅读更多 →
DSO Quad示波器校准全攻略:从原理到实践,提升测量精度

DSO Quad示波器校准全攻略:从原理到实践,提升测量精度

1. 从一次测量偏差说起:为什么你的DSO Quad需要校准?手头这台DSO Quad示波器,相信很多电子爱好者和硬件工程师都不陌生。它小巧、便携,价格亲民,是调试单片机、传感器、电源电路的得力助手。但不知道你有没有遇到过这样…

2026/10/16 21:43:33 阅读更多 →
电竞俱乐部运营:从BLG事件看选手轮换与团队稳定性管理

电竞俱乐部运营:从BLG事件看选手轮换与团队稳定性管理

最近电竞圈关于BLG战队和选手Bin的讨论热度很高,很多粉丝和观众都在关心一个核心问题: 为什么顶尖选手的“休息”或“轮换”会引发如此大的争议,甚至让粉丝“破防”? 这背后其实不只是选手的个人选择,更折射出当前职…

2026/10/19 15:55:06 阅读更多 →

最新新闻

AI率怎么降?六款降AI痕迹工具实测与论文改写全流程指南

AI率怎么降?六款降AI痕迹工具实测与论文改写全流程指南

1. 先搞懂AI率怎么来的,才知道怎么降1.1 “AI率”不是一个简单的数字2026毕业季,论文圈最让人上火的不是查重率,而是AI率。不少同学用AI帮忙开了个头、搭了框架,结果交到导师那里,被一句“这段明显是AI写的”打了回来。…

2026/10/20 11:06:01 阅读更多 →
127、RDMA编程最佳实践:使用Completion Channel与Event Channel

127、RDMA编程最佳实践:使用Completion Channel与Event Channel

从一次诡异的丢包说起 去年做分布式存储项目,凌晨三点被值班电话叫醒——集群某节点RDMA写操作间歇性超时。抓包看链路层一切正常,ibv_poll_cq返回的WC状态码全是IBV_WC_SUCCESS,但上层应用就是收不到数据完成通知。折腾到天亮才发现,问题出在Completion Channel的轮询策略…

2026/10/20 11:06:01 阅读更多 →
126、RDMA编程最佳实践:避免CPU忙等与上下文切换

126、RDMA编程最佳实践:避免CPU忙等与上下文切换

一、从一次线上事故说起 去年双十一大促,我们团队负责的分布式存储集群突然出现严重的尾延迟抖动。监控面板上,P99延迟从正常的200μs飙升到5ms,整整25倍的恶化。更诡异的是,CPU利用率并不高,只有30%左右,但上下文切换次数暴涨到每秒几十万次。 我登录到出问题的机器,…

2026/10/20 11:06:01 阅读更多 →
125、RDMA编程最佳实践:批量提交WR与Doorbell优化

125、RDMA编程最佳实践:批量提交WR与Doorbell优化

一、从一次诡异的性能抖动说起 去年在调一个分布式存储的RDMA路径时,遇到一个让人抓狂的问题:单连接小IO(4KB)的延迟曲线,每隔几十秒就会出现一个尖刺,峰值延迟从正常的5μs直接跳到200μs+。抓了ibv_devinfo、perftest、甚至用ftrace跟踪内核路径,最后发现罪魁祸首竟然…

2026/10/20 11:06:01 阅读更多 →
124、RDMA编程最佳实践:内存池与预注册

124、RDMA编程最佳实践:内存池与预注册

一、一个让我熬夜到凌晨三点的bug 去年做分布式存储项目,集群压测到80万IOPS时,某个节点突然出现大量“Memory Registration Failed”错误。排查了三天,最后发现是内存池设计时没处理好MR(Memory Region)的生命周期——某个线程释放了内存块,但对应的MR还在硬件缓存里挂…

2026/10/20 11:06:01 阅读更多 →
【虎鲸进化论 24】监控与巡检:它开始盯着一切

【虎鲸进化论 24】监控与巡检:它开始盯着一切

【虎鲸进化论 24】监控与巡检:它开始盯着一切 本文是「虎鲸进化论」系列第 25 篇(正文第 24 篇)。上一篇:自动回归;下一篇预告:数据修复与清理 —— 它管起了烂摊子。 前言 上一篇的结尾问:没人…

2026/10/20 11:05:01 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/20 9:50:45 阅读更多 →