Vue开发中的实用技巧

1,640 阅读5分钟

动态导入本地图片

假设我们有这么一个功能,后台返回图片的名称,前端需要自己拼接路径获取本地图片,假定这些资源是存在我们前端的 assets/images,如果你采取传统的字符串拼接的方式:

//template
<img :src="'@/assets/images' + imgUrl" alt="">

发现图片无法显示,打开控制台审查元素,发现路径并没有正确解析,这个跟 webpack 编译打包有关系,在编译过程中目录结构改变导致的。

我们只需要一个 require 方法就可以完美解决这个问题:

//template
<img :src="require('@/assets/images' + imgUrl)" alt="">

刷新瞧瞧,是不是可以了~

开发和生产的路由配置

配置路由的时候,开发环境下不需要使用 lazy-loading 加载 , 仅在生产环境使用即可,因为开发模式使用 lazy-loading 会导致 webpack 热更新比较慢。

可以创建2个 js ,分别为 _import_development.js_import_production.js 用来加载我们的组件

ps : 我的页面是在 views 文件夹下

// _import_development.js
module.exports = file => require('@/views' + file + '.vue').default;
// _import_production.js
module.exports = file => () => import('@/views/' + file + '.vue')

之后我们可以在我们的路由文件中通过当前运行环境( process.env.NODE_ENV )来加载不同的导入文件js,大概就变成下面的样子了。

// router.js

const _import = require('./_import_' + process.env.NODE_ENV);

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
    routes: [
    	{
            path: '/',
            name: 'Index',
            component: _import('/index')
    	}
    ],
    mode: 'history'
})

带参数的自定义指令

我们平时书写自定义指令大部分都是以下的这种方式:

Vue.directive('background', {
  inserted: function (el) {
    // 修改背景色
    el.style.backgroundColor = 'red'
  }
})

这样可能在某些场景下显得不够灵活,其实我们是可以给指令传递参数的,我们可以将上面的代码改成下面这样:

Vue.directive('background', {
  inserted: function (el,binding) {
    // 修改背景色
    el.style.backgroundColor = binding.value
  }
})

其中第二个参数 binding 是一个对象,包含下面这些属性:

  • name:指令名,不包括 v- 前缀。
  • value:指令的绑定值,例如:v-background="'red'" 中,绑定值为 red
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。
  • expression :字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
  • arg :传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
  • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

使用时就可以

//固定值
<div class="" v-background="'red'">Hello World</div>

//动态传值
<div class="" v-background="color">Hello World</div>

其中 color 绑定的是 data 里面的 color 值。

如果你觉得这么还不够灵活,我想动态修改参数可以吗?当然没问题,请看下面:

Vue.directive('style', {
  inserted: function (el,binding) {
    el.style[binding.arg] = binding.value;
  }
})

这么一来,你想修改啥直接写就是啦。

// 修改背景色
<div class="" v-style:[`background`]="'red'">Hello World</div>

如果我想批量修改,so easy 传个对象就好了,如下

Vue.directive('style', {
  inserted: function (el,binding) {
    for( let key in binding.value){
      el.style[key] = binding.value[key]
    }
  }
})

// 批量修改
<div class="" v-style="{ color : 'white' , background : 'red'}">Hello World</div>

带参数过滤器

过滤器通常在** 双花括号插值**和 v-bind 表达式 中使用,经常是为了来格式化一些文本之类的。

它跟自定义指令一样,也是可以带参数的,不过过滤器比起指令要简单的多。

假设我们需要将后端传过来的时间戳格式化一下,一般的这么写就可以了:

// ps: 这里引入了一个 moment 包

Vue.filter('formatDate',function (val) {
    return moment.unix(val).format('YYYY-MM-DD HH:mm:ss')
})

后来为了让用户可以自定义显示格式,后端增加了一个formate字段,我们不得不修改我们的过滤器,这时候就需要给过滤器加参数,来解决这个问题

Vue.filter('formatDate',function (val,format) {
    return moment.unix(val).format(format)
})

调用的时候只需要这么传入即可:

<div class="">{{ timestamp | formatDate('YYYY/MM/DD HH:mm:ss') }}</div>

过滤器第一个参数仍然是原始的值,YYYY/MM/DD HH:mm:ss 作为第二个参数传到了 format 中,这样的拓展性是不是更好了呢~

$attrs解决数据多级传递

$attrs 官方解释是包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

理解起来一头雾水,其主要意思是父组件往子组件传没有在props里声明过的值时,子组件可以通过$attrs接受,且只包含父组件没有在props里声明的值。

通常我们如果需要从父组件接收传递很多个值,那么我们就需要在 props 里声明需要接受的值,如果孙子组件也需要,那么就又要重复在props中声明,显得非常繁琐。

例如我们现在有 ABC 三个组件,是父子孙的关系,如果 BC 组件都要从 A 组件中继承一系列的属性:

// com-a
<template>
    <com-b :title='title' :desc='desc' :date='date'></com-b>
</template>

// com-b
<template>
    <com-c v-bind='$attrs'></com-c>
</template>

这时候我们在 B 组件中通过 $attrs 就可以获取父组件传递的 titledescdate,此外我们只要给 C 组件绑定 v-bind='$attrs' , 同理,C 组件内部也就可以通过 $attrs 获取到 A 里面的值了~

ps: BC 组件的 DOM 上会绑定 A 传过来的属性,Vue 内部默认是这么处理的,要去掉的话给 BC组件加上 inheritAttrs : false 属性即可。

