Vue開發中的實用技巧

動態導入本地圖片

假設我們有這么一個功能,后臺返回圖片的名稱,前端需要自己拼接路徑獲取本地圖片,假定這些資源是存在我們前端的 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 一下就搞定了,現在你可以在任意組件中調用你的公共組件了!

該方法在批量處理一些文件的時候會有奇效!

以上就是我日常開發中遇到或者總結到的一些東西,如果你有更好更優雅的方式,記得分享哈~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容