vue-cli方式开发微信小程序自定义tabbar

465 阅读4分钟

微信小程序自带tabbar样式固定,无法满足业务上一些个性化诉求如tabbar中间位置凸起显示,这个时候就需要自定义tabbar出场了。自定义tabbar其实也有很多方法,比如引入第三方包就可以相对完美且高效的“解决问题”,那么手写怎么实现呢?以下为手动实现自定义tabbar的一些笔记总结(仍有诸多不足,仅供学习及笔记记录,如有问题或建议,欢迎指正交流),给有需要的小伙伴参考:

项目准备

vue3+typescript+vite+微信小程序+pinia

基础配置

  • cd到工程目录
  • npx degit dcloudio/uni-preset-vue#vite-ts [你的项目名称],此处注意创建的是uni-app默认的vite+typescript模板项目,需要其他模板,可以访问官网查询
  • 配置mainfest.json,找到mp-weixin项,配置所需的关键信息,示例:
"mp-weixin" : {
        "appid" : "你的appid",
        "setting" : {
            "urlCheck" : false
        },
        "usingComponents" : true
    },
  • npm i初始化项目依赖(喜欢用yarn可以自由切换)
  • 项目运行
npm run dev:mp-weixin
  • 可视化调测:打开微信小程序开发者工具,选择导入项目,选择工程目录下的dist/dev/mp-weixin导入即可查看微信小程序效果,不赘述。

集成pinia

  • 安装
npm i pinia -S
  • 挂载
import { createSSRApp } from "vue";
import { createPinia } from 'pinia'
import App from "./App.vue";

export function createApp() {
  const app = createSSRApp(App)
    .use(createPinia())
  return {
    app,
  };
}

拓展配置

个人开发习惯,不需要集成这些的可以跳过

集成sass

  • 注意此处有坑:安装sass-loader,注意指定版本10,否则可能会出现vue与sass的兼容导致报错
npm i sass -D
npm i sass-loader@10 -D

集成unplugin-auto-import

作用:配置后可以自动导入refreactive...这些,提升开发效率

  • 安装
npm i unplugin-auto-import -D
  • 找到tsconfig.json文件,在include选项中增加 auto-imports.d.ts 类型声明文件,示例
"include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "./auto-imports.d.ts",
  ],
  • 找到vite.config.ts,配置自动导入配置项AutoImport({imports: ['vue']})
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import AutoImport from 'unplugin-auto-import/vite'
const path = require('path');

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [uni(), AutoImport({
    imports: ['vue']
  })],
});

设置@路径引用

  • 找到vite.config.ts配置@路径
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import AutoImport from 'unplugin-auto-import/vite'
const path = require('path');

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [uni(), AutoImport({
    imports: ['vue']
  })],

  resolve: {
    // 配置路径别名
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
});


全局样式配置

  • 项目根目录创建wx_theme.json
{
	"light": {
		"navBgColor": "#0165FF",
		"navTxtStyle": "white",
    "pullBgColor":"#FFFFFF"
	},
	"dark": {
		"navBgColor": "#292929",
		"navTxtStyle": "white",
    "pullBgColor":"#FFFFFF"
	}
}
  • mainfest.json找到mp-weixin,挂载wx_theme.json
"mp-weixin" : {
        /* 小程序特有相关 */
        "appid" : "wx5aac7e8c80869c9a",
        "setting" : {
            "urlCheck" : false
        },
        "usingComponents" : true,
        "darkmode" : false,
        "themeLocation":"wx_theme.json"
    },
  • 在package.json中使用,如:
	"globalStyle": {
		"navigationBarTextStyle": "@navTxtStyle",
		"navigationBarTitleText": "移山·获取答案的快乐",
		"navigationBarBackgroundColor": "@navBgColor",
		"backgroundColor": "@pullBgColor",
		"app-plus": {
			"background": "#0165FF"
		}
	},

过程演示(自定义中间凸起的tabbar并应用)

定义自定义tabbar组件

  • 在src\components新建custom-tabbar(也即src\components\custom-tabbar),在custom-tabbar文件夹下新建index.vue

uni-app自带的easycom模式,components符合如下规范的会自动导入(无需手工引入):

  • 组件命名必须是小写字母,使用短横线连接单词。例如:my-component
  • 组件名/组件名.vue形式命名

显然此处src\components\custom-tabbar\index.vue无法自动导入,后文会交代如何导入或者挂载,在这里先买个彩蛋

