普通视图

发现新文章,点击刷新页面。
昨天以前首页

uniapp + Vue 自定义组件封装:自定义样式从入门到实战

作者 远山枫谷
2026年3月8日 15:57

uniapp + Vue 自定义组件封装:自定义样式从入门到实战

今天沉浸式学习了 uniapp 中 Vue 自定义组件的封装,重点突破了「自定义样式」这个核心难点——很多新手封装组件时,要么样式冲突、要么无法灵活适配不同场景,其实掌握关键技巧后,自定义样式可以做到既规范又灵活。这篇笔记就把今天的学习成果整理出来,从基础封装到样式自定义,一步步拆解,适合和我一样正在入门的小伙伴参考~

先明确核心目标:封装的自定义组件,不仅要实现复用性,还要支持外部灵活修改样式,同时避免样式污染全局,兼顾易用性和规范性。下面从「组件基础封装」→「自定义样式实现」→「避坑实战」三个维度,结合具体代码讲解,全程可复制实操。

一、基础铺垫:自定义组件的基本封装流程

在 uniapp 中封装 Vue 自定义组件,和纯 Vue 项目思路一致,但要适配 uniapp 的页面结构和语法规范,核心步骤就3步,先搭好基础框架:

1. 新建组件文件

在项目的 components 目录下,新建组件文件夹(如 my-custom-btn),创建 my-custom-btn.vue 文件,这是组件的核心文件。

2. 编写组件基础结构

组件由 template(结构)、script(逻辑)、style(样式)三部分组成,先写一个简单的按钮组件作为示例,后续逐步完善样式自定义:

<template>
  <!-- 组件基础结构 -->
  <view class="custom-btn" @click="handleClick"&gt;
    &lt;slot&gt;默认按钮&lt;/slot&gt; <!-- 插槽支持外部传入按钮文本 -->
  </view>
</template>

<script>
export default {
  name: 'MyCustomBtn', // 组件名称,必填(便于注册和识别)
  props: {
    // 先定义基础props,后续添加样式相关props
    type: {
      type: String,
      default: 'primary' // 按钮默认类型
    }
  },
  methods: {
    handleClick() {
      // 组件点击事件,通过$emit向父组件传值
      this.$emit('click', '按钮被点击啦')
    }
  }
}
</script>

<style scoped>
/* 基础样式,先写固定样式,后续改为可自定义 */
.custom-btn {
  width: 120rpx;
  height: 60rpx;
  line-height: 60rpx;
  text-align: center;
  border-radius: 30rpx;
  background-color: #007aff; /* 默认蓝色 */
  color: #fff;
  font-size: 28rpx;
}
</style>

3. 注册并使用组件

组件封装好后,需要在页面中注册才能使用,有两种注册方式,根据复用频率选择:

方式1:局部注册(仅当前页面使用)
<template>
  &lt;view&gt;
    <!-- 使用自定义组件 -->
    <my-custom-btn @click="handleBtnClick">点击我</my-custom-btn>
  </view>
</template>

<script>
// 引入组件
import MyCustomBtn from '@/components/my-custom-btn/my-custom-btn.vue'
export default {
  components: {
    MyCustomBtn // 注册组件
  },
  methods: {
    handleBtnClick(msg) {
      console.log(msg) // 接收组件传过来的事件
    }
  }
}
</script>
方式2:全局注册(所有页面可使用)

main.js 中注册,无需在每个页面单独引入:

import Vue from 'vue'
import MyCustomBtn from '@/components/my-custom-btn/my-custom-btn.vue'
// 全局注册组件
Vue.component('MyCustomBtn', MyCustomBtn)

二、核心重点:自定义样式的3种实现方式

这是今天学习的核心!封装组件时,固定样式无法满足不同页面的需求(比如有的页面需要红色按钮,有的需要圆角更大),因此需要支持「外部传入样式」,同时避免样式污染。推荐3种实用方式,从简单到灵活,按需选择。

方式1:通过 props 传值控制样式(最基础、最常用)

核心思路:在组件中定义样式相关的 props(如背景色、字体大小、圆角等),外部使用组件时,通过传入 props 覆盖默认样式,适合样式修改场景较少的情况。

修改上面的按钮组件,添加样式相关 props:

