Skip to content

Commit 98c3995

Browse files
authored
Merge pull request #2474 from didi/inner-master
Inner master
2 parents 64fb4c9 + 5315b19 commit 98c3995

7 files changed

Lines changed: 665 additions & 71 deletions

File tree

.agents/skills/mpx-rn-dev-guide/SKILL.md

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ Mpx 是一个以微信小程序语法为基础、进行了类 Vue 语法拓展
4444

4545
无论是适配改造还是新建组件,跨端兼容均需严格遵循以下通用约束:
4646

47+
### 跨平台兼容约束
48+
49+
产物代码须在原平台与 RN 平台均能正常运行。引入「RN 支持但原平台不支持」的写法(如 `numberOfLines@ios|android|harmony` / `hairlineWidth` 等 RN 等效实现)时,**不要替换原平台已有写法**,而应:
50+
51+
- 用条件编译将该 RN 写法限定在 RN 平台输出(模板属性后缀 `@ios|android|harmony`、样式与脚本 `@mpx-if` 包裹等);
52+
- 同步用条件编译保留原平台原有写法,避免改造引入原平台行为退化。
53+
54+
该原则贯穿模板 / 脚本 / 样式 / JSON 四个维度,下方各约束与样式约束 #3 的「双轨」条目即此原则的具体落地。
55+
4756
### 模板(template)约束
4857

