天天看點

【Vuejs】710- 10個Vue開發技巧助力成為更好的工程師

【Vuejs】710- 10個Vue開發技巧助力成為更好的工程師

作者: chanwahfung

優雅更新props

更新 ​

​prop​

​​ 在業務中是很常見的需求,但在子元件中不允許直接修改 ​

​prop​

​​,因為這種做法不符合單向資料流的原則,在開發模式下還會報出警告。是以大多數人會通過 ​

​$emit​

​​ 觸發自定義事件,在父元件中接收該事件的傳值來更新 ​

​prop​

​。

child.vue:

export defalut {
    props: {
        title: String  
    },
    methods: {
        changeTitle(){
            this.$emit('change-title', 'hello')
        }
    }
}      

parent.vue:

<child :title="title" @change-title="changeTitle"></child>      
export default {
    data(){
        return {
            title: 'title'
        }  
    },
    methods: {
        changeTitle(title){
            this.title = title
        }
    }
}      

這種做法沒有問題,我也常用這種手段來更新 ​

​prop​

​​。但如果你隻是想單純的更新 ​

​prop​

​​,沒有其他的操作。那麼 ​

​sync​

​ 修飾符能夠讓這一切都變得特别簡單。

parent.vue:

<child :title.sync="title"></child>      

child.vue:

export defalut {
    props: {
        title: String  
    },
    methods: {
        changeTitle(){
            this.$emit('update:title', 'hello')
        }
    }
}      

隻需要在綁定屬性上添加 ​

​.sync​

​​,在子元件内部就可以觸發 ​

​update:屬性名​

​​ 來更新 ​

​prop​

​。可以看到這種手段确實簡潔且優雅,這讓父元件的代碼中減少一個“沒必要的函數”。

參考文檔

provide/inject

這對選項需要一起使用,以允許一個祖先元件向其所有子孫後代注入一個依賴,不論元件層次有多深,并在其上下遊關系成立的時間裡始終生效。

簡單來說,一個元件将自己的屬性通過 ​

​provide​

​​ 暴露出去,其下面的子孫元件 ​

​inject​

​ 即可接收到暴露的屬性。

App.vue:

export default {
    provide() {
        return {
            app: this
        }
    } 
}      

child.vue:

export default {
    inject: ['app'],
    created() {
        console.log(this.app) // App.vue執行個體
    }
}      

在 2.5.0+ 版本可以通過設定預設值使其變成可選項:

export default {
    inject: {
        app: {
            default: () => ({})
        }
    },
    created() {
        console.log(this.app) 
    }
}      

如果你想為 ​

​inject​

​​ 的屬性變更名稱,可以使用 ​

​from​

​ 來表示其來源:

export default {
    inject: {
        myApp: {
            // from的值和provide的屬性名保持一緻
            from: 'app',
            default: () => ({})
        }
    },
    created() {
        console.log(this.myApp) 
    }
}      

需要注意的是 ​

​provide​

​​ 和 ​

​inject​

​ 主要在開發高階插件/元件庫時使用。并不推薦用于普通應用程式代碼中。但是某些時候,或許它能幫助到我們。

參考文檔

小型狀态管理器

大型項目中的資料狀态會比較複雜,一般都會使用 ​

​vuex​

​ 來管理。但在一些小型項目或狀态簡單的項目中,為了管理幾個狀态而引入一個庫,顯得有些笨重。

在 2.6.0+ 版本中,新增的 ​

​Vue.observable​

​ 可以幫助我們解決這個尴尬的問題,它能讓一個對象變成響應式資料:

// store.js
import Vue from 'vue'

export const state = Vue.observable({ 
  count: 0 
})      

使用:

<div @click="setCount">{{ count }}</div>      
import {state} from '../store.js'

export default {
    computed: {
        count() {
            return state.count
        }
    },
    methods: {
        setCount() {
            state.count++
        }
    }
}      

當然你也可以自定義 ​

​mutation​

​ 來複用更改狀态的方法:

import Vue from 'vue'

export const state = Vue.observable({ 
  count: 0 
})

export const mutations = {
  SET_COUNT(payload) {
    if (payload > 0) {
        state.count = payload
    } 
  }
}      

使用:

import {state, mutations} from '../store.js'

export default {
    computed: {
        count() {
            return state.count
        }
    },
    methods: {
        setCount() {
            mutations.SET_COUNT(100)
        }
    }
}      

參考文檔

解除安裝watch觀察

通常定義資料觀察,會使用選項的方式在 ​

​watch​

​ 中配置:

export default {
    data() {
        return {
            count: 1      
        }
    },
    watch: {
        count(newVal) {
            console.log('count 新值:'+newVal)
        }
    }
}      

除此之外,資料觀察還有另一種函數式定義的方式:

