Taro 开发文档 - 开发规范、常见问题、性能优化(React、Ts)

1,511 阅读21分钟

一、开发规范

  1. 文件命名规范:页面使用小驼峰命名、组件使用大驼峰命名
  2. 组件必需使用Function hooks语法;页面推荐使用hooks,也可使用class
  3. 所有文件行数不超过500
  4. 函数组件的 ts 类型、入参写法统一,命名大驼峰命名
const DialogBlack: React.FC<Props> = props => {
    // 组件内容
}
  1. 样式中不要用标签选择器。在H5中不识别
  2. css文件中尺寸单位使用px、百分比。px单位会根据多端自动适配,如:100px在微信小程序中会转化为100rpxh5中会转化为对应的rem。如果不希望被转换,可以使用PX
  3. 添加到pages中的页面,都需要有page.config.ts 文件,否则H5中会报错
  4. list循环元素,必须添加唯一key,否则H5中会报错
  5. 开发工具需安装eslintPrettier
  6. 事件命名统一使用on开头,如onSave
  7. if 语句执行模块统一使用 {} 包裹
  8. 函数有返回值时需要添加相应类型,没有返回值则默认为void类型
  9. 函数需要添加注释:事件描述,参数说明,返回值说明(有返回值时)
  10. 接口需要加异常捕获(需要抛出异常或有业务特殊处理时)。
  11. 页面级别的组件名根据文件名来命名
  12. tip 用公共的方法 => import { tips} from “@/modules/utils/log”
  13. 定时器如setTimeout,在页面销毁时要清除,可以使用 useTimeOut 自定义 hook,做了销毁自动清除的处理
  14. 自定义hooks文件名前加use前缀
  15. 数据的定义位置:当组件间共享数据,数据应该存放在最近的父组件中。如果数据不存在共享,就应该属于对应组件私有
  16. 公共组件(多次使用的组件)中的样式定义,有通用class名需要用父class包裹,或者使用不会冲突的选择器。taro打包会把公共组件样式抽离到公共样式文件,导致其它页面有相同class样式会冲突。
// 公共组件 A
// ❌ css 通用类名active非常容易冲突,会影响项目中有active类名样式
.active {}

// ✅ 
// 应修改为不容易冲突的class选择器
.parentClass .active {}
// 或
.customClass.active {}
  1. useState数据拆分原则
  • 不是每个属性都要写一个 useState
  • 不是所有属性都要写在一个 useState
  • 根据state赋值及使用场景,相同模块或同一对象下属性可定义在一个useState
  1. 多个模块复用的state及相关逻辑可使用自定义hooks封装

二、常见问题

1. 父组件传递函数到子组件时,参数类型如何定义?

interface Props {
  // 函数为useState的set函数, boolean为赋值类型
  setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
  // 可定义入参及返回值类型
  toGrantAuthorization: (type: boolean) => void;
  // 普通函数类型,不限制入参及返回值。不推荐
  setLoginInfo: Function;
}

2. 是否可以直接在开发工具中查看页面数据

可以通过 react-devtools 查看数据和调试。

3. usestate是否可以修改对象的某个数据(单个key)

// 函数式更新
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

// 或使用useReducer

4. 在js中写行内样式,宽高、字体大小等尺寸如何实现多端自适应

  • 如果是直接赋值,如 style={{height: 20rpx}}。需要借助taroapi

css中书写的px尺寸单位,在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换。

const height = Taro.pxTransform(20) // 小程序:rpx,H5:rem

style={{height: height }}
  • 如果是通过API计算的具体的实际尺寸,则不需要转化。 如 Taro.getSystemInfoSync() 获取到的 screenHeightstatusBarHeight等,可以直接通过js赋值,获取的已经是屏幕实际px尺寸。
const scrollHeight = screenHeight - statusBarHeight;
  • 如果是API获取的实际尺寸和自定义尺寸组合计算,则自定义尺寸需要通过utils中的rpx2px函数转化为当前屏幕下的实际尺寸(保证参与运算的各个值在同一维度--该例为适配后的实际尺寸)
