一、开发规范
- 文件命名规范:页面使用小驼峰命名、组件使用大驼峰命名
- 组件必需使用
Function hooks语法;页面推荐使用hooks,也可使用class - 所有文件行数不超过
500行 - 函数组件的
ts类型、入参写法统一,命名大驼峰命名
const DialogBlack: React.FC<Props> = props => {
// 组件内容
}
- 样式中不要用标签选择器。在
H5中不识别 css文件中尺寸单位使用px、百分比。px单位会根据多端自动适配,如:100px在微信小程序中会转化为100rpx,h5中会转化为对应的rem。如果不希望被转换,可以使用PX。- 添加到
pages中的页面,都需要有page.config.ts文件,否则H5中会报错 list循环元素,必须添加唯一key,否则H5中会报错- 开发工具需安装
eslint、Prettier - 事件命名统一使用
on开头,如onSave if语句执行模块统一使用{}包裹- 函数有返回值时需要添加相应类型,没有返回值则默认为
void类型 - 函数需要添加注释:事件描述,参数说明,返回值说明(有返回值时)
- 接口需要加异常捕获(需要抛出异常或有业务特殊处理时)。
- 页面级别的组件名根据文件名来命名
tip用公共的方法 =>import { tips} from “@/modules/utils/log”- 定时器如
setTimeout,在页面销毁时要清除,可以使用useTimeOut自定义hook,做了销毁自动清除的处理 - 自定义
hooks文件名前加use前缀 - 数据的定义位置:当组件间共享数据,数据应该存放在最近的父组件中。如果数据不存在共享,就应该属于对应组件私有
- 公共组件(多次使用的组件)中的样式定义,有通用
class名需要用父class包裹,或者使用不会冲突的选择器。taro打包会把公共组件样式抽离到公共样式文件,导致其它页面有相同class样式会冲突。
// 公共组件 A
// ❌ css 通用类名active非常容易冲突,会影响项目中有active类名样式
.active {}
// ✅
// 应修改为不容易冲突的class选择器
.parentClass .active {}
// 或
.customClass.active {}
useState数据拆分原则
- 不是每个属性都要写一个
useState - 不是所有属性都要写在一个
useState中 - 根据
state赋值及使用场景,相同模块或同一对象下属性可定义在一个useState中
- 多个模块复用的
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}}。需要借助taro的api。
css中书写的px尺寸单位,在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换。
const height = Taro.pxTransform(20) // 小程序:rpx,H5:rem
style={{height: height }}
- 如果是通过
API计算的具体的实际尺寸,则不需要转化。 如Taro.getSystemInfoSync()获取到的screenHeight、statusBarHeight等,可以直接通过js赋值,获取的已经是屏幕实际px尺寸。
const scrollHeight = screenHeight - statusBarHeight;
- 如果是
API获取的实际尺寸和自定义尺寸组合计算,则自定义尺寸需要通过utils中的rpx2px函数转化为当前屏幕下的实际尺寸(保证参与运算的各个值在同一维度--该例为适配后的实际尺寸)
const scrollHeight = screenHeight - statusBarHeight - rpx2px(88);
5. 原小程序中全局数据通信使用的jgb的eventBus,该项目中用什么方法。
解决方案:使用taro的Taro.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.createSelectorQuery、Taro.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还是更新前的
比如如下场景:当useState的set函数执行后,立即执行指定函数(该函数内部依赖最新的state值),此时函数内获取到的state还是更新前的值。
这是因为 useState的set 是异步赋值。只有到了下一次组件重新渲染时,拿到的才是更新后的值。
依赖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这种多端框架,都是会将一个前端框架(React、Vue)作为开发者的开发框架,开发者基于框架的语法开发完代码后,再通过Taro的编译工具,将源代码分别编译出可以在不同端运行的代码。
所以在终端运行时,很多内容最终还是会调用终端的Api来实现。比如基于React开发,使用setState更新数据时,通过Taro的编译,最终在小程序中更新数据会调用小程序setData的Api。
所以我们再做性能优化时,可以分别从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中设置数据的方式
相对于原生的微信小程序,Taro中setData设置数据,包含了节点的信息(节点的类型、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);
});
打印内容如下:
方式三
在开发者工具中找到 taro 运行时库,在 diff 方法前后打断点或 log,观察 state、小程序 data 和 diff 后将要被 setData 的数据。参考文档
5. 案例分析(一):对于选择城市页面input框快速输入时抖动问题分析
当页面数据量大时,更新数据卡顿,导致出现输入框输入值来回切换问题。通过优化方案进行优化后解决了卡顿的问题。
详情查看:《性能优化 - 输入框快速输入时,输入框内容自动来回切换》
6. 案例分析(二):Taro UI 的 Indexes 索引选择器,数据量大时的卡顿问题
Taro UI的Indexes 索引选择器,当数据量达到2000条时,就非常卡顿,并且提示更新的数据量过大,在组件初始化和点击右侧索引进行滚动时,都会有该提示,而自定义数据结构性能会好很多。这个插件需要进行优化。
详情查看:《Taro UI 的 Indexes 索引选择器存在问题及优化点》
7. 在Taro中分析项目的依赖
一、安装 webpack-bundle-analyzer
npm install webpack-bundle-analyzer -D
二、然后在 config/index 的 mini.webpackChain 中添加如下配置:
const config = {
...
mini: {
webpackChain (chain, webpack) {
chain.plugin('analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
}
}
}
三、执行 build 相关命令,就会自动打开浏览器查看项目的构成
参考
-
在Taro开发过程中发现问题,如何进行处理,可以参考《Taro debug指南》
优秀物料
-
taro-hooks :为
Taro设计的常用Hooks库 -
Taro3-虚拟列表 支持节点不等高
-
taro3骨架屏插件
Taro page初始化setData()需要传递一个比较大的数据,导致初始化页面时会一段白屏的时间,该插件可以解决这个问题。缺点是需要自己写wxml代码