toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
21 stars 1 forks source link

Taro 自定义 TabBar for H5 示例 #331

Open toFrankie opened 4 months ago

toFrankie commented 4 months ago

配图源自 Freepik

背景

最近在做 Taro 项目,有微信小程序和 H5 两端,因导航栏样式无法满足要求,需实现自定义 TabBar。

目前 Taro for H5 未默认支持自定义 TabBar,详见 #10049

开始之前

相关链接:

注意事项:

实现思路:

对于微信小程序来说,Taro 已经支持了,这个不多说,按要求写就能正常显示。而 H5 则在 TabBar 页面中引入 custom-tab-bar 即可。有两个问题,一是内置的 TabBar 仍然存在,通过样式将其屏蔽掉。二是,在 TabBar 页面切换时,由于组件不会重新挂载,可能不会触发重新渲染,为避免 Tab 的高亮状态不正确,需在 onShow 时机进行设置。

引入 custom-tab-bar 页面的方式有两种,一是在每个 TabBar 页面中手动引入(一般不会很多),二是通过插件(比如 taro-inject-component-loader)自动插入到每个页面。后者,还需要在组件内进一步判断,若是 TabBar 的路由则显示自定义 TabBar 的内容,否则不显示。

下文以 React 为例,并选择第二种方式导入自定义 TabBar。

实现

小程序配置 app.config.js,记得完整配置 tabBar

export default defineAppConfig({
  tabBar: {
+   custom: true,
    // ...
  },
})

安装 taro-inject-component-loader,如果手动引入 custom-tab-bar 可以忽略。

$ pnpm add taro-inject-component-loader -D

编译配置 config/index.js 如下:

import path from 'path'

export default {
  // ...
  alias: {
+   '@': path.resolve('./src'),
  },
  h5: {
    // ...
    webpackChain(chain) {
      chain.merge({
        module: {
          rule: {
            injectBaseComponentLoader: {
              test: /\.jsx$/,
              use: [
                {
+                 loader: 'taro-inject-component-loader',
+                 options: {
+                   importPath: '@/custom-tab-bar',
+                 },
                },
              ],
            },
          },
        },
      })
    },
  },
}

可关注下 taro-inject-component-loaderisPage 的默认配置是否满足要求,特别是有分包时,它只会插入到页面组件里,详见

编写 custom-tab-bar 组件,几个注意点:

src/constants/index.js 👇

export const IS_H5 = process.env.TARO_ENV === 'h5'

export const ROUTE = {
  INDEX: '/pages/index/index',
  MINE: '/pages/mine/index',
}

export const TAB_BAR_ROUTES = [ROUTE.INDEX, ROUTE.MINE]

export const EVENT_NAME = {
  TAB_BAR_PAGE_VISIBLE: 'tab_bar_page_visible',
}

src/custom-tab-bar/index.jsx 👇

展开 ```jsx import { useMemo, useState } from 'react' import { View, Image } from '@tarojs/components' import Taro, { eventCenter } from '@tarojs/taro' import { IS_H5, EVENT_NAME, TAB_BAR_ROUTES } from '@/constants' import indexIcon from '@/images/icon-index.png' import indexIconActive from '@/images/icon-index-active.png' import mineIcon from '@/images/icon-mine.png' import mineIconActive from '@/images/icon-mine-active.png' // 样式文件碍于篇幅原因,就不贴出来了,请看文末完整示例 import './index.scss' const tabBarConfig = { color: '#7A7E83', selectedColor: '#3CC51F', backgroundColor: '#F7F7F7', borderStyle: 'black', list: [ { iconPath: IS_H5 ? indexIcon : '../images/icon-index.png', selectedIconPath: IS_H5 ? indexIconActive : '../images/icon-index-active.png', pagePath: '/pages/index/index', text: '首页', }, { iconPath: IS_H5 ? mineIcon : '../images/icon-mine.png', selectedIconPath: IS_H5 ? mineIconActive : '../images/icon-mine-active.png', pagePath: '/pages/mine/index', text: '我的', }, ], } export default function CustomTabBar() { const [selected, setSelected] = useState(-1) const onChange = (index, url) => { setSelected(index) Taro.switchTab({ url }) } const currentRoute = useMemo(() => { const pages = Taro.getCurrentPages() const currentPage = pages[pages.length - 1] const route = currentPage.route?.split('?')[0] return IS_H5 ? route : `/${route}` }, []) const isTabBarPage = useMemo(() => { return tabBarConfig.list.some(item => { // 如有做路由映射,此处可能要调整判断条件 const matched = TAB_BAR_ROUTES.find(route => route === currentRoute) return matched && item.pagePath === matched }) }, [currentRoute]) // 以避免多余的监听,特别是 rerender 时 useState(() => { if (!isTabBarPage) return eventCenter.on(EVENT_NAME.TAB_BAR_PAGE_VISIBLE, index => setSelected(index)) }) const element = useMemo(() => { if (IS_H5 && !isTabBarPage) return null return ( {tabBarConfig.list.map((item, index) => ( onChange(index, item.pagePath)} > {item.text} ))} ) }, [selected, isTabBarPage]) return element } ```

抽成 Hook src/hooks/use-tab-bar.js 👇

import Taro, { useDidShow } from '@tarojs/taro'

import { EVENT_NAME } from '@/constants'

export default function useTabBar(selectedIndex) {
  useDidShow(() => {
    Taro.eventCenter.trigger(EVENT_NAME.TAB_BAR_PAGE_VISIBLE, selectedIndex)
  })
}

页面使用 src/pages/index/index.jsx 👇

import { View, Text } from '@tarojs/components'
import useTabBar from '@/hooks/use-tab-bar'
import './index.scss'

export default function Index() {
  useTabBar(0)

  return (
    <View className="index">
      <Text>首页</Text>
    </View>
  )
}

H5 端内置的 TabBar 组件还是会渲染的,要通过样式来隐藏。src/app.scss 👇

.taro-tabbar__tabbar {
  display: none !important;
}

最后

完整示例 👉 taro-custom-tab-bar

有个体验上的问题,自定义 TabBar 在各个 TabBar 页面初始化时都会创建一个新的组件实例导致,导致切换 Tab 时图片闪烁,可以关注 #7302

The end.

ZhouJiahao228 commented 1 month ago

RN 可以兼容么

toFrankie commented 1 month ago

嗯...没写过 RN,你可以试试~