const scrollHeight = screenHeight - statusBarHeight - rpx2px(88);

5. 原小程序中全局数据通信使用的jgb的eventBus,该项目中用什么方法。

解决方案:使用taroTaro.eventCenter替换。

基本使用
// 监听一个事件,接受参数
Taro.eventCenter.on('eventName', (arg) => {
  // doSth
})

// 触发事件,传入参数
Taro.eventCenter.trigger('eventName', arg)

// 取消监听一个事件 - 移除该事件所有 handler
Taro.eventCenter.off('eventName')

// 取消监听一个事件 - 指定 handler(需要和监听时handler引用一致)
Taro.eventCenter.off('eventName', handler1)

// 取消监听所有事件
Taro.eventCenter.off()
示例:解绑一个事件的指定 handler

比如:当多个页面监听同一个事件时,页面卸载只需要解绑当前页面的 handler;或一个事件有多个 handler 时,只解绑其中一个。

// 使用useCallback保持事件引用,确保监听和解绑时事件一致
const openWxSubFc = useCallback(() => {
  setIsShow(true);
}, []);

useEffect(() => {
  // 事件监听
  Taro.eventCenter.on('OPEN_WX_SUB', openWxSubFc);
  return () => {
    // 取消指定 handler 监听
    Taro.eventCenter.off('OPEN_WX_SUB', openWxSubFc);
  };
});

参考文档

6. useState更新值依赖之前值时结果错误问题

在连续调用setState,并依赖前一次state值时,如果有异步调用,就会导致最终的结果与预期的结果不符。

解决方案:

  • 可采用setState的函数式更新,参考文档
  • 考虑使用useReducer,更适合用于管理包含多个子值的 state 对象
  • 根据业务情况,独立的state可考虑单独设置useState
部分setState失败场景可参考:hooks的闭包陷阱

7. 小程序中的onLoad生命周期,taro使用什么替换(需要拿到页面参数)

解决方案:

  • Class组件 onLoad生命周期,使用与小程序基本一致
  • Functional组件
// useRouter
const router = Taro.useRouter<{ id: string }>()
useEffect(() => {
    const { id } = router.params
    // do somethong
}, [])

useRouter 的泛型参数会作为返回值的 params 的类型使用。泛型参数接收 key/value 都为 string 类型的对象。

useRouter 的返回值 TS 类型定义
// useRouter 的返回值类型定义
interface RouterInfo<TParams extends Partial<Record<string, string>> = Partial<Record<string, string>>> {
    /**
     * 路由参数。
     */
    params: TParams

    /**
     * 页面路径。
     */
    path: string

    onReady: string
    onHide: string
    onShow: string

    shareTicket: string | undefined
    scene: number | undefined
}

8. Function组件中获取当前页面实例。

使用Taro.getCurrentInstance(),可获取小程序的 app、page 对象、路由参数等数据,其中page对象为当前页面实例,可替代小程序页面中的this。但频繁调用它可能会导致问题。因此推荐把 Taro.getCurrentInstance() 的结果在组件中保存起来,之后直接使用。

// getCurrentInstance获取当前页面实例
const currentPage_1 = Taro.getCurrentInstance().page

// getCurrentPages 通过页面栈获取当前页
const pages = Taro.getCurrentPages();
const currentPage_2 = pages[pages.length - 1]

// 2种方式获取的当前页面实例一致
currentPage_1 === currentPage_2 // true

9. 页面间跳转时发送数据与接受数据

// 通过eventChannel向被打开页面传送数据
Taro.navigateTo(
  { 
    url: 'detail?id=1',
    success: function (res) {
      // 向被打开页发送数据
      res.eventChannel.emit('acceptDataFromList', { data: 'list' })
    },
    events: {
      // 监听获取被打开页面传送到当前页面的数据
      acceptDataFromDetail: function(data) {
        console.log("获取详情页发送过来的数据", data)
      }
    }
  }
)