$listeners 的用法也比较类似,不赘述了~

跨组件通信的另一种方式

想到跨组件通信,可能会想到 eventBusvuex 之类的方法,实际上我们可以借助 vue 本身的依赖注入这种方案优雅实现

我们首先需要在 main.js 中,定义一个 eventHub , 这是我们的关键点

// main.js
new Vue({
    el: '#app',
    router,
    store,
    components: {
        App
    },
    data: {
        eventHub: new Vue()
    },
    template: '<App/>'
})

之后,在我们在需要监控的组件的生命周期中绑定一下:

// com-a
mounted () {
    this.$root.eventHub.$on('update',(data)=>{
    	console.log(data);
    })
},
beforeDestroy () {
    this.$root.eventHub.$off('update')
}

其他组件要触发改事件只需要一句话:

emitEevent () {
    this.$root.eventHub.$emit('update',{ msg : 'hello world' })
}

值得注意的是,一定要在 beforeDestroy 生命周期中通过 $off 取消监听,不然会重复监听导致触发多次,如果只需要触发一次事件的话,$once 绑定会更加不错。

函数式组件

Vue 里的函数式组件和 React 中的无状态组件有些类似,如果说一个组件没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法,那么这时候我们可以考虑使用函数式组件。

函数式组件跟普通组件相比,因为没有状态管理,声明周期,只是函数,所以渲染开销低很多,以此可以优化我们的代码。

通常函数式组件的声明方式有2种(局部组件为例):

一种是模版渲染方式加上 functional 关键字创建

<template functional>
/***/
</template>

另一种是通过 render 渲染函数,并加上 functional 属性来标识创建,这种方式比模版更接近编译器,更加底层,渲染会更加迅速。

export default {
    functional: true,
    // Props 是可选的
    props: {
        // ...
    },
    render: function (createElement, context) {
        // ...
    }
}

关于 render 函数,由于篇幅太长,这边不在赘述,想要了解更多细节和配置参数,可以参考官网的解释

由于函数式组件没有实例,为了弥补这个问题,组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • children: VNode 子节点的数组
  • slots: 一个函数,返回了包含所有插槽的对象
  • scopedSlots: 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners: 一个包含了所有父组件为当前组件注册的事件监听器的对象
  • injections: 如果使用了 inject 选项,则该对象包含了应当被注入的属性。

看起来雨里雾里,其实没那么高深!

我们来个例子更直观,我们现在需要渲染一个列表,没有具体的交互,仅做展示使用,为了优化代码,我们决定使用函数式组件来渲染。

列表数据格式如下:

// list-data
[{
    id : 1 ,
    title : '学习Vue'
},{
    id : 2 ,
    title : '学习React'
},{
    id : 3 ,
    title : '学习Angular'
}]

如果我们通过模版方式来做的的话,我们可以定义如下:

// todoList.vue
<template functional>
    <ul>
        <li v-for="item in props.todoList">{{ item.title }}</li>
    </ul>
</template>

如果我们通过 render 函数来创建的话(.js文件,不是 .vue文件), 那么应该是这样:

export default {
    functional: true,
    props: {
    	todoList: {
            type: Array,
            default: () => []
    	}
    },
    render: function (createElement, context) {
    	let oLi = context.props.todoList.map(item => {
            return createElement('li', {
            	domProps: {
                    innerHTML: item.title
            	}
            })
    	})
    	return createElement('ul', {}, oLi)
    }
}

我们通过 import 在父组件导入一下,然后看看结果

效果一样,虽然 template 方式看起来简单的多,但是很多时候我还是比较倾向于 render 方式,因为它在修改或者条件判断的时候会比较方便,也省去了不少 v-ifv-show 这些指令,看起来更加优雅。

自动化导入component

如果你定义了一系列的牛X的公共组件,然而你每次需要频繁的去 import xxx from '../xxx',还要

components : {
    ComponentA
    ComponentB
    ...
}

下面有种一劳永逸的方法,让你解放双手~

需要借助 webpack 里面的 require.context 方法,简单了解一下,通过它获取一个特定的上下文,然后从中读取指定目录下的文件和文件内容。

该方法有三个参数 directoryuseSubdirectoriesuseSubdirectories

  • directory {String} -读取文件的路径
  • useSubdirectories {Boolean} -是否遍历文件的子目录
  • regExp {RegExp} -匹配文件的正则

如果我要遍历 components 目录下的所有公共组件,我就可以这么做:

首先在components目录下创建一个 baseComponent.js 文件,

// baseComponent.js

import Vue from 'vue'
const autoRequireComponent = require.context('./', false, /.vue$/)

autoRequireComponent.keys().forEach(file => {
    //获取组件配置信息
    const componentConfig = autoRequireComponent(file).default
    //获取组件的名称 , 将 ./UButton.vue 名称替换成 UButton
    const componentName = file.replace(/^\.\//, '').replace(/\.\w+$/, '')
    //注册组件
    Vue.component(componentName, componentConfig)
})

其中 autoRequireComponent.keys() 返回的就是一个文件名称的数组,类似于 ['UButton','UInput'],然后遍历并通过 Vue.component 方法注册到全局

最后在 main.jsimport 一下就搞定了,现在你可以在任意组件中调用你的公共组件了!

该方法在批量处理一些文件的时候会有奇效!

以上就是我日常开发中遇到或者总结到的一些东西,如果你有更好更优雅的方式,记得分享哈~