4958
1. **基础组件优先**:使用 [模板能力参考 · 基础组件](./references/rn-template-reference.md#基础组件) 中标注 RN 支持的基础组件与其支持属性/事件,不要使用 RN 不支持的属性或事件;如用户通过 `rnConfig.customBuiltInComponents` 编译配置扩充拓展了基础组件能力,以用户说明为准。
@@ -67,17 +76,20 @@ Mpx 是一个以微信小程序语法为基础、进行了类 Vue 语法拓展
6776
2. **优先使用模板指令进行动态样式绑定**:使用 `wx:class` / `wx:style` 指令,避免在 `class` / `style` 属性内拼接 `{{}}` 插值表达式。
6877
- **Bad Example**: `<view class="item {{isActive ? 'active' : ''}}">`
6978
- **Good Example**: `<view class="item" wx:class="{{ {active: isActive} }}">`
70-
3. **优先使用跨端兼容方案**:常见样式属性的跨端兼容方案见 [样式开发最佳实践](./references/rn-style-practice.md),包括:
71-
- 复合选择器 → 等效单类选择器
72-
- 子元素伪类(`:first-child` 等)→ `wx:class` + `index` 判断
73-
- 伪元素(`::before` / `::after`)→ 真实节点替代
74-
- 点击态伪类(`:active`)→ `hover-class` / `hover-stay-time`
75-
- 1rpx 极细线 → `hairlineWidth` 条件编译
76-
- `rem` / `em``rpx`
77-
- 数值型 `font-weight``normal` / `bold`
78-
- 文本溢出 → 模板属性 `numberOfLines` 条件编译
79-
- 隐藏元素 → 尺寸归零 + `overflow: hidden`,避免 `display: none`
80-
- `grid` / `float` → Flex 布局
79+
3. **优先使用跨端兼容方案**:常见样式属性的跨端兼容方案见 [样式开发最佳实践](./references/rn-style-practice.md)。下表区分两类处理方式:
80+
- **等效替换**(替换后写法在原平台与 RN 平台均生效,可直接全平台使用)
81+
- 复合选择器 → 等效单类选择器
82+
- 子元素伪类(`:first-child` 等)→ `wx:class` + `index` 判断
83+
- 伪元素(`::before` / `::after`)→ 真实节点替代
84+
- 点击态伪类(`:active`)→ `hover-class` / `hover-stay-time`
85+
- `rem` / `em``rpx`
86+
- 数值型 `font-weight``normal` / `bold`
87+
- 隐藏元素:避免 `display: none`,使用尺寸归零 + `overflow: hidden`
88+
- `grid` / `float` → Flex 布局
89+
- **双轨保留**(原平台原写法用条件编译保留,RN 侧新增等效实现,**禁止只保留 RN 一侧**
90+
- 文本溢出:原平台保留 `white-space` / `text-overflow` / `overflow: hidden` 样式(用样式条件编译包裹整条规则),RN 侧通过模板属性 `numberOfLines@ios|android|harmony` 等效实现
91+
- 1rpx 极细线:原平台保留 `1rpx` 边框写法(用样式条件编译),RN 侧用 `hairlineWidth` 等效
92+
8193
4. **保留单位注释**:保留原始样式中的 `/*use rpx*/``/*use px*/` 注释,编译期会据此批量切换样式单位。
8294

8395
### JSON 配置约束
@@ -160,12 +172,15 @@ Mpx 是一个以微信小程序语法为基础、进行了类 Vue 语法拓展
160172

161173
#### 5. 编译校验
162174

163-
完成上述改造后,使用 `scripts/compile-validate.js` 对修改后的 `.mpx` 文件进行真实编译校验,参见下文 [编译校验脚本](#编译校验脚本)。若校验失败,按错误分类回到对应步骤进行修正。
175+
完成上述改造后,使用本 skill 自带的编译校验脚本对修改后的 `.mpx` 文件进行真实编译校验(位于 skill 目录下的 `scripts/compile-validate.js`**不在宿主项目根目录下**,参见下文 [编译校验脚本](#编译校验脚本)。若校验失败,按错误分类回到对应步骤进行修正。
164176

165177
#### 6. 检查与确认
166178

167179
按 SFC 各区块逐项核对,并确认条件编译与编译校验已收尾:
168180

181+
**跨平台兼容**
182+
- [ ] 引入的「RN 支持但原平台不支持」的写法均已通过条件编译限定在 RN 输出;原平台原有写法已用条件编译保留,未因 RN 适配而被替换或删除。
183+
169184
**模板(template)**
170185
- [ ] `<template>` 中使用的基础组件、属性、事件均在 [模板能力参考](./references/rn-template-reference.md) 标注为 RN 支持,或已通过模板条件编译进行平台隔离;如用户通过 `rnConfig.customBuiltInComponents` 编译配置扩充拓展了基础组件能力,以用户说明为准。
171186
- [ ] 动态 `class` / `style` 已改造为 `wx:class` / `wx:style` 指令,未在属性值内使用 `{{}}` 拼接。
@@ -223,15 +238,17 @@ Mpx 是一个以微信小程序语法为基础、进行了类 Vue 语法拓展
223238

224239
#### 3. 编译校验
225240

226-
使用 `scripts/compile-validate.js` 校验新建组件,参见下文 [编译校验脚本](#编译校验脚本)。建议同时校验所有目标平台(如 `--target=wx,ios,web`),确保跨端兼容。
241+
使用本 skill 自带的编译校验脚本(位于 skill 目录下的 `scripts/compile-validate.js`**不在宿主项目根目录下**校验新建组件,参见下文 [编译校验脚本](#编译校验脚本)。建议同时校验所有目标平台(如 `--target=wx,ios,web`),确保跨端兼容。
227242

228243
#### 4. 检查与确认
229244

230245
复用 [任务一 · 检查与确认](#6-检查与确认) 的清单,确认新建组件不引入任何 RN 不兼容写法。
231246

232247
## 编译校验脚本
233248

234-
`scripts/compile-validate.js` 是基于宿主项目 `@mpxjs/mpx-cli-service` 的真实编译校验工具,会自动从输入文件向上探测项目根目录、加载工程编译配置、按指定 `target` 进行编译,并按 `style / template / script / json / dependency / other` 分类聚合错误与警告。改造或新建组件后建议作为强制环节运行。
249+
> **脚本位置**:编译校验脚本随本 skill 一同分发,位于 **skill 目录下** 的 `scripts/compile-validate.js`(即 `<skill-root>/scripts/compile-validate.js`),下文所有命令示例均使用 **指向 skill 目录的路径**调用该脚本,不要尝试在宿主项目根目录或 `node_modules` 中查找它。
250+
251+
该脚本基于宿主项目内安装的 `@mpxjs/mpx-cli-service` 进行真实编译校验:会自动从输入 `.mpx` 文件向上探测宿主项目根目录、加载工程编译配置、按指定 `target` 进行编译,并按 `style / template / script / json / dependency / other` 分类聚合错误与警告。改造或新建组件后建议作为强制环节运行。
235252

236253
子组件忽略策略按宿主项目 `@mpxjs/webpack-plugin` 版本自动切换:
237254

@@ -253,21 +270,23 @@ Mpx 是一个以微信小程序语法为基础、进行了类 Vue 语法拓展
253270

254271
### 使用示例
255272

273+
> 下方示例中的 `<skill-root>` 表示本 skill 在宿主环境中的实际安装路径(例如 `.agents/skills/mpx-rn-dev-guide`、`.claude/skills/mpx-rn-dev-guide` 或 `~/.claude/skills/mpx-rn-dev-guide` 等,以实际安装位置为准);调用时使用该绝对路径,不要在宿主项目根目录下查找 `scripts/compile-validate.js`。
274+
256275
```bash
257276
# 单组件、默认 target=ios
258-
node .agents/skills/mpx-rn-dev-guide/scripts/compile-validate.js src/components/foo.mpx
277+
node <skill-root>/scripts/compile-validate.js src/components/foo.mpx
259278

260279
# 显式指定为页面(影响 entry 与 partialCompileRules 形态)
261-
node .agents/skills/mpx-rn-dev-guide/scripts/compile-validate.js src/pages/index.mpx --type=page --target=ios
280+
node <skill-root>/scripts/compile-validate.js src/pages/index.mpx --type=page --target=ios
262281

263282
# 跨端多目标校验
264-
node .agents/skills/mpx-rn-dev-guide/scripts/compile-validate.js src/components/foo.mpx --target=wx,ios,web
283+
node <skill-root>/scripts/compile-validate.js src/components/foo.mpx --target=wx,ios,web
265284

266285
# 输出结构化 JSON 便于二次处理
267-
node .agents/skills/mpx-rn-dev-guide/scripts/compile-validate.js src/components/foo.mpx --target=ios --json
286+
node <skill-root>/scripts/compile-validate.js src/components/foo.mpx --target=ios --json
268287

269288
# 同时递归校验子组件(默认行为是仅校验目标自身)
270-
node .agents/skills/mpx-rn-dev-guide/scripts/compile-validate.js src/components/foo.mpx --target=ios --no-ignore-sub-components
289+
node <skill-root>/scripts/compile-validate.js src/components/foo.mpx --target=ios --no-ignore-sub-components
271290
```
272291

273292
校验失败时按错误的 `category` 字段回到对应任务步骤定位与修正问题,再次运行直至通过。

.agents/skills/mpx-rn-dev-guide/references/rn-style-practice.md

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- [点击态处理 (:active)](#点击态处理-active)
1010
- [样式单位使用建议](#样式单位使用建议)
1111
- [优先使用 px 和 rpx 单位](#优先使用-px-和-rpx-单位)
12-
- [百分比用于相对布局](#百分比用于相对布局)
12+
- [使用百分比](#使用百分比)
1313
- [1 像素边框(极细线)](#1-像素边框极细线)
1414
- [避免使用不兼容的单位 (rem/em)](#避免使用不兼容的单位-remem)
1515
- [谨慎使用 font-weight 数值](#谨慎使用-font-weight-数值)
@@ -426,7 +426,7 @@ px 和 rpx 在 RN 与小程序平台都具备良好兼容性,建议优先使
426426
</style>
427427
```
428428

429-
### 百分比用于相对布局
429+
### 使用百分比
430430

431431
百分比单位在 RN 平台的处理分为两类:**React Native 原生支持的百分比****框架特殊处理的百分比**
432432

@@ -468,76 +468,80 @@ px 和 rpx 在 RN 与小程序平台都具备良好兼容性,建议优先使
468468
**⚠️ 需要辅助属性的场景:**
469469

470470
1. **`font-size` 的百分比**需要传递 `parent-font-size` 辅助属性
471-
2. **`calc()` 中的百分比**需要传递相应的辅助属性`calc()` 是框架模拟支持的特性)
471+
2. **`calc()` 中出现的任何百分比**都需要传递相应的 `parent-width` / `parent-height` 辅助属性`calc()` 是框架模拟支持的特性)
472472

473473
```html
474474
<template>
475475
<!-- 场景1:font-size 百分比需要 parent-font-size -->
476476
<view parent-font-size="{{16}}" class="text" />
477477

478-
<!-- 场景2:calc() 中的百分比需要辅助属性 -->
479-
<view parent-width="{{750}}" parent-height="{{1000}}" class="box" />
478+
<!-- 场景2:仅在无法替代时,calc() 中的百分比需要父级布局宽高辅助计算 -->
479+
<view id="calc-parent" class="calc-parent" wx:ref>
480+
<view
481+
wx:if="{{layoutReady}}"
482+
class="box"
483+
parent-width="{{parentWidth}}"
484+
parent-height="{{parentHeight}}"
485+
/>
486+
</view>
480487
</template>
481488

489+
<script>
490+
export default {
491+
data () {
492+
return {
493+
parentWidth: 0,
494+
parentHeight: 0,
495+
layoutReady: false
496+
}
497+
},
498+
ready() {
499+
this.createSelectorQuery()
500+
.select('#calc-parent')
501+
.boundingClientRect()
502+
.exec((res) => {
503+
const rect = res && res[0]
504+
if (rect) {
505+
this.parentWidth = rect.width
506+
this.parentHeight = rect.height
507+
this.layoutReady = true
508+
}
509+
})
510+
}
511+
}
512+
</script>
513+
482514
<style>
483515
.text {
484516
font-size: 120%; /* 需要 parent-font-size */
485517
}
486518
519+
.calc-parent {
520+
width: 100%;
521+
height: 400rpx;
522+
}
523+
487524
.box {
488-
/* calc() 中的百分比需要辅助属性 */
525+
/* calc() 中的百分比需要辅助属性参与计算 */
489526
width: calc(50% - 20rpx); /* 需要 parent-width */
490527
height: calc(30% + 10rpx); /* 需要 parent-height */
491-
492-
/* calc() 中的 translateX/Y 百分比会自动测量 */
493-
transform: translateX(calc(50% + 10rpx)); /* 自动测量元素 width */
528+
transform: translateX(calc(50% + 10rpx)); /* calc 内含百分比,同样需要 parent-width */
494529
}
495530
</style>
496531
```
497532

498-
**❌ 避免的场景:**
533+
如需使用 `calc() + 百分比`,父级宽高写死时可以直接传入固定的 `parent-width` / `parent-height`。更常见的场景,是使用 `createSelectorQuery()` 等布局查询 API 获取父级真实布局宽高后再传递给使用 `calc()` 的节点;在宽高信息获取完成前,建议先隐藏该节点展示(如使用 `wx:if` 延迟渲染,或使用 `opacity: 0` 隐藏),避免未完成辅助计算时出现闪动或错误布局。
499534

500-
```html
501-
<template>
502-
<text class="text-bad">不推荐:字号用百分比(易漏 parent-font-size)</text>
503-
<text class="text-good">推荐:字号用 rpx</text>
504-
<view class="box-bad">不推荐:calc 内百分比缺辅助属性</view>
505-
<view class="box-good">推荐:calc 内用 rpx</view>
506-
</template>
507-
508-
<style>
509-
/* 避免:字体大小使用百分比 */
510-
.text-bad {
511-
font-size: 120%; /* 需要 parent-font-size,容易出错 */
512-
}
513-
514-
/* 推荐:使用 rpx */
515-
.text-good {
516-
font-size: 32rpx;
517-
}
518-
519-
/* 避免:calc() 中使用百分比但不传递辅助属性 */
520-
.box-bad {
521-
width: calc(50% - 20rpx); /* 需要 parent-width,否则会报错 */
522-
}
523-
524-
/* 推荐:使用 rpx */
525-
.box-good {
526-
width: calc(375rpx - 20rpx);
527-
}
528-
</style>
529-
```
535+
这种兼容写法会带来一定的性能与体验开销。一般不建议使用 `calc() + 百分比`,优先考虑 RN 原生支持的百分比布局、Flex 布局、rpx / vw / vh 或固定尺寸表达;仅在确实无法替代时使用该兼容写法。
530536

531537
**最佳实践:**
532538

533539
1. **优先使用 rpx**:对于固定尺寸,rpx 是最可靠的选择
534540
2. **放心使用百分比**`width`, `height`, `padding`, `margin` 等属性的百分比由 RN 原生支持,可以放心使用
535-
3. **避免字体百分比**`font-size` 的百分比需要辅助属性,建议使用 rpx 代替
536-
4. **谨慎使用 calc() 中的百分比**`calc()` 是框架模拟支持的,其中的百分比需要辅助属性,建议在 `calc()` 中使用 rpx 代替百分比
541+
3. **谨慎使用字体百分比**`font-size` 的百分比需要 `parent-font-size` 辅助属性,建议使用 rpx 代替
542+
4. **谨慎使用 calc() 中的百分比**该写法需要 `parent-width` / `parent-height` 辅助计算,通常还要查询父级布局并延迟展示,存在性能与体验开销;优先使用原生百分比、Flex、rpx / vw / vh 或固定尺寸替代
537543
5. **使用 vh/vw**:对于视口相关的尺寸,vh/vw 是更好的选择
538544

539-
<span id="1像素边框极细线"></span>
540-
541545
### 1 像素边框(极细线)
542546

543547
在移动端开发中,常需要实现物理像素为 1px 的极细边框。

packages/webpack-plugin/lib/platform/json/wx/index.js

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const { isOriginTag, isBuildInWebTag, isBuildInReactTag } = require('../../../ut
66
const getBuildInTagComponent = require('../../../utils/get-build-tag-component')
77

88
module.exports = function getSpec ({ warn, error }) {
9+
const reactModes = ['ios', 'android', 'harmony']
10+
911
function print (mode, path, value, isError) {
1012
const msg = `Json path <${path}> is not supported in ${mode} environment!`
1113
isError ? error(msg, { path, value }) : warn(msg, { path, value })
@@ -32,6 +34,14 @@ module.exports = function getSpec ({ warn, error }) {
3234
}
3335
}
3436

37+
function createReactRule (test, processor) {
38+
const rule = { test }
39+
reactModes.forEach(mode => {
40+
rule[mode] = processor
41+
})
42+
return rule
43+
}
44+
3545
/**
3646
* @desc 在app.mpx里配置usingComponents作为全局组件
3747
*/
@@ -220,6 +230,7 @@ module.exports = function getSpec ({ warn, error }) {
220230
},
221231
jd: deletePath()
222232
},
233+
createReactRule('enablePullDownRefresh|onReachBottomDistance', deletePath()),
223234
{
224235
test: 'navigationBarBackgroundColor',
225236
ali (input) {
@@ -247,8 +258,13 @@ module.exports = function getSpec ({ warn, error }) {
247258
{
248259
test: 'backgroundColorTop|backgroundColorBottom',
249260
ali: deletePath(),
250-
swan: deletePath()
261+
swan: deletePath(),
262+
ios: deletePath(),
263+
android: deletePath(),
264+
harmony: deletePath()
251265
},
266+
createReactRule('backgroundColor|backgroundTextStyle', deletePath()),
267+
createReactRule('pageOrientation', deletePath()),
252268
{
253269
test: 'navigationBarTextStyle|navigationStyle|backgroundTextStyle',
254270
ali: deletePath()
@@ -382,7 +398,10 @@ module.exports = function getSpec ({ warn, error }) {
382398
qq: deletePath(),
383399
swan: deletePath(),
384400
tt: deletePath(),
385-
jd: deletePath()
401+
jd: deletePath(),
402+
ios: deletePath(),
403+
android: deletePath(),
404+
harmony: deletePath()
386405
},
387406
{
388407
test: 'preloadRule',
@@ -394,14 +413,20 @@ module.exports = function getSpec ({ warn, error }) {
394413
qq: deletePath(true),
395414
swan: deletePath(true),
396415
tt: deletePath(),
397-
jd: deletePath(true)
416+
jd: deletePath(true),
417+
ios: deletePath(true),
418+
android: deletePath(true),
419+
harmony: deletePath(true)
398420
},
399421
{
400422
test: 'plugins',
401423
qq: deletePath(true),
402424
swan: deletePath(true),
403425
tt: deletePath(),
404-
jd: deletePath(true)
426+
jd: deletePath(true),
427+
ios: deletePath(true),
428+
android: deletePath(true),
429+
harmony: deletePath(true)
405430
},
406431
{
407432
test: 'usingComponents',
@@ -427,19 +452,28 @@ module.exports = function getSpec ({ warn, error }) {
427452
{
428453
test: 'debug',
429454
ali: deletePath(),
430-
swan: deletePath()
455+
swan: deletePath(),
456+
ios: deletePath(),
457+
android: deletePath(),
458+
harmony: deletePath()
431459
},
432460
{
433461
test: 'requiredBackgroundModes',
434462
ali: deletePath(),
435-
tt: deletePath()
463+
tt: deletePath(),
464+
ios: deletePath(),
465+
android: deletePath(),
466+
harmony: deletePath()
436467
},
437468
{
438469
test: 'workers',
439470
jd: deletePath(),
440471
ali: deletePath(),
441472
swan: deletePath(),
442-
tt: deletePath()
473+
tt: deletePath(),
474+
ios: deletePath(),
475+
android: deletePath(),
476+
harmony: deletePath()
443477
},
444478
{
445479
test: 'subpackages|subPackages',
@@ -454,6 +488,8 @@ module.exports = function getSpec ({ warn, error }) {
454488
ali: deletePath(),
455489
jd: deletePath()
456490
},
491+
createReactRule('navigateToMiniProgramAppIdList', deletePath()),
492+
createReactRule('tabBar', deletePath(true)),
457493
{
458494
test: 'tabBar',
459495
ali: getTabBarRule(),
@@ -470,7 +506,10 @@ module.exports = function getSpec ({ warn, error }) {
470506
swan: getWindowRule(),
471507
tt: getWindowRule(),
472508
ks: getWindowRule(),
473-
jd: getWindowRule()
509+
jd: getWindowRule(),
510+
ios: getWindowRule(),
511+
android: getWindowRule(),
512+
harmony: getWindowRule()
474513
}
475514
]
476515
}

0 commit comments

Comments
 (0)