// 被打开页面
const currentPage = Taro.getCurrentInstance().page
const eventChannel = currentPage.getOpenerEventChannel()
// 非navigateTo跳转到的页面,eventChannel值为“{}”。避免通过分享或其他方式直接访问页面时,调用.on/.emit等方法报错。添加if判断
if (eventChannel.on) {
  // 获取打开页发送过来的数据
  eventChannel.on('acceptDataFromList', function(data) {
    console.log("获取到列表页发送的数据", data)
  })
  
  // 向打开页发送数据
  eventChannel.emit('acceptDataFromDetail', {data: 'detail'});
}

10. Function组件中获取当前组件实例(小程序组件实例)。

Taro中,部分与wxml相关的api,如:Taro.createSelectorQueryTaro.createIntersectionObserver,在自定义组件(小程序组件)中使用时,需要使用this.createSelectorQuery来代替。但是在Function组件中,是无法直接获取到当前自定义组件的this实例的。

如何获取当前小程序组件实例可以查看: 《Function组件中获取当前小程序组件实例》

11. 不会触发组件视图更新的变量如何定义。如定时器Id、单纯js逻辑中使用的变量等。

可以使用useRef定义,组件重新渲染后值也会保留

// 定义单个属性,如定时器
const time = useRef<NodeJS.Timeout>();
time.current = setTimeout(function() {
  // do something
})

// 定义多个属性,如对象
const customData = useRef({});
customData.current = {
  a: 1,
  b: 2
}

12. ts的接口类型定义中,可能有未知key及value类型的属性如何定义

interface SystemInfo {
  brand: string; // 必须属性
  version?: string; // 可选属性
  [propName: string]: any; // 未知其他属性
}

13. 部分场景下类型为null或undefined时,ts报错处理

可通过 可选链操作符值的前置判断 处理。

  • 通过 可选链操作符 解决对象可能为空的情况
// 提示 car 可能为 null
setName(car.name)

// 解决方案:可选链操作符,当car为null时,也不会报错,car?.name 返回值为 undefined
setName(car?.name)

可选链操作符

  • 通过 值的前置判断 进行类型保护

如下,clearTimeOut 时类型报错。提示不能将TimeOut类型赋值给 TimeOut | Undefined

// 提示以上类型错误
clearTimeout(time.current)

// 解决方案:空值提前判断
time.current && clearTimeout(time.current);

14. 原class组件中防抖装饰器在function中如何替换

// class组件事件的防抖装饰器
@debounce(400)
switchTab() {
    // do something
}

// function组件中防抖使用
const switchTab = useDebounce(() => {
    // do something
}, 400)

需要定义一个防抖的自定义hook

如果直接使用防抖函数,在2次触发事件的中间,如果刚好触发了组件更新,switchTab 会被赋值为一个新的函数,此时防抖就会失效了,第二次触发的事件和第一次触发的事件不在一个定时器的判断中。事件还是会触发2次。

15. 指定数据类型的对象,初始值为空时如何定义类型?

比如,useState 定义对象属性时,指定了数据类型,对象初始值为空对象,但是 ts 会报类型错误。可以通过联合类型解决,如下:

// Props中指定了属性时,以下会报类型错误:{} 中没有 Props 类型中定义的属性
const [data, setData] = useState<Props>({})

// 解决方案1:初始值定义时可以给个联合类型,即 目标类型 | 全局空对象类型
const [data, setData] = useState<Props | EmptyObject>({})

// 解决方案2:添加类型断言,指定类型为我们定义的类型
const [data, setData] = useState<Props>({} as Props)

16. useState的set函数执行后,获取的state还是更新前的

比如如下场景:当useStateset函数执行后,立即执行指定函数(该函数内部依赖最新的state值),此时函数内获取到的state还是更新前的值。

这是因为 useStateset 是异步赋值。只有到了下一次组件重新渲染时,拿到的才是更新后的值。

依赖state执行函数时,应使用useEffect,将依赖的state作为依赖数组项。

// 例:每次currentTab更新时,都会执行getOrderLists函数,并在函数内部使用最新的currentTab作为接口入参