<template>
  <view 
    class="custom-btn" 
    @click="handleClick"
    :style="{
      backgroundColor: bgColor,
      color: textColor,
      borderRadius: borderRadius,
      fontSize: fontSize + 'rpx'
    }"
  >
    <slot>默认按钮</slot>
  </view>
</template>

<script>
export default {
  name: 'MyCustomBtn',
  props: {
    type: {
      type: String,
      default: 'primary'
    },
    // 样式相关props,都设置默认值,保证外部不传入时也能正常显示
    bgColor: {
      type: String,
      default: '#007aff' // 默认蓝色
    },
    textColor: {
      type: String,
      default: '#fff' // 默认白色文本
    },
    borderRadius: {
      type: String,
      default: '30rpx' // 默认圆角
    },
    fontSize: {
      type: Number,
      default: 28 // 默认字体大小(单位rpx,外部传入数字即可)
    }
  },
  methods: {
    handleClick() {
      this.$emit('click', '按钮被点击啦')
    }
  }
}
</script>

<style scoped>
/* 保留基础样式,动态样式通过:style绑定 */
.custom-btn {
  width: 120rpx;
  height: 60rpx;
  line-height: 60rpx;
  text-align: center;
}
</style>

外部使用时,传入需要修改的样式 props 即可,未传入的会使用默认值:

<!-- 自定义背景色和文本色 -->
<my-custom-btn 
  bgColor="#ff3333" 
  textColor="#fff"
  @click="handleBtnClick"
>
  红色按钮
</my-custom-btn>

<!-- 自定义圆角和字体大小 -->
<my-custom-btn 
  borderRadius="10rpx" 
  fontSize="32"
  @click="handleBtnClick"
>
  小字体按钮
</my-custom-btn>

方式2:通过 style 传入自定义类(灵活度更高)

核心思路:组件支持外部传入自定义 class,通过 :class 绑定,实现更复杂的样式自定义(比如渐变、阴影、hover效果),适合样式差异较大的场景。

修改组件,添加 customClass props,用于接收外部传入的类名:

<template>
  <view 
    class="custom-btn" 
    :class="customClass" // 绑定外部传入的类
    @click="handleClick"
  >
    <slot>默认按钮</slot>
  </view>
</template>

<script>
export default {
  name: 'MyCustomBtn',
  props: {
    // 新增:接收外部自定义类名
    customClass: {
      type: String,
      default: ''
    },
    // 保留之前的基础props
    bgColor: {
      type: String,
      default: '#007aff'
    }
  },
  // ... 其他代码不变
}
</script>

<style scoped>
.custom-btn {
  width: 120rpx;
  height: 60rpx;
  line-height: 60rpx;
  text-align: center;
  border-radius: 30rpx;
  background-color: v-bind(bgColor); // 也可以用v-bind绑定props中的样式
  color: #fff;
  font-size: 28rpx;
}
</style>

外部页面中,先定义自定义样式类,再传入组件:

<template>
  <view>
    <my-custom-btn 
      customClass="gradient-btn" 
      @click="handleBtnClick"
    >
      渐变按钮
    </my-custom-btn>
  </view>
</template>