<template>
  <view class="tabbar">
    <view
      class="tabbar-item"
      v-for="tabbarItem in tabbarStore.tabbarList"
      :key="tabbarItem.id"
      @click="changeTab(tabbarItem.id)"
      :class="
        tabbarStore.currentTabId === 2 &&
        tabbarStore.currentTabId === tabbarItem.id
          ? 'center-bottom'
          : ''
      "
      :style="
        tabbarStore.currentTabId === 2 &&
        tabbarStore.currentTabId === tabbarItem.id
          ? { borderBottom: '1px solid #0165FF' }
          : ''
      "
    >
      <block v-if="tabbarItem.id !== 2">
        <image
          :src="
            tabbarItem.id === tabbarStore.currentTabId
              ? tabbarItem.selectedIconPath
              : tabbarItem.iconPath
          "
          class="sides"
        ></image>
        <text
          class="tabbar-item-text"
          :style="{
            color:
              tabbarItem.id === tabbarStore.currentTabId
                ? props.activeColor
                : ''
          }"
          >{{
            tabbarItem.id === tabbarStore.currentTabId
              ? selectedText
              : tabbarItem.text
          }}</text
        >
      </block>
      <block v-else>
        <view class="center-top"></view>
        <image :src="tabbarItem.selectedIconPath" class="center"></image>
      </block>
    </view>
  </view>
</template>

<script setup lang="ts">
import useTabbarStore from '@/store/tabbarStore'

const tabbarStore = useTabbarStore()
tabbarStore._loadTabbar()

const props = withDefaults(
  defineProps<{
    defaultColor: string
    activeColor: string
    currentId: number
  }>(),
  {
    defaultColor: () => '',
    activeColor: () => '#0165FF',
    currentId: () => -1
  }
)

const selectedText = ref<string>('•')

const changeTab = (id: number) => {
  const prevTabId = tabbarStore.currentTabId
  const url = tabbarStore.changeTabbar(id)
  // uni.switchTab({ url })
  if (prevTabId === id) {
    console.log('不需要切换但是需要刷新')
  }
}

if (props.currentId * 1 >= 0) {
  changeTab(props.currentId * 1)
}
</script>

<style lang="scss" scoped>
$border-color: #f2f2f2;
$bgc-color: #ffffff;
$selected-main-color: #0165ff;
$global-height: 50px;
.tabbar {
  position: fixed;
  margin-top: 6rpx;
  bottom: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
  width: 100%;
  height: $global-height;
  border-top: 1px solid $border-color;
  &-item {
    flex: 1;
    box-sizing: border-box;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    .sides {
      width: 24px;
      height: 24px;
    }

    &-text {
      font-size: 10px;
    }
  }
}

.center {
  width: 48px;
  height: ($global-height - 2);
  float: right;
  position: absolute;
  bottom: 7px;
}
.center-top {
  float: right;
  position: absolute;
  bottom: 30px;
  box-sizing: border-box;
  width: 70px;
  height: ($global-height + 10);
  background-color: $bgc-color;
  border-radius: 50%;
  border-top: 1px solid $border-color;
  margin-bottom: -25px;
}
.center-bottom {
  border-bottom: 2px solid $selected-main-color;
}
</style>