// ❌ 不推荐
setCurrentTab(index)
getOrderLists() // 此时函数内部获取到的currentTab是更新前的

// ✅ 推荐
useEffect(() => {
    getOrderLists() // 每次获取的currentTab都是最新的
}, [currentTab])

参考:如何获取接口数据

17. useref指向自定义组件时,调用该组件内部方法,ts报类型错误

// 解决方案

// 1、声明组件数据类型,需包含调用的组件内部方法
interface GiftComponent {
  viewGift: (GiftItem, number) => void
}

// 2、在useRef时指定组件类型
const childRef = useRef<GiftComponent>(null)

// 3、调用子组件内部方法(通过可选链"?"解决childRef.current可能为null问题)
childRef.current?.viewGift(gift, gift.activityId)

18. 全局公共属性的获取

// 获取系统信息:页面高度、导航高度、机型信息等
Taro.getApp().globalSystemInfo

// 获取自定义全局信息
Taro.getApp().globalData
// globalData内容
globalData: {
  isCurrentPageLigon: false, // 当前是否是登录页,接口响应拦截器中跳转到登录页防重
  isGetLocation: false, // 是否已在门店列表页获取定位
  isOnLoadHomePopup: false, // 首页是否获取过首页弹窗
  isChangeCar: false, // 默认车型是否有变更,切换/更新/添加车型后设置为true,部分页面onshow时判断变更则重新获取车型数据
  positionLonLat: {}, // 当前定位的经纬度
  changeCarParams: {} // 车型添加成功后跳转到指定页面传递的参数
}

19. 小程序tabbar页面,是否可以在地址栏中携带参数并正常获取?

如:会员中心首页,获取地址栏中taskId参数。分为以下2种场景:

  • 小程序内部通过Taro.switchTab方法跳转到的页面,无法携带参数,获取不到参数内容
  • 通过外部方式(如太阳码)投放出去的页面带上了参数,访问时可以正常获取到

20. useEffect的依赖数组项的值始终为undefined时,是否默认会执行一次?

组件初始化时会执行一次

// 如下:next的初始值为undefined,useEffect内部函数依然会执行一次
const [next, setNext] = useState();
useEffect(() => {
    console.log('页面初始化', next);
}, [next]);

// 等同于 初始化执行一次 + watch到依赖数组项值变更再次执行

21. Taro的api中,有些ts属性缺失,如何处理?

可以通过扩展Taro内置的ts类型。如下,新建一个全局的ts声明文件:

import Taro from '@tarojs/taro';

// Taro原生属性扩展
declare module '@tarojs/taro' {
  // 页面实例
  interface PageInstance {
    selectComponent: (_selector: string) => ComponentInstance // 选择子组件方法
  }
  // 小程序原生组件实例
  interface ComponentInstance {
    createSelectorQuery: () => Taro.SelectorQuery;
    createIntersectionObserver: (options?: Taro.createIntersectionObserver.Option | undefined) => Taro.IntersectionObserver
  }
  // createIntersectionObserver api返回的对象
  namespace IntersectionObserver {
    interface ObserveCallbackResult {
      id: string
    }
  }
}

对于其他的第三方插件的ts属性扩展,同理。参考文档

22. 对于可能多次触发接口的场景,如何确保最终的值是最后一次接口调用返回值(处理无序的响应)

如下:在商品列表中,通过搜索、筛选等方式会调用获取商品列表接口,如果上一次接口还未返回,又调用了新的接口;而接口返回结果的顺序不可控的,有可能先调用的后返回,此时可能就会出现页面的筛选条件与结果不一致。

针对这种问题,通常有以下几个解决方案:

  • 通过useEffect的特性,当上一次接口还未处理完时,调用新接口前先设置上一次赋值变量为 false,这样后续上次的接口调用完了,也不会做赋值操作。这样就保证了只有最后一个接口调用成功才会赋值。如下:
