From 849b54646c3e83eb8755207cbefcafc5785f2796 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 16:30:23 +0800 Subject: [PATCH 01/20] feat: add rn section-list component --- .../references/rn-template-reference.md | 85 ++++ docs-vitepress/api/compile.md | 1 + .../guide/extend/extend-component.md | 253 ++++++++++ docs-vitepress/guide/rn/component.md | 71 ++- packages/webpack-plugin/lib/index.js | 3 + .../template/wx/component-config/index.js | 4 - .../wx/component-config/sticky-header.js | 23 - .../wx/component-config/sticky-section.js | 23 - .../lib/resolver/ExtendComponentsPlugin.js | 60 +++ .../components/ali/mpx-section-list.mpx | 0 .../components/ali/mpx-sticky-header.mpx | 0 .../components/ali/mpx-sticky-section.mpx | 0 .../components/extends/section-list.mpx | 1 + .../components/extends/sticky-header.mpx | 1 + .../components/extends/sticky-section.mpx | 1 + .../components/react/mpx-section-list.tsx | 475 ++++++++++++++++++ .../components/web/mpx-section-list.vue | 0 .../components/wx/mpx-section-list.mpx | 0 .../components/wx/mpx-sticky-header.mpx | 0 .../components/wx/mpx-sticky-section.mpx | 0 .../lib/template-compiler/compiler.js | 10 +- packages/webpack-plugin/lib/utils/const.js | 22 + .../resolver/extend-components-plugin.spec.js | 59 +++ .../template-compiler/rn-template.spec.js | 18 + 24 files changed, 1038 insertions(+), 72 deletions(-) create mode 100644 docs-vitepress/guide/extend/extend-component.md delete mode 100644 packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js delete mode 100644 packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js create mode 100644 packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js create mode 100644 packages/webpack-plugin/lib/runtime/components/ali/mpx-section-list.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-header.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-section.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx create mode 100644 packages/webpack-plugin/lib/runtime/components/web/mpx-section-list.vue create mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-header.mpx create mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-section.mpx create mode 100644 packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js diff --git a/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md b/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md index d5baaf3789..3399c5426b 100644 --- a/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md +++ b/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md @@ -58,6 +58,7 @@ - [video](#video) - [web-view](#web-view) - [root-portal](#root-portal) + - [section-list](#section-list) - [sticky-section](#sticky-section) - [sticky-header](#sticky-header) - [cover-view](#cover-view) @@ -565,6 +566,8 @@ Mpx 输出 RN 内置支持了大部分常用的基础组件,详情见下方文 **自定义覆盖与扩展**:当某个内置基础组件在 RN 上不满足业务需要、需替换为自定义实现,或希望在模板中直接使用一组宿主特有的基础组件时,可在 `@mpxjs/webpack-plugin` 的编译配置 `rnConfig.customBuiltInComponents` 中声明自定义组件 —— **同名组件会覆盖**框架内置实现,**新名称则作为扩展基础组件**注入到模板编译期识别表中,无需在每个 `.mpx` 的 `usingComponents` 中重复注册即可在模板中以基础组件方式使用。该配置在模板编译阶段生效,并非应用入口的运行时 `Mpx.config.rnConfig` 配置。 +自定义基础组件如果最终渲染到 React Native 原生组件,需要先消费或映射自身支持的属性、事件,再过滤已处理的小程序属性以及 Mpx 内部辅助属性,避免它们继续透传到原生节点上。例如 `enable-var`、`enable-offset`、`enable-background`、`external-var-context`、`parent-font-size`、`parent-width`、`parent-height`、`enable-text-pass-through` 以及组件已处理的 `bind*` / `catch*` 事件属性。 + ### 通用属性 通用属性除了前述的[模板指令](#模板指令)和[通用事件](#通用事件)绑定外,还包括以下属性: @@ -1342,10 +1345,82 @@ level 有效值: - style 样式中不支持使用百分比计算、css variable +### section-list + +RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。使用前需要在 `usingComponents` 中注册: + +```json +{ + "usingComponents": { + "section-list": "@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list" + } +} +``` + +#### 属性 + +| 属性名 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| height | string/number | | 组件高度 | +| width | string/number | | 组件宽度 | +| listData | array | | 列表数据,分组头数据需包含 `isSectionHeader: true` | +| generic:recycle-item | string | | 列表项抽象节点组件名 | +| generic:section-header | string | | 分组头抽象节点组件名 | +| generic:list-header | string | | 列表头抽象节点组件名 | +| generic:list-footer | string | | 列表尾抽象节点组件名 | +| itemHeight | object | | 列表项高度配置,支持 `value` 或 `getter(item, index)` | +| sectionHeaderHeight | object | | 分组头高度配置,支持 `value` 或 `getter(item, index)` | +| listHeaderHeight | object | | 列表头高度配置,支持 `value` 或 `getter()` | +| useListHeader | boolean | `false` | 是否渲染列表头 | +| listHeaderData | object | | 列表头数据 | +| useListFooter | boolean | `false` | 是否渲染列表尾 | +| listFooterData | object | | 列表尾数据 | +| enable-sticky | boolean | `false` | 是否开启分组头吸顶 | +| enhanced | boolean | `false` | 是否开启增强能力 | +| bounces | boolean | `true` | iOS 边界弹性控制,开启 `enhanced` 后生效 | +| enable-back-to-top | boolean | `false` | 点击状态栏回到顶部,仅 iOS 支持 | +| end-reached-threshold | number | `0.1` | 触底事件触发阈值 | +| refresher-enabled | boolean | `false` | 是否开启下拉刷新 | +| refresher-triggered | boolean | `false` | 当前下拉刷新状态 | +| show-scrollbar | boolean | `true` | 是否显示滚动条 | +| scroll-event-throttle | number | `0` | scroll 事件触发频率 | +| simultaneous-handlers | array\ | `[]` | 允许多个外部手势同时识别 | +| wait-for | array\ | `[]` | 等待外部手势失败后再识别 | + +#### 事件 + +| 事件名 | 说明 | +| --- | --- | +| bindscroll | 滚动时触发 | +| bindscrolltolower | 滚动到底部时触发 | +| bindrefresherrefresh | 自定义下拉刷新被触发 | + +#### 方法 + +| 方法名 | 说明 | +| --- | --- | +| scrollToIndex({ index, animated, viewOffset, viewPosition }) | 滚动到指定原始索引 | + +#### 注意事项 + +- `generic:*` 指向的抽象节点组件也需要在当前页面或组件的 `usingComponents` 中注册。 +- 使用 `itemHeight`、`sectionHeaderHeight`、`listHeaderHeight` 提供稳定高度,可减少滚动定位异常。 +- 开启 `enable-sticky` 且快速滑动时,自定义分组头可能出现闪烁,这是 RN `SectionList` 底层机制限制。 + ### sticky-section 吸顶布局容器,仅支持作为 `` 的直接子节点 +可直接作为基础标签使用,也可在 `usingComponents` 中注册: + +```json +{ + "usingComponents": { + "sticky-section": "@mpxjs/webpack-plugin/lib/runtime/components/extends/sticky-section" + } +} +``` + #### 注意事项 - sticky-section 目前仅支持 RN、web 以及微信小程序环境,其他环境暂不支持。微信小程序中使用需开启 skyline 渲染模式 @@ -1354,6 +1429,16 @@ level 有效值: 吸顶布局容器,仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 +可直接作为基础标签使用,也可在 `usingComponents` 中注册: + +```json +{ + "usingComponents": { + "sticky-header": "@mpxjs/webpack-plugin/lib/runtime/components/extends/sticky-header" + } +} +``` + #### 属性 | 属性名 | 类型 | 默认值 | 说明 | diff --git a/docs-vitepress/api/compile.md b/docs-vitepress/api/compile.md index 4ba857f4f9..253f0dfd7f 100644 --- a/docs-vitepress/api/compile.md +++ b/docs-vitepress/api/compile.md @@ -922,6 +922,7 @@ module.exports = defineConfig({ - 页面/组件 **主模版** 以及 **子模版**(如通过 import 引入的模版)均会应用本配置。 - **key / value** 及路径书写要求与 Web 一节一致,此处不再重复;详见 [webConfig.customBuiltInComponents](#webconfig-custombuiltincomponents)。 +- 自定义基础组件如果最终渲染到 React Native 原生组件,需要先消费或映射自身支持的属性、事件,再过滤已处理的小程序属性以及 Mpx 内部辅助属性,避免它们继续透传到原生节点上。例如 `enable-var`、`enable-offset`、`enable-background`、`external-var-context`、`parent-font-size`、`parent-width`、`parent-height`、`enable-text-pass-through` 以及组件已处理的 `bind*` / `catch*` 事件属性。 #### rnConfig.loadChunkAsync(运行时) diff --git a/docs-vitepress/guide/extend/extend-component.md b/docs-vitepress/guide/extend/extend-component.md new file mode 100644 index 0000000000..de8b9c332b --- /dev/null +++ b/docs-vitepress/guide/extend/extend-component.md @@ -0,0 +1,253 @@ +# Mpx 扩展组件 + +除基础组件外,Mpx 额外提供一些扩展组件。扩展组件需要在页面或组件的 `usingComponents` 中注册后使用。 + +```html + +``` + +Mpx 会根据当前编译的目标平台(wx/ali/web/ios/android/harmony),自动解析到对应平台的扩展组件实现。 + + +## section-list + +跨端虚拟列表组件,可自定义分组头、列表头、列表项,自动分段渲染兼容各端。 + +支持平台:微信小程序、支付宝小程序、Web、RN + +### 属性 + +| 属性名 | 类型 | 默认值 | 说明 | 支持平台 | +|-----------------------|-------------|----------|------------------------|-----------| +| height | String/Number | 100% | 组件高度 | 微信小程序、支付宝小程序、Web、RN | +| width | String/Number | 100% | 组件宽度 | 微信小程序、支付宝小程序、Web、RN | +| listData | Array | [] | 列表数据,如需使用列表分组头 `section-header`,对应 item 的数据需要包含 `isSectionHeader: true` 标识 | 微信小程序、支付宝小程序、Web、RN | +| enable-sticky | Boolean | false | 启用分组吸顶 | 微信小程序、支付宝小程序、Web、RN
⚠️微信小程序环境,需要使用 skyline 渲染模式,webview 模式不支持;web 环境仅支持移动端,不支持 pc 端 | +| scroll-with-animation | Boolean | false | 滚动动画 | 微信小程序、支付宝小程序、Web、RN | +| useListHeader | Boolean | false | 使用自定义列表头 | 微信小程序、支付宝小程序、Web、RN | +| listHeaderData | Object | {} | 列表头数据 | 微信小程序、支付宝小程序、Web、RN | +| useListFooter | Boolean | false | 使用自定义列表页脚 | 微信小程序、支付宝小程序、Web、RN | +| listFooterData | Object | {} | 列表头数据 | 微信小程序、支付宝小程序、Web、RN | +| generic:recycle-item | String | | 列表项,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | +| generic:section-header | String | | 列表分组头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | +| generic:list-header | String | | 列表头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | +| generic:list-footer | String | | 列表页脚,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | +| itemHeight | Object | {} | 列表项高度配置(支持 getter/value),必须配置 | 微信小程序、支付宝小程序、Web、RN | +| sectionHeaderHeight | Object | {} | 分组头部高度配置(getter/value),若使用了自定义分组头必须配置 | 微信小程序、支付宝小程序、Web、RN | +| listHeaderHeight | Object | {} | 列表头部高度配置(getter/value),若使用了列表头必须配置 | 微信小程序、支付宝小程序、Web、RN | +| bufferScale | Number | 1 | 渲染缓冲区行数(虚拟滚动优化) | 仅支付宝小程序/web支持 | +| minRenderCount | Number | 10 | 最小渲染项目数 | 仅支付宝小程序/web支持 | + +#### `itemHeight`/`sectionHeaderHeight`/`listHeaderHeight` 格式说明 + +高度相关属性支持如下格式: + +```js +height: { + value: 400, // 定高 + getter: function (item, index) { + const seed = item.id % 2 || 0 + const heights = [100, 300] + return heights[seed] + } +} +``` + +**说明:** +- `value`:默认高度(所有项相同高度时直接用 value 即可)。 +- `getter`:函数形式,可接收每一项的数据和索引,按需返回不同高度(动态高度需求时使用)。 +- `getter` 优先级 大于 value。 + +> 建议性能要求较高(如超大数据集)优先使用 `value` 定高。 + +### 事件 + +| 事件名 | 说明 | 支持平台 | +|-----------------------|-----------------------------------|--------------| +| bindscroll | 滚动时触发,返回滚动信息 | 微信小程序、支付宝小程序、Web、RN | +| bindscrolltolower | 滚动到底部/触底通知 | 微信小程序、支付宝小程序、Web、RN | +| bindscrollToIndex | 组件方法,滚动到指定索引 | 微信小程序、支付宝小程序、Web、RN | + +`scrollToIndex({ index, animated, viewPosition })` 参数说明: +- `index`:目标索引 +- `animated`:是否滚动动画 +- `viewOffset`:滚动偏移量 +- `viewPosition`:滚动定位,0:顶部, 0.5:中间, 1:底部 + +### 用法示例 + +```js + + + +``` + +### 其它说明 + +- 当使用了列表项、列表头或者自定义分组头,必须配置对应 item/sectionHeader/listHeader 的 height 相关参数,否则会出现滚动异常情况。 +- 可直接调用 ref 实例执行 `scrollToIndex` 方法实现滚动。 +- 如果用户滑动的速度超过渲染的速度,则会先看到空白的内容,这是为了长列表优化不得不作出的妥协。 +- 当某行滑出渲染区域之外后,其内部状态将不会保留。 +- 在 RN 环境,section-list 通过 RN 提供的 SectionList 实现分组吸顶。受 RN 底层实现机制限制,开启 `enable-sticky` 且快速滑动时,自定义分组头有时会出现闪烁现象。此问题需要等待 RN 官方修复,我们会持续关注并跟进。 +- 若某行需要使用 `section-header` 对应的抽象节点渲染,则该行数据必须包含 `isSectionHeader: true` 字段;否则默认使用 `recycle-item` 对应的抽象节点渲染 + + +## sticky-section + +吸顶布局容器,仅支持作为 `` 的直接子节点 + +支持平台:微信小程序(仅 skyline 支持)、支付宝小程序、Web、RN + +### 用法示例 + +```html + + + +``` + + +## sticky-header + +吸顶头部组件,支持在滚动容器中实现元素吸顶效果。仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 + +支持平台:微信小程序(仅 skyline 支持)、支付宝小程序、Web、RN + +### 属性 + +| 属性名 | 类型 | 默认值 | 说明 | 支持平台 | +|-------|------|--------|------|---------| +| offsetTop | Number | 0 | 吸顶距离顶部的偏移量 | 微信小程序、支付宝小程序、Web、RN | +| padding | Array | - | 内边距配置 [top, right, bottom, left] | 微信小程序、支付宝小程序、Web、RN | +| scrollViewId | String | '' | 滚动容器的 id, 支付宝环境必传, 值与选择器 id 值一致 | 支付宝小程序 | +| stickyId | String | '' | 吸顶元素的唯一标识,支付宝环境必传,值与选择器 id 值一致 | 支付宝小程序 | +| enablePolling | Boolean | false | 启用轮询刷新 | 支付宝小程序 | +| pollingDuration | Number | 300 | 轮询间隔时间(毫秒) | 支付宝小程序 | + +### 事件 + +| 事件名 | 说明 | 支持平台 | +|-------|------|---------| +| stickontopchange | 吸顶状态改变时触发,返回 { isStickOnTop, id } | 微信小程序、支付宝小程序、Web、RN | + +**注意**: +- 支付宝小程序中该功能基于 IntersectionObserver 实现,但在支付宝平台上,IntersectionObserver 的回调可能存在触发不及时或不触发的情况,进而导致 stickontopchange 事件无法及时触发,或 sticky-header 吸附位置异常。 + +为此我们提供了 enablePolling 属性。开启后将通过定时轮询的方式校验 sticky-header 当前吸附状态是否正确,若发现异常会自动进行修正。建议在支付宝平台根据实际情况按需开启该配置。 + +- RN 环境的 sticky-header 更适用于内容稳定,状态不常变更的场景使用,目前如果 sticky-header 还在动画过程中就触发组件更新(如在bindstickontopchange 回调中立刻更新 state)、scroll-view 内容高度由多变少、通过修改 scroll-into-view、scroll-top 让 scroll-view 滚动,以上场景在安卓上都可能会导致闪烁或抖动 + + +### 用法示例 + +```html + + + +``` \ No newline at end of file diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index 9f726bfe32..eb37604337 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -5,7 +5,7 @@ ### 目录概览 {#directory-overview} - #### 基础组件 -**容器组件**:[view](#view) · [scroll-view](#scroll-view) · [swiper](#swiper) · [swiper-item](#swiper-item) · [movable-area](#movable-area) · [movable-view](#movable-view) · [root-portal](#root-portal) · [sticky-section](#sticky-section) · [sticky-header](#sticky-header) · [cover-view](#cover-view) +**容器组件**:[view](#view) · [scroll-view](#scroll-view) · [swiper](#swiper) · [swiper-item](#swiper-item) · [movable-area](#movable-area) · [movable-view](#movable-view) · [root-portal](#root-portal) · [section-list](#section-list) · [cover-view](#cover-view) **媒体组件**:[image](#image) · [video](#video) · [canvas](#canvas) @@ -732,33 +732,66 @@ API > > - style 样式不支持中使用百分比计算、css variable -### sticky-section -吸顶布局容器,仅支持作为 `` 的直接子节点 +### section-list +RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。使用前需要在 `usingComponents` 中注册: -> [!tip] 注意 -> -> - sticky-section 目前仅支持 RN 、web 以及微信小程序环境,其他环境暂不支持。微信小程序中使用需开启 skyline 渲染模式 - -### sticky-header -吸顶布局容器,仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 +```json +{ + "usingComponents": { + "section-list": "@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list" + } +} +``` 属性 -| 属性名 | 类型 | 默认值 | 说明 | -| ----------------------- | ------- | ------------- | ---------------------------------------------------------- | -| offset-top | number | `0` | 吸顶时与顶部的距离 | -| padding | array | `[0, 0, 0, 0] ` | 长度为 4 的数组,按 top、right、bottom、left 顺序指定内边距 | +| 属性名 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| height | string/number | | 组件高度 | +| width | string/number | | 组件宽度 | +| listData | array | | 列表数据,分组头数据需包含 `isSectionHeader: true` | +| generic:recycle-item | string | | 列表项抽象节点组件名 | +| generic:section-header | string | | 分组头抽象节点组件名 | +| generic:list-header | string | | 列表头抽象节点组件名 | +| generic:list-footer | string | | 列表尾抽象节点组件名 | +| itemHeight | object | | 列表项高度配置,支持 `value` 或 `getter(item, index)` | +| sectionHeaderHeight | object | | 分组头高度配置,支持 `value` 或 `getter(item, index)` | +| listHeaderHeight | object | | 列表头高度配置,支持 `value` 或 `getter()` | +| useListHeader | boolean | `false` | 是否渲染列表头 | +| listHeaderData | object | | 列表头数据 | +| useListFooter | boolean | `false` | 是否渲染列表尾 | +| listFooterData | object | | 列表尾数据 | +| enable-sticky | boolean | `false` | 是否开启分组头吸顶 | +| enhanced | boolean | `false` | 是否开启增强能力 | +| bounces | boolean | `true` | iOS 边界弹性控制,开启 `enhanced` 后生效 | +| enable-back-to-top | boolean | `false` | 点击状态栏回到顶部,仅 iOS 支持 | +| end-reached-threshold | number | `0.1` | 触底事件触发阈值 | +| refresher-enabled | boolean | `false` | 是否开启下拉刷新 | +| refresher-triggered | boolean | `false` | 当前下拉刷新状态 | +| show-scrollbar | boolean | `true` | 是否显示滚动条 | +| scroll-event-throttle | number | `0` | scroll 事件触发频率 | +| simultaneous-handlers | array\ | `[]` | 允许多个外部手势同时识别 | +| wait-for | array\ | `[]` | 等待外部手势失败后再识别 | 事件 -| 事件名 | 说明 | -| ----------------| --------------------------------------------------- | -| bindstickontopchange | 吸顶状态变化事件, `event.detail = { isStickOnTop }`,当 sticky-header 吸顶时为 true,否则为 false | +| 事件名 | 说明 | +| --- | --- | +| bindscroll | 滚动时触发 | +| bindscrolltolower | 滚动到底部时触发 | +| bindrefresherrefresh | 自定义下拉刷新被触发 | + +方法 + +| 方法名 | 说明 | +| --- | --- | +| scrollToIndex({ index, animated, viewOffset, viewPosition }) | 滚动到指定原始索引 | > [!tip] 注意 > -> - sticky-header 目前仅支持 RN 、web 以及微信小程序环境,其他环境暂不支持。微信小程序中使用需开启 skyline 渲染模式 -> - RN 环境的 sticky-header 更适用于内容稳定,状态不常变更的场景使用,目前如果 sticky 还在动画过程中就触发组件更新(如在bindstickontopchange 回调中立刻更新 state)、scroll-view 内容高度由多变少、通过修改 scroll-into-view、scroll-top 让 scroll-view 滚动,以上场景在安卓上都可能会导致闪烁或抖动 +> - `generic:*` 指向的抽象节点组件也需要在当前页面或组件的 `usingComponents` 中注册。 +> - 使用 `itemHeight`、`sectionHeaderHeight`、`listHeaderHeight` 提供稳定高度,可减少滚动定位异常。 +> - 开启 `enable-sticky` 且快速滑动时,自定义分组头可能出现闪烁,这是 RN `SectionList` 底层机制限制。 ### cover-view 视图容器。 @@ -843,4 +876,4 @@ Mpx 完全支持自定义组件功能,组件创建、属性配置、生命周 } }) -``` \ No newline at end of file +``` diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index f287cdeeaf..899c5c8edc 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -30,6 +30,7 @@ const AddEnvPlugin = require('./resolver/AddEnvPlugin') const PackageEntryPlugin = require('./resolver/PackageEntryPlugin') const DynamicRuntimePlugin = require('./resolver/DynamicRuntimePlugin') const FixDescriptionInfoPlugin = require('./resolver/FixDescriptionInfoPlugin') +const ExtendComponentsPlugin = require('./resolver/ExtendComponentsPlugin') // const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency') // const HarmonyImportSideEffectDependency = require('webpack/lib/dependencies/HarmonyImportSideEffectDependency') // const RequireHeaderDependency = require('webpack/lib/dependencies/RequireHeaderDependency') @@ -404,6 +405,7 @@ class MpxWebpackPlugin { const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file') const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, this.options.normalNpmPackages, 'file') const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules) + const extendComponentsPlugin = new ExtendComponentsPlugin('described-resolve', this.options.mode, 'resolve') if (Array.isArray(compiler.options.resolve.plugins)) { compiler.options.resolve.plugins.push(addModePlugin) @@ -418,6 +420,7 @@ class MpxWebpackPlugin { } compiler.options.resolve.plugins.push(packageEntryPlugin) compiler.options.resolve.plugins.push(new FixDescriptionInfoPlugin()) + compiler.options.resolve.plugins.push(extendComponentsPlugin) compiler.options.resolve.plugins.push(dynamicPlugin) const optimization = compiler.options.optimization diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js index b5a65229fb..32e044fefc 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js @@ -42,8 +42,6 @@ const wxs = require('./wxs') const fixComponentName = require('./fix-component-name') const customBuiltInComponent = require('./custom-built-in-component') const rootPortal = require('./root-portal') -const stickyHeader = require('./sticky-header') -const stickySection = require('./sticky-section') /** * 未命中上方任一组件 test 的标签,仍须走 normalizeComponentRules 中的通用 @@ -142,8 +140,6 @@ module.exports = function getComponentConfigs ({ warn, error }) { hyphenTagName({ print }), label({ print }), rootPortal({ print }), - stickyHeader({ print }), - stickySection({ print }), defaultCatchAllComponentConfig() ] } diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js deleted file mode 100644 index e698892a6a..0000000000 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js +++ /dev/null @@ -1,23 +0,0 @@ -const TAG_NAME = 'sticky-header' - -module.exports = function ({ print }) { - return { - test: TAG_NAME, - android (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-header' - }, - ios (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-header' - }, - harmony (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-header' - }, - web (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-header' - } - } -} diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js deleted file mode 100644 index 150dd6122c..0000000000 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js +++ /dev/null @@ -1,23 +0,0 @@ -const TAG_NAME = 'sticky-section' - -module.exports = function ({ print }) { - return { - test: TAG_NAME, - android (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-section' - }, - ios (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-section' - }, - harmony (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-section' - }, - web (tag, { el }) { - el.isBuiltIn = true - return 'mpx-sticky-section' - } - } -} diff --git a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js new file mode 100644 index 0000000000..1e58225e81 --- /dev/null +++ b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js @@ -0,0 +1,60 @@ +const { EXTEND_COMPONENT_CONFIG } = require('../utils/const') + +/** + * 扩展组件路径解析插件 + * 将 @mpxjs/webpack-plugin/lib/runtime/components/extends/[component-name] 格式的路径 + * 解析为对应平台的实际组件路径 + */ +module.exports = class ExtendComponentsPlugin { + constructor (source, mode, target) { + this.source = source + this.target = target + this.mode = mode + } + + apply (resolver) { + const target = resolver.ensureHook(this.target) + const mode = this.mode + + resolver.getHook(this.source).tapAsync('ExtendComponentsPlugin', (request, resolveContext, callback) => { + const requestPath = request.request + if (!requestPath || !requestPath.startsWith('@mpxjs/webpack-plugin/lib/runtime/components/extends/')) { + return callback() + } + + // 匹配 @mpxjs/webpack-plugin/lib/runtime/components/extends/[component-name] + const extendsMatch = requestPath.match(/^@mpxjs\/webpack-plugin\/lib\/runtime\/components\/extends\/(.+)$/) + + if (!extendsMatch) { + return callback() + } + + const componentName = extendsMatch[1] + + // 检查组件是否在配置中 + if (!EXTEND_COMPONENT_CONFIG[componentName]) { + return callback(new Error(`Extended component "${componentName}" was not found. Available extended components: ${Object.keys(EXTEND_COMPONENT_CONFIG).join(', ')}`)) + } + + // 获取当前模式下的组件路径 + const componentConfig = EXTEND_COMPONENT_CONFIG[componentName] + const newRequest = componentConfig[mode] + + if (!newRequest) { + return callback(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${Object.keys(componentConfig).join(', ')}`)) + } + + const obj = Object.assign({}, request, { + request: newRequest + }) + + resolver.doResolve( + target, + obj, + `resolve extend component: ${componentName} to ${newRequest}`, + resolveContext, + callback + ) + }) + } +} diff --git a/packages/webpack-plugin/lib/runtime/components/ali/mpx-section-list.mpx b/packages/webpack-plugin/lib/runtime/components/ali/mpx-section-list.mpx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-header.mpx b/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-header.mpx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-section.mpx b/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-section.mpx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx b/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx new file mode 100644 index 0000000000..549d19aba8 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx @@ -0,0 +1 @@ + diff --git a/packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx b/packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx new file mode 100644 index 0000000000..fa855fa0f3 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx @@ -0,0 +1 @@ + diff --git a/packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx b/packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx new file mode 100644 index 0000000000..1c1842aa59 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx @@ -0,0 +1 @@ + diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx new file mode 100644 index 0000000000..3c166d9e08 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -0,0 +1,475 @@ +import { forwardRef, useRef, useState, useEffect, useMemo, createElement, useImperativeHandle, memo } from 'react' +import type { ComponentType } from 'react' +import { SectionList, RefreshControl, NativeSyntheticEvent, NativeScrollEvent } from 'react-native' +import type { SectionListData, SectionListProps as RNSectionListProps } from 'react-native' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import useInnerProps, { getCustomEvent } from './getInnerListeners' +import { extendObject, useLayout, useTransformStyle, GestureHandler, flatGesture } from './utils' +interface ListItem { + isSectionHeader?: boolean; + _originalItemIndex?: number; + [key: string]: any; +} + +interface SectionExtra { + headerData: ListItem | null; + hasSectionHeader?: boolean; + _originalItemIndex?: number; +} + +interface Section extends SectionExtra { + data: ListItem[]; +} + +type RNSection = SectionListData + +const TypedSectionList = SectionList as unknown as ComponentType> + +interface ItemHeightType { + value?: number; + getter?: (item: any, index: number) => number; +} + +interface MpxSectionListProps { + enhanced?: boolean; + bounces?: boolean; + scrollEventThrottle?: number; + height?: number | string; + width?: number | string; + listData?: ListItem[]; + generichash?: string; + style?: Record; + itemHeight?: ItemHeightType; + sectionHeaderHeight?: ItemHeightType; + listHeaderData?: any; + listHeaderHeight?: ItemHeightType; + useListHeader?: boolean; + listFooterData?: any; + useListFooter?: boolean; + 'genericrecycle-item'?: string; + 'genericsection-header'?: string; + 'genericlist-header'?: string; + 'genericlist-footer'?: string; + 'enable-var'?: boolean; + 'external-var-context'?: any; + 'parent-font-size'?: number; + 'parent-width'?: number; + 'parent-height'?: number; + 'enable-sticky'?: boolean; + 'enable-back-to-top'?: boolean; + 'end-reached-threshold'?: number; + 'refresher-enabled'?: boolean; + 'show-scrollbar'?: boolean; + 'refresher-triggered'?: boolean; + 'wait-for'?: Array; + 'simultaneous-handlers'?: Array; + bindrefresherrefresh?: (event: any) => void; + bindscrolltolower?: (event: any) => void; + bindscroll?: (event: any) => void; + [key: string]: any; +} + +interface ScrollPositionParams { + index: number; + animated?: boolean; + viewOffset?: number; + viewPosition?: number; +} + +const getGeneric = (generichash: string, generickey: string) => { + if (!generichash || !generickey) return null + const GenericComponent = global.__mpxGenericsMap?.[generichash]?.[generickey]?.() + if (!GenericComponent) return null + + return memo(forwardRef((props: any, ref: any) => { + return createElement(GenericComponent, extendObject({}, { + ref: ref + }, props)) + })) +} + +const _SectionList = forwardRef((props = {}, ref) => { + const { + enhanced = false, + bounces = true, + scrollEventThrottle = 0, + height, + width, + listData, + generichash, + style = {}, + itemHeight = {}, + sectionHeaderHeight = {}, + listHeaderHeight = {}, + listHeaderData = null, + useListHeader = false, + listFooterData = null, + useListFooter = false, + 'genericrecycle-item': genericrecycleItem, + 'genericsection-header': genericsectionHeader, + 'genericlist-header': genericListHeader, + 'genericlist-footer': genericListFooter, + 'enable-var': enableVar, + 'external-var-context': externalVarContext, + 'parent-font-size': parentFontSize, + 'parent-width': parentWidth, + 'parent-height': parentHeight, + 'enable-sticky': enableSticky = false, + 'enable-back-to-top': enableBackToTop = false, + 'end-reached-threshold': onEndReachedThreshold = 0.1, + 'refresher-enabled': refresherEnabled, + 'show-scrollbar': showScrollbar = true, + 'refresher-triggered': refresherTriggered, + 'simultaneous-handlers': originSimultaneousHandlers, + 'wait-for': waitFor + } = props + + const [refreshing, setRefreshing] = useState(!!refresherTriggered) + + const scrollViewRef = useRef(null) + const sectionListGestureRef = useRef() + + const indexMap = useRef<{ [key: string]: string | number }>({}) + + const reverseIndexMap = useRef<{ [key: string]: number }>({}) + + const { + hasSelfPercent, + setWidth, + setHeight + } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }) + + const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef }) + + useEffect(() => { + if (refreshing !== refresherTriggered) { + setRefreshing(!!refresherTriggered) + } + }, [refresherTriggered]) + + const onRefresh = () => { + const { bindrefresherrefresh } = props + bindrefresherrefresh && + bindrefresherrefresh( + getCustomEvent('refresherrefresh', {}, { layoutRef }, props) + ) + } + + const onEndReached = () => { + const { bindscrolltolower } = props + bindscrolltolower && + bindscrolltolower( + getCustomEvent('scrolltolower', {}, { layoutRef }, props) + ) + } + + const onScroll = (event: NativeSyntheticEvent) => { + const { bindscroll } = props + bindscroll && + bindscroll( + getCustomEvent('scroll', event.nativeEvent, { layoutRef }, props) + ) + } + + // 通过sectionIndex和rowIndex获取原始索引 + const getOriginalIndex = (sectionIndex: number, rowIndex: number | 'header'): number => { + const key = `${sectionIndex}_${rowIndex}` + return reverseIndexMap.current[key] ?? -1 // 如果找不到,返回-1 + } + + const scrollToIndex = ({ index, animated, viewOffset = 0, viewPosition = 0 }: ScrollPositionParams) => { + if (scrollViewRef.current) { + // 通过索引映射表快速定位位置 + const position = indexMap.current[index] + const [sectionIndex, itemIndex] = (position as string).split('_') + scrollViewRef.current.scrollToLocation?.({ + itemIndex: itemIndex === 'header' ? 0 : Number(itemIndex) + 1, + sectionIndex: Number(sectionIndex) || 0, + animated, + viewOffset, + viewPosition + }) + } + } + + const getItemHeight = ({ sectionIndex, rowIndex }: { sectionIndex: number, rowIndex: number }) => { + if (!itemHeight) { + return 0 + } + if ((itemHeight as ItemHeightType).getter) { + const item = convertedListData[sectionIndex].data[rowIndex] + // 使用getOriginalIndex获取原始索引 + const originalIndex = getOriginalIndex(sectionIndex, rowIndex) + return (itemHeight as ItemHeightType).getter?.(item, originalIndex) || 0 + } else { + return (itemHeight as ItemHeightType).value || 0 + } + } + + const getSectionHeaderHeight = ({ sectionIndex }: { sectionIndex: number }) => { + const item = convertedListData[sectionIndex] + const { hasSectionHeader } = item + // 使用getOriginalIndex获取原始索引 + const originalIndex = getOriginalIndex(sectionIndex, 'header') + if (!hasSectionHeader) return 0 + if ((sectionHeaderHeight as ItemHeightType).getter) { + return (sectionHeaderHeight as ItemHeightType).getter?.(item, originalIndex) || 0 + } else { + return (sectionHeaderHeight as ItemHeightType).value || 0 + } + } + + const convertedListData = useMemo(() => { + const sections: Section[] = [] + let currentSection: Section | null = null + // 清空之前的索引映射 + indexMap.current = {} + // 清空反向索引映射 + reverseIndexMap.current = {} + + // 处理 listData 为空的情况 + if (!listData || !listData.length) { + return sections + } + + listData.forEach((item: ListItem, index: number) => { + if (item.isSectionHeader) { + // 如果已经存在一个 section,先把它添加到 sections 中 + if (currentSection) { + sections.push(currentSection) + } + // 创建新的 section + currentSection = { + headerData: item, + data: [], + hasSectionHeader: true, + _originalItemIndex: index + } + // 为 section header 添加索引映射 + const sectionIndex = sections.length + indexMap.current[index] = `${sectionIndex}_header` + // 添加反向索引映射 + reverseIndexMap.current[`${sectionIndex}_header`] = index + } else { + // 如果没有当前 section,创建一个默认的 + if (!currentSection) { + // 创建默认section (无header的section) + currentSection = { + headerData: null, + data: [], + hasSectionHeader: false, + _originalItemIndex: -1 + } + } + // 将 item 添加到当前 section 的 data 中 + const itemIndex = currentSection.data.length + currentSection.data.push(extendObject({}, item, { + _originalItemIndex: index + })) + let sectionIndex + // 为 item 添加索引映射 - 存储格式为: "sectionIndex_itemIndex" + if (!currentSection.hasSectionHeader && sections.length === 0) { + // 在默认section中(第一个且无header) + sectionIndex = 0 + indexMap.current[index] = `${sectionIndex}_${itemIndex}` + } else { + // 在普通section中 + sectionIndex = sections.length + indexMap.current[index] = `${sectionIndex}_${itemIndex}` + } + // 添加反向索引映射 + reverseIndexMap.current[`${sectionIndex}_${itemIndex}`] = index + } + }) + // 添加最后一个 section + if (currentSection) { + sections.push(currentSection) + } + return sections + }, [listData]) + + const { getItemLayout } = useMemo(() => { + const layouts: Array<{ length: number, offset: number, index: number }> = [] + let offset = 0 + + if (useListHeader) { + // 计算列表头部的高度 + offset += listHeaderHeight.getter?.() || listHeaderHeight.value || 0 + } + + // 遍历所有 sections + convertedListData.forEach((section: Section, sectionIndex: number) => { + // 添加 section header 的位置信息 + const headerHeight = getSectionHeaderHeight({ sectionIndex }) + layouts.push({ + length: headerHeight, + offset, + index: layouts.length + }) + offset += headerHeight + + // 添加该 section 中所有 items 的位置信息 + section.data.forEach((item: ListItem, itemIndex: number) => { + const contentHeight = getItemHeight({ sectionIndex, rowIndex: itemIndex }) + layouts.push({ + length: contentHeight, + offset, + index: layouts.length + }) + offset += contentHeight + }) + + // 添加该 section 尾部位置信息 + // 因为即使 sectionList 没传 renderSectionFooter,getItemLayout 中的 index 的计算也会包含尾部节点 + layouts.push({ + length: 0, + offset, + index: layouts.length + }) + }) + return { + itemLayouts: layouts, + getItemLayout: (data: any, index: number) => layouts[index] + } + }, [convertedListData, useListHeader, itemHeight.value, itemHeight.getter, sectionHeaderHeight.value, sectionHeaderHeight.getter, listHeaderHeight.value, listHeaderHeight.getter]) + + const scrollAdditionalProps = extendObject( + { + alwaysBounceVertical: false, + alwaysBounceHorizontal: false, + scrollEventThrottle: scrollEventThrottle, + scrollsToTop: enableBackToTop, + showsHorizontalScrollIndicator: showScrollbar, + onEndReachedThreshold, + ref: scrollViewRef, + bounces: false, + stickySectionHeadersEnabled: enableSticky, + onScroll: onScroll, + onEndReached: onEndReached + }, + layoutProps + ) + + const nativeGesture = useMemo(() => { + const simultaneousHandlers = flatGesture(originSimultaneousHandlers) + const waitForHandlers = flatGesture(waitFor) + const gesture = Gesture.Native().withRef(sectionListGestureRef as any) + if (simultaneousHandlers && simultaneousHandlers.length) { + gesture.simultaneousWithExternalGesture(...simultaneousHandlers) + } + if (waitForHandlers && waitForHandlers.length) { + gesture.requireExternalGestureToFail(...waitForHandlers) + } + return gesture + }, [originSimultaneousHandlers, waitFor]) + + if (enhanced) { + extendObject(scrollAdditionalProps, { + bounces + }) + } + if (refresherEnabled) { + extendObject(scrollAdditionalProps, { + refreshing: refreshing + }) + } + + useImperativeHandle(ref, () => { + return extendObject({}, props, { + gestureRef: sectionListGestureRef, + scrollToIndex + }) + }) + + const innerProps = useInnerProps(extendObject({}, props, scrollAdditionalProps), [ + 'id', + 'show-scrollbar', + 'lower-threshold', + 'refresher-triggered', + 'refresher-enabled', + 'bindrefresherrefresh', + 'simultaneous-handlers', + 'wait-for' + ], { layoutRef }) + + // 使用 ref 保存最新的数据,避免数据变化时组件销毁重建 + const listHeaderDataRef = useRef(listHeaderData) + listHeaderDataRef.current = listHeaderData + + const listFooterDataRef = useRef(listFooterData) + listFooterDataRef.current = listFooterData + + // 使用 useMemo 获取 GenericComponent 并创建渲染函数,避免每次组件更新都重新创建函数引用导致不必要的重新渲染 + const renderItem = useMemo( + () => { + const ItemComponent = getGeneric(generichash, genericrecycleItem) + if (!ItemComponent) return undefined + return ({ item }: { item: ListItem }) => createElement(ItemComponent, { itemData: item }) + }, + [generichash, genericrecycleItem] + ) + + const renderSectionHeader = useMemo( + () => { + const SectionHeaderComponent = getGeneric(generichash, genericsectionHeader) + if (!SectionHeaderComponent) return undefined + return (sectionData: { section: RNSection }) => { + if (!sectionData.section.hasSectionHeader) return null + return createElement(SectionHeaderComponent, { itemData: sectionData.section.headerData }) + } + }, + [generichash, genericsectionHeader] + ) + + const ListHeaderComponent = useMemo( + () => { + if (!useListHeader) return null + const ListHeaderGenericComponent = getGeneric(generichash, genericListHeader) + if (!ListHeaderGenericComponent) return null + return () => createElement(ListHeaderGenericComponent, { listHeaderData: listHeaderDataRef.current }) + }, + [useListHeader, generichash, genericListHeader] + ) + + const ListFooterComponent = useMemo( + () => { + if (!useListFooter) return null + const ListFooterGenericComponent = getGeneric(generichash, genericListFooter) + if (!ListFooterGenericComponent) return null + return () => createElement(ListFooterGenericComponent, { listFooterData: listFooterDataRef.current }) + }, + [useListFooter, generichash, genericListFooter] + ) + + const sectionListProps: RNSectionListProps = extendObject( + { + style: [{ height, width }, style, layoutStyle], + sections: convertedListData, + renderItem: renderItem, + getItemLayout: getItemLayout, + ListHeaderComponent: useListHeader ? ListHeaderComponent : null, + ListFooterComponent: useListFooter ? ListFooterComponent : null, + renderSectionHeader: renderSectionHeader, + refreshControl: refresherEnabled + ? createElement(RefreshControl, { + onRefresh: onRefresh, + refreshing: refreshing + }) + : undefined + }, + innerProps + ) + + return createElement( + GestureDetector, + { gesture: nativeGesture }, + createElement( + TypedSectionList, + sectionListProps + ) + ) +}) + +_SectionList.displayName = 'MpxSectionList' + +export default _SectionList diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-section-list.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-section-list.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-header.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-header.mpx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-section.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-section.mpx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js index a7001e56d8..0c6fa222c2 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -1,7 +1,7 @@ const JSON5 = require('json5') const he = require('he') const config = require('../config') -const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID, MPX_TAG_PAGE_SELECTOR, MPX_TEMPLATE_COMPONENT_PREFIX, STYLE_PAD_PLACEHOLDER } = require('../utils/const') +const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID, MPX_TAG_PAGE_SELECTOR, EXTEND_COMPONENT_CONFIG, MPX_TEMPLATE_COMPONENT_PREFIX, STYLE_PAD_PLACEHOLDER } = require('../utils/const') const normalize = require('../utils/normalize') const { normalizeCondition } = require('../utils/match-condition') const isValidIdentifierStr = require('../utils/is-valid-identifier-str') @@ -2465,7 +2465,11 @@ function isComponentNode (el) { // 处理模版时无法获取真实的usingComponents信息,除了小程序基础组件和框架内建组件外都识别为用户组件 return isRealNode(el) && !isNativeMiniTag(el.tag) && !el.isBuiltIn } - return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' || componentGenerics[el.tag] + return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' || componentGenerics[el.tag] || isExtendComponentNode(el) +} + +function isExtendComponentNode (el) { + return EXTEND_COMPONENT_CONFIG[el.tag]?.[mode] } function getComponentInfo (el) { @@ -2473,7 +2477,7 @@ function getComponentInfo (el) { } function isReactComponent (el) { - return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn + return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn && !isExtendComponentNode(el) } function processWebClass (classLikeAttrName, classLikeAttrValue, el, options, processingWebTemplate) { diff --git a/packages/webpack-plugin/lib/utils/const.js b/packages/webpack-plugin/lib/utils/const.js index 86f414d5a1..0b286d9b48 100644 --- a/packages/webpack-plugin/lib/utils/const.js +++ b/packages/webpack-plugin/lib/utils/const.js @@ -1,3 +1,6 @@ +const componentPrefixPath = '@mpxjs/webpack-plugin/lib/runtime/components' +const reactComponentPath = `${componentPrefixPath}/react/dist` + module.exports = { MPX_PROCESSED_FLAG: 'mpx_processed', MPX_DISABLE_EXTRACTOR_CACHE: 'mpx_disable_extractor_cache', @@ -7,6 +10,25 @@ module.exports = { MPX_ROOT_VIEW: 'mpx-root-view', // 根节点类名 MPX_APP_MODULE_ID: 'mpx-app-scope', // app文件moduleId PARENT_MODULE_ID: '__pid', + EXTEND_COMPONENT_CONFIG: { + 'section-list': { + ios: `${reactComponentPath}/mpx-section-list.jsx`, + android: `${reactComponentPath}/mpx-section-list.jsx`, + harmony: `${reactComponentPath}/mpx-section-list.jsx` + }, + 'sticky-header': { + web: `${componentPrefixPath}/web/mpx-sticky-header.vue`, + ios: `${reactComponentPath}/mpx-sticky-header.jsx`, + android: `${reactComponentPath}/mpx-sticky-header.jsx`, + harmony: `${reactComponentPath}/mpx-sticky-header.jsx` + }, + 'sticky-section': { + web: `${componentPrefixPath}/web/mpx-sticky-section.vue`, + ios: `${reactComponentPath}/mpx-sticky-section.jsx`, + android: `${reactComponentPath}/mpx-sticky-section.jsx`, + harmony: `${reactComponentPath}/mpx-sticky-section.jsx` + } + }, MPX_TAG_PAGE_SELECTOR: 'mpx-page', // web / template is:具名 wx 模版子组件标签前缀(与 compiler 中 AST 替换一致) MPX_TEMPLATE_COMPONENT_PREFIX: 'mpx-tpl-', diff --git a/packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js b/packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js new file mode 100644 index 0000000000..0dd427c0ae --- /dev/null +++ b/packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js @@ -0,0 +1,59 @@ +const ExtendComponentsPlugin = require('../../lib/resolver/ExtendComponentsPlugin') + +function resolveExtendComponent ({ mode, request }) { + const plugin = new ExtendComponentsPlugin('source', mode, 'target') + let handler + const resolver = { + ensureHook: jest.fn((hook) => hook), + getHook: jest.fn(() => ({ + tapAsync: (name, fn) => { + handler = fn + } + })), + doResolve: jest.fn((target, resolveRequest, message, resolveContext, callback) => { + callback(null, resolveRequest) + }) + } + + plugin.apply(resolver) + + return new Promise((resolve) => { + handler({ request }, {}, (err, result) => { + resolve({ err, result, resolver }) + }) + }) +} + +describe('ExtendComponentsPlugin', () => { + it('resolves section-list to rn implementation', async () => { + const { err, result, resolver } = await resolveExtendComponent({ + mode: 'ios', + request: '@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list?isComponent=true' + }) + + expect(err).toBeFalsy() + expect(result.request).toBe('@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-section-list.jsx') + expect(resolver.doResolve).toHaveBeenCalled() + }) + + it('rejects section-list in unsupported modes', async () => { + const { err, resolver } = await resolveExtendComponent({ + mode: 'web', + request: '@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list' + }) + + expect(err).toBeTruthy() + expect(err.message).toContain('cannot be used in web mode') + expect(resolver.doResolve).not.toHaveBeenCalled() + }) + + it('resolves sticky components registration path', async () => { + const { err, result } = await resolveExtendComponent({ + mode: 'android', + request: '@mpxjs/webpack-plugin/lib/runtime/components/extends/sticky-header' + }) + + expect(err).toBeFalsy() + expect(result.request).toBe('@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-sticky-header.jsx') + }) +}) diff --git a/packages/webpack-plugin/test/template-compiler/rn-template.spec.js b/packages/webpack-plugin/test/template-compiler/rn-template.spec.js index da74604a2e..9d280cc350 100644 --- a/packages/webpack-plugin/test/template-compiler/rn-template.spec.js +++ b/packages/webpack-plugin/test/template-compiler/rn-template.spec.js @@ -160,6 +160,24 @@ describe('RN template support', () => { expect(output).toContain('createElement(getComponent("custom-comp"), null, createElement(getComponent("mpx-inline-text"), null, "content"))') }) + it('should process extend component tags as component nodes', () => { + const parsed = compiler.parse('', { + mode: 'ios', + srcMode: 'wx', + defs: {}, + usingComponentsInfo: {}, + componentGenerics: {}, + externalClasses: [], + filePath: 'test.mpx', + warn: console.warn, + error: console.error + }) + const output = genNodeReact(parsed.root) + expect(output).toContain('bindscroll: (this.onScroll)') + expect(output).toContain('"genericrecycle-item": "item"') + expect(output).toContain('generichash: undefined') + }) + it('should handle wxs in sub template', () => { const input = ` From f8694a7bf8f8512a615582b3e8ee534040495227 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 17:13:13 +0800 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81section=20foote?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/react/mpx-section-list.tsx | 76 +++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index 3c166d9e08..3a884241e2 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -7,13 +7,16 @@ import useInnerProps, { getCustomEvent } from './getInnerListeners' import { extendObject, useLayout, useTransformStyle, GestureHandler, flatGesture } from './utils' interface ListItem { isSectionHeader?: boolean; + isSectionFooter?: boolean; _originalItemIndex?: number; [key: string]: any; } interface SectionExtra { headerData: ListItem | null; + footerData: ListItem | null; hasSectionHeader?: boolean; + hasSectionFooter?: boolean; _originalItemIndex?: number; } @@ -41,6 +44,7 @@ interface MpxSectionListProps { style?: Record; itemHeight?: ItemHeightType; sectionHeaderHeight?: ItemHeightType; + sectionFooterHeight?: ItemHeightType; listHeaderData?: any; listHeaderHeight?: ItemHeightType; useListHeader?: boolean; @@ -48,6 +52,7 @@ interface MpxSectionListProps { useListFooter?: boolean; 'genericrecycle-item'?: string; 'genericsection-header'?: string; + 'genericsection-footer'?: string; 'genericlist-header'?: string; 'genericlist-footer'?: string; 'enable-var'?: boolean; @@ -100,6 +105,7 @@ const _SectionList = forwardRef((props = {}, ref) => { style = {}, itemHeight = {}, sectionHeaderHeight = {}, + sectionFooterHeight = {}, listHeaderHeight = {}, listHeaderData = null, useListHeader = false, @@ -107,6 +113,7 @@ const _SectionList = forwardRef((props = {}, ref) => { useListFooter = false, 'genericrecycle-item': genericrecycleItem, 'genericsection-header': genericsectionHeader, + 'genericsection-footer': genericsectionFooter, 'genericlist-header': genericListHeader, 'genericlist-footer': genericListFooter, 'enable-var': enableVar, @@ -172,7 +179,7 @@ const _SectionList = forwardRef((props = {}, ref) => { } // 通过sectionIndex和rowIndex获取原始索引 - const getOriginalIndex = (sectionIndex: number, rowIndex: number | 'header'): number => { + const getOriginalIndex = (sectionIndex: number, rowIndex: number | 'header' | 'footer'): number => { const key = `${sectionIndex}_${rowIndex}` return reverseIndexMap.current[key] ?? -1 // 如果找不到,返回-1 } @@ -182,9 +189,15 @@ const _SectionList = forwardRef((props = {}, ref) => { // 通过索引映射表快速定位位置 const position = indexMap.current[index] const [sectionIndex, itemIndex] = (position as string).split('_') + const targetSectionIndex = Number(sectionIndex) || 0 + const targetItemIndex = itemIndex === 'header' + ? 0 + : itemIndex === 'footer' + ? convertedListData[targetSectionIndex].data.length + 1 + : Number(itemIndex) + 1 scrollViewRef.current.scrollToLocation?.({ - itemIndex: itemIndex === 'header' ? 0 : Number(itemIndex) + 1, - sectionIndex: Number(sectionIndex) || 0, + itemIndex: targetItemIndex, + sectionIndex: targetSectionIndex, animated, viewOffset, viewPosition @@ -219,6 +232,19 @@ const _SectionList = forwardRef((props = {}, ref) => { } } + const getSectionFooterHeight = ({ sectionIndex }: { sectionIndex: number }) => { + const item = convertedListData[sectionIndex] + const { hasSectionFooter } = item + // 使用getOriginalIndex获取原始索引 + const originalIndex = getOriginalIndex(sectionIndex, 'footer') + if (!hasSectionFooter) return 0 + if ((sectionFooterHeight as ItemHeightType).getter) { + return (sectionFooterHeight as ItemHeightType).getter?.(item, originalIndex) || 0 + } else { + return (sectionFooterHeight as ItemHeightType).value || 0 + } + } + const convertedListData = useMemo(() => { const sections: Section[] = [] let currentSection: Section | null = null @@ -241,8 +267,10 @@ const _SectionList = forwardRef((props = {}, ref) => { // 创建新的 section currentSection = { headerData: item, + footerData: null, data: [], hasSectionHeader: true, + hasSectionFooter: false, _originalItemIndex: index } // 为 section header 添加索引映射 @@ -250,14 +278,37 @@ const _SectionList = forwardRef((props = {}, ref) => { indexMap.current[index] = `${sectionIndex}_header` // 添加反向索引映射 reverseIndexMap.current[`${sectionIndex}_header`] = index + } else if (item.isSectionFooter) { + // 如果没有当前 section,创建一个默认的 + if (!currentSection) { + // 创建默认section (无header的section) + currentSection = { + headerData: null, + footerData: null, + data: [], + hasSectionHeader: false, + hasSectionFooter: false, + _originalItemIndex: -1 + } + } + const sectionIndex = sections.length + currentSection.footerData = item + currentSection.hasSectionFooter = true + indexMap.current[index] = `${sectionIndex}_footer` + // 添加反向索引映射 + reverseIndexMap.current[`${sectionIndex}_footer`] = index + sections.push(currentSection) + currentSection = null } else { // 如果没有当前 section,创建一个默认的 if (!currentSection) { // 创建默认section (无header的section) currentSection = { headerData: null, + footerData: null, data: [], hasSectionHeader: false, + hasSectionFooter: false, _originalItemIndex: -1 } } @@ -321,17 +372,19 @@ const _SectionList = forwardRef((props = {}, ref) => { // 添加该 section 尾部位置信息 // 因为即使 sectionList 没传 renderSectionFooter,getItemLayout 中的 index 的计算也会包含尾部节点 + const footerHeight = getSectionFooterHeight({ sectionIndex }) layouts.push({ - length: 0, + length: footerHeight, offset, index: layouts.length }) + offset += footerHeight }) return { itemLayouts: layouts, getItemLayout: (data: any, index: number) => layouts[index] } - }, [convertedListData, useListHeader, itemHeight.value, itemHeight.getter, sectionHeaderHeight.value, sectionHeaderHeight.getter, listHeaderHeight.value, listHeaderHeight.getter]) + }, [convertedListData, useListHeader, itemHeight.value, itemHeight.getter, sectionHeaderHeight.value, sectionHeaderHeight.getter, sectionFooterHeight.value, sectionFooterHeight.getter, listHeaderHeight.value, listHeaderHeight.getter]) const scrollAdditionalProps = extendObject( { @@ -421,6 +474,18 @@ const _SectionList = forwardRef((props = {}, ref) => { [generichash, genericsectionHeader] ) + const renderSectionFooter = useMemo( + () => { + const SectionFooterComponent = getGeneric(generichash, genericsectionFooter) + if (!SectionFooterComponent) return undefined + return (sectionData: { section: RNSection }) => { + if (!sectionData.section.hasSectionFooter) return null + return createElement(SectionFooterComponent, { itemData: sectionData.section.footerData }) + } + }, + [generichash, genericsectionFooter] + ) + const ListHeaderComponent = useMemo( () => { if (!useListHeader) return null @@ -450,6 +515,7 @@ const _SectionList = forwardRef((props = {}, ref) => { ListHeaderComponent: useListHeader ? ListHeaderComponent : null, ListFooterComponent: useListFooter ? ListFooterComponent : null, renderSectionHeader: renderSectionHeader, + renderSectionFooter: renderSectionFooter, refreshControl: refresherEnabled ? createElement(RefreshControl, { onRefresh: onRefresh, From 24646a85d9d9b0ac16bd8a0d7584f865fe802b1c Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 17:17:16 +0800 Subject: [PATCH 03/20] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85section=20foote?= =?UTF-8?q?r=20=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guide/extend/extend-component.md | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs-vitepress/guide/extend/extend-component.md b/docs-vitepress/guide/extend/extend-component.md index de8b9c332b..c305916beb 100644 --- a/docs-vitepress/guide/extend/extend-component.md +++ b/docs-vitepress/guide/extend/extend-component.md @@ -27,24 +27,26 @@ Mpx 会根据当前编译的目标平台(wx/ali/web/ios/android/harmony), |-----------------------|-------------|----------|------------------------|-----------| | height | String/Number | 100% | 组件高度 | 微信小程序、支付宝小程序、Web、RN | | width | String/Number | 100% | 组件宽度 | 微信小程序、支付宝小程序、Web、RN | -| listData | Array | [] | 列表数据,如需使用列表分组头 `section-header`,对应 item 的数据需要包含 `isSectionHeader: true` 标识 | 微信小程序、支付宝小程序、Web、RN | +| listData | Array | [] | 列表数据,如需使用列表分组头 `section-header`,对应 item 的数据需要包含 `isSectionHeader: true` 标识;如需使用列表分组尾 `section-footer`,对应 item 的数据需要包含 `isSectionFooter: true` 标识 | 微信小程序、支付宝小程序、Web、RN | | enable-sticky | Boolean | false | 启用分组吸顶 | 微信小程序、支付宝小程序、Web、RN
⚠️微信小程序环境,需要使用 skyline 渲染模式,webview 模式不支持;web 环境仅支持移动端,不支持 pc 端 | | scroll-with-animation | Boolean | false | 滚动动画 | 微信小程序、支付宝小程序、Web、RN | | useListHeader | Boolean | false | 使用自定义列表头 | 微信小程序、支付宝小程序、Web、RN | | listHeaderData | Object | {} | 列表头数据 | 微信小程序、支付宝小程序、Web、RN | | useListFooter | Boolean | false | 使用自定义列表页脚 | 微信小程序、支付宝小程序、Web、RN | -| listFooterData | Object | {} | 列表头数据 | 微信小程序、支付宝小程序、Web、RN | +| listFooterData | Object | {} | 列表页脚数据 | 微信小程序、支付宝小程序、Web、RN | | generic:recycle-item | String | | 列表项,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | | generic:section-header | String | | 列表分组头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | +| generic:section-footer | String | | 列表分组尾,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | | generic:list-header | String | | 列表头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | | generic:list-footer | String | | 列表页脚,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | | itemHeight | Object | {} | 列表项高度配置(支持 getter/value),必须配置 | 微信小程序、支付宝小程序、Web、RN | | sectionHeaderHeight | Object | {} | 分组头部高度配置(getter/value),若使用了自定义分组头必须配置 | 微信小程序、支付宝小程序、Web、RN | +| sectionFooterHeight | Object | {} | 分组尾部高度配置(getter/value),若使用了自定义分组尾必须配置 | 微信小程序、支付宝小程序、Web、RN | | listHeaderHeight | Object | {} | 列表头部高度配置(getter/value),若使用了列表头必须配置 | 微信小程序、支付宝小程序、Web、RN | | bufferScale | Number | 1 | 渲染缓冲区行数(虚拟滚动优化) | 仅支付宝小程序/web支持 | | minRenderCount | Number | 10 | 最小渲染项目数 | 仅支付宝小程序/web支持 | -#### `itemHeight`/`sectionHeaderHeight`/`listHeaderHeight` 格式说明 +#### `itemHeight`/`sectionHeaderHeight`/`sectionFooterHeight`/`listHeaderHeight` 格式说明 高度相关属性支持如下格式: @@ -86,12 +88,14 @@ height: { Date: Tue, 2 Jun 2026 17:22:20 +0800 Subject: [PATCH 04/20] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E5=8D=95?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/extend-components-plugin.spec.js | 59 ------------------- .../template-compiler/rn-template.spec.js | 18 ------ 2 files changed, 77 deletions(-) delete mode 100644 packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js diff --git a/packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js b/packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js deleted file mode 100644 index 0dd427c0ae..0000000000 --- a/packages/webpack-plugin/test/resolver/extend-components-plugin.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -const ExtendComponentsPlugin = require('../../lib/resolver/ExtendComponentsPlugin') - -function resolveExtendComponent ({ mode, request }) { - const plugin = new ExtendComponentsPlugin('source', mode, 'target') - let handler - const resolver = { - ensureHook: jest.fn((hook) => hook), - getHook: jest.fn(() => ({ - tapAsync: (name, fn) => { - handler = fn - } - })), - doResolve: jest.fn((target, resolveRequest, message, resolveContext, callback) => { - callback(null, resolveRequest) - }) - } - - plugin.apply(resolver) - - return new Promise((resolve) => { - handler({ request }, {}, (err, result) => { - resolve({ err, result, resolver }) - }) - }) -} - -describe('ExtendComponentsPlugin', () => { - it('resolves section-list to rn implementation', async () => { - const { err, result, resolver } = await resolveExtendComponent({ - mode: 'ios', - request: '@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list?isComponent=true' - }) - - expect(err).toBeFalsy() - expect(result.request).toBe('@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-section-list.jsx') - expect(resolver.doResolve).toHaveBeenCalled() - }) - - it('rejects section-list in unsupported modes', async () => { - const { err, resolver } = await resolveExtendComponent({ - mode: 'web', - request: '@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list' - }) - - expect(err).toBeTruthy() - expect(err.message).toContain('cannot be used in web mode') - expect(resolver.doResolve).not.toHaveBeenCalled() - }) - - it('resolves sticky components registration path', async () => { - const { err, result } = await resolveExtendComponent({ - mode: 'android', - request: '@mpxjs/webpack-plugin/lib/runtime/components/extends/sticky-header' - }) - - expect(err).toBeFalsy() - expect(result.request).toBe('@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-sticky-header.jsx') - }) -}) diff --git a/packages/webpack-plugin/test/template-compiler/rn-template.spec.js b/packages/webpack-plugin/test/template-compiler/rn-template.spec.js index 9d280cc350..da74604a2e 100644 --- a/packages/webpack-plugin/test/template-compiler/rn-template.spec.js +++ b/packages/webpack-plugin/test/template-compiler/rn-template.spec.js @@ -160,24 +160,6 @@ describe('RN template support', () => { expect(output).toContain('createElement(getComponent("custom-comp"), null, createElement(getComponent("mpx-inline-text"), null, "content"))') }) - it('should process extend component tags as component nodes', () => { - const parsed = compiler.parse('', { - mode: 'ios', - srcMode: 'wx', - defs: {}, - usingComponentsInfo: {}, - componentGenerics: {}, - externalClasses: [], - filePath: 'test.mpx', - warn: console.warn, - error: console.error - }) - const output = genNodeReact(parsed.root) - expect(output).toContain('bindscroll: (this.onScroll)') - expect(output).toContain('"genericrecycle-item": "item"') - expect(output).toContain('generichash: undefined') - }) - it('should handle wxs in sub template', () => { const input = ` From 0b8c7420e4ff417a663a5e800829c844788a3b7d Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 19:03:41 +0800 Subject: [PATCH 05/20] =?UTF-8?q?fix:=20=E6=89=8B=E5=8A=BF=E5=8D=8F?= =?UTF-8?q?=E5=90=8C=E6=94=AF=E6=8C=81=E5=8E=9F=E7=94=9F=E6=89=8B=E5=8A=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webpack-plugin/lib/runtime/components/react/utils.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index f33f491cdc..1d81fc0ee5 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -736,6 +736,7 @@ export function usePrevious (value: T): T | undefined { export interface GestureHandler { nodeRefs?: Array<{ getNodeInstance: () => { nodeRef: unknown } }> current?: unknown + handlerTag?: Number } export function flatGesture (gestures: Array = []) { @@ -744,7 +745,10 @@ export function flatGesture (gestures: Array = []) { return gesture.nodeRefs .map((item: { getNodeInstance: () => any }) => item.getNodeInstance()?.instance?.gestureRef || {}) } - return gesture?.current ? [gesture] : [] + if (gesture && ('current' in gesture || gesture.handlerTag !== undefined)) { + return [gesture] + } + return [] })) || [] } From 86f62439c08ff8115603bde4fa92512f3f095dfd Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 19:06:55 +0800 Subject: [PATCH 06/20] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/ali/mpx-section-list.mpx | 0 .../lib/runtime/components/ali/mpx-sticky-header.mpx | 0 .../lib/runtime/components/ali/mpx-sticky-section.mpx | 0 .../lib/runtime/components/web/mpx-section-list.vue | 0 .../webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx | 0 .../lib/runtime/components/wx/mpx-sticky-header.mpx | 0 .../lib/runtime/components/wx/mpx-sticky-section.mpx | 0 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/webpack-plugin/lib/runtime/components/ali/mpx-section-list.mpx delete mode 100644 packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-header.mpx delete mode 100644 packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-section.mpx delete mode 100644 packages/webpack-plugin/lib/runtime/components/web/mpx-section-list.vue delete mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx delete mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-header.mpx delete mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-section.mpx diff --git a/packages/webpack-plugin/lib/runtime/components/ali/mpx-section-list.mpx b/packages/webpack-plugin/lib/runtime/components/ali/mpx-section-list.mpx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-header.mpx b/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-header.mpx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-section.mpx b/packages/webpack-plugin/lib/runtime/components/ali/mpx-sticky-section.mpx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-section-list.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-section-list.vue deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-section-list.mpx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-header.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-header.mpx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-section.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-sticky-section.mpx deleted file mode 100644 index e69de29bb2..0000000000 From fa1033986f0bfe960daed381f7a8c1be939ed946 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 19:19:43 +0800 Subject: [PATCH 07/20] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0section-list?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 7 ++- docs-vitepress/api/compile.md | 1 - docs-vitepress/guide/rn/component.md | 61 ------------------- 3 files changed, 5 insertions(+), 64 deletions(-) diff --git a/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md b/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md index 3399c5426b..23b77ba7a8 100644 --- a/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md +++ b/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md @@ -1363,13 +1363,15 @@ RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。 | --- | --- | --- | --- | | height | string/number | | 组件高度 | | width | string/number | | 组件宽度 | -| listData | array | | 列表数据,分组头数据需包含 `isSectionHeader: true` | +| listData | array | | 列表数据,分组头数据需包含 `isSectionHeader: true`,分组尾数据需包含 `isSectionFooter: true` | | generic:recycle-item | string | | 列表项抽象节点组件名 | | generic:section-header | string | | 分组头抽象节点组件名 | +| generic:section-footer | string | | 分组尾抽象节点组件名 | | generic:list-header | string | | 列表头抽象节点组件名 | | generic:list-footer | string | | 列表尾抽象节点组件名 | | itemHeight | object | | 列表项高度配置,支持 `value` 或 `getter(item, index)` | | sectionHeaderHeight | object | | 分组头高度配置,支持 `value` 或 `getter(item, index)` | +| sectionFooterHeight | object | | 分组尾高度配置,支持 `value` 或 `getter(item, index)` | | listHeaderHeight | object | | 列表头高度配置,支持 `value` 或 `getter()` | | useListHeader | boolean | `false` | 是否渲染列表头 | | listHeaderData | object | | 列表头数据 | @@ -1404,7 +1406,8 @@ RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。 #### 注意事项 - `generic:*` 指向的抽象节点组件也需要在当前页面或组件的 `usingComponents` 中注册。 -- 使用 `itemHeight`、`sectionHeaderHeight`、`listHeaderHeight` 提供稳定高度,可减少滚动定位异常。 +- 分组头和分组尾分别通过 `listData` 中的 `isSectionHeader`、`isSectionFooter` 标记声明,并分别由 `generic:section-header`、`generic:section-footer` 渲染。 +- 使用 `itemHeight`、`sectionHeaderHeight`、`sectionFooterHeight`、`listHeaderHeight` 提供稳定高度,可减少滚动定位异常。 - 开启 `enable-sticky` 且快速滑动时,自定义分组头可能出现闪烁,这是 RN `SectionList` 底层机制限制。 ### sticky-section diff --git a/docs-vitepress/api/compile.md b/docs-vitepress/api/compile.md index 253f0dfd7f..4ba857f4f9 100644 --- a/docs-vitepress/api/compile.md +++ b/docs-vitepress/api/compile.md @@ -922,7 +922,6 @@ module.exports = defineConfig({ - 页面/组件 **主模版** 以及 **子模版**(如通过 import 引入的模版)均会应用本配置。 - **key / value** 及路径书写要求与 Web 一节一致,此处不再重复;详见 [webConfig.customBuiltInComponents](#webconfig-custombuiltincomponents)。 -- 自定义基础组件如果最终渲染到 React Native 原生组件,需要先消费或映射自身支持的属性、事件,再过滤已处理的小程序属性以及 Mpx 内部辅助属性,避免它们继续透传到原生节点上。例如 `enable-var`、`enable-offset`、`enable-background`、`external-var-context`、`parent-font-size`、`parent-width`、`parent-height`、`enable-text-pass-through` 以及组件已处理的 `bind*` / `catch*` 事件属性。 #### rnConfig.loadChunkAsync(运行时) diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index eb37604337..2713327c6d 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -731,67 +731,6 @@ API > [!tip] 注意 > > - style 样式不支持中使用百分比计算、css variable - -### section-list -RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。使用前需要在 `usingComponents` 中注册: - -```json -{ - "usingComponents": { - "section-list": "@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list" - } -} -``` - -属性 - -| 属性名 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| height | string/number | | 组件高度 | -| width | string/number | | 组件宽度 | -| listData | array | | 列表数据,分组头数据需包含 `isSectionHeader: true` | -| generic:recycle-item | string | | 列表项抽象节点组件名 | -| generic:section-header | string | | 分组头抽象节点组件名 | -| generic:list-header | string | | 列表头抽象节点组件名 | -| generic:list-footer | string | | 列表尾抽象节点组件名 | -| itemHeight | object | | 列表项高度配置,支持 `value` 或 `getter(item, index)` | -| sectionHeaderHeight | object | | 分组头高度配置,支持 `value` 或 `getter(item, index)` | -| listHeaderHeight | object | | 列表头高度配置,支持 `value` 或 `getter()` | -| useListHeader | boolean | `false` | 是否渲染列表头 | -| listHeaderData | object | | 列表头数据 | -| useListFooter | boolean | `false` | 是否渲染列表尾 | -| listFooterData | object | | 列表尾数据 | -| enable-sticky | boolean | `false` | 是否开启分组头吸顶 | -| enhanced | boolean | `false` | 是否开启增强能力 | -| bounces | boolean | `true` | iOS 边界弹性控制,开启 `enhanced` 后生效 | -| enable-back-to-top | boolean | `false` | 点击状态栏回到顶部,仅 iOS 支持 | -| end-reached-threshold | number | `0.1` | 触底事件触发阈值 | -| refresher-enabled | boolean | `false` | 是否开启下拉刷新 | -| refresher-triggered | boolean | `false` | 当前下拉刷新状态 | -| show-scrollbar | boolean | `true` | 是否显示滚动条 | -| scroll-event-throttle | number | `0` | scroll 事件触发频率 | -| simultaneous-handlers | array\ | `[]` | 允许多个外部手势同时识别 | -| wait-for | array\ | `[]` | 等待外部手势失败后再识别 | - -事件 - -| 事件名 | 说明 | -| --- | --- | -| bindscroll | 滚动时触发 | -| bindscrolltolower | 滚动到底部时触发 | -| bindrefresherrefresh | 自定义下拉刷新被触发 | - -方法 - -| 方法名 | 说明 | -| --- | --- | -| scrollToIndex({ index, animated, viewOffset, viewPosition }) | 滚动到指定原始索引 | - -> [!tip] 注意 -> -> - `generic:*` 指向的抽象节点组件也需要在当前页面或组件的 `usingComponents` 中注册。 -> - 使用 `itemHeight`、`sectionHeaderHeight`、`listHeaderHeight` 提供稳定高度,可减少滚动定位异常。 -> - 开启 `enable-sticky` 且快速滑动时,自定义分组头可能出现闪烁,这是 RN `SectionList` 底层机制限制。 ### cover-view 视图容器。 From e862610ab7f304cc0353998d161b97ffe94eea08 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 19:23:52 +0800 Subject: [PATCH 08/20] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0section-list?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 205 ++++++++++++------ 1 file changed, 142 insertions(+), 63 deletions(-) diff --git a/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md b/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md index 23b77ba7a8..7b01af3486 100644 --- a/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md +++ b/.agents/skills/mpx-rn-dev-guide/references/rn-template-reference.md @@ -2,67 +2,148 @@ ## 目录 -- [数据绑定](#数据绑定) - - [文本、属性与表达式](#文本属性与表达式) -- [模板指令](#模板指令) -- [事件处理](#事件处理) - - [通用事件](#通用事件) - - [事件绑定语法](#事件绑定语法) - - [注意事项](#注意事项-1) -- [Slot](#slot) - - [默认插槽](#默认插槽) - - [具名插槽与 `multipleSlots`](#具名插槽与-multipleslots) -- [WXML 模板](#wxml-模板) - - [模板内联定义](#模板内联定义) - - [import 外联引入](#import-外联引入) - - [注意事项](#注意事项-2) -- [i18n 国际化](#i18n-国际化) - - [工程配置示例](#工程配置示例) - - [模板中使用翻译函数](#模板中使用翻译函数) - - [JS 中使用翻译函数](#js-中使用翻译函数) - - [注意事项](#注意事项-3) -- [无障碍访问](#无障碍访问) - - [模板属性](#模板属性) - - [示例](#示例) - - [注意事项](#注意事项-4) -- [基础组件](#基础组件) - - [通用属性](#通用属性) - - [view](#view) - - [text](#text) - - [scroll-view](#scroll-view) - - [swiper](#swiper) - - [swiper-item](#swiper-item) - - [movable-area](#movable-area) - - [movable-view](#movable-view) - - [image](#image) - - [icon](#icon) - - [button](#button) - - [label](#label) - - [checkbox](#checkbox) - - [checkbox-group](#checkbox-group) - - [radio](#radio) - - [radio-group](#radio-group) - - [form](#form) - - [input](#input) - - [textarea](#textarea) - - [progress](#progress) - - [picker-view](#picker-view) - - [picker-view-column](#picker-view-column) - - [picker](#picker) - - [slider](#slider) - - [switch](#switch) - - [navigator](#navigator) - - [rich-text](#rich-text) - - [canvas](#canvas) - - [camera](#camera) - - [video](#video) - - [web-view](#web-view) - - [root-portal](#root-portal) - - [section-list](#section-list) - - [sticky-section](#sticky-section) - - [sticky-header](#sticky-header) - - [cover-view](#cover-view) - - [cover-image](#cover-image) +- [跨端输出 RN 模板能力参考](#跨端输出-rn-模板能力参考) + - [目录](#目录) + - [数据绑定](#数据绑定) + - [文本、属性与表达式](#文本属性与表达式) + - [注意事项](#注意事项) + - [模板指令](#模板指令) + - [事件处理](#事件处理) + - [通用事件](#通用事件) + - [事件绑定语法](#事件绑定语法) + - [注意事项](#注意事项-1) + - [Slot](#slot) + - [默认插槽](#默认插槽) + - [具名插槽与 `multipleSlots`](#具名插槽与-multipleslots) + - [WXML 模板](#wxml-模板) + - [模板内联定义](#模板内联定义) + - [import 外联引入](#import-外联引入) + - [注意事项](#注意事项-2) + - [i18n 国际化](#i18n-国际化) + - [工程配置示例](#工程配置示例) + - [模板中使用翻译函数](#模板中使用翻译函数) + - [JS 中使用翻译函数](#js-中使用翻译函数) + - [注意事项](#注意事项-3) + - [无障碍访问](#无障碍访问) + - [模板属性](#模板属性) + - [示例](#示例) + - [注意事项](#注意事项-4) + - [基础组件](#基础组件) + - [通用属性](#通用属性) + - [view](#view) + - [属性](#属性) + - [事件](#事件) + - [注意事项](#注意事项-5) + - [text](#text) + - [属性](#属性-1) + - [注意事项](#注意事项-6) + - [scroll-view](#scroll-view) + - [属性](#属性-2) + - [事件](#事件-1) + - [注意事项](#注意事项-7) + - [swiper](#swiper) + - [属性](#属性-3) + - [事件](#事件-2) + - [swiper-item](#swiper-item) + - [属性](#属性-4) + - [movable-area](#movable-area) + - [movable-view](#movable-view) + - [属性](#属性-5) + - [事件](#事件-3) + - [注意事项](#注意事项-8) + - [image](#image) + - [属性](#属性-6) + - [事件](#事件-4) + - [注意事项](#注意事项-9) + - [icon](#icon) + - [属性](#属性-7) + - [button](#button) + - [属性](#属性-8) + - [注意事项](#注意事项-10) + - [label](#label) + - [注意事项](#注意事项-11) + - [checkbox](#checkbox) + - [属性](#属性-9) + - [checkbox-group](#checkbox-group) + - [事件](#事件-5) + - [radio](#radio) + - [属性](#属性-10) + - [radio-group](#radio-group) + - [事件](#事件-6) + - [form](#form) + - [事件](#事件-7) + - [input](#input) + - [属性](#属性-11) + - [事件](#事件-8) + - [textarea](#textarea) + - [属性](#属性-12) + - [事件](#事件-9) + - [注意事项](#注意事项-12) + - [progress](#progress) + - [属性](#属性-13) + - [事件](#事件-10) + - [注意事项](#注意事项-13) + - [picker-view](#picker-view) + - [属性](#属性-14) + - [事件](#事件-11) + - [picker-view-column](#picker-view-column) + - [picker](#picker) + - [属性](#属性-15) + - [事件](#事件-12) + - [普通选择器:mode = selector](#普通选择器mode--selector) + - [属性](#属性-16) + - [多列选择器:mode = multiSelector](#多列选择器mode--multiselector) + - [属性与事件](#属性与事件) + - [多列选择器:时间选择器:mode = time](#多列选择器时间选择器mode--time) + - [属性](#属性-17) + - [多列选择器:时间选择器:mode = date](#多列选择器时间选择器mode--date) + - [属性](#属性-18) + - [省市区选择器:mode = region](#省市区选择器mode--region) + - [属性](#属性-19) + - [slider](#slider) + - [属性](#属性-20) + - [事件](#事件-13) + - [注意事项](#注意事项-14) + - [switch](#switch) + - [属性](#属性-21) + - [事件](#事件-14) + - [navigator](#navigator) + - [属性](#属性-22) + - [rich-text](#rich-text) + - [属性](#属性-23) + - [canvas](#canvas) + - [事件](#事件-15) + - [API](#api) + - [注意事项](#注意事项-15) + - [camera](#camera) + - [属性](#属性-24) + - [事件](#事件-16) + - [API](#api-1) + - [注意事项](#注意事项-16) + - [video](#video) + - [属性](#属性-25) + - [事件](#事件-17) + - [注意事项](#注意事项-17) + - [web-view](#web-view) + - [属性](#属性-26) + - [事件](#事件-18) + - [注意事项](#注意事项-18) + - [root-portal](#root-portal) + - [属性](#属性-27) + - [注意事项](#注意事项-19) + - [section-list](#section-list) + - [属性](#属性-28) + - [事件](#事件-19) + - [方法](#方法) + - [注意事项](#注意事项-20) + - [sticky-section](#sticky-section) + - [注意事项](#注意事项-21) + - [sticky-header](#sticky-header) + - [属性](#属性-29) + - [事件](#事件-20) + - [注意事项](#注意事项-22) + - [cover-view](#cover-view) + - [cover-image](#cover-image) --- @@ -566,8 +647,6 @@ Mpx 输出 RN 内置支持了大部分常用的基础组件,详情见下方文 **自定义覆盖与扩展**:当某个内置基础组件在 RN 上不满足业务需要、需替换为自定义实现,或希望在模板中直接使用一组宿主特有的基础组件时,可在 `@mpxjs/webpack-plugin` 的编译配置 `rnConfig.customBuiltInComponents` 中声明自定义组件 —— **同名组件会覆盖**框架内置实现,**新名称则作为扩展基础组件**注入到模板编译期识别表中,无需在每个 `.mpx` 的 `usingComponents` 中重复注册即可在模板中以基础组件方式使用。该配置在模板编译阶段生效,并非应用入口的运行时 `Mpx.config.rnConfig` 配置。 -自定义基础组件如果最终渲染到 React Native 原生组件,需要先消费或映射自身支持的属性、事件,再过滤已处理的小程序属性以及 Mpx 内部辅助属性,避免它们继续透传到原生节点上。例如 `enable-var`、`enable-offset`、`enable-background`、`external-var-context`、`parent-font-size`、`parent-width`、`parent-height`、`enable-text-pass-through` 以及组件已处理的 `bind*` / `catch*` 事件属性。 - ### 通用属性 通用属性除了前述的[模板指令](#模板指令)和[通用事件](#通用事件)绑定外,还包括以下属性: From cf8a6be9fae769615cab50c2611c29b27963ed1e Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 19:32:36 +0800 Subject: [PATCH 09/20] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0section-list?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 96 +------------------ 1 file changed, 3 insertions(+), 93 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index f29db7dd7e..6ba0fd5e39 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -131,17 +131,12 @@ - [root-portal](#root-portal) - [属性](#属性-27) - [注意事项](#注意事项-19) - - [section-list](#section-list) + - [sticky-section](#sticky-section) + - [注意事项](#注意事项-20) + - [sticky-header](#sticky-header) - [属性](#属性-28) - [事件](#事件-19) - - [方法](#方法) - - [注意事项](#注意事项-20) - - [sticky-section](#sticky-section) - [注意事项](#注意事项-21) - - [sticky-header](#sticky-header) - - [属性](#属性-29) - - [事件](#事件-20) - - [注意事项](#注意事项-22) - [cover-view](#cover-view) - [cover-image](#cover-image) @@ -1428,85 +1423,10 @@ level 有效值: - style 样式中不支持使用百分比计算、css variable -### section-list - -RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。使用前需要在 `usingComponents` 中注册: - -```json -{ - "usingComponents": { - "section-list": "@mpxjs/webpack-plugin/lib/runtime/components/extends/section-list" - } -} -``` - -#### 属性 - -| 属性名 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| height | string/number | | 组件高度 | -| width | string/number | | 组件宽度 | -| listData | array | | 列表数据,分组头数据需包含 `isSectionHeader: true`,分组尾数据需包含 `isSectionFooter: true` | -| generic:recycle-item | string | | 列表项抽象节点组件名 | -| generic:section-header | string | | 分组头抽象节点组件名 | -| generic:section-footer | string | | 分组尾抽象节点组件名 | -| generic:list-header | string | | 列表头抽象节点组件名 | -| generic:list-footer | string | | 列表尾抽象节点组件名 | -| itemHeight | object | | 列表项高度配置,支持 `value` 或 `getter(item, index)` | -| sectionHeaderHeight | object | | 分组头高度配置,支持 `value` 或 `getter(item, index)` | -| sectionFooterHeight | object | | 分组尾高度配置,支持 `value` 或 `getter(item, index)` | -| listHeaderHeight | object | | 列表头高度配置,支持 `value` 或 `getter()` | -| useListHeader | boolean | `false` | 是否渲染列表头 | -| listHeaderData | object | | 列表头数据 | -| useListFooter | boolean | `false` | 是否渲染列表尾 | -| listFooterData | object | | 列表尾数据 | -| enable-sticky | boolean | `false` | 是否开启分组头吸顶 | -| enhanced | boolean | `false` | 是否开启增强能力 | -| bounces | boolean | `true` | iOS 边界弹性控制,开启 `enhanced` 后生效 | -| enable-back-to-top | boolean | `false` | 点击状态栏回到顶部,仅 iOS 支持 | -| end-reached-threshold | number | `0.1` | 触底事件触发阈值 | -| refresher-enabled | boolean | `false` | 是否开启下拉刷新 | -| refresher-triggered | boolean | `false` | 当前下拉刷新状态 | -| show-scrollbar | boolean | `true` | 是否显示滚动条 | -| scroll-event-throttle | number | `0` | scroll 事件触发频率 | -| simultaneous-handlers | array\ | `[]` | 允许多个外部手势同时识别 | -| wait-for | array\ | `[]` | 等待外部手势失败后再识别 | - -#### 事件 - -| 事件名 | 说明 | -| --- | --- | -| bindscroll | 滚动时触发 | -| bindscrolltolower | 滚动到底部时触发 | -| bindrefresherrefresh | 自定义下拉刷新被触发 | - -#### 方法 - -| 方法名 | 说明 | -| --- | --- | -| scrollToIndex({ index, animated, viewOffset, viewPosition }) | 滚动到指定原始索引 | - -#### 注意事项 - -- `generic:*` 指向的抽象节点组件也需要在当前页面或组件的 `usingComponents` 中注册。 -- 分组头和分组尾分别通过 `listData` 中的 `isSectionHeader`、`isSectionFooter` 标记声明,并分别由 `generic:section-header`、`generic:section-footer` 渲染。 -- 使用 `itemHeight`、`sectionHeaderHeight`、`sectionFooterHeight`、`listHeaderHeight` 提供稳定高度,可减少滚动定位异常。 -- 开启 `enable-sticky` 且快速滑动时,自定义分组头可能出现闪烁,这是 RN `SectionList` 底层机制限制。 - ### sticky-section 吸顶布局容器,仅支持作为 `` 的直接子节点 -可直接作为基础标签使用,也可在 `usingComponents` 中注册: - -```json -{ - "usingComponents": { - "sticky-section": "@mpxjs/webpack-plugin/lib/runtime/components/extends/sticky-section" - } -} -``` - #### 注意事项 - sticky-section 目前仅支持 RN、web 以及微信小程序环境,其他环境暂不支持。微信小程序中使用需开启 skyline 渲染模式 @@ -1515,16 +1435,6 @@ RN 环境下的分组列表组件,基于 React Native `SectionList` 实现。 吸顶布局容器,仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 -可直接作为基础标签使用,也可在 `usingComponents` 中注册: - -```json -{ - "usingComponents": { - "sticky-header": "@mpxjs/webpack-plugin/lib/runtime/components/extends/sticky-header" - } -} -``` - #### 属性 | 属性名 | 类型 | 默认值 | 说明 | From 1ee12fe5b79dd3a2c1d138e0781856e3704260cb Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 19:36:39 +0800 Subject: [PATCH 10/20] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0section-list?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 197 ++++++------------ 1 file changed, 60 insertions(+), 137 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index 6ba0fd5e39..801d56e5b2 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -2,143 +2,66 @@ ## 目录 -- [跨端输出 RN 模板能力参考](#跨端输出-rn-模板能力参考) - - [目录](#目录) - - [数据绑定](#数据绑定) - - [文本、属性与表达式](#文本属性与表达式) - - [注意事项](#注意事项) - - [模板指令](#模板指令) - - [事件处理](#事件处理) - - [通用事件](#通用事件) - - [事件绑定语法](#事件绑定语法) - - [注意事项](#注意事项-1) - - [Slot](#slot) - - [默认插槽](#默认插槽) - - [具名插槽与 `multipleSlots`](#具名插槽与-multipleslots) - - [WXML 模板](#wxml-模板) - - [模板内联定义](#模板内联定义) - - [import 外联引入](#import-外联引入) - - [注意事项](#注意事项-2) - - [i18n 国际化](#i18n-国际化) - - [工程配置示例](#工程配置示例) - - [模板中使用翻译函数](#模板中使用翻译函数) - - [JS 中使用翻译函数](#js-中使用翻译函数) - - [注意事项](#注意事项-3) - - [无障碍访问](#无障碍访问) - - [模板属性](#模板属性) - - [示例](#示例) - - [注意事项](#注意事项-4) - - [基础组件](#基础组件) - - [通用属性](#通用属性) - - [view](#view) - - [属性](#属性) - - [事件](#事件) - - [注意事项](#注意事项-5) - - [text](#text) - - [属性](#属性-1) - - [注意事项](#注意事项-6) - - [scroll-view](#scroll-view) - - [属性](#属性-2) - - [事件](#事件-1) - - [注意事项](#注意事项-7) - - [swiper](#swiper) - - [属性](#属性-3) - - [事件](#事件-2) - - [swiper-item](#swiper-item) - - [属性](#属性-4) - - [movable-area](#movable-area) - - [movable-view](#movable-view) - - [属性](#属性-5) - - [事件](#事件-3) - - [注意事项](#注意事项-8) - - [image](#image) - - [属性](#属性-6) - - [事件](#事件-4) - - [注意事项](#注意事项-9) - - [icon](#icon) - - [属性](#属性-7) - - [button](#button) - - [属性](#属性-8) - - [注意事项](#注意事项-10) - - [label](#label) - - [注意事项](#注意事项-11) - - [checkbox](#checkbox) - - [属性](#属性-9) - - [checkbox-group](#checkbox-group) - - [事件](#事件-5) - - [radio](#radio) - - [属性](#属性-10) - - [radio-group](#radio-group) - - [事件](#事件-6) - - [form](#form) - - [事件](#事件-7) - - [input](#input) - - [属性](#属性-11) - - [事件](#事件-8) - - [textarea](#textarea) - - [属性](#属性-12) - - [事件](#事件-9) - - [注意事项](#注意事项-12) - - [progress](#progress) - - [属性](#属性-13) - - [事件](#事件-10) - - [注意事项](#注意事项-13) - - [picker-view](#picker-view) - - [属性](#属性-14) - - [事件](#事件-11) - - [picker-view-column](#picker-view-column) - - [picker](#picker) - - [属性](#属性-15) - - [事件](#事件-12) - - [普通选择器:mode = selector](#普通选择器mode--selector) - - [属性](#属性-16) - - [多列选择器:mode = multiSelector](#多列选择器mode--multiselector) - - [属性与事件](#属性与事件) - - [多列选择器:时间选择器:mode = time](#多列选择器时间选择器mode--time) - - [属性](#属性-17) - - [多列选择器:时间选择器:mode = date](#多列选择器时间选择器mode--date) - - [属性](#属性-18) - - [省市区选择器:mode = region](#省市区选择器mode--region) - - [属性](#属性-19) - - [slider](#slider) - - [属性](#属性-20) - - [事件](#事件-13) - - [注意事项](#注意事项-14) - - [switch](#switch) - - [属性](#属性-21) - - [事件](#事件-14) - - [navigator](#navigator) - - [属性](#属性-22) - - [rich-text](#rich-text) - - [属性](#属性-23) - - [canvas](#canvas) - - [事件](#事件-15) - - [API](#api) - - [注意事项](#注意事项-15) - - [camera](#camera) - - [属性](#属性-24) - - [事件](#事件-16) - - [API](#api-1) - - [注意事项](#注意事项-16) - - [video](#video) - - [属性](#属性-25) - - [事件](#事件-17) - - [注意事项](#注意事项-17) - - [web-view](#web-view) - - [属性](#属性-26) - - [事件](#事件-18) - - [注意事项](#注意事项-18) - - [root-portal](#root-portal) - - [属性](#属性-27) - - [注意事项](#注意事项-19) - - [sticky-section](#sticky-section) - - [注意事项](#注意事项-20) - - [sticky-header](#sticky-header) - - [属性](#属性-28) - - [事件](#事件-19) - - [注意事项](#注意事项-21) - - [cover-view](#cover-view) - - [cover-image](#cover-image) +- [数据绑定](#数据绑定) + - [文本、属性与表达式](#文本属性与表达式) +- [模板指令](#模板指令) +- [事件处理](#事件处理) + - [通用事件](#通用事件) + - [事件绑定语法](#事件绑定语法) + - [注意事项](#注意事项-1) +- [Slot](#slot) + - [默认插槽](#默认插槽) + - [具名插槽与 `multipleSlots`](#具名插槽与-multipleslots) +- [WXML 模板](#wxml-模板) + - [模板内联定义](#模板内联定义) + - [import 外联引入](#import-外联引入) + - [注意事项](#注意事项-2) +- [i18n 国际化](#i18n-国际化) + - [工程配置示例](#工程配置示例) + - [模板中使用翻译函数](#模板中使用翻译函数) + - [JS 中使用翻译函数](#js-中使用翻译函数) + - [注意事项](#注意事项-3) +- [无障碍访问](#无障碍访问) + - [模板属性](#模板属性) + - [示例](#示例) + - [注意事项](#注意事项-4) +- [基础组件](#基础组件) + - [通用属性](#通用属性) + - [view](#view) + - [text](#text) + - [scroll-view](#scroll-view) + - [swiper](#swiper) + - [swiper-item](#swiper-item) + - [movable-area](#movable-area) + - [movable-view](#movable-view) + - [image](#image) + - [icon](#icon) + - [button](#button) + - [label](#label) + - [checkbox](#checkbox) + - [checkbox-group](#checkbox-group) + - [radio](#radio) + - [radio-group](#radio-group) + - [form](#form) + - [input](#input) + - [textarea](#textarea) + - [progress](#progress) + - [picker-view](#picker-view) + - [picker-view-column](#picker-view-column) + - [picker](#picker) + - [slider](#slider) + - [switch](#switch) + - [navigator](#navigator) + - [rich-text](#rich-text) + - [canvas](#canvas) + - [camera](#camera) + - [video](#video) + - [web-view](#web-view) + - [root-portal](#root-portal) + - [sticky-section](#sticky-section) + - [sticky-header](#sticky-header) + - [cover-view](#cover-view) + - [cover-image](#cover-image) --- From d4a9fe10d7670cf06c95821fdc627cc341226cdb Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Tue, 2 Jun 2026 20:22:00 +0800 Subject: [PATCH 11/20] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20section-list?= =?UTF-8?q?=20=E6=BB=9A=E5=8A=A8=E7=B4=A2=E5=BC=95=E4=B8=8E=E5=A4=B4?= =?UTF-8?q?=E5=B0=BE=E9=AB=98=E5=BA=A6=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/react/mpx-section-list.tsx | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index 3a884241e2..a6991c14ef 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -136,7 +136,7 @@ const _SectionList = forwardRef((props = {}, ref) => { const scrollViewRef = useRef(null) const sectionListGestureRef = useRef() - const indexMap = useRef<{ [key: string]: string | number }>({}) + const indexMap = useRef<{ [key: string]: string }>({}) const reverseIndexMap = useRef<{ [key: string]: number }>({}) @@ -185,24 +185,24 @@ const _SectionList = forwardRef((props = {}, ref) => { } const scrollToIndex = ({ index, animated, viewOffset = 0, viewPosition = 0 }: ScrollPositionParams) => { - if (scrollViewRef.current) { - // 通过索引映射表快速定位位置 - const position = indexMap.current[index] - const [sectionIndex, itemIndex] = (position as string).split('_') - const targetSectionIndex = Number(sectionIndex) || 0 - const targetItemIndex = itemIndex === 'header' - ? 0 - : itemIndex === 'footer' - ? convertedListData[targetSectionIndex].data.length + 1 - : Number(itemIndex) + 1 - scrollViewRef.current.scrollToLocation?.({ - itemIndex: targetItemIndex, - sectionIndex: targetSectionIndex, - animated, - viewOffset, - viewPosition - }) - } + if (!scrollViewRef.current) return + // 通过索引映射表快速定位位置 + const position = indexMap.current[index] + if (!position) return + const [sectionIndex, itemIndex] = position.split('_') + const targetSectionIndex = Number(sectionIndex) || 0 + const targetItemIndex = itemIndex === 'header' + ? 0 + : itemIndex === 'footer' + ? convertedListData[targetSectionIndex].data.length + 1 + : Number(itemIndex) + 1 + scrollViewRef.current.scrollToLocation?.({ + itemIndex: targetItemIndex, + sectionIndex: targetSectionIndex, + animated, + viewOffset, + viewPosition + }) } const getItemHeight = ({ sectionIndex, rowIndex }: { sectionIndex: number, rowIndex: number }) => { @@ -219,30 +219,16 @@ const _SectionList = forwardRef((props = {}, ref) => { } } - const getSectionHeaderHeight = ({ sectionIndex }: { sectionIndex: number }) => { + const getSectionExtraHeight = ({ sectionIndex, type }: { sectionIndex: number, type: 'header' | 'footer' }) => { const item = convertedListData[sectionIndex] - const { hasSectionHeader } = item - // 使用getOriginalIndex获取原始索引 - const originalIndex = getOriginalIndex(sectionIndex, 'header') - if (!hasSectionHeader) return 0 - if ((sectionHeaderHeight as ItemHeightType).getter) { - return (sectionHeaderHeight as ItemHeightType).getter?.(item, originalIndex) || 0 - } else { - return (sectionHeaderHeight as ItemHeightType).value || 0 - } - } - - const getSectionFooterHeight = ({ sectionIndex }: { sectionIndex: number }) => { - const item = convertedListData[sectionIndex] - const { hasSectionFooter } = item - // 使用getOriginalIndex获取原始索引 - const originalIndex = getOriginalIndex(sectionIndex, 'footer') - if (!hasSectionFooter) return 0 - if ((sectionFooterHeight as ItemHeightType).getter) { - return (sectionFooterHeight as ItemHeightType).getter?.(item, originalIndex) || 0 - } else { - return (sectionFooterHeight as ItemHeightType).value || 0 + const isHeader = type === 'header' + if (!(isHeader ? item.hasSectionHeader : item.hasSectionFooter)) return 0 + const sectionExtraHeight = (isHeader ? sectionHeaderHeight : sectionFooterHeight) as ItemHeightType + if (sectionExtraHeight.getter) { + const sectionExtraData = isHeader ? item.headerData : item.footerData + return sectionExtraHeight.getter?.(sectionExtraData, getOriginalIndex(sectionIndex, type)) || 0 } + return sectionExtraHeight.value || 0 } const convertedListData = useMemo(() => { @@ -351,7 +337,7 @@ const _SectionList = forwardRef((props = {}, ref) => { // 遍历所有 sections convertedListData.forEach((section: Section, sectionIndex: number) => { // 添加 section header 的位置信息 - const headerHeight = getSectionHeaderHeight({ sectionIndex }) + const headerHeight = getSectionExtraHeight({ sectionIndex, type: 'header' }) layouts.push({ length: headerHeight, offset, @@ -372,7 +358,7 @@ const _SectionList = forwardRef((props = {}, ref) => { // 添加该 section 尾部位置信息 // 因为即使 sectionList 没传 renderSectionFooter,getItemLayout 中的 index 的计算也会包含尾部节点 - const footerHeight = getSectionFooterHeight({ sectionIndex }) + const footerHeight = getSectionExtraHeight({ sectionIndex, type: 'footer' }) layouts.push({ length: footerHeight, offset, From 78639542189b3dbcef4cc67e37907cded6ad7328 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Wed, 3 Jun 2026 19:25:51 +0800 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20=E5=86=85=E5=BB=BA=20sticky=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=B9=B6=E5=AE=8C=E5=96=84=20RN=20section-li?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 sticky-header、sticky-section 接入模板内建组件转换 - 调整 RN section-list 属性命名与头尾高度配置 - 同步更新扩展组件、RN 组件文档及 mpx2rn skill 参考 --- .../references/rn-template-reference.md | 56 ++++++ .../guide/extend/extend-component.md | 185 ++++++------------ docs-vitepress/guide/rn/component.md | 60 +++++- .../template/wx/component-config/index.js | 4 + .../wx/component-config/sticky-header.js | 23 +++ .../wx/component-config/sticky-section.js | 23 +++ .../components/react/mpx-section-list.tsx | 78 +++++--- .../lib/runtime/components/react/utils.tsx | 4 +- .../lib/template-compiler/compiler.js | 10 +- packages/webpack-plugin/lib/utils/const.js | 12 -- 10 files changed, 275 insertions(+), 180 deletions(-) create mode 100644 packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js create mode 100644 packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index 801d56e5b2..79b406e79b 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -58,6 +58,7 @@ - [video](#video) - [web-view](#web-view) - [root-portal](#root-portal) + - [section-list](#section-list) - [sticky-section](#sticky-section) - [sticky-header](#sticky-header) - [cover-view](#cover-view) @@ -1346,6 +1347,61 @@ level 有效值: - style 样式中不支持使用百分比计算、css variable +### section-list + +跨端虚拟列表组件,可自定义分组头、列表头、列表项,自动分段渲染兼容各端。 + +#### 属性 + +| 属性名 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| height | string \| number | `100%` | 组件高度 | +| width | string \| number | `100%` | 组件宽度 | +| list-data | array | `[]` | 列表数据;分组头数据需包含 `isSectionHeader: true`,分组尾数据需包含 `isSectionFooter: true` | +| enable-sticky | boolean | `false` | 启用分组吸顶 | +| scroll-event-throttle | number | `0` | 控制 scroll 事件触发频率 | +| enhanced | boolean | `false` | 开启滚动增强能力 | +| bounces | boolean | `true` | iOS 下边界弹性控制,需同时开启 `enhanced` | +| use-list-header | boolean | `false` | 使用自定义列表头 | +| list-header-data | object | `{}` | 列表头数据 | +| use-list-footer | boolean | `false` | 使用自定义列表页脚 | +| list-footer-data | object | `{}` | 列表页脚数据 | +| generic:recycle-item | string | | 列表项抽象节点组件名 | +| generic:section-header | string | | 列表分组头抽象节点组件名 | +| generic:section-footer | string | | 列表分组尾抽象节点组件名 | +| generic:list-header | string | | 列表头抽象节点组件名 | +| generic:list-footer | string | | 列表页脚抽象节点组件名 | +| item-height | object | `{}` | 列表项高度配置,支持 `getter` / `value` | +| section-header-height | object | `{}` | 分组头部高度配置,支持 `getter` / `value` | +| section-footer-height | object | `{}` | 分组尾部高度配置,支持 `getter` / `value` | +| list-header-height | number | `0` | 列表头部固定高度,不支持 `getter` / `value` | +| enable-back-to-top | boolean | `false` | 点击状态栏时滚动到顶部,仅 iOS 环境支持 | +| end-reached-threshold | number | `0.1` | 触底事件触发阈值 | +| refresher-enabled | boolean | `false` | 开启自定义下拉刷新 | +| refresher-triggered | boolean | `false` | 设置当前下拉刷新状态,true 表示已触发 | +| show-scrollbar | boolean | `true` | 滚动条显隐控制 | +| simultaneous-handlers | array\ | `[]` | RN 环境特有属性,允许多个手势同时识别和处理 | +| wait-for | array\ | `[]` | RN 环境特有属性,允许延迟激活处理某些手势 | + +#### 事件 + +| 事件名 | 说明 | +| --- | --- | +| bindscroll | 滚动时触发,返回滚动信息 | +| bindscrolltolower | 滚动到底部 / 触底通知 | +| bindrefresherrefresh | 自定义下拉刷新被触发 | + +#### 方法 + +| 方法名 | 说明 | +| --- | --- | +| scrollToIndex | 通过 ref 获取实例后可调用,`scrollToIndex({ index, animated, viewOffset, viewPosition })`,用于滚动到指定索引 | + +#### 注意事项 + +- 当使用列表项、列表头、自定义分组头或者自定义分组尾,必须配置对应 `item-height`、`section-header-height`、`section-footer-height`、`list-header-height` 高度参数,否则会出现滚动异常。 +- RN 环境中,section-list 通过 RN 的 `SectionList` 实现分组吸顶。开启 `enable-sticky` 且快速滑动时,自定义分组头有时会出现闪烁,属于 RN 底层实现限制。 + ### sticky-section 吸顶布局容器,仅支持作为 `` 的直接子节点 diff --git a/docs-vitepress/guide/extend/extend-component.md b/docs-vitepress/guide/extend/extend-component.md index c305916beb..11232ea726 100644 --- a/docs-vitepress/guide/extend/extend-component.md +++ b/docs-vitepress/guide/extend/extend-component.md @@ -19,36 +19,43 @@ Mpx 会根据当前编译的目标平台(wx/ali/web/ios/android/harmony), 跨端虚拟列表组件,可自定义分组头、列表头、列表项,自动分段渲染兼容各端。 -支持平台:微信小程序、支付宝小程序、Web、RN +支持平台:RN ### 属性 -| 属性名 | 类型 | 默认值 | 说明 | 支持平台 | -|-----------------------|-------------|----------|------------------------|-----------| -| height | String/Number | 100% | 组件高度 | 微信小程序、支付宝小程序、Web、RN | -| width | String/Number | 100% | 组件宽度 | 微信小程序、支付宝小程序、Web、RN | -| listData | Array | [] | 列表数据,如需使用列表分组头 `section-header`,对应 item 的数据需要包含 `isSectionHeader: true` 标识;如需使用列表分组尾 `section-footer`,对应 item 的数据需要包含 `isSectionFooter: true` 标识 | 微信小程序、支付宝小程序、Web、RN | -| enable-sticky | Boolean | false | 启用分组吸顶 | 微信小程序、支付宝小程序、Web、RN
⚠️微信小程序环境,需要使用 skyline 渲染模式,webview 模式不支持;web 环境仅支持移动端,不支持 pc 端 | -| scroll-with-animation | Boolean | false | 滚动动画 | 微信小程序、支付宝小程序、Web、RN | -| useListHeader | Boolean | false | 使用自定义列表头 | 微信小程序、支付宝小程序、Web、RN | -| listHeaderData | Object | {} | 列表头数据 | 微信小程序、支付宝小程序、Web、RN | -| useListFooter | Boolean | false | 使用自定义列表页脚 | 微信小程序、支付宝小程序、Web、RN | -| listFooterData | Object | {} | 列表页脚数据 | 微信小程序、支付宝小程序、Web、RN | -| generic:recycle-item | String | | 列表项,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | -| generic:section-header | String | | 列表分组头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | -| generic:section-footer | String | | 列表分组尾,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | -| generic:list-header | String | | 列表头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | -| generic:list-footer | String | | 列表页脚,抽象节点组件名,对应组件需要通过 usingComponents 注册 | 微信小程序、支付宝小程序、Web、RN | -| itemHeight | Object | {} | 列表项高度配置(支持 getter/value),必须配置 | 微信小程序、支付宝小程序、Web、RN | -| sectionHeaderHeight | Object | {} | 分组头部高度配置(getter/value),若使用了自定义分组头必须配置 | 微信小程序、支付宝小程序、Web、RN | -| sectionFooterHeight | Object | {} | 分组尾部高度配置(getter/value),若使用了自定义分组尾必须配置 | 微信小程序、支付宝小程序、Web、RN | -| listHeaderHeight | Object | {} | 列表头部高度配置(getter/value),若使用了列表头必须配置 | 微信小程序、支付宝小程序、Web、RN | -| bufferScale | Number | 1 | 渲染缓冲区行数(虚拟滚动优化) | 仅支付宝小程序/web支持 | -| minRenderCount | Number | 10 | 最小渲染项目数 | 仅支付宝小程序/web支持 | - -#### `itemHeight`/`sectionHeaderHeight`/`sectionFooterHeight`/`listHeaderHeight` 格式说明 - -高度相关属性支持如下格式: +| 属性名 | 类型 | 默认值 | 说明 | +|-----------------------|-------------|----------|------------------------| +| height | String/Number | 100% | 组件高度 | +| width | String/Number | 100% | 组件宽度 | +| list-data | Array | [] | 列表数据,如需使用列表分组头 `section-header`,对应 item 的数据需要包含 `isSectionHeader: true` 标识;如需使用列表分组尾 `section-footer`,对应 item 的数据需要包含 `isSectionFooter: true` 标识 | +| enable-sticky | Boolean | false | 启用分组吸顶 | +| scroll-event-throttle | Number | 0 | RN 环境特有属性,控制 scroll 事件触发频率 | +| enhanced | Boolean | false | RN 环境特有属性,开启滚动增强能力 | +| bounces | Boolean | true | RN 环境特有属性,iOS 下边界弹性控制,需同时开启 `enhanced` | +| use-list-header | Boolean | false | 使用自定义列表头 | +| list-header-data | Object | {} | 列表头数据 | +| use-list-footer | Boolean | false | 使用自定义列表页脚 | +| list-footer-data | Object | {} | 列表页脚数据 | +| generic:recycle-item | String | | 列表项,抽象节点组件名,对应组件需要通过 usingComponents 注册 | +| generic:section-header | String | | 列表分组头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | +| generic:section-footer | String | | 列表分组尾,抽象节点组件名,对应组件需要通过 usingComponents 注册 | +| generic:list-header | String | | 列表头,抽象节点组件名,对应组件需要通过 usingComponents 注册 | +| generic:list-footer | String | | 列表页脚,抽象节点组件名,对应组件需要通过 usingComponents 注册 | +| item-height | Object | {} | 列表项高度配置(支持 getter/value),必须配置 | +| section-header-height | Object | {} | 分组头部高度配置(getter/value),若使用了自定义分组头必须配置 | +| section-footer-height | Object | {} | 分组尾部高度配置(getter/value),若使用了自定义分组尾必须配置 | +| list-header-height | Number | 0 | 列表头部高度,若使用了列表头必须配置 | +| enable-back-to-top | Boolean | false | 点击状态栏时滚动到顶部,仅 iOS 环境支持 | +| end-reached-threshold | Number | 0.1 | 触底事件触发阈值 | +| refresher-enabled | Boolean | false | 开启自定义下拉刷新 | +| refresher-triggered | Boolean | false | 设置当前下拉刷新状态,true 表示已触发 | +| show-scrollbar | Boolean | true | 滚动条显隐控制 | +| simultaneous-handlers | Array\ | [] | RN 环境特有属性,允许多个手势同时识别和处理 | +| wait-for | Array\ | [] | RN 环境特有属性,允许延迟激活处理某些手势 | + +#### `item-height`/`section-header-height`/`section-footer-height` 格式说明 {#section-list-height-config} + +`item-height`、`section-header-height`、`section-footer-height` 支持如下格式: ```js height: { @@ -68,15 +75,23 @@ height: { > 建议性能要求较高(如超大数据集)优先使用 `value` 定高。 +`list-header-height` 仅支持 Number 类型,用于声明列表头部固定高度,不支持 `getter` / `value` 配置对象。 + ### 事件 -| 事件名 | 说明 | 支持平台 | -|-----------------------|-----------------------------------|--------------| -| bindscroll | 滚动时触发,返回滚动信息 | 微信小程序、支付宝小程序、Web、RN | -| bindscrolltolower | 滚动到底部/触底通知 | 微信小程序、支付宝小程序、Web、RN | -| bindscrollToIndex | 组件方法,滚动到指定索引 | 微信小程序、支付宝小程序、Web、RN | +| 事件名 | 说明 | +|-----------------------|-----------------------------------| +| bindscroll | 滚动时触发,返回滚动信息 | +| bindscrolltolower | 滚动到底部/触底通知 | +| bindrefresherrefresh | 自定义下拉刷新被触发 | -`scrollToIndex({ index, animated, viewPosition })` 参数说明: +### 方法 {#section-list-methods} + +| 方法名 | 说明 | +|-------|------| +| scrollToIndex | 滚动到指定索引 | + +`scrollToIndex({ index, animated, viewOffset, viewPosition })` 参数说明: - `index`:目标索引 - `animated`:是否滚动动画 - `viewOffset`:滚动偏移量 @@ -92,13 +107,12 @@ height: { generic:list-header="list-header" width="{{width}}" height="{{height}}" - listData="{{dataList}}" - itemHeight="{{ itemHeight }}" - sectionHeaderHeight="{{headerHeight}}" - sectionFooterHeight="{{footerHeight}}" - listHeaderHeight="{{listHeaderHeight}}" - bufferScale="{{bufferScale}}" - useListHeader="{{true}}" + list-data="{{dataList}}" + item-height="{{ itemHeight }}" + section-header-height="{{headerHeight}}" + section-footer-height="{{footerHeight}}" + list-header-height="{{listHeaderHeight}}" + use-list-header="{{true}}" enable-sticky="{{true}}" /> -``` - - -## sticky-header - -吸顶头部组件,支持在滚动容器中实现元素吸顶效果。仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 - -支持平台:微信小程序(仅 skyline 支持)、支付宝小程序、Web、RN - -### 属性 - -| 属性名 | 类型 | 默认值 | 说明 | 支持平台 | -|-------|------|--------|------|---------| -| offsetTop | Number | 0 | 吸顶距离顶部的偏移量 | 微信小程序、支付宝小程序、Web、RN | -| padding | Array | - | 内边距配置 [top, right, bottom, left] | 微信小程序、支付宝小程序、Web、RN | -| scrollViewId | String | '' | 滚动容器的 id, 支付宝环境必传, 值与选择器 id 值一致 | 支付宝小程序 | -| stickyId | String | '' | 吸顶元素的唯一标识,支付宝环境必传,值与选择器 id 值一致 | 支付宝小程序 | -| enablePolling | Boolean | false | 启用轮询刷新 | 支付宝小程序 | -| pollingDuration | Number | 300 | 轮询间隔时间(毫秒) | 支付宝小程序 | - -### 事件 - -| 事件名 | 说明 | 支持平台 | -|-------|------|---------| -| stickontopchange | 吸顶状态改变时触发,返回 { isStickOnTop, id } | 微信小程序、支付宝小程序、Web、RN | - -**注意**: -- 支付宝小程序中该功能基于 IntersectionObserver 实现,但在支付宝平台上,IntersectionObserver 的回调可能存在触发不及时或不触发的情况,进而导致 stickontopchange 事件无法及时触发,或 sticky-header 吸附位置异常。 - -为此我们提供了 enablePolling 属性。开启后将通过定时轮询的方式校验 sticky-header 当前吸附状态是否正确,若发现异常会自动进行修正。建议在支付宝平台根据实际情况按需开启该配置。 - -- RN 环境的 sticky-header 更适用于内容稳定,状态不常变更的场景使用,目前如果 sticky-header 还在动画过程中就触发组件更新(如在bindstickontopchange 回调中立刻更新 state)、scroll-view 内容高度由多变少、通过修改 scroll-into-view、scroll-top 让 scroll-view 滚动,以上场景在安卓上都可能会导致闪烁或抖动 - - -### 用法示例 - -```html - - - -``` \ No newline at end of file diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index 8fe13974f5..22261f6f40 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -5,7 +5,7 @@ ### 目录概览 {#directory-overview} - #### 基础组件 -**容器组件**:[view](#view) · [scroll-view](#scroll-view) · [swiper](#swiper) · [swiper-item](#swiper-item) · [movable-area](#movable-area) · [movable-view](#movable-view) · [root-portal](#root-portal) · [section-list](#section-list) · [cover-view](#cover-view) +**容器组件**:[view](#view) · [scroll-view](#scroll-view) · [swiper](#swiper) · [swiper-item](#swiper-item) · [movable-area](#movable-area) · [movable-view](#movable-view) · [root-portal](#root-portal) · [sticky-section](#sticky-section) · [sticky-header](#sticky-header) · [cover-view](#cover-view) **媒体组件**:[image](#image) · [video](#video) · [canvas](#canvas) @@ -736,6 +736,64 @@ API > > - style 样式不支持中使用百分比计算、css variable + +## sticky-section + +吸顶布局容器,仅支持作为 `` 的直接子节点 + +支持平台:微信小程序(仅 skyline 支持)、Web、RN + +### 用法示例 + +```html + +``` + +## sticky-header + +吸顶头部组件,支持在滚动容器中实现元素吸顶效果。仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 + +支持平台:微信小程序(仅 skyline 支持)、Web、RN + +### 属性 + +| 属性名 | 类型 | 默认值 | 说明 | 支持平台 | +|-------|------|--------|------|---------| +| offsetTop | Number | 0 | 吸顶距离顶部的偏移量 | 微信小程序、Web、RN | +| padding | Array | - | 内边距配置 [top, right, bottom, left] | 微信小程序、Web、RN | + +### 事件 + +| 事件名 | 说明 | 支持平台 | +|-------|------|---------| +| stickontopchange | 吸顶状态改变时触发,返回 { isStickOnTop, id } | 微信小程序、Web、RN | + +**注意**: +- RN 环境的 sticky-header 更适用于内容稳定,状态不常变更的场景使用,目前如果 sticky-header 还在动画过程中就触发组件更新(如在bindstickontopchange 回调中立刻更新 state)、scroll-view 内容高度由多变少、通过修改 scroll-into-view、scroll-top 让 scroll-view 滚动,以上场景在安卓上都可能会导致闪烁或抖动 + + +### 用法示例 + +```html + +``` + ### cover-view 视图容器。 功能同 [view 组件](#view),在 cover-view 中只能嵌套 cover-view 或 cover-image 组件。 diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js index 32e044fefc..b5a65229fb 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js @@ -42,6 +42,8 @@ const wxs = require('./wxs') const fixComponentName = require('./fix-component-name') const customBuiltInComponent = require('./custom-built-in-component') const rootPortal = require('./root-portal') +const stickyHeader = require('./sticky-header') +const stickySection = require('./sticky-section') /** * 未命中上方任一组件 test 的标签,仍须走 normalizeComponentRules 中的通用 @@ -140,6 +142,8 @@ module.exports = function getComponentConfigs ({ warn, error }) { hyphenTagName({ print }), label({ print }), rootPortal({ print }), + stickyHeader({ print }), + stickySection({ print }), defaultCatchAllComponentConfig() ] } diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js new file mode 100644 index 0000000000..382c15cd9f --- /dev/null +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js @@ -0,0 +1,23 @@ +const TAG_NAME = 'sticky-header' + +module.exports = function ({ print }) { + return { + test: TAG_NAME, + android (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-header' + }, + ios (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-header' + }, + harmony (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-header' + }, + web (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-header' + } + } +} \ No newline at end of file diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js new file mode 100644 index 0000000000..214b61441d --- /dev/null +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js @@ -0,0 +1,23 @@ +const TAG_NAME = 'sticky-section' + +module.exports = function ({ print }) { + return { + test: TAG_NAME, + android (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-section' + }, + ios (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-section' + }, + harmony (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-section' + }, + web (tag, { el }) { + el.isBuiltIn = true + return 'mpx-sticky-section' + } + } +} \ No newline at end of file diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index a6991c14ef..4ec10aa006 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -36,20 +36,20 @@ interface ItemHeightType { interface MpxSectionListProps { enhanced?: boolean; bounces?: boolean; - scrollEventThrottle?: number; height?: number | string; width?: number | string; - listData?: ListItem[]; generichash?: string; style?: Record; - itemHeight?: ItemHeightType; - sectionHeaderHeight?: ItemHeightType; - sectionFooterHeight?: ItemHeightType; - listHeaderData?: any; - listHeaderHeight?: ItemHeightType; - useListHeader?: boolean; - listFooterData?: any; - useListFooter?: boolean; + 'scroll-event-throttle'?: number; + 'list-data'?: ListItem[]; + 'item-height'?: ItemHeightType; + 'section-header-height'?: ItemHeightType; + 'section-footer-height'?: ItemHeightType; + 'list-header-data'?: any; + 'list-header-height'?: number; + 'use-list-header'?: boolean; + 'list-footer-data'?: any; + 'use-list-footer'?: boolean; 'genericrecycle-item'?: string; 'genericsection-header'?: string; 'genericsection-footer'?: string; @@ -97,20 +97,20 @@ const _SectionList = forwardRef((props = {}, ref) => { const { enhanced = false, bounces = true, - scrollEventThrottle = 0, height, width, - listData, generichash, style = {}, - itemHeight = {}, - sectionHeaderHeight = {}, - sectionFooterHeight = {}, - listHeaderHeight = {}, - listHeaderData = null, - useListHeader = false, - listFooterData = null, - useListFooter = false, + 'list-data': listData, + 'scroll-event-throttle': scrollEventThrottle = 0, + 'item-height': itemHeight = {}, + 'section-header-height': sectionHeaderHeight = {}, + 'section-footer-height': sectionFooterHeight = {}, + 'list-header-height': listHeaderHeight = 0, + 'list-header-data': listHeaderData = null, + 'use-list-header': useListHeader = false, + 'list-footer-data': listFooterData = null, + 'use-list-footer': useListFooter = false, 'genericrecycle-item': genericrecycleItem, 'genericsection-header': genericsectionHeader, 'genericsection-footer': genericsectionFooter, @@ -331,7 +331,7 @@ const _SectionList = forwardRef((props = {}, ref) => { if (useListHeader) { // 计算列表头部的高度 - offset += listHeaderHeight.getter?.() || listHeaderHeight.value || 0 + offset += listHeaderHeight } // 遍历所有 sections @@ -370,7 +370,7 @@ const _SectionList = forwardRef((props = {}, ref) => { itemLayouts: layouts, getItemLayout: (data: any, index: number) => layouts[index] } - }, [convertedListData, useListHeader, itemHeight.value, itemHeight.getter, sectionHeaderHeight.value, sectionHeaderHeight.getter, sectionFooterHeight.value, sectionFooterHeight.getter, listHeaderHeight.value, listHeaderHeight.getter]) + }, [convertedListData, useListHeader, itemHeight.value, itemHeight.getter, sectionHeaderHeight.value, sectionHeaderHeight.getter, sectionFooterHeight.value, sectionFooterHeight.getter, listHeaderHeight]) const scrollAdditionalProps = extendObject( { @@ -381,7 +381,7 @@ const _SectionList = forwardRef((props = {}, ref) => { showsHorizontalScrollIndicator: showScrollbar, onEndReachedThreshold, ref: scrollViewRef, - bounces: false, + bounces: enhanced ? bounces : false, stickySectionHeadersEnabled: enableSticky, onScroll: onScroll, onEndReached: onEndReached @@ -402,11 +402,6 @@ const _SectionList = forwardRef((props = {}, ref) => { return gesture }, [originSimultaneousHandlers, waitFor]) - if (enhanced) { - extendObject(scrollAdditionalProps, { - bounces - }) - } if (refresherEnabled) { extendObject(scrollAdditionalProps, { refreshing: refreshing @@ -414,19 +409,42 @@ const _SectionList = forwardRef((props = {}, ref) => { } useImperativeHandle(ref, () => { - return extendObject({}, props, { + return { gestureRef: sectionListGestureRef, scrollToIndex - }) + } }) const innerProps = useInnerProps(extendObject({}, props, scrollAdditionalProps), [ 'id', + 'enhanced', + 'height', + 'width', + 'list-data', + 'item-height', + 'section-header-height', + 'section-footer-height', + 'list-header-height', + 'list-header-data', + 'use-list-header', + 'list-footer-data', + 'use-list-footer', + 'genericrecycle-item', + 'genericsection-header', + 'genericsection-footer', + 'genericlist-header', + 'genericlist-footer', 'show-scrollbar', 'lower-threshold', + 'scroll-event-throttle', + 'enable-sticky', + 'enable-back-to-top', + 'end-reached-threshold', 'refresher-triggered', 'refresher-enabled', 'bindrefresherrefresh', + 'bindscrolltolower', + 'bindscroll', 'simultaneous-handlers', 'wait-for' ], { layoutRef }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index daefc133ad..fb2e80ff7a 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -816,7 +816,7 @@ export function usePrevious (value: T): T | undefined { export interface GestureHandler { nodeRefs?: Array<{ getNodeInstance: () => { nodeRef: unknown } }> current?: unknown - handlerTag?: Number + handlerTag?: number } export function flatGesture (gestures: Array = []) { @@ -825,7 +825,7 @@ export function flatGesture (gestures: Array = []) { return gesture.nodeRefs .map((item: { getNodeInstance: () => any }) => item.getNodeInstance()?.instance?.gestureRef || {}) } - if (gesture && ('current' in gesture || gesture.handlerTag !== undefined)) { + if (gesture && (gesture.current || gesture.handlerTag !== undefined)) { return [gesture] } return [] diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js index 87a487c3da..9024cb6e32 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -1,7 +1,7 @@ const JSON5 = require('json5') const he = require('he') const config = require('../config') -const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID, MPX_TAG_PAGE_SELECTOR, EXTEND_COMPONENT_CONFIG, MPX_TEMPLATE_COMPONENT_PREFIX, STYLE_PAD_PLACEHOLDER } = require('../utils/const') +const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID, MPX_TAG_PAGE_SELECTOR, MPX_TEMPLATE_COMPONENT_PREFIX, STYLE_PAD_PLACEHOLDER } = require('../utils/const') const normalize = require('../utils/normalize') const { normalizeCondition } = require('../utils/match-condition') const isValidIdentifierStr = require('../utils/is-valid-identifier-str') @@ -2471,11 +2471,7 @@ function isComponentNode (el) { // 处理模版时无法获取真实的usingComponents信息,除了小程序基础组件和框架内建组件外都识别为用户组件 return isRealNode(el) && !isNativeMiniTag(el.tag) && !el.isBuiltIn } - return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' || componentGenerics[el.tag] || isExtendComponentNode(el) -} - -function isExtendComponentNode (el) { - return EXTEND_COMPONENT_CONFIG[el.tag]?.[mode] + return usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component' || componentGenerics[el.tag] } function getComponentInfo (el) { @@ -2483,7 +2479,7 @@ function getComponentInfo (el) { } function isReactComponent (el) { - return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn && !isExtendComponentNode(el) + return !isComponentNode(el) && isRealNode(el) && !el.isBuiltIn } function processWebClass (classLikeAttrName, classLikeAttrValue, el, options, processingWebTemplate) { diff --git a/packages/webpack-plugin/lib/utils/const.js b/packages/webpack-plugin/lib/utils/const.js index 0b286d9b48..270708ad40 100644 --- a/packages/webpack-plugin/lib/utils/const.js +++ b/packages/webpack-plugin/lib/utils/const.js @@ -15,18 +15,6 @@ module.exports = { ios: `${reactComponentPath}/mpx-section-list.jsx`, android: `${reactComponentPath}/mpx-section-list.jsx`, harmony: `${reactComponentPath}/mpx-section-list.jsx` - }, - 'sticky-header': { - web: `${componentPrefixPath}/web/mpx-sticky-header.vue`, - ios: `${reactComponentPath}/mpx-sticky-header.jsx`, - android: `${reactComponentPath}/mpx-sticky-header.jsx`, - harmony: `${reactComponentPath}/mpx-sticky-header.jsx` - }, - 'sticky-section': { - web: `${componentPrefixPath}/web/mpx-sticky-section.vue`, - ios: `${reactComponentPath}/mpx-sticky-section.jsx`, - android: `${reactComponentPath}/mpx-sticky-section.jsx`, - harmony: `${reactComponentPath}/mpx-sticky-section.jsx` } }, MPX_TAG_PAGE_SELECTOR: 'mpx-page', From 5bfb8398de8faaae57ffbb17d8b53e8252a497df Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Wed, 3 Jun 2026 20:33:59 +0800 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=20RN=20sticky?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6=E4=B8=8E=20section-list=20=E5=A4=B4?= =?UTF-8?q?=E5=B0=BE=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 sticky-header/sticky-section 占位组件 - 更新 RN 组件文档中的 sticky-section/sticky-header 说明 - 调整 section-list 头尾组件数据传递,确保 listHeaderData/listFooterData 更新触发渲染 --- .../wx/component-config/sticky-header.js | 2 +- .../wx/component-config/sticky-section.js | 2 +- .../components/extends/sticky-header.mpx | 1 - .../components/extends/sticky-section.mpx | 1 - .../components/react/mpx-section-list.tsx | 35 +++++++++++-------- 5 files changed, 22 insertions(+), 19 deletions(-) delete mode 100644 packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx delete mode 100644 packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js index 382c15cd9f..e698892a6a 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-header.js @@ -20,4 +20,4 @@ module.exports = function ({ print }) { return 'mpx-sticky-header' } } -} \ No newline at end of file +} diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js index 214b61441d..150dd6122c 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/sticky-section.js @@ -20,4 +20,4 @@ module.exports = function ({ print }) { return 'mpx-sticky-section' } } -} \ No newline at end of file +} diff --git a/packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx b/packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx deleted file mode 100644 index fa855fa0f3..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/extends/sticky-header.mpx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx b/packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx deleted file mode 100644 index 1c1842aa59..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/extends/sticky-section.mpx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index 4ec10aa006..61cd7ed760 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -449,13 +449,6 @@ const _SectionList = forwardRef((props = {}, ref) => { 'wait-for' ], { layoutRef }) - // 使用 ref 保存最新的数据,避免数据变化时组件销毁重建 - const listHeaderDataRef = useRef(listHeaderData) - listHeaderDataRef.current = listHeaderData - - const listFooterDataRef = useRef(listFooterData) - listFooterDataRef.current = listFooterData - // 使用 useMemo 获取 GenericComponent 并创建渲染函数,避免每次组件更新都重新创建函数引用导致不必要的重新渲染 const renderItem = useMemo( () => { @@ -490,26 +483,38 @@ const _SectionList = forwardRef((props = {}, ref) => { [generichash, genericsectionFooter] ) - const ListHeaderComponent = useMemo( + const ListHeaderGenericComponent = useMemo( () => { if (!useListHeader) return null - const ListHeaderGenericComponent = getGeneric(generichash, genericListHeader) - if (!ListHeaderGenericComponent) return null - return () => createElement(ListHeaderGenericComponent, { listHeaderData: listHeaderDataRef.current }) + return getGeneric(generichash, genericListHeader) }, [useListHeader, generichash, genericListHeader] ) - const ListFooterComponent = useMemo( + const ListFooterGenericComponent = useMemo( () => { if (!useListFooter) return null - const ListFooterGenericComponent = getGeneric(generichash, genericListFooter) - if (!ListFooterGenericComponent) return null - return () => createElement(ListFooterGenericComponent, { listFooterData: listFooterDataRef.current }) + return getGeneric(generichash, genericListFooter) }, [useListFooter, generichash, genericListFooter] ) + const ListHeaderComponent = useMemo( + () => { + if (!ListHeaderGenericComponent) return null + return createElement(ListHeaderGenericComponent, { listHeaderData }) + }, + [ListHeaderGenericComponent, listHeaderData] + ) + + const ListFooterComponent = useMemo( + () => { + if (!ListFooterGenericComponent) return null + return createElement(ListFooterGenericComponent, { listFooterData }) + }, + [ListFooterGenericComponent, listFooterData] + ) + const sectionListProps: RNSectionListProps = extendObject( { style: [{ height, width }, style, layoutStyle], From a0a478ac796fa13082850c41654ec5326dc512aa Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Wed, 3 Jun 2026 20:37:09 +0800 Subject: [PATCH 14/20] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0sticky=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs-vitepress/guide/rn/component.md | 68 ++++++++-------------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index 22261f6f40..7efcef6255 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -736,63 +736,33 @@ API > > - style 样式不支持中使用百分比计算、css variable - -## sticky-section - +### sticky-section 吸顶布局容器,仅支持作为 `` 的直接子节点 -支持平台:微信小程序(仅 skyline 支持)、Web、RN - -### 用法示例 - -```html - -``` - -## sticky-header - -吸顶头部组件,支持在滚动容器中实现元素吸顶效果。仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 - -支持平台:微信小程序(仅 skyline 支持)、Web、RN - -### 属性 +> [!tip] 注意 +> +> - sticky-section 目前仅支持 RN 、web 以及微信小程序环境,其他环境暂不支持。微信小程序中使用需开启 skyline 渲染模式 -| 属性名 | 类型 | 默认值 | 说明 | 支持平台 | -|-------|------|--------|------|---------| -| offsetTop | Number | 0 | 吸顶距离顶部的偏移量 | 微信小程序、Web、RN | -| padding | Array | - | 内边距配置 [top, right, bottom, left] | 微信小程序、Web、RN | +### sticky-header +吸顶布局容器,仅支持作为 `` 的直接子节点或 `sticky-section` 组件直接子节点 -### 事件 +属性 -| 事件名 | 说明 | 支持平台 | -|-------|------|---------| -| stickontopchange | 吸顶状态改变时触发,返回 { isStickOnTop, id } | 微信小程序、Web、RN | +| 属性名 | 类型 | 默认值 | 说明 | +| ----------------------- | ------- | ------------- | ---------------------------------------------------------- | +| offset-top | number | `0` | 吸顶时与顶部的距离 | +| padding | array | `[0, 0, 0, 0] ` | 长度为 4 的数组,按 top、right、bottom、left 顺序指定内边距 | -**注意**: -- RN 环境的 sticky-header 更适用于内容稳定,状态不常变更的场景使用,目前如果 sticky-header 还在动画过程中就触发组件更新(如在bindstickontopchange 回调中立刻更新 state)、scroll-view 内容高度由多变少、通过修改 scroll-into-view、scroll-top 让 scroll-view 滚动,以上场景在安卓上都可能会导致闪烁或抖动 - +事件 -### 用法示例 +| 事件名 | 说明 | +| ----------------| --------------------------------------------------- | +| bindstickontopchange | 吸顶状态变化事件, `event.detail = { isStickOnTop }`,当 sticky-header 吸顶时为 true,否则为 false | -```html - -``` +> [!tip] 注意 +> +> - sticky-header 目前仅支持 RN 、web 以及微信小程序环境,其他环境暂不支持。微信小程序中使用需开启 skyline 渲染模式 +> - RN 环境的 sticky-header 更适用于内容稳定,状态不常变更的场景使用,目前如果 sticky 还在动画过程中就触发组件更新(如在bindstickontopchange 回调中立刻更新 state)、scroll-view 内容高度由多变少、通过修改 scroll-into-view、scroll-top 让 scroll-view 滚动,以上场景在安卓上都可能会导致闪烁或抖动 ### cover-view 视图容器。 From 35f15576691bd1c6dc306b9e5a4135a61f89d801 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 4 Jun 2026 15:31:04 +0800 Subject: [PATCH 15/20] =?UTF-8?q?refactor(webpack-plugin):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84extendComponentPlugin=E6=B3=A8=E5=86=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webpack-plugin/lib/index.js | 6 +- .../lib/resolver/AddEnvPlugin.js | 2 +- .../lib/resolver/AddModePlugin.js | 2 +- .../lib/resolver/ExtendComponentsPlugin.js | 56 +++++++++++-------- packages/webpack-plugin/lib/utils/const.js | 10 ---- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index 899c5c8edc..b81a52dc56 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -405,12 +405,13 @@ class MpxWebpackPlugin { const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file') const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, this.options.normalNpmPackages, 'file') const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules) - const extendComponentsPlugin = new ExtendComponentsPlugin('described-resolve', this.options.mode, 'resolve') + const extendComponentsPlugin = new ExtendComponentsPlugin('before-file', this.options.mode, 'resolve') if (Array.isArray(compiler.options.resolve.plugins)) { + compiler.options.resolve.plugins.push(extendComponentsPlugin) compiler.options.resolve.plugins.push(addModePlugin) } else { - compiler.options.resolve.plugins = [addModePlugin] + compiler.options.resolve.plugins = [extendComponentsPlugin, addModePlugin] } if (this.options.env) { compiler.options.resolve.plugins.push(addEnvPlugin) @@ -420,7 +421,6 @@ class MpxWebpackPlugin { } compiler.options.resolve.plugins.push(packageEntryPlugin) compiler.options.resolve.plugins.push(new FixDescriptionInfoPlugin()) - compiler.options.resolve.plugins.push(extendComponentsPlugin) compiler.options.resolve.plugins.push(dynamicPlugin) const optimization = compiler.options.optimization diff --git a/packages/webpack-plugin/lib/resolver/AddEnvPlugin.js b/packages/webpack-plugin/lib/resolver/AddEnvPlugin.js index c11691cc60..8c1ea3ffb0 100644 --- a/packages/webpack-plugin/lib/resolver/AddEnvPlugin.js +++ b/packages/webpack-plugin/lib/resolver/AddEnvPlugin.js @@ -19,7 +19,7 @@ module.exports = class AddEnvPlugin { const envPattern = new RegExp(`\\.${env}(\\.|$)`) resolver.getHook(this.source).tapAsync('AddEnvPlugin', (request, resolveContext, callback) => { - if (request.env) { + if (request.__mpxResolvedExtendComponent || request.env) { return callback() } const obj = { diff --git a/packages/webpack-plugin/lib/resolver/AddModePlugin.js b/packages/webpack-plugin/lib/resolver/AddModePlugin.js index 88098f233f..446e5bfdbf 100644 --- a/packages/webpack-plugin/lib/resolver/AddModePlugin.js +++ b/packages/webpack-plugin/lib/resolver/AddModePlugin.js @@ -21,7 +21,7 @@ module.exports = class AddModePlugin { const defaultModePattern = new RegExp(`\\.${defaultMode}(\\.|$)`) resolver.getHook(this.source).tapAsync('AddModePlugin', (request, resolveContext, callback) => { - if (request.mode || request.env) { + if (request.__mpxResolvedExtendComponent || request.mode || request.env) { return callback() } const obj = { diff --git a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js index 1e58225e81..3e62043d69 100644 --- a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js +++ b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js @@ -1,4 +1,9 @@ -const { EXTEND_COMPONENT_CONFIG } = require('../utils/const') +const toPosix = require('../utils/to-posix') +const EXTEND_COMPONENT_RELATIVE_PATH = './lib/runtime/components/extends/' +const EXTEND_COMPONENT_TARGET_PATH = '@mpxjs/webpack-plugin/lib/runtime/components/react/dist' +const EXTEND_COMPONENTS = { + 'section-list': ['ios', 'android', 'harmony'] +} /** * 扩展组件路径解析插件 @@ -17,44 +22,49 @@ module.exports = class ExtendComponentsPlugin { const mode = this.mode resolver.getHook(this.source).tapAsync('ExtendComponentsPlugin', (request, resolveContext, callback) => { - const requestPath = request.request - if (!requestPath || !requestPath.startsWith('@mpxjs/webpack-plugin/lib/runtime/components/extends/')) { - return callback() - } - - // 匹配 @mpxjs/webpack-plugin/lib/runtime/components/extends/[component-name] - const extendsMatch = requestPath.match(/^@mpxjs\/webpack-plugin\/lib\/runtime\/components\/extends\/(.+)$/) - - if (!extendsMatch) { + const componentName = getComponentName(request) + if (!componentName) { return callback() } - const componentName = extendsMatch[1] - // 检查组件是否在配置中 - if (!EXTEND_COMPONENT_CONFIG[componentName]) { - return callback(new Error(`Extended component "${componentName}" was not found. Available extended components: ${Object.keys(EXTEND_COMPONENT_CONFIG).join(', ')}`)) + const supportedModes = EXTEND_COMPONENTS[componentName] + if (!supportedModes) { + return callback(new Error(`Extended component "${componentName}" was not found. Available extended components: ${Object.keys(EXTEND_COMPONENTS).join(', ')}`)) } // 获取当前模式下的组件路径 - const componentConfig = EXTEND_COMPONENT_CONFIG[componentName] - const newRequest = componentConfig[mode] - - if (!newRequest) { - return callback(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${Object.keys(componentConfig).join(', ')}`)) + if (!supportedModes.includes(mode)) { + return callback(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${supportedModes.join(', ')}`)) } + const newRequest = `${EXTEND_COMPONENT_TARGET_PATH}/mpx-${componentName}.jsx` - const obj = Object.assign({}, request, { - request: newRequest + const redirectRequest = Object.assign({}, request, { + request: newRequest, + fullySpecified: false, + __mpxResolvedExtendComponent: true }) resolver.doResolve( target, - obj, + redirectRequest, `resolve extend component: ${componentName} to ${newRequest}`, resolveContext, - callback + (err, result) => { + if (err) return callback(err) + if (!result) return callback(new Error(`Extended component "${componentName}" resolved to "${newRequest}", but the target file was not found.`)) + callback(null, result) + } ) }) } } + +function getComponentName (request) { + const descriptionFileData = request.descriptionFileData + const relativePath = request.relativePath && toPosix(request.relativePath) + + if (!descriptionFileData || descriptionFileData.name !== '@mpxjs/webpack-plugin' || !relativePath || !relativePath.startsWith(EXTEND_COMPONENT_RELATIVE_PATH)) return + + return relativePath.slice(EXTEND_COMPONENT_RELATIVE_PATH.length).replace(/\.[^/.]+$/, '') +} diff --git a/packages/webpack-plugin/lib/utils/const.js b/packages/webpack-plugin/lib/utils/const.js index 270708ad40..86f414d5a1 100644 --- a/packages/webpack-plugin/lib/utils/const.js +++ b/packages/webpack-plugin/lib/utils/const.js @@ -1,6 +1,3 @@ -const componentPrefixPath = '@mpxjs/webpack-plugin/lib/runtime/components' -const reactComponentPath = `${componentPrefixPath}/react/dist` - module.exports = { MPX_PROCESSED_FLAG: 'mpx_processed', MPX_DISABLE_EXTRACTOR_CACHE: 'mpx_disable_extractor_cache', @@ -10,13 +7,6 @@ module.exports = { MPX_ROOT_VIEW: 'mpx-root-view', // 根节点类名 MPX_APP_MODULE_ID: 'mpx-app-scope', // app文件moduleId PARENT_MODULE_ID: '__pid', - EXTEND_COMPONENT_CONFIG: { - 'section-list': { - ios: `${reactComponentPath}/mpx-section-list.jsx`, - android: `${reactComponentPath}/mpx-section-list.jsx`, - harmony: `${reactComponentPath}/mpx-section-list.jsx` - } - }, MPX_TAG_PAGE_SELECTOR: 'mpx-page', // web / template is:具名 wx 模版子组件标签前缀(与 compiler 中 AST 替换一致) MPX_TEMPLATE_COMPONENT_PREFIX: 'mpx-tpl-', From 88293aabc355073ab739ef7d7b438f54399ae47f Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 4 Jun 2026 21:43:04 +0800 Subject: [PATCH 16/20] refactor(webpack-plugin): remove unused section-list original inde --- .../runtime/components/react/mpx-section-list.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index 61cd7ed760..e2fa793b7a 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -8,7 +8,6 @@ import { extendObject, useLayout, useTransformStyle, GestureHandler, flatGesture interface ListItem { isSectionHeader?: boolean; isSectionFooter?: boolean; - _originalItemIndex?: number; [key: string]: any; } @@ -17,7 +16,6 @@ interface SectionExtra { footerData: ListItem | null; hasSectionHeader?: boolean; hasSectionFooter?: boolean; - _originalItemIndex?: number; } interface Section extends SectionExtra { @@ -256,8 +254,7 @@ const _SectionList = forwardRef((props = {}, ref) => { footerData: null, data: [], hasSectionHeader: true, - hasSectionFooter: false, - _originalItemIndex: index + hasSectionFooter: false } // 为 section header 添加索引映射 const sectionIndex = sections.length @@ -273,8 +270,7 @@ const _SectionList = forwardRef((props = {}, ref) => { footerData: null, data: [], hasSectionHeader: false, - hasSectionFooter: false, - _originalItemIndex: -1 + hasSectionFooter: false } } const sectionIndex = sections.length @@ -294,15 +290,12 @@ const _SectionList = forwardRef((props = {}, ref) => { footerData: null, data: [], hasSectionHeader: false, - hasSectionFooter: false, - _originalItemIndex: -1 + hasSectionFooter: false } } // 将 item 添加到当前 section 的 data 中 const itemIndex = currentSection.data.length - currentSection.data.push(extendObject({}, item, { - _originalItemIndex: index - })) + currentSection.data.push(item) let sectionIndex // 为 item 添加索引映射 - 存储格式为: "sectionIndex_itemIndex" if (!currentSection.hasSectionHeader && sections.length === 0) { From 4d05fe5e52725b5b92a4923927327dca427108ce Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Fri, 5 Jun 2026 13:57:02 +0800 Subject: [PATCH 17/20] =?UTF-8?q?=20fix(webpack-plugin):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20mpx-section-list=20height/width=20=E4=B8=8D?= =?UTF-8?q?=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 组合 style 移入 scrollAdditionalProps,避免被 innerProps.style 覆盖导致 height/width/layoutStyle 丢失 - 对齐 mpx-scroll-view,未显式设置 flex/flexGrow 时默认追加 flexGrow: 0, 避免 RN ScrollView baseStyle 的 flexGrow: 1 把 height 拉伸成 100% --- .../lib/runtime/components/react/mpx-section-list.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index e2fa793b7a..0bf94dbde9 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -3,6 +3,7 @@ import type { ComponentType } from 'react' import { SectionList, RefreshControl, NativeSyntheticEvent, NativeScrollEvent } from 'react-native' import type { SectionListData, SectionListProps as RNSectionListProps } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import { hasOwn } from '@mpxjs/utils' import useInnerProps, { getCustomEvent } from './getInnerListeners' import { extendObject, useLayout, useTransformStyle, GestureHandler, flatGesture } from './utils' interface ListItem { @@ -367,6 +368,12 @@ const _SectionList = forwardRef((props = {}, ref) => { const scrollAdditionalProps = extendObject( { + style: [ + hasOwn(style, 'flex') || hasOwn(style, 'flexGrow') ? null : { flexGrow: 0 }, + { height, width }, + style, + layoutStyle + ], alwaysBounceVertical: false, alwaysBounceHorizontal: false, scrollEventThrottle: scrollEventThrottle, @@ -510,7 +517,6 @@ const _SectionList = forwardRef((props = {}, ref) => { const sectionListProps: RNSectionListProps = extendObject( { - style: [{ height, width }, style, layoutStyle], sections: convertedListData, renderItem: renderItem, getItemLayout: getItemLayout, From 7d78e918419602c885b0ba3db930656f084b547b Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Fri, 5 Jun 2026 18:27:43 +0800 Subject: [PATCH 18/20] =?UTF-8?q?fix(webpack-plugin):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=20=20=20ExtendComponentsPlugin=20=E5=88=B0=20file=20=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ExtendComponentsPlugin 从 before-file -> resolve 改为 before-file -> file - 在 file 阶段直接重定向扩展组件的 path/ relativePath - 跳过已处理的扩展组件请求,避免重复解析 --- packages/webpack-plugin/lib/index.js | 2 +- .../lib/resolver/ExtendComponentsPlugin.js | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index b81a52dc56..fb15e70301 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -405,7 +405,7 @@ class MpxWebpackPlugin { const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file') const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, this.options.normalNpmPackages, 'file') const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules) - const extendComponentsPlugin = new ExtendComponentsPlugin('before-file', this.options.mode, 'resolve') + const extendComponentsPlugin = new ExtendComponentsPlugin('before-file', this.options.mode, 'file') if (Array.isArray(compiler.options.resolve.plugins)) { compiler.options.resolve.plugins.push(extendComponentsPlugin) diff --git a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js index 3e62043d69..2c7b81a0a9 100644 --- a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js +++ b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js @@ -1,6 +1,7 @@ +const path = require('path') const toPosix = require('../utils/to-posix') const EXTEND_COMPONENT_RELATIVE_PATH = './lib/runtime/components/extends/' -const EXTEND_COMPONENT_TARGET_PATH = '@mpxjs/webpack-plugin/lib/runtime/components/react/dist' +const EXTEND_COMPONENT_TARGET_SUB_PATH = 'lib/runtime/components/react/dist/' const EXTEND_COMPONENTS = { 'section-list': ['ios', 'android', 'harmony'] } @@ -22,6 +23,8 @@ module.exports = class ExtendComponentsPlugin { const mode = this.mode resolver.getHook(this.source).tapAsync('ExtendComponentsPlugin', (request, resolveContext, callback) => { + if (request.__mpxResolvedExtendComponent) return callback() + const componentName = getComponentName(request) if (!componentName) { return callback() @@ -37,22 +40,24 @@ module.exports = class ExtendComponentsPlugin { if (!supportedModes.includes(mode)) { return callback(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${supportedModes.join(', ')}`)) } - const newRequest = `${EXTEND_COMPONENT_TARGET_PATH}/mpx-${componentName}.jsx` + const targetSubPath = `${EXTEND_COMPONENT_TARGET_SUB_PATH}mpx-${componentName}.jsx` + const targetRelativePath = `./${targetSubPath}` + const targetPath = path.join(request.descriptionFileRoot, targetSubPath) const redirectRequest = Object.assign({}, request, { - request: newRequest, - fullySpecified: false, + path: targetPath, + relativePath: targetRelativePath, __mpxResolvedExtendComponent: true }) resolver.doResolve( target, redirectRequest, - `resolve extend component: ${componentName} to ${newRequest}`, + `resolve extend component: ${componentName} to ${targetPath}`, resolveContext, (err, result) => { if (err) return callback(err) - if (!result) return callback(new Error(`Extended component "${componentName}" resolved to "${newRequest}", but the target file was not found.`)) + if (!result) return callback(new Error(`Extended component "${componentName}" resolved to "${targetPath}", but the target file was not found.`)) callback(null, result) } ) From f58fe1576d3f34b4db8af57a8c8d34b3b7866d7c Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 15 Jun 2026 11:50:50 +0800 Subject: [PATCH 19/20] =?UTF-8?q?=20fix(webpack-plugin):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20ExtendComponentsPlugin=20=20=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E4=B8=8E=E7=BB=84=E4=BB=B6=E5=90=8D=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webpack-plugin/lib/index.js | 2 +- .../lib/resolver/ExtendComponentsPlugin.js | 40 +++++++++++++------ .../components/extends/section-list.mpx | 15 ++++++- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/webpack-plugin/lib/index.js b/packages/webpack-plugin/lib/index.js index fb15e70301..c6b092d6ab 100644 --- a/packages/webpack-plugin/lib/index.js +++ b/packages/webpack-plugin/lib/index.js @@ -405,7 +405,7 @@ class MpxWebpackPlugin { const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file') const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, this.options.normalNpmPackages, 'file') const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules) - const extendComponentsPlugin = new ExtendComponentsPlugin('before-file', this.options.mode, 'file') + const extendComponentsPlugin = new ExtendComponentsPlugin('before-file', this.options.mode, 'file', compiler) if (Array.isArray(compiler.options.resolve.plugins)) { compiler.options.resolve.plugins.push(extendComponentsPlugin) diff --git a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js index 2c7b81a0a9..48ae934c87 100644 --- a/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js +++ b/packages/webpack-plugin/lib/resolver/ExtendComponentsPlugin.js @@ -1,6 +1,6 @@ const path = require('path') const toPosix = require('../utils/to-posix') -const EXTEND_COMPONENT_RELATIVE_PATH = './lib/runtime/components/extends/' +const EXTEND_COMPONENT_PATH_REGEXP = /\/lib\/runtime\/components\/extends\// const EXTEND_COMPONENT_TARGET_SUB_PATH = 'lib/runtime/components/react/dist/' const EXTEND_COMPONENTS = { 'section-list': ['ios', 'android', 'harmony'] @@ -12,16 +12,26 @@ const EXTEND_COMPONENTS = { * 解析为对应平台的实际组件路径 */ module.exports = class ExtendComponentsPlugin { - constructor (source, mode, target) { + constructor (source, mode, target, compiler) { this.source = source this.target = target this.mode = mode + this.currentCompilation = null + compiler.hooks.thisCompilation.tap('ExtendComponentsPlugin', (compilation) => { + this.currentCompilation = compilation + }) } apply (resolver) { const target = resolver.ensureHook(this.target) const mode = this.mode + const pushError = (err) => { + if (this.currentCompilation) { + this.currentCompilation.errors.push(err) + } + } + resolver.getHook(this.source).tapAsync('ExtendComponentsPlugin', (request, resolveContext, callback) => { if (request.__mpxResolvedExtendComponent) return callback() @@ -33,12 +43,14 @@ module.exports = class ExtendComponentsPlugin { // 检查组件是否在配置中 const supportedModes = EXTEND_COMPONENTS[componentName] if (!supportedModes) { - return callback(new Error(`Extended component "${componentName}" was not found. Available extended components: ${Object.keys(EXTEND_COMPONENTS).join(', ')}`)) + pushError(new Error(`Extended component "${componentName}" was not found. Available extended components: ${Object.keys(EXTEND_COMPONENTS).join(', ')}`)) + return callback() } // 获取当前模式下的组件路径 if (!supportedModes.includes(mode)) { - return callback(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${supportedModes.join(', ')}`)) + pushError(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${supportedModes.join(', ')}`)) + return callback() } const targetSubPath = `${EXTEND_COMPONENT_TARGET_SUB_PATH}mpx-${componentName}.jsx` const targetRelativePath = `./${targetSubPath}` @@ -56,8 +68,14 @@ module.exports = class ExtendComponentsPlugin { `resolve extend component: ${componentName} to ${targetPath}`, resolveContext, (err, result) => { - if (err) return callback(err) - if (!result) return callback(new Error(`Extended component "${componentName}" resolved to "${targetPath}", but the target file was not found.`)) + if (err) { + pushError(err) + return callback() + } + if (!result) { + pushError(new Error(`Extended component "${componentName}" resolved to "${targetPath}", but the target file was not found.`)) + return callback() + } callback(null, result) } ) @@ -66,10 +84,8 @@ module.exports = class ExtendComponentsPlugin { } function getComponentName (request) { - const descriptionFileData = request.descriptionFileData - const relativePath = request.relativePath && toPosix(request.relativePath) - - if (!descriptionFileData || descriptionFileData.name !== '@mpxjs/webpack-plugin' || !relativePath || !relativePath.startsWith(EXTEND_COMPONENT_RELATIVE_PATH)) return - - return relativePath.slice(EXTEND_COMPONENT_RELATIVE_PATH.length).replace(/\.[^/.]+$/, '') + if (!request.path) return + const requestPath = toPosix(request.path) + if (!EXTEND_COMPONENT_PATH_REGEXP.test(requestPath)) return + return path.basename(requestPath, path.extname(requestPath)) } diff --git a/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx b/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx index 549d19aba8..6f515e863b 100644 --- a/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx +++ b/packages/webpack-plugin/lib/runtime/components/extends/section-list.mpx @@ -1 +1,14 @@ - + + + + From c7b288fc50ac01a46f5fc7d3402ae3caf137341a Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 15 Jun 2026 13:29:27 +0800 Subject: [PATCH 20/20] =?UTF-8?q?refactor(webpack-plugin):=20=E7=AE=80?= =?UTF-8?q?=E5=8C=96=20mpx-section-list=20=E7=BB=84=E4=BB=B6=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getGeneric 去掉多余的 memo(forwardRef(...)) 二次包装,直接返回 generics map 里的组件 - refreshing 改为派生值,删除 useState + useEffect 同步外部 prop 的镜像写法,避免多一次 commit - refresher-enabled 时的 refreshing 字段合并进 scrollAdditionalProps 主对象,移除事后 mutate --- .../components/react/mpx-section-list.tsx | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx index 0bf94dbde9..39a38b1cfa 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-section-list.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useRef, useState, useEffect, useMemo, createElement, useImperativeHandle, memo } from 'react' +import { forwardRef, useRef, useMemo, createElement, useImperativeHandle } from 'react' import type { ComponentType } from 'react' import { SectionList, RefreshControl, NativeSyntheticEvent, NativeScrollEvent } from 'react-native' import type { SectionListData, SectionListProps as RNSectionListProps } from 'react-native' @@ -82,14 +82,7 @@ interface ScrollPositionParams { const getGeneric = (generichash: string, generickey: string) => { if (!generichash || !generickey) return null - const GenericComponent = global.__mpxGenericsMap?.[generichash]?.[generickey]?.() - if (!GenericComponent) return null - - return memo(forwardRef((props: any, ref: any) => { - return createElement(GenericComponent, extendObject({}, { - ref: ref - }, props)) - })) + return global.__mpxGenericsMap?.[generichash]?.[generickey]?.() || null } const _SectionList = forwardRef((props = {}, ref) => { @@ -130,7 +123,7 @@ const _SectionList = forwardRef((props = {}, ref) => { 'wait-for': waitFor } = props - const [refreshing, setRefreshing] = useState(!!refresherTriggered) + const refreshing = !!refresherTriggered const scrollViewRef = useRef(null) const sectionListGestureRef = useRef() @@ -147,12 +140,6 @@ const _SectionList = forwardRef((props = {}, ref) => { const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef }) - useEffect(() => { - if (refreshing !== refresherTriggered) { - setRefreshing(!!refresherTriggered) - } - }, [refresherTriggered]) - const onRefresh = () => { const { bindrefresherrefresh } = props bindrefresherrefresh && @@ -386,6 +373,7 @@ const _SectionList = forwardRef((props = {}, ref) => { onScroll: onScroll, onEndReached: onEndReached }, + refresherEnabled ? { refreshing } : null, layoutProps ) @@ -402,12 +390,6 @@ const _SectionList = forwardRef((props = {}, ref) => { return gesture }, [originSimultaneousHandlers, waitFor]) - if (refresherEnabled) { - extendObject(scrollAdditionalProps, { - refreshing: refreshing - }) - } - useImperativeHandle(ref, () => { return { gestureRef: sectionListGestureRef,