定义tabbarList状态管理

  • 根目录新建store文件夹,新建tabbarStore.ts,对外暴露state、actions等(路径:src\store\tabbarStore.ts
import { defineStore } from 'pinia'
import type { TabbarItem, TabbrList } from '@/typings'
import tabbarRawData from '@/data/tabbar'
import useMate from '@/hooks/useMate'


const useTabbarStore = defineStore('tabbarStore', {
  state() {
    const tabbarList: TabbrList = []
    const currentTabId: number = Number(uni.getStorageSync('currentTabId')) || 0
    const _isloaded: boolean = false

    return {
      tabbarList,
      currentTabId,
      _isloaded
    }
  },
  actions: {
    _loadTabbar() {
      if (this._isloaded) return
      console.log('_loadTabbar is working')
      this.tabbarList = tabbarRawData
      this._isloaded = true
      uni.setStorageSync('tabbarList', JSON.stringify(this.tabbarList))
    },

    changeTabbar(id: number): string {
      const foundedItem = useMate('id', id, this.tabbarList)
      this.currentTabId = foundedItem.id
      uni.setStorageSync('currentTabId', this.currentTabId + '')
      return `/${foundedItem.pagePath}`

    }
  },

})

export default useTabbarStore

自定义hook对数组find查询进行简单封装

  • 根目录新建hooks目录,新增useMate.ts文件(路径:src\hooks\useMate.ts
const mate = <K extends keyof T, T>(key: K, val: T[keyof T], rawData: T[]): T => {
  return rawData.find(item => {
    return item[key] === val
  }) as T
}

export default mate

自定义tabbar自动导入或挂载

根据需要选择合适的挂载方式,建议二者选其一,笔者选择的是全局挂载

自动导入配置

  • 找到pages.jsoneasycom新增如下配置
"easycom": {
		"autoscan": true,
		"custom": {
			"^custom-(.*)": "@/components/custom-$1/index.vue"
		}
	},

全局挂载

  • 找到main.ts,设置自定义tabbar全局挂载
import { createSSRApp } from "vue";
import { createPinia } from 'pinia'
import tabBar from '@/components/custom-tabbar/index.vue'



import App from "./App.vue";
export function createApp() {
  const app = createSSRApp(App)
    .use(createPinia())
    .component('customTabBar', tabBar)
  return {
    app,
  };
}

pages页面简单定义

因为主要作测试用,页面都非常简单(寒颤),敬请见谅

  • 太过简单,就不一一列举了,总之有5个页面,分别为首页消息添加喜欢我的
<!-- src\pages\home\index.vue -->
<template>
  <view>我是首页</view>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>



<!-- src\pages\message\index.vue -->
<template>
  <view>我是消息页面</view>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>


<!-- src\pages\add\index.vue -->
<template>
  <view>我是添加页面</view>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>



<!-- src\pages\follow\index.vue -->
<template>
  <view>我是关注页面</view>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>



<!-- src\pages\my\index.vue -->
<template>
  <view>我是我的页面</view>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>

修改pages.json生效自定义tabbar组件

  • "custom": true,
  • iconPathselectedIconPath根据需要自行设置
{
	"pages": [
		{
			"path": "pages/home/index",
			"style": {
				"enablePullDownRefresh": false
			}
		},
		{
			"path": "pages/message/index",
			"style": {
				"enablePullDownRefresh": false
			}
		},
		{
			"path": "pages/add/index",
			"style": {
				"enablePullDownRefresh": false
			}
		},
		{
			"path": "pages/follow/index",
			"style": {
				"enablePullDownRefresh": false
			}
		},
		{
			"path": "pages/my/index",
			"style": {
				"enablePullDownRefresh": false
			}
		}
	],
	"tabBar": {
		"custom": true,
		"selectedColor": "#0165FF",
		"list": [
			{
				"pagePath": "pages/home/index",
				"text": "首页",
				"iconPath": "static/tabIcons/shouye.png",
				"selectedIconPath": "static/tabIcons/shouye-selected.png"
			},
			{
				"pagePath": "pages/message/index",
				"text": "消息",
				"iconPath": "static/tabIcons/tongzhi.png",
				"selectedIconPath": "static/tabIcons/tongzhi-selected.png"
			},
			{
				"pagePath": "pages/add/index",
				"text": "",
				"iconPath": "static/tabIcons/add.png",
				"selectedIconPath": "static/tabIcons/add.png"
			},
			{
				"pagePath": "pages/follow/index",
				"text": "关注",
				"iconPath": "static/tabIcons/xihuan1.png",
				"selectedIconPath": "static/tabIcons/xihuan1-selected.png"
			},
			{
				"pagePath": "pages/my/index",
				"text": "我的",
				"iconPath": "static/tabIcons/wode.png",
				"selectedIconPath": "static/tabIcons/wode-selected.png"
			}
		]
	},
	"globalStyle": {
		"navigationBarTextStyle": "@navTxtStyle",
		"navigationBarTitleText": "移山·获取答案的快乐",
		"navigationBarBackgroundColor": "@navBgColor",
		"backgroundColor": "@pullBgColor",
		"app-plus": {
			"background": "#0165FF"
		}
	},
	"lazyCodeLoading": "requiredComponents"
}

pages目录下新建tab聚合页面

由于uni-app暂时不支持component :is动态绑定组件,暂时只能使用v-if这种非常不优雅的方式实现,如uni-app支持动态组件了,还请告知)

  • pages目录下创建组合页面src\pages\combination-page\index.vue
<template>
  <Home v-if="tabbarStore.currentTabId === 0"></Home>
  <Message v-if="tabbarStore.currentTabId === 1"></Message>
  <Add v-if="tabbarStore.currentTabId === 2"></Add>
  <Follow v-if="tabbarStore.currentTabId === 3"></Follow>
  <My v-if="tabbarStore.currentTabId === 4"></My>
  <customTabBar :currentId="0"></customTabBar>
</template>

<script setup lang="ts">
import useTabbarStore from '@/store/tabbarStore'
const tabbarStore = useTabbarStore()

import Home from '@/pages/home/index.vue'
import Message from '@/pages/message/index.vue'
import Add from '@/pages/add/index.vue'
import Follow from '@/pages/follow/index.vue'
import My from '@/pages/my/index.vue'
</script>

<style lang="scss" scoped></style>

  • 在pages.json中注册src\pages\combination-page\index.vue,强烈建议设置在第一个索引位置,因为会默认展示
"pages": [
		{
			"path": "pages/combination-page/index",
			"style": {
				"enablePullDownRefresh": false
			}
		},...]

效果展示

image.png

image.png

以上就是手动实现自定义tabbar的一些笔记总结,仍有诸多不足,仅供学习及笔记记录,如有问题或建议,欢迎指正交流,后续有迭代优化会持续更新,敬请期待~