Vue 项目中的常用小技巧

Vue 项目中的常用小技巧

技术栈:Vue2.0 + TypeScript + vue-property-decorator + ElementUI

$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

实现双向数据绑定的方法就是valuethis.$emit('input'), 但我们知道 prop的数据是不能改的,所以当子组件接收到value,要用另一个变量去保存,如果是引用类型的value,则需要深拷贝。

  1. 计算属性会创建一个响应式数据

    <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>
    
  2. .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是发布订阅的模式,有以下几个方法

  1. $on注册 $on(name: string, callback: Function)
  2. $emit触发 $emit(name)
  3. $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 提供了ProvideinjectProvideReactiveInjectReactive的装饰器。

实际上 vue-property-decorator 提供的 provide / inject 是不具备响应式的

\单向数据流响应式
Provide / Inject
ProvideReactive / InjectReactiveOK

祖先组件提供 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 / InjectProvideReactive / 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);
      }
    });
  }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值