Vue 项目中的常用小技巧
技术栈:Vue2.0 + TypeScript + vue-property-decorator + ElementUI
$listeners $attrs
- 双向数据绑定的几种方式
eventBus
provide / inject
- 利用Mixins提高代码可维护性`
- 动态组件
- ::v-deep 和 /deep/ 和 >>>
- 巧用装饰器
- require.context
$listeners $attrs
比如有三层组件 grandParent.vue、parent.vue、child.vue
如果props想从 grandParent 传到 child中,会先传到parent,然后在parent中传给child,
// grandParent.vue
<template>
<div>
<Parent :parentName="parentName" :childName="childName" @on-child-click="handleChildClick" />
</div>
</template>
<script lang="ts">
import Parent from './parent.vue';
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component({
components: {
Parent,
},
})
export default class GrandParent extends Vue {
public parentName = 'parent';
public childName = 'child';
public handleChildClick(name: string) {
alert('child name: ' + name);
}
}
</script>
Parent.vue 可以用$attrs
把剩下没用到的prop传递给child.vue, 而v-on="$listeners"
可以将子组件emit的事件传递给父组件
<template>
<div>
{{ parentName }}
<Child v-bind="$attrs" v-on="$listeners"/>
</div>
</template>
<script lang="ts">
import Child from './child.vue';
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component({
components: {
Child,
},
})
export default class Parent extends Vue {
@Prop({ default: '' }) public parentName!: string;
}
</script>
Child.vue就可以在prop接收到数据
<template>
<div @click="handleClick">
{{ childName }}
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component({})
export default class Child extends Vue {
@Prop({ default: '' }) public childName!: string;
public handleClick() {
this.$emit('on-child-click', this.childName);
}
}
</script>
当数据传递和emit事件触发需要经过中间层,而中间层不需要这些数据的时候, 可以通过 v-bind="$attrs"
和 v-on="$listeners"
来做。
双向数据绑定的几种方式
computed、.sync
实现双向数据绑定的方法就是value
和this.$emit('input')
, 但我们知道 prop的数据是不能改的,所以当子组件接收到value,要用另一个变量去保存,如果是引用类型的value,则需要深拷贝。
-
计算属性会创建一个响应式数据
<template> <div> {{ childData }} <Child v-model="childData"/> </div> </template> <script lang="ts"> import Child from './child.vue'; import { Component, Vue, Prop } from 'vue-property-decorator'; @Component({ components: { Child, }, }) export default class Parent extends Vue { public childData = [1, 2, 3]; } </script> // child.vue <template> <button @click="handleClick">add</button> </template> <script lang="ts"> import { Component, Vue, Prop } from 'vue-property-decorator'; import cloneDeep from './util'; @Component({}) export default class Child extends Vue { @Prop() public value!: Array<number>; // 这里的get、set就是computed的写法。 get currentData() { return cloneDeep(this.value); } set currentData(val: Array<number>) { this.$emit('input', val); } public handleClick() { this.currentData.push(this.currentData[this.currentData.length - 1] + 1); } } </script>
-
.sync
的方法更方便,我们只需要将传递的属性加上propName.sync
, 更新的时候this.$emit('update:propName', newValue)
就可以实现双向数据绑定。
eventBus 事件总线
创建全局的事件总线
import Vue from 'vue';
let bus:any = null;
const create = function () {
if (!bus) {
bus = new Vue();
}
return bus;
};
export const Bus = create();
eventbus是发布订阅的模式,有以下几个方法
$on
注册$on(name: string, callback: Function)
$emit
触发$emit(name)
$off
解除$off(name)
发送消息:
import { Component, Vue } from 'vue-property-decorator';
import { Bus } from '@/utils/event-bus';
@Component({})
export default class EventBus extends Vue {
public name = 'Event Bus';
public handleChangeName() {
Bus.$emit('changeName', 'Event Bus Name');
}
}
其他页面接收消息
import { Component, Vue } from 'vue-property-decorator';
import { Bus } from '@/utils/event-bus';
@Component({})
export default class EventBus extends Vue {
public name = '';
public mounted() {
Bus.$on('changeName', (name: string) => {
console.log('getName', name); // Event Bus Name
});
}
}
provide / inject
vue不建议provide / inject 在业务中使用,但做组件的时候问题不大,说说该功能。
功能: 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
vue-property-decorator 提供了Provide
和 inject
、ProvideReactive
和 InjectReactive
的装饰器。
实际上 vue-property-decorator 提供的 provide / inject 是不具备响应式的
\ | 单向数据流 | 响应式 |
---|---|---|
Provide / Inject | ❌ | ❌ |
ProvideReactive / InjectReactive | OK | ❌ |
祖先组件提供 provide 了一个 parentName,其子孙组件都可以通过inject去接收
<template>
<div>
{{ parentName }} <br/>
{{ parentNumber }} <br/>
<button @click="handleChangeParentName">Change Parent</button>
<Child />
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide, ProvideReactive } from 'vue-property-decorator';
import Child from './child.vue';
@Component({
components: {
Child,
},
})
export default class ProvideInject extends Vue {
@Provide()
parentName = 'parent';
@ProvideReactive()
parentNumber = 1;
handleChangeParentName() {
this.parentName = 'parent2';
this.parentNumber++;
}
}
</script>
子孙组件
<template>
<div>
{{ name }} <br>
{{ parentName }}<br>
{{ parentNumber }}<br>
</div>
</template>
<script lang="ts">
import { Component, Vue, Inject, InjectReactive } from 'vue-property-decorator';
@Component({})
export default class Child extends Vue {
public name = 'child';
@Inject() parentName: string;
@InjectReactive() parentNumber: number;
}
</script>
当点击 Change Parent 按钮时,发现子组件的parentNumber会更新,而parentName不会,这就是Provide / Inject
和 ProvideReactive / InjectReactive
的区别。
实现响应式:
可以通过传入了一个可监听的对象实现响应式。
祖先组件: 用Provide 提供了一个响应式对象 obj
<template>
<div>
{{ obj.currentName }} <br/>
<Child />
</div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from 'vue-property-decorator';
import Child from './child.vue';
@Component({
components: {
Child,
},
})
export default class ProvideInject extends Vue {
public name = 'parent';
@Provide()
obj = {
currentName: this.name,
initData: this.initData,
};
initData() {
console.log('Parent Init Data');
}
}
</script>
子孙组件,通过inject获取这个obj,而这个对象是响应式的,直接改变obj,会改变祖先组件的obj。
这样就可以更新祖先组件的数据,或者执行祖先组件的函数。
<template>
<div>
{{ name }} <br/>
{{ obj.currentName }}
<button @click="handleChangeParentName">Change Parent Name</button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Inject } from 'vue-property-decorator';
@Component({})
export default class Child extends Vue {
public name = 'child';
@Inject() obj:any;
handleChangeParentName() {
this.obj.currentName = 'parent2';
this.obj.initData();
}
}
</script>
利用Mixins提高代码可维护性
利用Mixins去抽取公共代码,比如把分页的代码抽取
import { Component, Vue } from 'vue-property-decorator';
@Component({})
export default class CommonMixins extends Vue{
public paginations = {
pageSize: 20,
total: 0,
currentPage: 1,
}
handleChangePageSize (pageSize: number, cb: Function) {
this.paginations.pageSize = pageSize;
cb();
}
handleChangePageNum (currentPage: number, cb: Function) {
this.paginations.currentPage = currentPage;
cb();
}
}
然后在业务页面中引入mixin, 可以传入多个。
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import CommonMixins from "./common-mixin";
import PermissionMixins from "./permission-mixin";
@Component({})
export default class Parent extends Mixins(CommonMixins, PermissionMixins) {
}
</script>
如果只需要一个的话,可以直接继承
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import CommonMixins from "./common-mixin";
@Component({})
export default class Parent extends CommonMixins {
}
</script>
动态组件
组件在加载都是同步的,但当页面内容很多,有些组件并不需要一开始就加载出来的比如弹窗类的组件,这些就可以用动态组件,当用户执行了某些操作后再加载出来。
<template>
<div>
主页面 <br/>
<button @click="handleClick1">点击记载组件1</button><br/>
<button @click="handleClick2">点击记载组件2</button><br/>
<component :is="child1"></component>
<component :is="child2"></component>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component({})
export default class AsyncComponent extends Vue {
public child1:Component = null;
public child2:Component = null;
handleClick1() {
this.child1 = require('./child1').default;
}
handleClick2() {
this.child2 = require('./child2').default;
}
}
</script>
component可以配合v-show去控制显示和隐藏,这样这个component只会mounted一次,优化性能。
[::v-deep 和 /deep/ 和 >>>
](::v-deep 和 /deep/ 和 >>>)
想更改ui组件样式,且加上scoped的时候可以这么使用
<style scoped>
>>> .ivu-tabs-tabpane {
background: #f1f1f1;
}
</style>
<style lang="scss" scoped>
/deep/ .ivu-tabs-tabpane {
background: #f1f1f1;
}
</style>
<style lang="scss" scoped>
::v-deep .ivu-tabs-tabpane {
background: #f1f1f1;
}
</style>
巧用装饰器
装饰器可以提高代码可读性、可维护性和功能复用
防抖函数
import debounce from 'lodash.debounce';
export function Debounce(delay: number, config: object = {}) {
return (target: any, prop: string) => {
return {
value: debounce(target[prop], delay, config),
};
};
}
使用
@Debounce(300)
onIdChange(val: string) {
this.$emit('idchange', val);
}
确认框
Element UI 的确认提示框,用装饰器封装起来
import Vue from 'vue';
interface ConfirmationConfig {
title: string;
message: string;
options?: object;
}
export function Confirmation(config: ConfirmationConfig) {
return function (target: any, name: string, descriptor: PropertyDescriptor) {
const fn = target[name];
let _instance: any = null;
descriptor.value = function (...args: any[]) {
Vue.prototype
.$confirm(
config.message,
config.title,
Object.assign(
{
showCancelButton: true,
beforeClose: (action: string, instance: any, done: Function) => {
_instance = instance;
instance.confirmButtonLoading = true;
if (action === 'confirm') {
fn.call(this, done, instance, ...args);
} else {
done();
}
},
},
config.options || {}
)
)
.then(() => {
_instance.confirmButtonLoading = false;
})
.catch(() => {
_instance.confirmButtonLoading = false;
});
};
return descriptor;
};
}
使用
@confirmation({
title: '标题',
message: '内容',
})
async handleTest(done: Function, instance: any) {
try {
await getNewTask();
done();
} catch (error) {
instance.confirmButtonLoading = false;
}
}
require.context
原本我们注册组件的时候需要一个个引入并且一个个注册,而且后面想加新的,又要再写上。
// import WmsTable from './wms-table/table/index';
import Table from './table/index.vue';
import CustomHooks from './custom-hooks/custom-hooks-actions/index';
import SFilter from './s-filter/filter-form';
import WButton from './button/index';
import CreateForm from './createForm/create-form/CreateForm.vue';
import Action from './table/action-table-column.vue';
import DetailItem from './detail-item.vue';
Vue.component('w-filter', SFilter);
Vue.component('w-button', WButton);
Vue.component('custom-hooks', CustomHooks);
Vue.component('create-form', CreateForm);
Vue.component('w-table', Table);
Vue.component('w-table-action', Action);
Vue.component('zonetime-date-picker', ZonetimeDatePicker);
Vue.component('detail', DetailItem);
注册全局组件的时候,不需要一个一个import,和一个个去注册,使用 require.context 可以自动导入模块,这样的好处在于,当我们新建一个组件,不用自己再去手写注册,而在一开始就帮我们自动完成。
const contexts = require.context('./', true, /\.(vue|ts)$/);
export default {
install (vm) {
contexts.keys().forEach(component => {
const componentEntity = contexts(component).default;
if (componentEntity.name) {
vm.component(componentEntity.name, componentEntity);
}
});
}
};