天天看点

分享一些Vue中实用的小技巧

Vue项目开发中一些常用实用的小技巧:

场景 1:开发vue项目的时候我们写了一堆基础UI组件,然后每次我们需要使用这些组件的时候,都得先import,然后声明components注册这些组件,这样的话如果我们封装了50个组件,那么就意味我们要写50个相同的组件引入语句,50个注册语句,大量的重复代码:

main.ts

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store/index';

import AppHeader from '@/components/app-header/app-header.vue';
import AppButton from '@/components/app-button/app-button.vue';
import AppModal from '@/components/app-modal/app-modal.vue';
import AppInput from '@/components/app-input/app-input.vue';

Vue.component('app-header', AppHeader);
Vue.component('app-button', AppButton);
Vue.component('app-modal', AppModal);
Vue.component('app-input', AppInput);
           

【优化解析】:如果遇到从一个文件夹引入很多模块的情况,我们可以借助一下神器webpack的一个api→ require.context() 方法来创建自己的(模块)上下文,它会遍历文件夹中的指定文件,然后自动导入,从而实现自动动态require组件。

语法:
/**
 * directory {String} -需要检索的文件目录
 * useSubdirectories {Boolean} -是否遍历文件的子目录
 * regExp {RegExp} - 匹配文件的正则 
**/
require.context(directory, useSubdirectories = false, regExp = /^.//);
           

【使用】

 (1)我们在components文件夹添加一个叫global.ts的文件,在这个文件里借助webpack动态将需要的基础组件统统打包进来。

import Vue from 'vue'

export function capitalizeFirstLetter(val: string) {
  return val.charAt(0).toUpperCase() + val.slice(1)
}

//找到components文件夹下以.vue命名的文件
export const files = require.context('@/components', true, /\.vue$/);

this.getComponents();

// 针对3中不同的组件设计模式提供不同的组件注册策略
export function getComponents() {
    this.files.keys().forEach(path => {
        const type = this.getType(path, this.files(path));
        
        switch (type) {
            case 'component':
                this.installComonent(Vue, this.files(path));
                break;
            case 'componentGroup':
                this.instalGroupComponent(Vue, this.files(path));
                break;
            case 'serveApi':
                this.installServeApi(Vue, this.files(path));
                break;
        }
    });
}

getType(path, file) {
    if (path.lastIndexOf('.ts') === -1) {
        return 'component';
    } else {
        if (file.default.install) {
            return 'serveApi';
        } else {
            return 'componentGroup';
        }
    }
}

instalGroupComponent(Vue, file) {
    const components = file.default;
    Object.keys(components).forEach(name => {
        Vue.component('Nb' + name, components[name]);
    });
}

installComonent(Vue, file) {
    const options = file.default;
    const name = options.name;
    Vue.component('Nb' + name, options);
}

installServeApi(Vue, file) {
    Vue.use(file.default);
}


// 最简单的使用
this.files.keys().forEach(fileName => {
  const componentConfig = this.files(fileName)
 
  const componentName = capitalizeFirstLetter(
    fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
    //因为得到的filename格式是: './baseButton.vue', 所以这里我们去掉头和尾,只保留真正的文件名
  )
 
  Vue.component(componentName, componentConfig.default || componentConfig)
})
           

(2)最后我们在main.ts中import 'components/global.js',然后我们就可以随时随地使用这些基础组件,无需手动引入了。 

场景 2:组件在创建的时候我们会通过接口获取一次列表,同时监听input框,每当发生变化的时候重新获取一次列表。

【解析】可以使用@Watch() 中的immediate属性 立即执行

@Watch

接受第一个参数为要监听的属性名 第二个属性为可选对象。

@Watch

所装饰的函数即监听到属性变化之后的操作.

有时候我们可能需要监听整个对象,例如我们针对一个对象中的某个属性进行监听,但是这个对象中有很多属性,任何一个属性的改变我们都需要重新发起请求,那么我们可以利用watch的deep属性进行深度监听,也就是监听复杂的数据类型。

immediate:true表示创建组件时立马执行一次。

import {Vue, Component, Watch} from 'vue-property-decorator';


可以直接利用 watch 的immediate和handler属性简写

@Watch('nameInput', { immediate: true, deep: true })
getData(newVal: Person, oldVal: Person){
    // todo...
}
           

场景 3:在使用

Vue

进行开发时,我们经常看到有些组件有些重复的 js 逻辑,这部分逻辑我们可以使用mixins(混合)来实现,有两种

mixins

的方法。

(1)是

vue-class-component

提供的

//定义要混合的类 mixins.ts
import Vue from 'vue';
import  Component  from 'vue-class-component';

@Component  // 一定要用Component修饰
export default class myMixins extends Vue {
    value: string = "Hello"
}

___________________________________________________________

// 引入
import  Component  {mixins}  from 'vue-class-component';
import myMixins from 'mixins.ts';

@Component
export class myComponent extends mixins(myMixins) {
                          // 直接extends myMinxins 也可以正常运行
      created(){
          console.log(this.value) // => Hello
    }
}
           

(2)是在

@Component

中混入

// 定义 mixins.ts
import { Vue, Component } from 'vue-property-decorator';


declare module 'vue/types/vue' {
    interface Vue {
        value: string;
    }
}

@Component
export default class myMixins extends Vue {
    value: string = 'Hello'
}


_________________________________________________________________


//引入mixin
import { Vue, Component, Prop } from 'vue-property-decorator';
import myMixins from '@static/js/mixins';

@Component({
    mixins: [myMixins]
})
export default class myComponent extends Vue{
    created(){
        console.log(this.value) // => Hello
    }
}
           

场景 3: 多个过滤器全局使用

// 多个过滤器全局注册
//定义filter.ts  //src/common/filters.ts
let dateFormat = value => value.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3') 
export { dateFormat }

//使用 /src/main.ts
import * as custom from './common/filters/custom'
Object.keys(custom).forEach(key => Vue.filter(key, custom[key]))
           

场景 4: 在使用Vue开发项目中我们经常用scoped属性表示它的样式只作用于当前模块,是样式私有化。

scoped渲染的规则/原理:

给HTML的DOM节点添加一个 不重复的data属性 来表示 唯一性;

在对应的 CSS选择器 末尾添加一个当前组件的 data属性选择器来私有化样式。

【问题】:使用了scoped属性后,我们组件内部的样式无法在外部被控制,(怎么都改不了样式)

【解决方案】:deep属性

.result-tab /deep/ {
  .nav-bar {
    a {
      flex: initial;
      height: 50px;
      font-size: 36px;
      margin-left: 40px;
      line-height: 100px;
      height: auto;
    }
  }
  .result-content {
    padding: 0 40px;
  }
}
           

场景 5:Modal弹框大体结构固定,只是中间内容有所变化,这种情况下就可以用到插槽。

通过slot可以向组件内部指定位置插入内容,通过slot可以实现父子传参

【使用】:

子组件:
<template>
    <div class="h-100">
        <div class="custom-modal" tabindex="-1">
            <div class="custom-modal-dialog custom-modal-dialog-margin upload-modal">
                <div class="custom-modal-content">
                    <div class="modal-body d-flex flex-column px-3 py-4">
                        <div class="modal-title mb-2 relative w-100">
                            <h5>{{ modalTitle | translate }}</h5>
                            <span class="clickable modal-close" @click="closeModal($event)">
                                <i class="gr gr-close gr-1x"></i>
                            </span>
                        </div>

                        <div class="modal-content">
                            // 作用域插槽,子组件将该user属性传递给父组件使用
                            <slot name="modal-content" :user="user"></slot>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="custom-modal-backdrop"></div>
    </div>
</template>

<script >
import { Component, Vue, Prop, Emit } from 'vue-property-decorator';
import ErrorToast from '@/utils/toast';

@Component({
    components: {}
})
export default class AppModal extends Vue {
    @Prop() modalTitle!: string;
    

    // 作用域插槽,子组件将该属性传递给父组件使用
    user: any = {
        name: 'jenny',
        jobTitle: 'CTO'
    }

    closeModal() {
        this.$ionic.modalController.dismiss();
    }
}
</script>


// 父组件
<template>
    <div class="h-100">
        <app-modal :modalTitle="'common.tip'">
            // 父组件使用子组件传过来的user属性, slotProps可以为任意名
            <div slot="modal-content" slot-scope="slotProps">
                {{ slotProps.user.name }}
                {{ slotProps.user.jobTitle }}
                <div class="mb-2">
                    <p>{{ 'my.item.logout.msg' | translate}}</p>
                    <ion-button
                        class="float-right border-radius-4"
                        @click="logout"
                    >{{ 'common.confirm' | translate }}</ion-button>
                </div>
            </div>
        </app-modal>
    </div>
</template>
           

场景 6:比如一个需求是从/post-page/a,跳转到/post-page/b。然后我们惊人的发现,页面跳转后数据竟然没更新?!

【原因】vue-router”智能地”发现这是同一个组件,然后它就决定要复用这个组件,所以你在created函数里写的方法压根就没执行。通常的解决方案是监听$route的变化来初始化数据。

【优化】可以给router-view添加一个unique的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。(虽然损失了一丢丢性能,但避免了无限的bug)。同时,注意我将key直接设置为路由的完整路径,一举两得。

<router-view :key="$route.fullpath"></router-view>
           

场景 7:实现动态换肤(比如 默认情况css会传递一个默认主题色给js, 当将用户选择某个主题色时再传给css)

js将变量值传给scss, 可以使用 

css var()

实现

:root {
  --main-bg-color: coral;
}
 
#div1 {
  background-color: var(--main-bg-color);
}
 