export default {
    data() {
        return {
            count: 1      
        }
    },
    created() {
        this.$watch('count', function(){
            console.log('count 新值:'+newVal)
        })
    }
}      

它和前者的作用一樣,但這種方式使定義資料觀察更靈活,而且 ​

​$watch​

​ 會傳回一個取消觀察函數,用來停止觸發回調:

let unwatchFn = this.$watch('count', function(){
    console.log('count 新值:'+newVal)
})
this.count = 2 // log: count 新值:2
unwatchFn()
this.count = 3 // 什麼都沒有發生...      

​$watch​

​ 第三個參數接收一個配置選項:

this.$watch('count', function(){
    console.log('count 新值:'+newVal)
}, {
    immediate: true // 立即執行watch
})      

參考文檔

巧用template

相信 ​

​v-if​

​ 在開發中是用得最多的指令,那麼你一定遇到過這樣的場景,多個元素需要切換,而且切換條件都一樣,一般都會使用一個元素包裹起來,在這個元素上做切換。

<div v-if="status==='ok'">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
</div>      

如果像上面的 div 隻是為了切換條件而存在,還導緻元素層級嵌套多一層,那麼它沒有“存在的意義”。

我們都知道在聲明頁面模闆時,所有元素需要放在 ​

​<template>​

​​ 元素内。除此之外,它還能在模闆内使用,​

​<template>​

​ 元素作為不可見的包裹元素,隻是在運作時做處理,最終的渲染結果并不包含它。

<template>
    <div>
        <template v-if="status==='ok'">
          <h1>Title</h1>
          <p>Paragraph 1</p>
          <p>Paragraph 2</p>
        </template>
    </div>
</template>      

同樣的,我們也可以在 ​

​<template>​

​​ 上使用 ​

​v-for​

​​ 指令,這種方式還能解決 ​

​v-for​

​​ 和 ​

​v-if​

​ 同時使用報出的警告問題。

<template v-for="item in 10">
    <div v-if="item % 2 == 0" :key="item">{{item}}</div>
</template>      

template使用v-if,

template使用v-for

過濾器複用

過濾器被用于一些常見的文本格式化,被添加在表達式的尾部,由“管道”符号訓示。

<div>{{ text | capitalize }}</div>      
export default {
    data() {
        return {
            text: 'hello'
        }  
    },
    filters: {
        capitalize: function (value) {
            if (!value) return ''
            value = value.toString()
            return value.charAt(0).toUpperCase() + value.slice(1)
         }
    }
}      

試想一個場景,不僅模闆内用到這個函數,在 ​

​method​

​​ 裡也需要同樣功能的函數。但過濾器無法通過 ​

​this​

​​ 直接引用,難道要在 ​

​methods​

​ 再定義一個同樣的函數嗎?

要知道,選項配置都會被存儲在執行個體的 ​

​$options​

​​ 中,是以隻需要擷取 ​

​this.$options.filters​

​ 就可以拿到執行個體中的過濾器。

export default {
    methods: {
        getDetail() {
            this.$api.getDetail({
                id: this.id
            }).then(res => {
                let capitalize = this.$options.filters.capitalize
                this.title = capitalize(res.data.title)
            })
        }
    }
}      

除了能擷取到執行個體的過濾器外,還能擷取到全局的過濾器,因為 ​

​this.$options.filters​

​​ 會順着 ​

​__proto__​

​ 向上查找,全局過濾器就存在原型中。

自定義指令擷取執行個體

有的情況下,當需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。像是項目中常用的權限指令,它能精确到某個子產品節點。大概思路為擷取權限清單,如果目前綁定權限不在清單中,則删除該節點元素。

Vue.directive('role', {
    inserted: function (el, binding, vnode) {
      let role = binding.value
      if(role){
        const applist = sessionStorage.getItem("applist")
        const hasPermission = role.some(item => applist.includes(item)) 
        // 是否擁有權限
        if(!hasPermission){
          el.remove() //沒有權限則删除子產品節點
        }
      }
    }
})      

自定義指令鈎子函數共接收3個參數,包括 ​

​el​

​​ (綁定指令的真實dom)、​

​binding​

​​ (指令相關資訊)、​

​vnode​

​ (節點的虛拟dom)。

假設現在業務發生變化,​

​applist​

​​ 存儲在 ​

​vuex​

​​ 裡, 但指令内想要使用執行個體上的屬性,或者是原型上的 ​

​$store​

​​。我們是沒有辦法擷取到的,因為鈎子函數内并沒有直接提供執行個體通路。​

​vnode​

​​ 作為目前的虛拟dom,它裡面可是綁定到執行個體上下文的,這時候通路 ​

​vnode.context​

​ 就可以輕松解決問題。