useEffect(() => {
    let ignore = false;
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      if (!ignore) setProduct(json);
    }
    
    fetchProduct();
    return () => { ignore = true };
}, [productId]);
  • 在新接口调用时,如果上一次接口未调用完,则取消上次接口的请求
  • 调用接口时,禁止用户发起新接口的操作。如添加loading效果,并且页面不可操作

23. useEffect中调用函数时,总是提示需要将函数作为依赖项?

有以下几种方式可以解决这个问题:

  • 把函数移动到 effect 内部。参考上一条的demo。适用于函数只有在effect内部使用时;
  • 把函数移动到你的组件之外;
  • 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
  • 万不得已的情况下,你可以把函数加入 effect 的依赖,但要把它的定义包裹进 useCallback Hook。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变。代码如下:
function ProductPage({ productId }) {
  // ✅ 用 useCallback 包裹以避免随渲染发生改变
  const fetchProduct = useCallback(() => {
    // ... Does something with productId ...
  }, [productId]); // ✅ useCallback 的所有依赖都被指定了

  return <ProductDetails fetchProduct={fetchProduct} />;
}

function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
  // ...
}

24. 怎么重置子组件 | 子组件数据,调用子组件方法

有时我们需要在父组件的指定时机,去重置子组件的数据或调用子组件的方法。

详情查看:《怎么重置组件、组件数据》

25. Taro中怎么解决弹出框的滚动穿透?

组件 View 增加 catchMove 属性,解决滚动穿透问题。在我们弹出框的 View 标签上,增加该属性即可。

// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />

26. React中实现动态组件

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

27. 编译警告 ⚠️[ mini-css-extract-plugin] Conflicting order

在编译项目时,如果发现这种编译警告,可以通过 《编译警告 [ mini-css-extract-plugin] Conflicting order》 解决这个问题。

28. 如何避免向下传递回调?

当我们的数据需要向下传递多层时,往往看起来会比较麻烦,那怎么避免这个情况,可以参考这里

29. 复杂的class类名判断,可借助classnames插件

30. 如何访问小程序的指定页面:添加编译模式

31. 常用的 CLI 命令

  • taro info:查看当前的环境及相关taro依赖的版本
  • taro doctor:诊断项目的依赖、设置、结构,以及代码的规范是否存在问题,并尝试给出解决方案
  • npm info @tarojs/cli:查看当前最新版本

参考文档

32. 如何阻止事件冒泡

e.stopPropagation()

<View
  onClick={e => {
    e.stopPropagation();
    doSomething();
  }}
>
</View>

三、性能优化

像类似Taro这种多端框架,都是会将一个前端框架(ReactVue)作为开发者的开发框架,开发者基于框架的语法开发完代码后,再通过Taro的编译工具,将源代码分别编译出可以在不同端运行的代码。

所以在终端运行时,很多内容最终还是会调用终端的Api来实现。比如基于React开发,使用setState更新数据时,通过Taro的编译,最终在小程序中更新数据会调用小程序setDataApi

