|
|
51CTO旗下网站
|
|
移动端

10个Vue开发技巧

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

作者:WahFung来源:前端工匠|2020-06-30 08:28

路由参数解耦

一般在组件内使用路由参数,大多数人会这样做:

  1. export default { 
  2.     methods: { 
  3.         getParamsId() { 
  4.             return this.$route.params.id 
  5.         } 
  6.     } 

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

正确的做法是通过 props 解耦

  1. const router = new VueRouter({ 
  2.     routes: [{ 
  3.         path: '/user/:id', 
  4.         component: User, 
  5.         props: true 
  6.     }] 
  7. }) 

将路由的 props 属性设置为 true 后,组件内可通过 props 接收到 params 参数

  1. export default { 
  2.     props: ['id'], 
  3.     methods: { 
  4.         getParamsId() { 
  5.             return this.id 
  6.         } 
  7.     } 

另外你还可以通过函数模式来返回 props

  1. const router = new VueRouter({ 
  2.     routes: [{ 
  3.         path: '/user/:id', 
  4.         component: User, 
  5.         props: (route) => ({ 
  6.             id: route.query.id 
  7.         }) 
  8.     }] 
  9. }) 

文档:router.vuejs.org/zh/guide/es…

函数式组件

函数式组件是无状态,它无法实例化,没有任何的生命周期和方法。创建函数式组件也很简单,只需要在模板添加 functional 声明即可。一般适合只依赖于外部数据的变化而变化的组件,因其轻量,渲染性能也会有所提高。

组件需要的一切都是通过 context 参数传递。它是一个上下文对象,具体属性查看文档。这里 props 是一个包含所有绑定属性的对象。

函数式组件:

  1. <template functional> 
  2.     <div class="list"> 
  3.         <div class="item" v-for="item in props.list" :key="item.id" @click="props.itemClick(item)"> 
  4.             <p>{{item.title}}</p> 
  5.             <p>{{item.content}}</p> 
  6.         </div> 
  7.     </div> 
  8. </template> 

父组件使用:

  1. <template> 
  2.     <div> 
  3.         <List :list="list" :itemClick="item => (currentItem = item)" /> 
  4.     </div> 
  5. </template> 
  1. import List from '@/components/List.vue' 
  2. export default { 
  3.     components: { 
  4.         List 
  5.     }, 
  6.     data() { 
  7.         return { 
  8.             list: [{ 
  9.                 title: 'title', 
  10.                 content: 'content' 
  11.             }], 
  12.             currentItem: '' 
  13.         } 
  14.     } 

文档:cn.vuejs.org/v2/guide/re…

样式穿透

在开发中修改第三方组件样式是很常见,但由于 scoped 属性的样式隔离,可能需要去除 scoped 或是另起一个 style 。这些做法都会带来副作用(组件样式污染、不够优雅),样式穿透在css预处理器中使用才生效。

我们可以使用 >>> 或 /deep/ 解决这一问题:

  1. <style scoped> 
  2. 外层 >>> .el-checkbox { 
  3.   display: block; 
  4.   font-size: 26px; 
  5.  
  6.   .el-checkbox__label { 
  7.     font-size: 16px; 
  8.   } 
  9. </style> 
  1. <style scoped> 
  2. /deep/ .el-checkbox { 
  3.   display: block; 
  4.   font-size: 26px; 
  5.  
  6.   .el-checkbox__label { 
  7.     font-size: 16px; 
  8.   } 
  9. </style> 

watch高阶使用

1. 立即执行

watch 是在监听属性改变时才会触发,有些时候,我们希望在组件创建后 watch 能够立即执行。

可能想到的的方法就是在 create 生命周期中调用一次,但这样的写法不优雅,或许我们可以使用这样的方法:

  1. export default { 
  2.     data() { 
  3.         return { 
  4.             name: 'Joe' 
  5.         } 
  6.     }, 
  7.     watch: { 
  8.         name: { 
  9.             handler: 'sayName', 
  10.             immediate: true 
  11.         } 
  12.     }, 
  13.     methods: { 
  14.         sayName() { 
  15.             console.log(this.name) 
  16.         } 
  17.     } 

深度监听在监听对象时,对象内部的属性被改变时无法触发 watch ,我们可以为其设置深度监听:

  1. export default { 
  2.     data: { 
  3.         studen: { 
  4.             name: 'Joe', 
  5.             skill: { 
  6.                 run: { 
  7.                     speed: 'fast' 
  8.                 } 
  9.             } 
  10.         } 
  11.     }, 
  12.     watch: { 
  13.         studen: { 
  14.             handler: 'sayName', 
  15.             deep: true 
  16.         } 
  17.     }, 
  18.     methods: { 
  19.         sayName() { 
  20.             console.log(this.studen) 
  21.         } 
  22.     } 

2. 触发监听

执行多个方法使用数组可以设置多项,形式包括字符串、函数、对象:

  1. export default { 
  2.     data: { 
  3.         name: 'Joe' 
  4.     }, 
  5.     watch: { 
  6.         name: [ 
  7.             'sayName1', 
  8.             function(newVal, oldVal) { 
  9.                 this.sayName2() 
  10.             }, 
  11.             { 
  12.                 handler: 'sayName3', 
  13.                 immaediate: true 
  14.             } 
  15.         ] 
  16.     }, 
  17.     methods: { 
  18.         sayName1() { 
  19.             console.log('sayName1==>', this.name) 
  20.         }, 
  21.         sayName2() { 
  22.             console.log('sayName2==>', this.name) 
  23.         }, 
  24.         sayName3() { 
  25.             console.log('sayName3==>', this.name) 
  26.         } 
  27.     } 

文档:cn.vuejs.org/v2/api/#wat…

watch监听多个变量

watch本身无法监听多个变量。但我们可以将需要监听的多个变量通过计算属性返回对象,再监听这个对象来实现“监听多个变量”

  1. export default { 
  2.     data() { 
  3.         return { 
  4.             msg1: 'apple', 
  5.             msg2: 'banana' 
  6.         } 
  7.     }, 
  8.     compouted: { 
  9.         msgObj() { 
  10.             const { msg1, msg2 } = this 
  11.             return { 
  12.                 msg1, 
  13.                 msg2 
  14.             } 
  15.         } 
  16.     }, 
  17.     watch: { 
  18.         msgObj: { 
  19.             handler(newVal, oldVal) { 
  20.                 if (newVal.msg1 != oldVal.msg1) { 
  21.                     console.log('msg1 is change') 
  22.                 } 
  23.                 if (newVal.msg2 != oldVal.msg2) { 
  24.                     console.log('msg2 is change') 
  25.                 } 
  26.             }, 
  27.             deep: true 
  28.         } 
  29.     } 

事件参数$event

$event 是事件对象的特殊变量,在一些场景能给我们实现复杂功能提供更多可用的参数

1. 原生事件

在原生事件中表现和默认的事件对象相同:

  1. <template> 
  2.     <div> 
  3.         <input type="text" @input="inputHandler('hello', $event)" /> 
  4.     </div> 
  5. </template> 
  1. export default { 
  2.     methods: { 
  3.         inputHandler(msg, e) { 
  4.             console.log(e.target.value) 
  5.         } 
  6.     } 

2. 自定义事件

在自定义事件中表现为捕获从子组件抛出的值

my-item.vue:

  1. export default { 
  2.     methods: { 
  3.         customEvent() { 
  4.             this.$emit('custom-event', 'some value') 
  5.         } 
  6.     } 
  7. 复制代码 

App.vue:

  1. <template> 
  2.     <div> 
  3.         <my-item v-for="(item, index) in list" @custom-event="customEvent(index, $event)"> 
  4.             </my-list> 
  5.     </div> 
  6. </template> 
  1. export default { 
  2.     methods: { 
  3.         customEvent(index, e) { 
  4.             console.log(e) // 'some value' 
  5.         } 
  6.     } 

文档:

  • cn.vuejs.org/v2/guide/ev…
  • cn.vuejs.org/v2/guide/co…

自定义组件双向绑定

组件 model 选项:

允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。

input 默认作为双向绑定的更新事件,通过 $emit 可以更新绑定的值

  1. <my-switch v-model="val"></my-switch> 
  1. export default { 
  2.     props: { 
  3.         value: { 
  4.             type: Boolean, 
  5.             default: false 
  6.         } 
  7.     }, 
  8.     methods: { 
  9.         switchChange(val) { 
  10.             this.$emit('input', val) 
  11.         } 
  12.     } 

修改组件的 model 选项,自定义绑定的变量和事件

  1. <my-switch v-model="num" value="some value"></my-switch> 
  1. export default { 
  2.     model: { 
  3.         prop: 'num', 
  4.         event: 'update' 
  5.     }, 
  6.     props: { 
  7.         value: { 
  8.             type: String, 
  9.             default: '' 
  10.         }, 
  11.         num: { 
  12.             type: Number, 
  13.             default: 0 
  14.         } 
  15.     }, 
  16.     methods: { 
  17.         numChange() { 
  18.             this.$emit('update', this.num++) 
  19.         } 
  20.     } 

文档:cn.vuejs.org/v2/api/#mod…

监听组件生命周期

通常我们监听组件生命周期会使用 $emit ,父组件接收事件来进行通知

子组件:

  1. export default { 
  2.     mounted() { 
  3.         this.$emit('listenMounted') 
  4.     } 

父组件:

  1. <template> 
  2.     <div> 
  3.         <List @listenMounted="listenMounted" /> 
  4.     </div> 
  5. </template> 

其实还有一种简洁的方法,使用 @hook 即可监听组件生命周期,组件内无需做任何改变。同样的, created 、 updated 等也可以使用此方法。

  1. <template> 
  2.     <List @hook:mounted="listenMounted" /> 
  3. </template> 

程序化的事件侦听器

比如,在页面挂载时定义计时器,需要在页面销毁时清除定时器。这看起来没什么问题。但仔细一看 this.timer 唯一的作用只是为了能够在 beforeDestroy 内取到计时器序号,除此之外没有任何用处。

  1. export default { 
  2.     mounted() { 
  3.         this.timer = setInterval(() => { 
  4.             console.log(Date.now()) 
  5.         }, 1000) 
  6.     }, 
  7.     beforeDestroy() { 
  8.         clearInterval(this.timer) 
  9.     } 

如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。

我们可以通过 $on 或 $once 监听页面生命周期销毁来解决这个问题:

  1. export default { 
  2.     mounted() { 
  3.         this.creatInterval('hello') 
  4.         this.creatInterval('world') 
  5.     }, 
  6.     creatInterval(msg) { 
  7.         let timer = setInterval(() => { 
  8.             console.log(msg) 
  9.         }, 1000) 
  10.         this.$once('hook:beforeDestroy', function() { 
  11.             clearInterval(timer) 
  12.         }) 
  13.     } 

使用这个方法后,即使我们同时创建多个计时器,也不影响效果。因为它们会在页面销毁后程序化的自主清除。

文档:cn.vuejs.org/v2/guide/co…

手动挂载组件

在一些需求中,手动挂载组件能够让我们实现起来更加优雅。比如一个弹窗组件,最理想的用法是通过命令式调用,就像 elementUI 的 this.$message 。而不是在模板中通过状态切换,这种实现真的很糟糕。

先来个最简单的例子:

  1. import Vue from 'vue' 
  2. import Message from './Message.vue' 
  3.  
  4. // 构造子类 
  5. let MessageConstructor = Vue.extend(Message) 
  6. // 实例化组件 
  7. let messageInstance = new MessageConstructor() 
  8. // $mount可以传入选择器字符串,表示挂载到该选择器 
  9. // 如果不传入选择器,将渲染为文档之外的的元素,你可以想象成 document.createElement()在内存中生成dom 
  10. messageInstance.$mount() 
  11. // messageInstance.$el获取的是dom元素 
  12. document.body.appendChild(messageInstance.$el) 

下面实现一个简易的 message 弹窗组件

Message/index.vue:

  1. <template> 
  2.     <div class="wrap"> 
  3.         <div class="message" :class="item.type" v-for="item in notices" :key="item._name"> 
  4.             <div class="content">{{item.content}}</div> 
  5.         </div> 
  6.     </div> 
  7. </template> 
  1. // 默认选项 
  2. const DefaultOptions = { 
  3.     duration: 1500, 
  4.     type: 'info', 
  5.     content: '这是一条提示信息!', 
  6. let mid = 0 
  7. export default { 
  8.     data() { 
  9.         return { 
  10.             notices: [] 
  11.         } 
  12.     }, 
  13.     methods: { 
  14.         add(notice = {}) { 
  15.             // name标识 用于移除弹窗 
  16.             let _name = this.getName() 
  17.             // 合并选项 
  18.             notice = Object.assign({ 
  19.                 _name 
  20.             }, DefaultOptions, notice) 
  21.  
  22.             this.notices.push(notice) 
  23.  
  24.             setTimeout(() => { 
  25.                 this.removeNotice(_name) 
  26.             }, notice.duration) 
  27.         }, 
  28.         getName() { 
  29.             return 'msg_' + (mid++) 
  30.         }, 
  31.         removeNotice(_name) { 
  32.             let index = this.notices.findIndex(item => item._name === _name) 
  33.             this.notices.splice(index, 1) 
  34.         } 
  35.     } 
  1. .wrap { 
  2.     position: fixed; 
  3.     top: 50px; 
  4.     left: 50%; 
  5.     display: flex; 
  6.     flex-direction: column; 
  7.     align-items: center; 
  8.     transform: translateX(-50%); 
  9.  
  10. .message { 
  11.     --borderWidth: 3px; 
  12.     min-width: 240px; 
  13.     max-width: 500px; 
  14.     margin-bottom: 10px; 
  15.     border-radius: 3px; 
  16.     box-shadow: 0 0 8px #ddd; 
  17.     overflow: hidden; 
  18.  
  19. .content { 
  20.     padding: 8px; 
  21.     line-height: 1.3; 
  22.  
  23. .message.info { 
  24.     border-left: var(--borderWidth) solid #909399; 
  25.     background: #F4F4F5; 
  26.  
  27. .message.success { 
  28.     border-left: var(--borderWidth) solid #67C23A; 
  29.     background: #F0F9EB; 
  30.  
  31. .message.error { 
  32.     border-left: var(--borderWidth) solid #F56C6C; 
  33.     background: #FEF0F0; 
  34.  
  35. .message.warning { 
  36.     border-left: var(--borderWidth) solid #E6A23C; 
  37.     background: #FDF6EC; 

Message/index.js:

  1. import Vue from 'vue' 
  2. import Index from './index.vue' 
  3.  
  4. let messageInstance = null 
  5. let MessageConstructor = Vue.extend(Index) 
  6.  
  7. let init = () => { 
  8.     messageInstance = new MessageConstructor() 
  9.     messageInstance.$mount() 
  10.     document.body.appendChild(messageInstance.$el) 
  11.  
  12. let caller = (options) => { 
  13.     if (!messageInstance) { 
  14.         init(options) 
  15.     } 
  16.     messageInstance.add(options) 
  17.  
  18. export default { 
  19.     // 返回 install 函数 用于 Vue.use 注册 
  20.     install(vue) { 
  21.         vue.prototype.$message = caller 
  22.     } 

main.js:

  1. import Message from '@/components/Message/index.js' 
  2.  
  3. Vue.use(Message) 

使用:

  1. this.$message({ 
  2.     type: 'success', 
  3.     content: '成功信息提示', 
  4.     duration: 3000 
  5. }) 

文档:cn.vuejs.org/v2/api/#vm-…

【编辑推荐】

  1. 前端老司机都在用的Chrome开发者工具15 个小技巧
  2. .NET开发人员的十大工具
  3. 前端开发与架构师
  4. 微软开发出一套能恢复严重退化老照片的新算法
  5. 前端面试必考题:React Hooks原理剖析
【责任编辑:赵宁宁 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微