#div2 {
  background-color: var(--main-bg-color);
}
           

 scss变量传给js(可以通过 css-modules 

:export

来实现)

// variable.scss
$theme: blue;

:export {
  theme: $theme;
}



// test.ts
import variable from '@/styles/variable.scss'
console.log(variable.theme) // blue
           

场景 8:平时写业务的时候,免不了会对第三方组件进行二次封装,比如我们基于el-input组件进行封装,这样的话需要传入很多个@Prop 或者传多个$Emit

【优化】我们可以使用

v-bind="$attrs"

:传递所有属性;

v-on="$listeners"

传递所有方法

场景 9:当需要在子组件中修改父组件值的时候可以用(.sync)

<base-item :data.sync="itemData"></base-item>

等同于以下:

<base-item :data="itemData" @update:data="val => itemData = val"></base-item>

this.$emit('update:data', newVal)
           

场景 10: 避免一些全局操作:

(1)在组件内选择节点的时候一定要切记避免使用 document.querySelector()等一系列的全局选择器。
    应该使用this.$el或者this.refs.xxx.$el的方式来选择 DOM。
    这样就能将你的操作局限在当前的组件内,能避免很多问题。

(2)经常会不可避免的需要注册一些全局性的事件,
    比如监听页面窗口的变化 window.addEventListener('resize', this.__resizeHandler),
    但再声明了之后一定要在 beforeDestroy或者destroyed生命周期注销它。
    window.removeEventListener('resize', this.__resizeHandler)避免造成不必要的消耗。

(3)避免过多的全局状态,不是所有的状态都需要存在 vuex 中的,应该根据业务进行合理的进行取舍。
    如果不可避免有很多的值需要存在 vuex 中,建议使用动态注册的方式。
    只是部分业务需要的状态处理,建议使用 Event Bus或者使用 简单的 store 模式。

(4)css 也应该尽量避免写太多的全局性的样式。除了一些全局公用的样式外,所以针对业务的或者组件的
    样式都应该使用命名空间的方式或者直接使用 vue-loader 提供的 scoped写法,避免一些全局冲突。
           

场景 11:组件中注册一些全局事件的时候,都需要在

mounted

中声明,在

destroyed

中销毁,但由于这个是写在两个生命周期内的,很容易忘记,如果你在组件摧毁阶段忘记移除的话,会造成内存的泄漏,而且都不太容易发现。

【解决】使用Hook

mounted() {
    window.addEventListener('resize', this.handleResize())
}

beforeDestory() {
    window.removeEventListener('resize', this.handleResize())
}


修改后:

mounted() {
    window.addEventListener('resize', this.handleResize());
    
    this.$on('hook:destroyed', () => {
        window.removeEventListener('resize', this.handleResize())
    })
}