所以我们再做性能优化时,可以分别从3个方面进行优化。

  • 基于前端开发框架的优化(如React:如减少更新范围、加快更新速度,最终调用小程序setData时数据量也会更少、更快。
  • 基于Taro的优化Taro将代码编译到小程序时,也会有一套自己的规则,如何让Taro编译的代码更优、执行更快。
  • 基于终端的优化(如微信小程序):最终代码执行还是在终端,终端本身也有自己的优化策略,基于终端的规则进行最终的优化。

下面我们就基于React开发,编译到微信小程序的场景,进行性能优化的介绍:

1. React 性能优化

参考 《React 性能优化》

2. 基于 Taro 的性能优化

在我们通过上面的方式对React代码进行优化之后。有时数据更新时,如果页面结构比较复杂、或者数据量大时,还是会遇到更新时的性能问题。那应该怎么进一步解决?

这种时候的性能问题,通常都是页面结构复杂,或节点数量太大引起的。这时可以借用小程序的原生自定义组件,以达到局部更新的效果,从而提升更新性能。

通过使用CustomWrapper这个基础组件,去包裹遇到更新性能问题的模块,提升更新时的性能。使用该组件包裹后,对后代节点的 setData 将由此自定义组件进行调用,达到局部更新的效果。

<View className='index'>
  <Text>Demo</Text>
  <CustomWrapper>
    <GoodsList />
  </CustomWrapper>
</View>

使用CustomWrapper组件包裹前,GoodsList 更新时 setData 的内容:

// 页面更新
page.setData({
  "root.cn.[0].cn.[0].cn.[0].cn.[0].markers": []
})

包裹后,setData 的数据结构:

// 组件更新
component.setData({
  "cn.[0].cn.[0].markers": []
})

需要注意的时,使用这个组件形成自定义组件后,部分样式或js的交互,就会被隔离开来,相当于在小程序中使用了一个自定义组件。

原理:

组件的 setData 只会引起当前组件和子组件的更新,可以降低虚拟DOM 更新时的计算开销。对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。

参考链接:

3. Taro中设置数据的方式

相对于原生的微信小程序,TarosetData设置数据,包含了节点的信息(节点的类型、class、其他属性等等),而微信小程序中只会set data中的数据。所以Taro中,DOM结构越复杂,setData中的数据量就会越大,也会更容易遇到性能问题。

详情查看:《Taro设置数据的方式》

4. 如何查看更新时的更新内容?

在遇到页面数据量较大时,有时会遇到数据更新时的性能问题,这时可以通过以下几种方式查看每次数据更新时的更新内容。根据每次数据变更更新了哪些数据,来判断优化方向。

如:

  • 哪些数据本次是可以不用更新的?
  • 哪些数据量太大?
  • 哪些数据更新操作应该隔离开?(默认所有的数据更新都是页面级别来更新的,如果我们的页面结构比较复杂,更新的性能就会下降)
方式一

当遇到性能问题时,在项目中打印 setData 的数据将非常有利于帮助定位问题。开发者可以通过进入 Taro 项目的 dist/taro.js 文件,搜索定位 .setData 的调用位置,然后对数据进行打印。或者修改其中属性值 Ct={prerender:!0,debug:!1},将其中 debug 的值改为 true,也会自动打印。

这种方式每次代码修改自动编译时时,tarojs又会被重置,不太方便。

方式二

可以根据小程序提供的页面性能统计信息接口,来获取每次setData执行的时间、更新的字段(无法获取具体更新的值,只能看到更新的字段)

const page = Taro.getCurrentInstance().page;
page?.setUpdatePerformanceListener({ withDataPaths: true }, res => {
    console.log('res', res);
});

打印内容如下:

image.png

方式三

在开发者工具中找到 taro 运行时库,在 diff 方法前后打断点或 log,观察 state、小程序 datadiff 后将要被 setData 的数据。参考文档

5. 案例分析(一):对于选择城市页面input框快速输入时抖动问题分析

当页面数据量大时,更新数据卡顿,导致出现输入框输入值来回切换问题。通过优化方案进行优化后解决了卡顿的问题。

详情查看:《性能优化 - 输入框快速输入时,输入框内容自动来回切换》

6. 案例分析(二):Taro UI 的 Indexes 索引选择器,数据量大时的卡顿问题

Taro UIIndexes 索引选择器,当数据量达到2000条时,就非常卡顿,并且提示更新的数据量过大,在组件初始化和点击右侧索引进行滚动时,都会有该提示,而自定义数据结构性能会好很多。这个插件需要进行优化。

详情查看:《Taro UI 的 Indexes 索引选择器存在问题及优化点》

7. 在Taro中分析项目的依赖

一、安装 webpack-bundle-analyzer
npm install webpack-bundle-analyzer -D
二、然后在 config/indexmini.webpackChain 中添加如下配置:
const config = {
  ...
  mini: {
    webpackChain (chain, webpack) {
      chain.plugin('analyzer')
        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
    }
  }
}
三、执行 build 相关命令,就会自动打开浏览器查看项目的构成

参考

优秀物料