優雅更新props 更新 prop 在業務中是很常見的需求,但在子組件中不允許直接修改 prop,因為這種做法不符合單向數據流的原則,在開發模式下還會報出警告。因此大多數人會通過 $emit 觸發自定義事件,在父組件中接收該事件的傳值來更新 prop。 child.vue: export defal ...
優雅更新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
參數:
- 文件夾路徑
- 是否遞歸查找子文件夾下的模塊
- 模塊匹配規則,一般匹配文件尾碼名
只要是需要批量引入的場景,都可以使用這種方法。包括一些公用的全局組件,只需往文件夾內新增組件即可使用,不需要再去註冊。如果還沒用上的小伙伴,一定要瞭解下,簡單實用又能提高效率。
路由懶載入(動態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
})
在書寫更少代碼的同時,我們也把“魔法註釋”給犧牲掉了。總所周知,代碼中沒辦法編寫動態註釋。這個問題很尷尬,難道就沒有兩全其美的辦法了嗎?
強大的 webpack
來救場了,從 webpack 2.6.0 開始,占位符 [index] 和 [request] 被支持為遞增的數字或實際解析的文件名。我們可以這樣使用“魔法註釋”:
const routes = routeOptions.map(route => {
if (!route.component) {
route = {
...route,
component: () => import(/* webpackChunkName: "[request]" */ `@/views/${route.name}.vue`)
}
}
return route
})
最後
往期相關文章: