babel-plugin-transform-taroapi 4.1.12-beta.23 → 4.1.12-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# H5 端编译流程与 transform-taroapi 执行顺序分析
|
|
2
|
-
|
|
3
|
-
## 1. 相关插件与调用顺序
|
|
4
|
-
|
|
5
|
-
### 1.1 modifyWebpackChain 注册顺序
|
|
6
|
-
|
|
7
|
-
| 插件 | 注册时机 | 作用 |
|
|
8
|
-
|-----|---------|-----|
|
|
9
|
-
| **taro-framework-react** | 框架插件 | `modifyH5WebpackChain`:设置 alias、process-import-taro-h5 loader(仅匹配 `taro-h5/dist/`)、fast-refresh |
|
|
10
|
-
| **taro-platform-h5** | 平台插件,`setupTransaction.addWrapper` | 追加 `transform-taroapi` 到 babel-loader 的 `plugins` |
|
|
11
|
-
| **plugin-inject-jdapi** | 业务插件,`onSetupClose` | 修改 transform-taroapi 配置(apis)、可将其移到 preset |
|
|
12
|
-
|
|
13
|
-
### 1.2 taro-platform-h5 的 Babel 修改
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
// taro-platform-h5/src/program.ts:98-111
|
|
17
|
-
const options = babelLoader.get('options')
|
|
18
|
-
babelLoader.set('options', {
|
|
19
|
-
...options,
|
|
20
|
-
plugins: [
|
|
21
|
-
...(options.plugins || []),
|
|
22
|
-
[require('babel-plugin-transform-taroapi'), { packageName, definition }],
|
|
23
|
-
],
|
|
24
|
-
})
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
**关键**:`transform-taroapi` 被 **追加** 到 `options.plugins` 末尾。
|
|
28
|
-
|
|
29
|
-
### 1.3 初始 Babel 配置来源
|
|
30
|
-
|
|
31
|
-
- **script rule**:`WebpackModule.getScriptRule()` 仅设置 `babel-loader` 且 `options: { compact: false }`
|
|
32
|
-
- **完整配置**:babel-loader 运行时从项目根目录加载 `babel.config.js`
|
|
33
|
-
- **babel.config.js** 典型内容:`presets: ['babel-preset-taro']`
|
|
34
|
-
|
|
35
|
-
## 2. babel-preset-taro 的配置结构
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
// babel-preset-taro/index.js
|
|
39
|
-
return {
|
|
40
|
-
sourceType: 'unambiguous',
|
|
41
|
-
overrides: [{
|
|
42
|
-
exclude: [/@babel[/|\\\\]runtime/, /core-js/, /\bwebpack\/buildin\b/],
|
|
43
|
-
presets: [preset-env, preset-ts, preset-react, ...],
|
|
44
|
-
plugins: [decorators, class-props, transform-runtime, dynamic-import-node, ...],
|
|
45
|
-
}],
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
- 对 **未** 命中 `exclude` 的文件(如 `list/index.tsx`),使用 override 的 `presets` 和 `plugins`
|
|
50
|
-
- `preset-env` 内含 `@babel/plugin-transform-optional-chaining`
|
|
51
|
-
- `transform-runtime` 会注入 `_slicedToArray` 等 helper
|
|
52
|
-
|
|
53
|
-
## 3. 最终 Babel 配置的合并方式
|
|
54
|
-
|
|
55
|
-
loader 传入的 `options` 会与 `babel.config.js` 合并,大致为:
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
babel.config.js: presets: [babel-preset-taro]
|
|
59
|
-
loader options: plugins: [..., transform-taroapi] // taro-platform-h5 追加
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Babel 对 **overrides** 的处理:
|
|
63
|
-
|
|
64
|
-
- 命中 override 时,会应用 override 的 `presets` 和 `plugins`
|
|
65
|
-
- 顶层 `plugins`(含 `transform-taroapi`)与 override 的合并顺序依赖 Babel 内部实现
|
|
66
|
-
- 若 override 的 plugins 先于顶层 plugins 执行,则 `optional-chaining`、`transform-runtime` 会先跑,预扫描时 AST 已被改写
|
|
67
|
-
|
|
68
|
-
## 4. order10 日志的结论
|
|
69
|
-
|
|
70
|
-
预扫描时 AST 已包含:
|
|
71
|
-
|
|
72
|
-
- 第一个 import:`_slicedToArray`(来自 transform-runtime)
|
|
73
|
-
- `Taro.JDMTA.isTrafficMapEnable?.()` 已被展开为三元表达式
|
|
74
|
-
|
|
75
|
-
说明在 `transform-taroapi` 的 `Program.enter` 执行前,`optional-chaining` 和 `transform-runtime` 已经处理过该文件。
|
|
76
|
-
|
|
77
|
-
## 5. 可选修复方案
|
|
78
|
-
|
|
79
|
-
### 方案 A:在 plugin-inject-jdapi 中移到 preset(已实现)
|
|
80
|
-
|
|
81
|
-
将 `transform-taroapi` 从 `plugins` 中移除,放入独立 preset,并 `presets.push()`,利用 presets 倒序执行,使其在 preset-env 之前运行。
|
|
82
|
-
|
|
83
|
-
### 方案 B:在 taro-platform-h5 中调整注入方式
|
|
84
|
-
|
|
85
|
-
在 `taro-platform-h5` 中不再简单追加到 `plugins`,而是:
|
|
86
|
-
|
|
87
|
-
- 使用 `presets.unshift({ plugins: [transform-taroapi] })`,或
|
|
88
|
-
- 通过其他方式保证其在 preset-env 之前执行
|
|
89
|
-
|
|
90
|
-
这样不依赖 plugin-inject-jdapi,所有 H5 项目都能受益。
|
|
91
|
-
|
|
92
|
-
### 方案 C:在 babel-preset-taro 中内置
|
|
93
|
-
|
|
94
|
-
在 `babel-preset-taro` 的 override 中,将 `transform-taroapi` 放在 `plugins` 数组最前面,确保先于 `optional-chaining` 执行。
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
# H5 端命名空间 API 可选链问题修复方案
|
|
2
|
-
|
|
3
|
-
## 问题描述
|
|
4
|
-
|
|
5
|
-
在 Taro H5 端,使用可选链语法调用命名空间 API 时,编译产物中会保留对 `Taro.JDMTA` 等不存在对象的引用,导致运行时报错:
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
TypeError: Cannot read properties of undefined (reading 'isTrafficMapEnable')
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
受影响的写法包括:
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
// 以下可选链写法在 H5 端均会出错
|
|
15
|
-
Taro?.JDMTA.isTrafficMapEnable()
|
|
16
|
-
Taro.JDMTA?.isTrafficMapEnable()
|
|
17
|
-
Taro.JDMTA.isTrafficMapEnable?.() // 最常见
|
|
18
|
-
Taro?.JDMTA?.isTrafficMapEnable()
|
|
19
|
-
Taro?.JDMTA.isTrafficMapEnable?.()
|
|
20
|
-
Taro.JDMTA?.isTrafficMapEnable?.()
|
|
21
|
-
Taro?.JDMTA?.isTrafficMapEnable?.()
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
而不带可选链的写法 `Taro.JDMTA.isTrafficMapEnable()` 一直是正常的。
|
|
25
|
-
|
|
26
|
-
## 根因分析
|
|
27
|
-
|
|
28
|
-
### Babel 的遍历模型
|
|
29
|
-
|
|
30
|
-
Babel 将所有插件的 visitor **合并到一次遍历**中,逐节点推进,对同一节点按插件顺序依次触发 visitor。这意味着:
|
|
31
|
-
|
|
32
|
-
- `transform-taroapi` 和 `@babel/plugin-transform-optional-chaining`(来自 `preset-env`)共享同一次遍历
|
|
33
|
-
- "插件 A 先于插件 B"只意味着**同一节点**上 A 的 visitor 先触发,而非 A 完整跑完再轮到 B
|
|
34
|
-
|
|
35
|
-
### 具体失败场景
|
|
36
|
-
|
|
37
|
-
以 `Taro.JDMTA.isTrafficMapEnable?.().then(res => {...})` 为例,AST 结构如下:
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
CallExpression (.then) ← 主遍历先访问这里
|
|
41
|
-
└─ MemberExpression (.then)
|
|
42
|
-
└─ OptionalCallExpression { optional: true } ← 我们想处理的节点
|
|
43
|
-
└─ MemberExpression (Taro.JDMTA.isTrafficMapEnable)
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
主遍历是自顶向下(enter)的:
|
|
47
|
-
|
|
48
|
-
1. **进入 `.then()` CallExpression** — `transform-taroapi` 先触发,callee 是 `.then`,不匹配,跳过
|
|
49
|
-
2. **紧接着** `@babel/plugin-transform-optional-chaining` 触发 — 检测到 callee 链中存在 `?.()` 可选链,**一次性展开整条链**,生成:
|
|
50
|
-
```js
|
|
51
|
-
(_Taro$JDMTA = Taro.JDMTA) === null || _Taro$JDMTA === void 0
|
|
52
|
-
? void 0
|
|
53
|
-
: _Taro$JDMTA.isTrafficMapEnable().then(res => {...})
|
|
54
|
-
```
|
|
55
|
-
3. 内层 `OptionalCallExpression` 节点**已经被替换**,`transform-taroapi` 永远无法访问到它
|
|
56
|
-
|
|
57
|
-
关键在于:外层节点先被访问,`optional-chaining` 在外层就把整棵子树改写了,内层节点在被任何插件处理前就已消失。
|
|
58
|
-
|
|
59
|
-
## 解决方案
|
|
60
|
-
|
|
61
|
-
### 核心思路:`Program.enter` 预扫描
|
|
62
|
-
|
|
63
|
-
在 `transform-taroapi` 的 `Program.enter` 中,通过 `ast.traverse()` 发起一次**独立的子遍历**。这次子遍历:
|
|
64
|
-
|
|
65
|
-
- 只有 `transform-taroapi` 自己的逻辑参与,没有其他插件
|
|
66
|
-
- **同步执行完毕**后,主遍历才继续
|
|
67
|
-
- 在所有插件开始处理具体节点之前,就把可选链降级完成
|
|
68
|
-
|
|
69
|
-
### 执行流程
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
┌─────────────────────────────────────────────────────┐
|
|
73
|
-
│ Program.enter 触发 │
|
|
74
|
-
│ │
|
|
75
|
-
│ 子遍历 1:找到 import,确定 taroName │
|
|
76
|
-
│ import Taro from '@tarojs/taro' │
|
|
77
|
-
│ → preScanTaroName = 'Taro' │
|
|
78
|
-
│ │
|
|
79
|
-
│ 子遍历 2:遍历所有 OptionalCallExpression │
|
|
80
|
-
│ Taro.JDMTA.isTrafficMapEnable?.() │
|
|
81
|
-
│ → 匹配 namespace API(JDMTA_isTrafficMapEnable) │
|
|
82
|
-
│ → replaceWith(CallExpression) │
|
|
83
|
-
│ → 变成 Taro.JDMTA.isTrafficMapEnable() │
|
|
84
|
-
│ │
|
|
85
|
-
│ ─── 子遍历完成,AST 已修改 ─── │
|
|
86
|
-
└─────────────────────────────────────────────────────┘
|
|
87
|
-
│
|
|
88
|
-
▼
|
|
89
|
-
┌─────────────────────────────────────────────────────┐
|
|
90
|
-
│ 主遍历继续(所有插件共享) │
|
|
91
|
-
│ │
|
|
92
|
-
│ @babel/plugin-transform-optional-chaining │
|
|
93
|
-
│ → 已经没有 Taro API 的可选链了,不会介入 │
|
|
94
|
-
│ │
|
|
95
|
-
│ transform-taroapi 的 CallExpression visitor │
|
|
96
|
-
│ → 匹配 Taro.JDMTA.isTrafficMapEnable() │
|
|
97
|
-
│ → 替换为 _JDMTA_isTrafficMapEnable() │
|
|
98
|
-
│ │
|
|
99
|
-
│ Program.exit │
|
|
100
|
-
│ → 生成 import { JDMTA_isTrafficMapEnable as │
|
|
101
|
-
│ _JDMTA_isTrafficMapEnable } from '@tarojs/taro' │
|
|
102
|
-
└─────────────────────────────────────────────────────┘
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### 代码改动说明
|
|
106
|
-
|
|
107
|
-
#### 1. `getTaroNamespaceCall` 辅助函数
|
|
108
|
-
|
|
109
|
-
统一识别各种可选链和 TS 包裹形态下的命名空间 API 调用:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
function getTaroNamespaceCall(t, callee, taroName, file)
|
|
113
|
-
: { namespaceName: string, methodName: string } | null
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
处理的 callee 形态包括:
|
|
117
|
-
- `MemberExpression`:`Taro.JDMTA.isTrafficMapEnable`
|
|
118
|
-
- `OptionalMemberExpression`:`Taro?.JDMTA?.isTrafficMapEnable`
|
|
119
|
-
- `OptionalCallExpression` 包裹:`Taro.JDMTA.isTrafficMapEnable?.()`
|
|
120
|
-
- `AssignmentExpression` 包裹:`(_tmp = Taro.JDMTA).isTrafficMapEnable`
|
|
121
|
-
- `TSAsExpression` / `TSNonNullExpression` 等 TS 类型包裹
|
|
122
|
-
|
|
123
|
-
#### 2. `Program.enter` 预扫描(核心)
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
Program: {
|
|
127
|
-
enter(ast) {
|
|
128
|
-
// ... 状态重置 ...
|
|
129
|
-
|
|
130
|
-
// 子遍历 1:找到 taroName
|
|
131
|
-
ast.traverse({
|
|
132
|
-
ImportDeclaration(importPath) { /* 从 import 中提取 taroName */ }
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// 子遍历 2:降级可选链
|
|
136
|
-
if (preScanTaroName) {
|
|
137
|
-
ast.traverse({
|
|
138
|
-
OptionalCallExpression(optPath) {
|
|
139
|
-
const nsInfo = getTaroNamespaceCall(...)
|
|
140
|
-
if (nsInfo && apis.has(flatName)) {
|
|
141
|
-
optPath.replaceWith(t.callExpression(
|
|
142
|
-
optPath.node.callee,
|
|
143
|
-
optPath.node.arguments
|
|
144
|
-
))
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
#### 3. `CallExpression|OptionalCallExpression` visitor
|
|
154
|
-
|
|
155
|
-
主遍历中的兜底处理,使用 `getTaroNamespaceCall` 识别命名空间 API 调用并替换为扁平标识符:
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
'CallExpression|OptionalCallExpression'(ast) {
|
|
159
|
-
const nsInfo = getTaroNamespaceCall(t, callee, taroName, this.file)
|
|
160
|
-
if (nsInfo && this.apis.has(flatName)) {
|
|
161
|
-
// 替换为扁平标识符,如 _JDMTA_isTrafficMapEnable
|
|
162
|
-
if (t.isOptionalCallExpression(ast.node)) {
|
|
163
|
-
ast.replaceWith(t.callExpression(identifier, ast.node.arguments))
|
|
164
|
-
} else {
|
|
165
|
-
ast.node.callee = identifier
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### 4. `MemberExpression|OptionalMemberExpression` visitor
|
|
172
|
-
|
|
173
|
-
处理非调用场景下的命名空间属性访问(如 `const fn = Taro.JDMTA.isTrafficMapEnable`)。
|
|
174
|
-
|
|
175
|
-
### 编译结果
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
// 编译前(任意可选链组合)
|
|
179
|
-
import Taro from '@tarojs/taro'
|
|
180
|
-
Taro.JDMTA.isTrafficMapEnable?.()
|
|
181
|
-
.then(res => console.log(res))
|
|
182
|
-
.catch(err => console.log(err))
|
|
183
|
-
|
|
184
|
-
// 编译后
|
|
185
|
-
import { JDMTA_isTrafficMapEnable as _JDMTA_isTrafficMapEnable } from '@tarojs/taro-h5'
|
|
186
|
-
_JDMTA_isTrafficMapEnable()
|
|
187
|
-
.then(res => console.log(res))
|
|
188
|
-
.catch(err => console.log(err))
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## 为什么不能靠调整插件执行顺序解决
|
|
192
|
-
|
|
193
|
-
`transform-taroapi` 本身就在 `@babel/plugin-transform-optional-chaining` 之前执行(plugins 在 presets 之前)。但由于 Babel 的合并遍历模型:
|
|
194
|
-
|
|
195
|
-
- **同节点竞争**:插件顺序靠前的先触发 → `transform-taroapi` 赢
|
|
196
|
-
- **父子节点竞争**:父节点先被访问 → `optional-chaining` 在父节点上改写整棵子树 → `transform-taroapi` 输
|
|
197
|
-
|
|
198
|
-
可选链的链式调用(`.then()/.catch()`)恰好触发了"父子节点竞争"场景。`Program.enter` 中的独立子遍历绕过了这个问题——在主遍历开始前就完成了可选链降级,无需和其他插件竞争。
|
|
199
|
-
|
|
200
|
-
## 调试方法
|
|
201
|
-
|
|
202
|
-
设置环境变量 `JDAPI_DEBUG_TAROAPI=true` 可以开启调试日志,观察预扫描和命名空间 API 匹配的情况:
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
JDAPI_DEBUG_TAROAPI=true npm run dev:h5
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
输出示例:
|
|
209
|
-
```
|
|
210
|
-
[jdapi-core-taroapi] pre-transform: strip optional call for JDMTA_isTrafficMapEnable in file: /src/pages/index.tsx
|
|
211
|
-
[jdapi-core-taroapi] getTaroNamespaceCall hit: JDMTA_isTrafficMapEnable file = /src/pages/index.tsx
|
|
212
|
-
```
|