Vue.directive('role', {
    inserted: function (el, binding, vnode) {
      let role = binding.value
      if(role){
        // vnode.context 為目前執行個體
        const applist = vnode.context.$store.state.applist
        const hasPermission = role.some(item => applist.includes(item)) 
        if(!hasPermission){
          el.remove()
        }
      }
    }
})      

優雅注冊插件

插件通常用來為 ​

​Vue​

​​ 添加全局功能。像常用的 ​

​vue-router​

​​、​

​vuex​

​​ 在使用時都是通過 ​

​Vue.use​

​​ 來注冊的。​

​Vue.use​

​​ 内部會自動尋找 ​

​install​

​​ 方法進行調用,接受的第一個參數是 ​

​Vue​

​ 構造函數。

一般在使用元件庫時,為了減小包體積,都是采用按需加載的方式。如果在入口檔案内逐個引入元件會讓 ​

​main.js​

​​ 越來越龐大,基于子產品化開發的思想,最好是單獨封裝到一個配置檔案中。配合上 ​

​Vue.use​

​,在入口檔案使用能讓人一目了然。

vant.config.js:

import {
  Toast,
  Button
} from 'vant'

const components = {
  Toast,
  Button
}

const componentsHandler = {
  install(Vue){
    Object.keys(components).forEach(key => Vue.use(components[key]))
  }
}

export default componentsHandler      

main.js:

import Vue from 'vue'
import vantCompoents from '@/config/vant.config'

Vue.config.productionTip = false

Vue.use(vantCompoents)

new Vue({
  render: h => h(App)
}).$mount('#app')      

參考文檔

自動化引入子產品

在開發中大型項目時,會将一個大功能拆分成一個個小功能,除了能便于子產品的複用,也讓子產品條理清晰,後期項目更好維護。

像 api 檔案一般按功能劃分子產品,在組合時可以使用 ​

​require.context​

​​ 一次引入檔案夾所有的子產品檔案,而不需要逐個子產品檔案去引入。每當新增子產品檔案時,就隻需要關注邏輯的編寫和子產品暴露,​

​require.context​

​ 會幫助我們自動引入。

需要注意 ​

​require.context​

​​ 并不是天生的,而是由 ​

​webpack​

​​ 提供。在建構時,​

​webpack​

​ 在代碼中解析它。

let importAll = require.context('./modules', false, /\.js$/)

class Api extends Request{
    constructor(){
        super()
        //importAll.keys()為子產品路徑數組
        importAll.keys().map(path =>{
            //相容處理:.default擷取ES6規範暴露的内容; 後者擷取commonJS規範暴露的内容
            let api = importAll(path).default || importAll(path)
            Object.keys(api).forEach(key => this[key] = api[key])
        })
    }
}

export default new Api()      

​require.context​

​ 參數:

  1. 檔案夾路徑
  2. 是否遞歸查找子檔案夾下的子產品
  3. 子產品比對規則,一般比對檔案字尾名

隻要是需要批量引入的場景,都可以使用這種方法。包括一些公用的全局元件,隻需往檔案夾内新增元件即可使用,不需要再去注冊。如果還沒用上的小夥伴,一定要了解下,簡單實用又能提高效率。

參考文檔

路由懶加載(動态chunkName)

路由懶加載作為性能優化的一種手段,它能讓路由元件延遲加載。通常我們還會為延遲加載的路由添加“魔法注釋”(webpackChunkName)來自定義包名,在打包時,該路由元件會被單獨打包出來。

let router = new Router({
  routes: [
    {
      path:'/login',
      name:'login',
      component: import(/* webpackChunkName: "login" */ `@/views/login.vue`)
    },
    {
      path:'/index',
      name:'index',
      component: import(/* webpackChunkName: "index" */ `@/views/index.vue`)
    },
    {
      path:'/detail',
      name:'detail',
      component: import(/* webpackChunkName: "detail" */ `@/views/detail.vue`)
    }
  ]
})      

上面這種寫法沒問題,但仔細一看它們結構都是相似的,作為一名出色的開發者,我們可以使用 ​

​map​

​ 循環來解決這種重複性的工作。

const routeOptions = [
  {
    path:'/login',
    name:'login',
  },
  {
    path:'/index',
    name:'index',
  },
  {
    path:'/detail',
    name:'detail',
  },
]

const routes = routeOptions.map(route => {
  if (!route.component) {
    route = {
      ...route,
      component: () => import(`@/views/${route.name}.vue`)
    }
  }
  return route
})

let router = new Router({
  routes
})      

在書寫更少代碼的同時,我們也把“魔法注釋”給犧牲掉了。總所周知,代碼中沒辦法編寫動态注釋。這個問題很尴尬,難道就沒有兩全其美的辦法了嗎?

const routes = routeOptions.map(route => {
  if (!route.component) {
    route = {
      ...route,
      component: () => import(/* webpackChunkName: "[request]" */ `@/views/${route.name}.vue`)
    }
  }
  return route
})