hookehuyr

docs: 添加 Taro 开发速查表文档

新增 Taro 快速参考指南,汇总禁止使用的 Web API、推荐使用的 Taro API、生命周期对比、样式规范、常见陷阱和性能优化建议。帮助开发者避免使用不兼容的 Web API 并遵循最佳实践,提升开发效率和代码质量。
1 +# Taro 开发速查表
2 +
3 +> **用途**: 快速查阅 Taro API 和常见陷阱,避免使用错误的 Web API
4 +
5 +## 🚫 禁止使用的 Web API
6 +
7 +### ❌ DOM 操作
8 +```javascript
9 +// ❌ 这些在小程序中不存在
10 +window.document
11 +document.getElementById
12 +document.querySelector
13 +document.querySelectorAll
14 +```
15 +
16 +### ❌ 浏览器存储
17 +```javascript
18 +// ❌ 使用 Taro 替代
19 +localStorage.setItem()
20 +localStorage.getItem()
21 +sessionStorage.setItem()
22 +sessionStorage.getItem()
23 +```
24 +
25 +### ❌ 浏览器路由
26 +```javascript
27 +// ❌ 使用 Taro 替代
28 +window.location.href
29 +window.location.pathname
30 +history.pushState()
31 +history.replaceState()
32 +```
33 +
34 +### ❌ 网络请求
35 +```javascript
36 +// ❌ 使用 Taro 替代
37 +fetch()
38 +XMLHttpRequest()
39 +```
40 +
41 +### ❌ 其他 Web API
42 +```javascript
43 +// ❌ 使用 Taro 替代
44 +window.scrollTo()
45 +window.addEventListener()
46 +requestAnimationFrame()
47 +```
48 +
49 +---
50 +
51 +## ✅ 必须使用的 Taro API
52 +
53 +### 🔄 路由导航
54 +```javascript
55 +import Taro from '@tarojs/taro'
56 +
57 +// 跳转到新页面(保留当前页)
58 +Taro.navigateTo({
59 + url: '/pages/detail/index?id=123'
60 +})
61 +
62 +// 关闭当前页,跳转到新页面
63 +Taro.redirectTo({
64 + url: '/pages/login/index'
65 +})
66 +
67 +// 跳转到 tabbar 页面
68 +Taro.switchTab({
69 + url: '/pages/index/index'
70 +})
71 +
72 +// 返回上一页
73 +Taro.navigateBack({
74 + delta: 1
75 +})
76 +
77 +// 获取页面参数(在页面中)
78 +import { useLoad } from '@tarojs/taro'
79 +useLoad((options) => {
80 + console.log('页面参数:', options.id)
81 +})
82 +```
83 +
84 +**推荐使用项目的 `useGo` hook**:
85 +```javascript
86 +import { useGo } from '@/hooks/useGo'
87 +const go = useGo()
88 +
89 +// 短路径跳转(自动补全)
90 +go('product-detail', { id: 123 }) // → pages/product-detail/index?id=123
91 +go('material-list') // → pages/material-list/index
92 +
93 +// 返回
94 +go.back()
95 +```
96 +
97 +### 💾 本地存储
98 +```javascript
99 +import Taro from '@tarojs/taro'
100 +
101 +// 异步存储
102 +await Taro.setStorage({ key: 'user', data: userInfo })
103 +const { data } = await Taro.getStorage({ key: 'user' })
104 +
105 +// 同步存储(谨慎使用,会阻塞线程)
106 +Taro.setStorageSync('token', 'xxxx')
107 +const token = Taro.getStorageSync('token')
108 +
109 +// 删除存储
110 +Taro.removeStorage({ key: 'user' })
111 +Taro.clearStorage() // 清空所有
112 +```
113 +
114 +### 🌐 网络请求
115 +```javascript
116 +import Taro from '@tarojs/taro'
117 +
118 +// 基础请求
119 +Taro.request({
120 + url: 'https://api.example.com/data',
121 + method: 'GET',
122 + data: { id: 123 },
123 + header: {
124 + 'content-type': 'application/json'
125 + }
126 +}).then(res => {
127 + console.log(res.data)
128 +})
129 +```
130 +
131 +**推荐使用项目的 `request.js` 封装**:
132 +```javascript
133 +import request from '@/utils/request'
134 +
135 +request({
136 + url: '/api/data',
137 + method: 'GET',
138 + params: { id: 123 }
139 +}).then(res => {
140 + if (res.data.code === 1) {
141 + console.log('成功:', res.data.data)
142 + }
143 +})
144 +```
145 +
146 +### 💬 提示反馈
147 +```javascript
148 +import Taro from '@tarojs/taro'
149 +
150 +// Toast 提示
151 +Taro.showToast({
152 + title: '操作成功',
153 + icon: 'success',
154 + duration: 2000
155 +})
156 +
157 +// Loading 提示
158 +Taro.showLoading({
159 + title: '加载中...',
160 + mask: true // 防止触摸穿透
161 +})
162 +// 操作完成后
163 +Taro.hideLoading()
164 +
165 +// Modal 弹窗
166 +Taro.showModal({
167 + title: '提示',
168 + content: '确定删除吗?',
169 + success: (res) => {
170 + if (res.confirm) {
171 + console.log('用户点击确定')
172 + }
173 + }
174 +})
175 +```
176 +
177 +### 🎯 设备能力
178 +```javascript
179 +import Taro from '@tarojs/taro'
180 +
181 +// 获取系统信息
182 +const { system, statusBarHeight } = await Taro.getSystemInfo()
183 +
184 +// 获取定位
185 +const { latitude, longitude } = await Taro.getLocation({
186 + type: 'wgs84'
187 +})
188 +
189 +// 选择图片
190 +const { tempFilePaths } = await Taro.chooseImage({
191 + count: 1,
192 + sizeType: ['compressed']
193 +})
194 +
195 +// 预览图片
196 +Taro.previewImage({
197 + current: 'current.jpg',
198 + urls: ['1.jpg', '2.jpg']
199 +})
200 +
201 +// 复制到剪贴板
202 +await Taro.setClipboardData({
203 + data: '复制的文本'
204 +})
205 +```
206 +
207 +### 🔍 选择器(替代 DOM 查询)
208 +```javascript
209 +import Taro from '@tarojs/taro'
210 +
211 +// 查询节点信息
212 +const query = Taro.createSelectorQuery()
213 +query.select('#myElement').boundingClientRect()
214 +query.exec((res) => {
215 + if (res[0]) {
216 + console.log('元素位置:', res[0])
217 + console.log('宽度:', res[0].width)
218 + console.log('高度:', res[0].height)
219 + }
220 +})
221 +```
222 +
223 +---
224 +
225 +## 🔄 生命周期对比
226 +
227 +### 页面生命周期
228 +
229 +| Vue 3 | Taro (页面) | 说明 | 推荐度 |
230 +|-------|-----------|------|--------|
231 +| ❌ `onMounted` | ✅ `useLoad` | 页面加载时触发一次 | ⭐⭐⭐ |
232 +| ❌ `onActivated` | ✅ `useShow` | 每次显示时触发 | ⭐⭐⭐ |
233 +| ❌ `onMounted` | ✅ `useReady` | 首次渲染完成 | ⭐⭐ |
234 +| ❌ `onDeactivated` | ✅ `useHide` | 页面隐藏时触发 | ⭐⭐ |
235 +
236 +**示例**:
237 +```vue
238 +<script setup>
239 +import { useLoad, useShow, useReady } from '@tarojs/taro'
240 +import { ref } from 'vue'
241 +
242 +const list = ref([])
243 +
244 +// ✅ 页面加载时触发一次
245 +useLoad((options) => {
246 + console.log('页面参数:', options)
247 + // 获取页面参数、初始化数据
248 + fetchInitialData(options.id)
249 +})
250 +
251 +// ✅ 每次显示时触发
252 +useShow(() => {
253 + console.log('页面显示')
254 + // 刷新数据、重新获取定位
255 + refreshList()
256 +})
257 +
258 +// ✅ 首次渲染完成
259 +useReady(() => {
260 + console.log('页面渲染完成')
261 + // 操作 DOM、初始化组件
262 + initChart()
263 +})
264 +
265 +// ❌ 避免在页面中使用 Vue 生命周期
266 +// import { onMounted } from 'vue'
267 +// onMounted(() => { ... }) // 可能不按预期工作
268 +</script>
269 +```
270 +
271 +### 组件生命周期
272 +
273 +组件可以使用 Vue 3 生命周期:
274 +```vue
275 +<script setup>
276 +import { onMounted, onUnmounted } from 'vue'
277 +
278 +onMounted(() => {
279 + // ✅ 组件挂载
280 +})
281 +
282 +onUnmounted(() => {
283 + // ✅ 组件卸载
284 +})
285 +</script>
286 +```
287 +
288 +---
289 +
290 +## 🎨 样式规范
291 +
292 +### 单位使用
293 +```vue
294 +<!-- ✅ 使用 px(Taro 自动转换为 rpx) -->
295 +<view style="width: 100px; height: 200px;">
296 + 内容
297 +</view>
298 +
299 +<!-- ❌ 避免直接写 rpx -->
300 +<view style="width: 100rpx;">
301 + 内容
302 +</view>
303 +```
304 +
305 +### 双设计宽度系统
306 +
307 +**项目配置**:
308 +- **NutUI 组件**: 参考 375px 设计稿
309 +- **自定义页面**: 参考 750px 设计稿
310 +
311 +```vue
312 +<!-- NutUI 组件: 375px 基准 -->
313 +<nut-button :custom-style="{ fontSize: '14px' }">
314 + 按钮
315 +</nut-button>
316 +
317 +<!-- 自定义元素: 750px 基准 -->
318 +<view class="custom-box" style="width: 750px;">
319 + 内容
320 +</view>
321 +```
322 +
323 +### 伪元素限制
324 +
325 +```vue
326 +<!-- ❌ 小程序不支持大部分伪元素 -->
327 +<style>
328 +.custom-element::before {
329 + content: ''; /* 不会生效 */
330 +}
331 +</style>
332 +
333 +<!-- ✅ 使用嵌套元素代替 -->
334 +<template>
335 + <view class="custom-element">
336 + <view class="custom-element-icon"></view>
337 + <text>内容</text>
338 + </view>
339 +</template>
340 +```
341 +
342 +---
343 +
344 +## 🔐 SessionID 管理(核心)
345 +
346 +### ✅ 正确实现
347 +
348 +```javascript
349 +// 动态获取 sessionid 并设置到请求头
350 +const sessionid = getSessionId() // 从 localStorage.sessionid 读取
351 +if (sessionid) {
352 + config.headers.cookie = sessionid // 设置到 cookie 字段
353 +}
354 +```
355 +
356 +### ❌ 错误实现
357 +
358 +```javascript
359 +// ❌ 静态 sessionid
360 +const sessionid = 'static_value'
361 +
362 +// ❌ 字段名错误
363 +config.headers.sessionid = sessionid
364 +
365 +// ❌ 不设置到请求头
366 +// 只读取了 sessionid,但没有设置到 headers
367 +```
368 +
369 +---
370 +
371 +## ⚠️ 常见陷阱
372 +
373 +### 1. NutUI textarea 样式无法覆盖
374 +
375 +**问题**:
376 +```vue
377 +<!-- ❌ 深度选择器无效 -->
378 +<nut-textarea v-model="content" class="custom-textarea" />
379 +```
380 +
381 +**解决**:
382 +```vue
383 +<!-- ✅ 使用原生 textarea -->
384 +<textarea
385 + v-model="content"
386 + class="custom-textarea"
387 + maxlength="200"
388 +/>
389 +```
390 +
391 +### 2. IconFont 动态切换不响应
392 +
393 +**问题**:
394 +```vue
395 +<!-- ❌ 图标不更新 -->
396 +<IconFont :name="iconName" />
397 +```
398 +
399 +**解决**:
400 +```vue
401 +<!-- ✅ 添加 key 强制重新渲染 -->
402 +<IconFont :name="iconName" :key="iconName" />
403 +```
404 +
405 +### 3. 嵌套弹窗层级冲突
406 +
407 +**问题**:
408 +```vue
409 +<!-- ❌ 真机上内层弹窗被外层弹窗遮挡 -->
410 +<PlanPopup>
411 + <nut-popup v-model:visible="showPicker">
412 + <nut-picker />
413 + </nut-popup>
414 +</PlanPopup>
415 +```
416 +
417 +**解决**:
418 +```vue
419 +<!-- ✅ 将内层弹窗提升到外层 -->
420 +<PlanPopup />
421 +<nut-popup
422 + v-model:visible="showPicker"
423 + :z-index="9999"
424 + :overlay="true"
425 +>
426 + <nut-picker />
427 +</nut-popup>
428 +```
429 +
430 +### 4. SVG 图标加载失败
431 +
432 +**问题**:
433 +```javascript
434 +// ❌ 字符串路径导致 500 错误
435 +const icons = {
436 + pdf: '/assets/images/icon/doc/pdf.svg'
437 +}
438 +```
439 +
440 +**解决**:
441 +```javascript
442 +// ✅ 使用 import 导入
443 +import pdfIcon from '@/assets/images/icon/doc/pdf.svg'
444 +const icons = { pdf: pdfIcon }
445 +```
446 +
447 +---
448 +
449 +## 📦 Composable 抽取原则
450 +
451 +**"第 3 次出现原则"**: 代码重复 3 次时必须抽取
452 +
453 +### 判断标准
454 +
455 +| 场景 | 抽取条件 | 抽取目标 |
456 +|------|---------|----------|
457 +| 代码重复 | ≥ 2 次 | 警惕 |
458 +| 代码重复 | ≥ 3 次 | **必须抽取** |
459 +| v-for 模板 | > 5 行 | 提取列表项组件 |
460 +| 组件模板 | > 150 行 | 拆分组件 |
461 +| 函数长度 | > 50 行 | 拆分函数 |
462 +
463 +### 项目中的 Composables
464 +
465 +| Composable | 用途 |
466 +|-----------|------|
467 +| `useSectionList` | 分组列表管理 |
468 +| `useFileOperation` | 文件下载、预览、打开 |
469 +| `useListItemClick` | 统一的列表点击处理 |
470 +
471 +**抽取示例**:
472 +```javascript
473 +// ✅ GOOD - 创建 useFileOperation Composable
474 +export function useFileOperation() {
475 + const viewFile = async (file) => {
476 + // 文件预览逻辑
477 + }
478 + const downloadFile = async (file) => {
479 + // 文件下载逻辑
480 + }
481 + return { viewFile, downloadFile }
482 +}
483 +
484 +// 使用
485 +const { viewFile } = useFileOperation()
486 +```
487 +
488 +---
489 +
490 +## 🔧 性能优化
491 +
492 +### 响应式数据优化
493 +
494 +```javascript
495 +import { shallowRef, markRaw } from 'vue'
496 +
497 +// ❌ BAD - 深度响应式
498 +const menuItems = ref([
499 + { icon: IconFont, name: 'heart' } // Vue 会深度代理组件对象
500 +])
501 +
502 +// ✅ GOOD - 浅层响应式
503 +const menuItems = shallowRef([
504 + { icon: markRaw(IconFont), name: 'heart' } // 避免深度代理
505 +])
506 +```
507 +
508 +### 图片优化
509 +
510 +```javascript
511 +// CDN 图片优化
512 +function optimizeImageUrl(url, width = 750, quality = 70) {
513 + if (!url || !url.includes('cdn.ipadbiz.cn')) {
514 + return url
515 + }
516 + return `${url}?imageMogr2/thumbnail/${width}x/quality/${quality}`
517 +}
518 +```
519 +
520 +---
521 +
522 +## 📝 代码规范
523 +
524 +### JSDoc 注释(必须)
525 +
526 +```javascript
527 +/**
528 + * 获取产品列表
529 + *
530 + * @description 从 API 获取产品列表数据
531 + * @param {Object} params - 查询参数
532 + * @param {number} params.page - 页码
533 + * @param {number} params.limit - 每页数量
534 + * @returns {Promise<Object>} 产品列表数据
535 + *
536 + * @example
537 + * const list = await getProductList({ page: 1, limit: 10 })
538 + */
539 +export async function getProductList(params) {
540 + // ...
541 +}
542 +```
543 +
544 +### 错误处理
545 +
546 +```javascript
547 +// ✅ GOOD - 统一错误处理
548 +try {
549 + await someAPI()
550 +} catch (err) {
551 + console.error('操作失败:', err)
552 + Taro.showToast({
553 + title: '操作失败,请重试',
554 + icon: 'none'
555 + })
556 +}
557 +```
558 +
559 +---
560 +
561 +## 🔍 调试技巧
562 +
563 +### 检查网络请求
564 +
565 +```javascript
566 +// 检查网络类型
567 +const { networkType } = await Taro.getNetworkType()
568 +if (networkType === 'none') {
569 + console.error('网络未连接')
570 +}
571 +```
572 +
573 +### 性能监控
574 +
575 +```javascript
576 +import { useReady } from '@tarojs/taro'
577 +
578 +const startTime = Date.now()
579 +
580 +useReady(() => {
581 + const loadTime = Date.now() - startTime
582 + console.log('页面加载耗时:', loadTime)
583 +})
584 +```
585 +
586 +---
587 +
588 +## 📚 参考资源
589 +
590 +- [Taro 官方文档](https://docs.taro.zone/)
591 +- [微信小程序开发文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
592 +- [项目经验教训总结](./lessons-learned.md)
593 +- [Taro 开发规范](~/.claude/rules/taro-patterns.md)
594 +- [小程序开发检查清单](~/.claude/rules/miniprogram-checklist.md)
595 +
596 +---
597 +
598 +**最后更新**: 2026-02-03
599 +**维护者**: Claude Code
600 +**项目**: Manulife WeApp