Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Taro 自定义 TabBar for H5 示例 #331

Open
toFrankie opened this issue Mar 15, 2024 · 2 comments
Open

Taro 自定义 TabBar for H5 示例 #331

toFrankie opened this issue Mar 15, 2024 · 2 comments
Labels
2024 2024 年撰写 React 与 React 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章

Comments

@toFrankie
Copy link
Owner

toFrankie commented Mar 15, 2024

配图源自 Freepik

背景

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

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

开始之前

相关链接:

注意事项:

  • 文件名为 custom-tab-bar,且放在 src 目录下。
  • 完整配置 tabBar 字段,一是为了向下兼容,二是不配置的情况下 H5 端切换页面的体验很差。后者不确定是否因为 Taro 不支持所以没做兼容。
  • 由于 H5 端未得到官方支持,因此配置 custom: true 编译为 H5 时是无效的。

实现思路:

对于微信小程序来说,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 组件,几个注意点:

  • 非 TabBar 页面,不显示 custom-tab-bar 组件内容。这个可以在 injectBaseComponentLoader.use[].options.isPage 通过自定义正则限制只在 TabBar 页注入组件。
  • 为避免非 TabBar 页面返回 TabBar 页面时,在切换的间隙,可能会出现 custom-tab-bar 隐藏 → 显示 → 隐藏的过程,影响用户体验,要使用一定的缓存处理。
  • 由于 TabBar 页面之间切换不一定能触发 rerender,因此要在 onShow 生命周期设置 Tab 的高亮状态,以确保 Tab 正确显示。
  • 由于只有在页面级别的组件内才会触发 onShow 生命周期(详见),因此这里使用 Taro.eventCenter 来监听页面组件的 onShow 生命周期。
  • 关于图片资源引用问题,小程序端可以不用 import 导入的方式,Taro 会自动根据 TabBar 配置处理(详见)。而 H5 由于官方未支持,则需要手动 import 导入。
  • 官方提供了一个 React 的示例,使用的是 Class Component,它可以通过 Taro.getTabBar() 获取组件实例,进而更新组件状态。但在 H5 端并未提供 Taro.getTabBar() 方法,因此它无法兼容小程序和 H5 两端。下面我用 Functional Component 并统一用 Taro 的消息机制来更新组件的状态。

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 👇

展开
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 (
      <View className="tab-bar">
        {tabBarConfig.list.map((item, index) => (
          <View
            key={item.pagePath}
            className="tab-bar-item"
            onClick={() => onChange(index, item.pagePath)}
          >
            <Image
              className="tab-bar-icon"
              src={selected === index ? item.selectedIconPath : item.iconPath}
            />
            <View
              className="tab-bar-text"
              style={{color: selected === index ? tabBarConfig.selectedColor : tabBarConfig.color}}
            >
              {item.text}
            </View>
          </View>
        ))}
      </View>
    )
  }, [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.

@toFrankie toFrankie added 2023 2023 年撰写 React 与 React 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章 2024 2024 年撰写 and removed 2023 2023 年撰写 labels Mar 15, 2024
@ZhouJiahao228
Copy link

RN 可以兼容么

@toFrankie
Copy link
Owner Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2024 2024 年撰写 React 与 React 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章
Projects
None yet
Development

No branches or pull requests

2 participants