一、引言
Mixin 的概念
在编程中,Mixin 是一种代码复用的技术,它允许你将多个类中的代码提取出来,形成一个独立的模块,并在需要的时候将其应用到其他类中。Mixin 可以用来实现代码的重用、扩展和定制。
Mixin 的主要作用
- 代码重用:通过将共同的代码提取到一个 Mixin 中,可以避免在多个类中重复编写相同的代码,从而提高代码的可维护性和可读性。
- 功能扩展:使用 Mixin 可以在不修改原始类的情况下,向类中添加新的功能或行为。这对于已经在使用的类特别有用,因为你可以通过添加 Mixin 来扩展其功能,而无需修改现有代码。
- 灵活定制:Mixin 允许你根据具体需求组合不同的功能,从而创建出具有特定行为的类。你可以选择应用一个或多个 Mixin,以及自定义 Mixin 的实现,以满足项目的特定要求。
- 更好的代码组织:Mixin 有助于将相关的功能组织到一个单独的模块中,使代码更易于理解和维护。
二、Vue 中的 Mixin
解释 Mixin 在 Vue 中的工作原理
在 Vue 中,Mixin 是一种用于代码复用的特性。它允许你将一个组件中的部分功能提取出来,并在其他组件中重复使用。
Mixin 的工作原理是通过将 Mixin 的内容合并到组件的选项中。当一个组件使用了 Mixin,它会将 Mixin 中的属性、方法和生命周期钩子函数合并到自己的选项中。这样,组件就可以访问和使用 Mixin 中定义的属性和方法。
如何在脚手架环境中创建和使用 Mixin
首先,创建一个名为mixinJs的文件,以便存放mixin。在该文件中,定义一个名为myMixin的mixin对象:
mixin.js file
export const myMixin = { data() { return { mixinData: 'Hello, I am data from Mixin' } }, methods: { mixinMethod() { alert("This method is from Mixin"); } }, mounted() { console.log(this.mixinData) } }
在这个mixin中定义了:
- 一个数据 mixinData
- 一个方法 mixinMethod
- 一个生命周期钩子函数 mounted
然后,在需要使用的组件中导入并使用这个mixin:
component file
import { myMixin } from './mixin.js' export default { name: 'YourComponent', mixins: [myMixin], methods: { yourComponentMethod () { this.mixinMethod() } } }
在组件中,我们使用mixins选项来引入myMixin。现在我们可以访问在mixin中定义的所有数据和方法,并在组件的生命周期钩子函数中使用它们。
在yourComponentMethod方法中,我调用了mixin中定义的mixinMethod方法,此方法将打印出一个警告信息。
实际上,也可以在组件中定义与mixin中相同的方法或生命周期钩子,Vue将优先使用组件内部的定义。
三、使用 Mixin 的注意事项
- 命名冲突:当应用多个 Mixin 到同一个类时,可能会出现命名冲突。为了避免这种情况,应该仔细设计 Mixin 中的属性和方法,并确保它们具有唯一的命名。
- 继承顺序:在应用多个 Mixin 时,继承顺序可能会影响代码的执行结果。如果两个 Mixin 中定义了同名的方法,那么子类将继承最近的方法。因此,在设计 Mixin 时,需要考虑它们的继承顺序。
- 可读性:由于 Mixin 可以在运行时动态地应用到类上,所以代码的可读性可能会受到影响。为了提高可读性,可以使用注释来说明 Mixin 的作用和应用方式。
- 性能:在某些情况下,使用 Mixin 可能会导致性能下降,特别是当应用大量的 Mixin 或在运行时动态地应用 Mixin 时。如果对性能有严格要求,可以考虑其他实现方式。
- 可维护性:随着项目的发展,可能会添加更多的 Mixin,这可能会增加代码的复杂性。为了保持代码的可维护性,应该定期审查和整理 Mixin,并确保它们的功能是必要的。
常见的 Mixin 用例
表单验证
- 表单验证 Mixin:这个 Mixin 可以用于验证表单中的输入数据,确保其符合特定的规则
创建一个表单验证 Mixin
此处编写的就是一个 Vue 组件实例的配置项,通过一定语法,可以直接混入到组件内部 data methods computed 生命周期函数
注意点:
- 如果此处和组件内,提供了同名的
data
或methods
,则组件内优先级更高 - 如果编写了生命周期函数,则
mixins
中的生命周期函数 和 页面的生命周期函数,会用数组管理,统一执行
export default { methods: { loginConfirm () { // 判断用户是否登录 if (!this.$store.getters.token) { this.$dialog.confirm({ title: '温馨提示', message: '此时需要先登录才能继续操作哦', confirmButtonText: '去登录', cancelButtonText: '再逛逛' }) .then(() => { this.$router.replace({ path: '/login', query: { backUrl: this.$route.fullPath } }) }) .catch(() => {}) return true } return false } } }
此处是对于该 mixin 的使用 用以进行表单的校验
<template> <div class="pay"> <van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" /> <!-- 地址相关 --> <div class="address"> <div class="left-icon"> <van-icon name="logistics" /> </div> <div class="info" v-if="selectedAddress.address_id"> <div class="info-content"> <span class="name">{{ selectedAddress.name }}</span> <span class="mobile">{{ selectedAddress.phone }}</span> </div> <div class="info-address"> {{ longAddressList }} </div> </div> <div class="info" v-else> 请选择配送地址 </div> <div class="right-icon"> <van-icon name="arrow" /> </div> </div> <!-- 订单明细 --> <div class="pay-list" v-if="order.goodsList"> <div class="list"> <div class="goods-item" v-for="item in order.goodsList" :key="item.goods_id"> <div class="left"> <img :src="item.goods_image" alt="" /> </div> <div class="right"> <p class="tit text-ellipsis-2"> {{ item.goods_name }} </p> <p class="info"> <span class="count">x{{ item.total_num }}</span> <span class="price">¥{{ item.total_pay_price }}</span> </p> </div> </div> </div> <div class="flow-num-box"> <span>共 {{ order.orderTotalNum }} 件商品,合计:</span> <span class="money">¥{{ order.orderTotalPrice }}</span> </div> <div class="pay-detail"> <div class="pay-cell"> <span>订单总金额:</span> <span class="red">¥{{ order.orderTotalPrice }}</span> </div> <div class="pay-cell"> <span>优惠券:</span> <span>无优惠券可用</span> </div> <div class="pay-cell"> <span>配送费用:</span> <span v-if="!selectedAddress">请先选择配送地址</span> <span v-else class="red">+¥0.00</span> </div> </div> <!-- 支付方式 --> <div class="pay-way"> <span class="tit">支付方式</span> <div class="pay-cell"> <span><van-icon name="balance-o" />余额支付(可用 ¥ {{ personal.balance }} 元)</span> <!-- <span>请先选择配送地址</span> --> <span class="red"><van-icon name="passed" /></span> </div> </div> <!-- 买家留言 --> <div class="buytips"> <textarea v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea> </div> </div> <!-- 底部提交 --> <div class="footer-fixed"> <div class="left">实付款:<span>¥{{ order.orderTotalPrice }}</span></div> <div class="tipsbtn" @click="submitOrder">提交订单</div> </div> </div> </template> <script> import { getAddressList } from '@/api/address' import { checkOrder, submitOrder } from '@/api/order' import loginConfirm from '@/mixins/loginConfirm' export default { name: 'PayIndex', mixins: [loginConfirm], data () { return { addresslist: [], order: {}, personal: {}, remark: '' // 留言 } }, created () { this.getAddressList() this.getOrderList() }, computed: { selectedAddress () { return this.addresslist[0] || {} }, longAddressList () { const region = this.selectedAddress.region return region.province + region.city + region.regin }, mode () { return this.$route.query.mode }, cartIds () { return this.$route.query.cartIds }, goodsId () { return this.$route.query.goodsId }, goodsSkuId () { return this.$route.query.goodsSkuId }, goodsNum () { return this.$route.query.goodsId } }, methods: { async getAddressList () { const { data: { list } } = await getAddressList() this.addresslist = list }, async getOrderList () { // 购物车结算 if (this.mode === 'cart') { const { data: { order, personal } } = await checkOrder(this.mode, { cartIds: this.cartIds }) this.order = order this.personal = personal } // 立刻购买结算 if (this.mode === 'buyNow') { const { data: { order, personal } } = await checkOrder(this.mode, { goodsId: this.goodsId, goodsSkuId: this.goodsSkuId, goodsNum: this.goodsNum }) this.order = order this.personal = personal } }, async submitOrder () { if (this.mode === 'cart') { await submitOrder(this.mode, { cartIds: this.cartIds, remark: this.remark }) } if (this.mode === 'buyNow') { await submitOrder(this.mode, { goodsId: this.goodsId, goodsSkuId: this.goodsSkuId, goodsNum: this.goodsNum, remark: this.remark }) } this.$toast.success('支付成功') this.$router.replace('/myorder') } } } </script> <style lang="less" scoped> .pay { padding-top: 46px; padding-bottom: 46px; ::v-deep { .van-nav-bar__arrow { color: #333; } } } .address { display: flex; align-items: center; justify-content: flex-start; padding: 20px; font-size: 14px; color: #666; position: relative; background: url(@/assets/border-line.png) bottom repeat-x; background-size: 60px auto; .left-icon { margin-right: 20px; } .right-icon { position: absolute; right: 20px; top: 50%; transform: translateY(-7px); } } .goods-item { height: 100px; margin-bottom: 6px; padding: 10px; background-color: #fff; display: flex; .left { width: 100px; img { display: block; width: 80px; margin: 10px auto; } } .right { flex: 1; font-size: 14px; line-height: 1.3; padding: 10px; padding-right: 0px; display: flex; flex-direction: column; justify-content: space-evenly; color: #333; .info { margin-top: 5px; display: flex; justify-content: space-between; .price { color: #fa2209; } } } } .flow-num-box { display: flex; justify-content: flex-end; padding: 10px 10px; font-size: 14px; border-bottom: 1px solid #efefef; .money { color: #fa2209; } } .pay-cell { font-size: 14px; padding: 10px 12px; color: #333; display: flex; justify-content: space-between; .red { color: #fa2209; } } .pay-detail { border-bottom: 1px solid #efefef; } .pay-way { font-size: 14px; padding: 10px 12px; border-bottom: 1px solid #efefef; color: #333; .tit { line-height: 30px; } .pay-cell { padding: 10px 0; } .van-icon { font-size: 20px; margin-right: 5px; } } .buytips { display: block; textarea { display: block; width: 100%; border: none; font-size: 14px; padding: 12px; height: 100px; } } .footer-fixed { position: fixed; background-color: #fff; left: 0; bottom: 0; width: 100%; height: 46px; line-height: 46px; border-top: 1px solid #efefef; font-size: 14px; display: flex; .left { flex: 1; padding-left: 12px; color: #666; span { color:#fa2209; } } .tipsbtn { width: 121px; background: linear-gradient(90deg,#f9211c,#ff6335); color: #fff; text-align: center; line-height: 46px; display: block; font-size: 14px; } } </style>
<template> <div class="prodetail" v-if="detail.goods_name"> <van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" /> <van-swipe :autoplay="3000" @change="onChange"> <van-swipe-item v-for="(image, index) in images" :key="index" > <img :src="image.external_url" /> </van-swipe-item> <template #indicator> <div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div> </template> </van-swipe> <!-- 商品说明 --> <div class="info"> <div class="title"> <div class="price"> <span class="now">¥ {{ detail.goods_price_min }}</span> <span class="oldprice">¥ {{ detail.goods_price_max }}</span> </div> <div class="sellcount">已售 {{ detail.goods_sales }} 件</div> </div> <div class="msg text-ellipsis-2"> {{ detail.goods_name }} </div> <div class="service"> <div class="left-words"> <span><van-icon name="passed" />七天无理由退货</span> <span><van-icon name="passed" />48小时发货</span> </div> <div class="right-icon"> <van-icon name="arrow" /> </div> </div> </div> <!-- 商品评价 --> <div class="comment" v-if="total > 0"> <div class="comment-title"> <div class="left">商品评价 ({{ total }}条)</div> <div class="right">查看更多 <van-icon name="arrow" /> </div> </div> <div class="comment-list"> <div class="comment-item" v-for="item in commentList" :key="item.comment_id"> <div class="top"> <img :src="item.user.avatar_url || defaultImg" alt=""> <div class="name">{{ item.user.nick_name }}</div> <van-rate :size="16" :value="item.score / 2" color="#ffd21e" void-icon="star" void-color="#eee"/> </div> <div class="content"> {{ item.content }} </div> <div class="time"> {{ item.create_item }} </div> </div> </div> </div> <!-- 商品描述 --> <div class="tips">商品描述</div> <div class="desc" v-html="detail.content"></div> <!-- 底部 --> <div class="footer"> <div @click="$router.push('/')" class="icon-home"> <van-icon name="wap-home-o" /> <span>首页</span> </div> <div class="icon-cart"> <span v-if="cartTotal > 0" class="num">{{ cartTotal }}</span> <van-icon @click="$router.push('/cart')" name="shopping-cart-o" /> <span>购物车</span> </div> <div @click="addFn" class="btn-add">加入购物车</div> <div @click="buyFn" class="btn-buy">立刻购买</div> </div> <!-- 加入购物车弹层 --> <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'"> <div class="product"> <div class="product-title"> <div class="left"> <img :src="detail.goods_image" alt=""> </div> <div class="right"> <div class="price"> <span>¥</span> <span class="nowprice">{{ detail.goods_price_min }}</span> </div> <div class="count"> <span>库存</span> <span>{{ detail.stock_total }}</span> </div> </div> </div> <div class="num-box"> <span>数量</span> <!-- v-model 本质上是 :value 和 @input 的简写--> <!-- 也就相当于是父传子 value 了--> <!-- 也就相当于是来了一个自定义事件 @input 了 --> <CountBox v-model="addCount"></CountBox> </div> <div class="showbtn" v-if="detail.stock_total > 0"> <div class="btn" v-if="mode === 'cart'" @click="addCart">加入购物车</div> <div @click="goBuyNow" class="btn now" v-else>立刻购买</div> </div> <div class="btn-none" v-else>该商品已抢完</div> </div> </van-action-sheet> </div> </template> <script> import { addCart } from '@/api/cart.js' import defaultImg from '@/assets/default-avatar.png' import { getProDetail, getProComments } from '@/api/product.js' import CountBox from '@/components/CountBox.vue' import loginConfirm from '@/mixins/loginConfirm' export default { name: 'ProDetail', mixins: [loginConfirm], components: { CountBox }, data () { return { images: [], current: 0, detail: {}, total: 0, commentList: [], defaultImg, showPannel: false, mode: 'cart', addCount: 1, // 数字框绑定的数据 cartTotal: 0 } }, computed: { goodsId () { return this.$route.params.id } }, async created () { this.getDetail() this.getComments() }, methods: { onChange (index) { this.current = index }, async getDetail () { const { data: { detail } } = await getProDetail(this.goodsId) this.detail = detail this.images = detail.goods_images }, async getComments () { const { data: { list, total } } = await getProComments(this.goodsId, 3) this.total = total this.commentList = list }, addFn () { this.mode = 'cart' this.showPannel = true }, buyFn () { this.mode = 'buyNow' this.showPannel = true }, async addCart () { // 未登录处理:需要弹出一个确认框 if (this.loginConfirm()) { return } const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id) this.cartTotal = data.cartTotal this.$toast('加入购物车成功') this.showPannel = false }, goBuyNow () { // 未登录处理:需要弹出一个确认框 if (this.loginConfirm()) { return } this.$router.push({ path: '/pay', query: { mode: 'buyNow', goodsId: this.goodsId, goodsSkuId: this.detail.skuList[0].goods_sku_id, goodsNum: this.addCount } }) } } } </script> <style lang="less" scoped> .prodetail { padding-top: 46px; ::v-deep .van-icon-arrow-left { color: #333; } img { display: block; width: 100%; } .custom-indicator { position: absolute; right: 10px; bottom: 10px; padding: 5px 10px; font-size: 12px; background: rgba(0, 0, 0, 0.1); border-radius: 15px; } .desc { width: 100%; overflow: scroll; ::v-deep img { display: block; width: 100%!important; } } .info { padding: 10px; } .title { display: flex; justify-content: space-between; .now { color: #fa2209; font-size: 20px; } .oldprice { color: #959595; font-size: 16px; text-decoration: line-through; margin-left: 5px; } .sellcount { color: #959595; font-size: 16px; position: relative; top: 4px; } } .msg { font-size: 16px; line-height: 24px; margin-top: 5px; } .service { display: flex; justify-content: space-between; line-height: 40px; margin-top: 10px; font-size: 16px; background-color: #fafafa; .left-words { span { margin-right: 10px; } .van-icon { margin-right: 4px; color: #fa2209; } } } .comment { padding: 10px; } .comment-title { display: flex; justify-content: space-between; .right { color: #959595; } } .comment-item { font-size: 16px; line-height: 30px; .top { height: 30px; display: flex; align-items: center; margin-top: 20px; img { width: 20px; height: 20px; } .name { margin: 0 10px; } } .time { color: #999; } } .footer { position: fixed; left: 0; bottom: 0; width: 100%; height: 55px; background-color: #fff; border-top: 1px solid #ccc; display: flex; justify-content: space-evenly; align-items: center; .icon-home, .icon-cart { display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 14px; .van-icon { font-size: 24px; } } .btn-add, .btn-buy { height: 36px; line-height: 36px; width: 120px; border-radius: 18px; background-color: #ffa900; text-align: center; color: #fff; font-size: 14px; } .btn-buy { background-color: #fe5630; } } } .tips { padding: 10px; } // 弹层样式 .product { .product-title { display: flex; .left { img { width: 90px; height: 90px; } margin: 10px; } .right { flex: 1; padding: 10px; .price { font-size: 14px; color: #fe560a; .nowprice { font-size: 24px; margin: 0 5px; } } } } .num-box { display: flex; justify-content: space-between; padding: 10px; align-items: center; } .btn, .btn-none { height: 40px; line-height: 40px; margin: 20px; border-radius: 20px; text-align: center; color: rgb(255, 255, 255); background-color: rgb(255, 148, 2); } .btn.now { background-color: #fe5630; } .btn-none { background-color: #cccccc; } } .footer .icon-cart { position: relative; padding: 0 6px; .num { z-index: 999; position: absolute; top: -2px; right: 0; min-width: 16px; padding: 0 4px; color: #fff; text-align: center; background-color: #ee0a24; border-radius: 50%; } } </style>
五、高级 Mixin 技巧
首先在脚手架环境下创建一个新的Vue项目
- 参数传递
有一个基础的 Mixin,打印一条消息,并且希望可以动态地改变这条消息:
baseMixin.js
export default { created() { console.log(this.message) this.message = '胡昌城是最牛逼的人' } }
然后我们在一个 Vue 组件中使用这个 Mixin,并传入参数:
HelloWorld.vue
import baseMixin from './baseMixin' export default { mixins: [baseMixin], data() { return { message: 'Hello World from mixin!' } } }
当组件创建的时候,mixin 的 created 生命周期钩子会被调用,会打印出 Hello World from mixin! 这条消息。
- 动态 Mixin
在组件选项或实例化 Vue 之后,动态添加 Mixin。
dynamicMixin.js
export default { created() { console.log('Dynamic mixin!') } }
HelloWorld.vue
import dynamicMixin from './dynamicMixin'; export default { created() { if (this.needsMixin) { this.$options.mixins.push(dynamicMixin) } }, data() { return { needsMixin: true } } }
如果 needsMixin 为 true,那么动态的 mixin 会被添加到组件中。
三、扩展 Mixin
可以使用 Mixin 来扩展另一个 Mixin 的功能:
baseMixin.js\
export default { methods: { hello() { console.log('Hello from base mixin!') } } }
extendedMixin.js
import baseMixin from './baseMixin'; export default { mixins: [baseMixin], methods: { hello() { this.$super.hello() console.log('Hello from extended mixin!') } }, created() { this.hello() } }
extendedMixin 扩展了 baseMixin 的 hello 方法。 这里使用了一个名为 $super 的特殊对象来调用基础 Mixin 中的方法。
六、总结
- Mixin 是一种在 JavaScript 中实现代码复用的设计模式。它的重要性和优势包括:
- 代码复用:Mixin 允许你将可复用的功能提取到独立的模块中,并在多个组件中共享这些功能,从而减少代码的冗余。
- 模块解耦:Mixin 有助于将复杂的组件分解成更小、更独立的模块,从而提高代码的可维护性和可读性。
- 灵活性:通过使用 Mixin,你可以在运行时动态地组合和扩展组件的功能,使代码更具灵活性和扩展性。
- 可定制性:Mixin 可以通过参数传递或扩展来实现定制化,允许你根据具体需求进行微调。
- 提高开发效率:使用 Mixin 可以更快地构建和复用代码,减少重复编写相同功能的时间和精力。