<style scoped>
/* 外部自定义样式类 */
.gradient-btn {
  background: linear-gradient(to right, #ff3366, #ff9900); /* 渐变背景 */
  box-shadow: 0 2rpx 10rpx rgba(255, 51, 102, 0.3); /* 阴影效果 */
}
.gradient-btn:hover {
  opacity: 0.9; /*  hover效果 */
}
</style>

注意:如果组件样式用了 scoped(避免样式污染),外部传入的类名可能无法生效,此时有两种解决方案:

  • 方案1:外部样式类不使用 scoped(不推荐,可能污染全局);
  • 方案2:组件中使用深度选择器 ::v-deep(推荐),修改组件样式如下:
<style scoped>
.custom-btn {
  /* 基础样式不变 */
}
/* 深度选择器:穿透scoped,让外部传入的类生效 */
::v-deep .gradient-btn {
  background: linear-gradient(to right, #ff3366, #ff9900);
  box-shadow: 0 2rpx 10rpx rgba(255, 51, 102, 0.3);
}
</style>

方式3:通过 slot 插入样式(极致灵活)

核心思路:如果组件的样式差异极大,甚至结构也有变化,可通过 slot 插入自定义样式(或整个结构),适合复杂场景,比如组件内部部分区域需要完全自定义。

修改组件,添加样式插槽(或结构插槽):

<template>
  &lt;view class="custom-btn" @click="handleClick"&gt;
    <!-- 插槽:支持外部传入整个内容(包括样式) -->
    <slot name="content">
      <view class="default-content">默认按钮</view>
    </slot>
  </view>
</template>

<script>
// ... 逻辑不变
</script>

<style scoped>
.custom-btn {
  width: 120rpx;
  height: 60rpx;
  line-height: 60rpx;
  text-align: center;
  border-radius: 30rpx;
  background-color: #007aff;
}
.default-content {
  color: #fff;
  font-size: 28rpx;
}
</style>

外部使用时,通过插槽插入自定义内容和样式,完全覆盖默认内容:

<my-custom-btn @click="handleBtnClick">
  <template #content>
    <view class="custom-content">
      <image src="/static/btn-icon.png" mode="widthFix" class="btn-icon"></image>
      <text class="btn-text">带图按钮</text>
    </view>
  </template>
</my-custom-btn>

<style scoped>
.custom-content {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #333;
  font-weight: bold;
}
.btn-icon {
  width: 30rpx;
  height: 30rpx;
  margin-right: 8rpx;
}
</style>

三、避坑指南:今天踩过的3个小坑

学习过程中遇到了几个常见问题,整理出来,帮大家少走弯路:

  1. 样式污染问题:忘记给组件样式加 scoped,导致组件样式影响全局页面,解决:给组件的 style 标签加上 scoped,如需穿透,用 ::v-deep
  2. props 传值类型错误:传入字体大小时,误传字符串(如 fontSize="32"),导致样式不生效,解决:props 中定义 fontSize 为 Number 类型,外部传入数字(如 fontSize="32" 改为 :fontSize="32",绑定数字)。
  3. uniapp 样式单位问题:习惯用 px 单位,导致不同设备适配异常,解决:uniapp 中推荐用 rpx 单位,自动适配不同屏幕,组件样式统一用 rpx。

四、学习总结

今天通过实操掌握了 uniapp 中 Vue 自定义组件封装的核心,尤其是自定义样式的3种实现方式,总结下来:

  • 简单样式修改:用 props 传值绑定 inline-style,高效快捷;
  • 复杂样式修改:用 props 传自定义类名 + 深度选择器,灵活度高;
  • 极致灵活场景:用 slot 插入自定义内容和样式,适配各种复杂需求。

其实自定义组件封装的核心就是「复用性」和「灵活性」,样式自定义更是如此——既要保证组件本身的规范性,又要支持外部按需修改。后续还要继续学习组件的生命周期、props 校验、事件传值等进阶内容,慢慢打磨组件封装能力~

如果小伙伴们有更好的样式自定义技巧,欢迎在评论区交流,一起进步!💪

一文理清页面/组件通信与 Store 全局状态管理

作者 远山枫谷
2026年3月7日 18:25

【小程序实战】告别繁琐传递!一文理清页面/组件通信与 Store 全局状态管理

📢 前言: 大家好,今天集中攻克了微信小程序开发中的两座大山:页面与组件的通信 以及 全局状态管理(Store)

在刚接触小程序时,我们通常习惯把所有逻辑都写在 Page 里;但随着项目变大,组件化是必经之路。而组件一旦多起来,数据怎么互相传递就成了头疼的问题。今天这篇笔记,就来总结一下我的学习成果,并分享几个避坑经验,希望能帮到正在学习小程序的你!


一、 页面与组件的“窃窃私语”:基础通信方式

在引入复杂的 Store 之前,我们必须先掌握原生的页面与组件通信方式。核心可以总结为三招:

1. 父传子:properties (属性绑定)

这是最基础的单向数据流。页面(父)通过属性将数据传递给组件(子)。

页面(父)端:

<!-- index.wxml -->
<my-component my-name="{{userName}}"></my-component>

组件(子)端:

// components/my-component/my-component.js
Component({
  properties: {
    myName: {
      type: String,
      value: '默认名字' // 默认值
    }
  }
})

2. 子传父:triggerEvent (事件绑定)

当组件内部发生了点击或数据改变,需要通知页面时,就需要用到自定义事件。

组件(子)端触发:

// 当点击按钮时触发
handleTap() {
  this.triggerEvent('myevent', { age: 18 }) // 传递对象给父级
}

页面(父)端接收:

<!-- index.wxml 绑定事件 -->
<my-component bind:myevent="handleChildEvent"></my-component>
// index.js 处理事件
handleChildEvent(e) {
  console.log('收到子组件的数据:', e.detail.age); // 输出 18
}

3. 父控子:selectComponent (获取组件实例)

有时候页面需要直接调用子组件里的方法,这时候可以通过给组件加 idclass,直接获取实例。

// 父页面的 js 中
const child = this.selectComponent('#my-child-id');
child.someMethod(); // 直接调用子组件的方法
// ⚠️ 经验:虽然好用,但不建议滥用,容易造成父子组件强耦合。

二、 告别“回调地狱”,拥抱 Store 全局状态管理

❓ 为什么需要 Store?

当遇到跨页面通信,或者兄弟组件通信(比如 A 组件的数据,C 组件也要用)时,如果用原生方法,你需要:A组件 -> 传给父页面 -> 传给B组件 -> ...。这种**“属性层层透传”**简直是噩梦!

这时候,Store(全局状态管理) 就闪亮登场了!在原生小程序中,我们通常使用 mobx-miniprogrammobx-miniprogram-bindings

1. 定义 Store (数据仓库)

首先创建一个 store.js,用来存放全局共享的数据和修改数据的方法。

import { observable, action } from 'mobx-miniprogram';

export const store = observable({
  // 1. 数据字段 (State)
  numA: 1,
  numB: 2,

  // 2. 计算属性 (Getters)
  get sum() {
    return this.numA + this.numB;
  },

  // 3. 修改数据的方法 (Actions)
  updateNumA: action(function (step) {
    this.numA += step;
  })
});

2. 在 Page 中使用 Store

在页面中使用,需要用到 createStoreBindings

import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from '../../store/store';

Page({
  onLoad() {
    // 绑定 Store
    this.storeBindings = createStoreBindings(this, {
      store,
      fields: ['numA', 'numB', 'sum'], // 需要的数据
      actions: ['updateNumA'] // 需要的方法
    })
  },
  
  onUnload() {
    // ⚠️ 重点:页面卸载时一定要解绑,防止内存泄漏!
    this.storeBindings.destroyStoreBindings();
  },

  btnHandler() {
    this.updateNumA(1); // 直接调用 store 中的 action
  }
})

3. 在 Component 中使用 Store

在组件中使用更加优雅,官方提供了一个 behavior

import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../../store/store';

Component({
  behaviors: [storeBindingsBehavior], // 引入 behavior
  storeBindings: {
    store,
    fields: {
      numA: () => store.numA, // 映射数据
      sum: 'sum'
    },
    actions: {
      updateNumA: 'updateNumA'
    }
  }
})

三、 💡 学习心得与“避坑”经验分享

经过今天的折腾,我对这两种方式有了更深的体会,总结了以下几条经验:

  1. 别把什么都塞进 Store 里! Store 确实“真香”,但千万别把什么数据都往里面丢。

    • 适合放 Store 的: 用户登录信息(Token、头像)、购物车数据、全局主题配置等(跨页面高度共享的数据)。
    • 适合放页面/组件内部(data)的: 表单的输入内容、弹窗的显示隐藏状态(isModalShow)、局部的 Loading 状态。保持局部状态的纯粹,代码才好维护。
  2. 时刻警惕内存泄漏 在 Page 中使用 createStoreBindings 时,必须、一定、千万要onUnload 生命周期里调用 destroyStoreBindings() 进行清理。如果你发现从小程序某个页面返回后,页面变卡或者数据出现诡异的重叠,大概率是忘记解绑了。

  3. 组件通信尽量保持“单向数据流” 即使有了 selectComponent,我们在开发组件时也应尽量遵循:父组件通过 properties 传值,子组件通过 triggerEvent 汇报。把子组件当成一个“黑盒”,这样写出来的组件复用性最高,不会因为换了个父页面就报错。


如果这篇文章对你有帮助,点个赞支持一下吧!你的鼓励是我持续分享的动力!


❌
❌