Vue3 新特性 + TypeScript 小實戰

上次將 Composition API 大致梳理了一遍 ,這次主要是想記錄一些 vue3 相較 vue2 新增出來的一些特性和一些方法上使用的變動,話不多說,直接開擼。

Teleport


我們日常開發中經常會遇到這樣一個場景,比如我們封裝一個彈層 msk 組件,但是它包裹在一個 position: relative 定位的父元素中,此時它將被局限在父元素的大小中,我們很難將彈層鋪滿整個窗口。而 Teleport 提供了一種干凈的方法,允許我們控制在 DOM 中哪個父節點下渲染了 HTML,而不必求助于全局狀態或將其拆分為兩個組件。舉個栗子:

<body>
  <div id="root"></div>
  <div id="center"></div>
</body>
<script>
const app = Vue.createApp({
  template: `
    <div class="mask-wrapper">
      <msk></msk>
    </div>
  `
})
app.component('msk', {
  template: `
    <div class="msk">111</div>
  `
})
app.mount('#root')
</script>

瀏覽器渲染結果如下:


未使用 teleport

這肯定不是我們想要實現的效果,我們希望蒙層是充滿整個窗口的,此時我們可以直接將蒙層組件通過 teleport 渲染到 body 下面或者我們指定的 dom 節點下面,teleport 上面有一個 to 的屬性,它接受一個 css query selector 作為參數。如下栗子:

<script>
const app = Vue.createApp({
  template: `
  <div class="mask-wrapper">
    // 使用 to 屬性將其掛載到 id = center 的 dom 節點下
    // 我們也可以直接使用 to = body 將其直接掛載到 body 中
    <teleport to="#center">
      <msk></msk>
    </teleport>
  </div>
`
})
</script>

emits


我們知道在 vue2 中父子組件傳值會用到 props$emit ,但是在 vue3 中新增了 emits ,它的主要作用是匯總該組件有哪些自定義事件,可以是一個數組寫法,也可以是一個對象寫法,同時在對象寫法中還支持自定義函數,可以在運行時驗證參數是否正確。

了解它的基礎用法之后我們將 teleport 中寫入的小栗子重寫,讓其組件通信完成最基本的顯示和隱藏的動態交互功能。當然我們在子組件通過 $emit 觸發的事件要統一寫入 emits 數組中進行管理。

const app = Vue.createApp({
  template: `
    <div class="mask-wrapper">
      <button @click="openMsk">打開彈層</button>
      <teleport to="#center">
        <msk :isOpen="isOpen" @closeMsk="closeMsk"></msk>
      </teleport>
    </div>
  `,
  setup() {
    const { ref } = Vue
    const isOpen = ref(false)
    const openMsk = () => {
      isOpen.value = true
    }
    const closeMsk = () => {
      isOpen.value = false
    }
    return { openMsk, isOpen, closeMsk }
  }
})
app.component('msk', {
  props: ['isOpen'],
  // 子組件中我們會向父組件觸發 `closeMsk` 事件,所以將其統一寫入 `emits` 中方便管理維護
  emits: ['closeMsk'], 
  template: `
    <div class="msk" v-show="isOpen">
      <button @click="closeMsk">關閉彈層</button>
    </div>
  `,
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk')
    }
    return { closeMsk }
  }
})
app.mount('#root')

當然我們也可以在 emits 中使用對象寫法,并且傳入驗證的自定義函數:

app.component('msk', {
  props: ['isOpen'],
  emits: {
    // 'closeMsk': null, 無需驗證
    'closeMsk': (payload) => {
      return payload === 111 // 事件觸發時驗證傳入的值是否為 111
      // 驗證失敗,因為我傳入的是 222
     // 無效的事件參數:事件“closeMsk”的事件驗證失敗。
    }
  },
  setup(props, context) {
    const closeMsk = () => {
      context.emit('closeMsk', 222)
    }
    return { closeMsk }
  }
}

小伙伴們可以試一試,當然即使我傳入的值和驗證時的值不匹配但是并不會影響這個事件的正常執行,只是會在瀏覽器中給出警告提示。

Suspense


teleport 組件一樣,這也是 vue3.0 新推出來的一個全新的組件,它的主要作用是和異步組件一起使用,我們可以現在這里回憶一下 vue2.0 中我們是如何使用動態組件和異步組件的。

動態組件

vue 2.0vue3.0 動態組件的使用方式基本差不多,都是根據數據的變化,結合 component 這個標簽,來隨時動態切換組件的實現。這里簡單做個小回顧:

const app = Vue.createApp({
  setup() {
    const { ref, keepAlive } = Vue
    const currentItem = ref('first-item')
    const handleClick = () => {
      if (currentItem.value === 'first-item') {
        currentItem.value = 'second-item'
      } else {
        currentItem.value = 'first-item'
      }
    }
    return { currentItem, handleClick }
  },
  template: `
    <keep-alive>
      <component :is="currentItem"></component>
    </keep-alive>
    <button @click="handleClick">組件切換</button>
  `
})
app.component('first-item', {
  template: `
    <div>hello world</div>
  `
})
app.component('second-item', {
  template: `
    <input type="text" />
  `
})
app.mount('#root')
異步組件

以前,異步組件是通過將組件定義為返回 Promise 的函數來創建的,這里可以直接查看 vue2.0 中如何定義異步組件。但是在 vue3.0 中現在,由于函數式組件被定義為純函數,因此異步組件的定義需要通過將其包裝在新的 defineAsyncComponent 助手方法中來顯式地定義,其實也很簡單,看栗子就知道了:

const { defineAsyncComponent } = Vue
const app = Vue.createApp({
  template: `
    <div>
      <async-show></async-show>
      <async-common-item></async-common-item>
    </div>
  `
})
app.component('asyncShow', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve({
        template: `<div>我將在 1s 之后被渲染出來</div>`
      })
    }, 1000)
  })
}))
app.component('async-common-item', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve({
        template: `<div>我將在 3s 之后被渲染出來</div>`
      })
    }, 3000)
  })
}))

defineAsyncComponent 接受返回 Promise 的工廠函數。從服務器檢索組件定義后,應調用 Promiseresolve 回調。你也可以調用 reject(reason),來表示加載失敗。

接下來就可以引入我們的主角 Suspense 組件,他可以用來接收一個或多個異步組件,它本身支持兩個具名插槽,一個承載異步插件返回等待狀態的插槽,一個承載異步插件返回成功狀態的插槽。舉個小栗子:

const { defineAsyncComponent } = Vue
const app = Vue.createApp({
  template: `
  <Suspense >
    <template #default> // 異步組件成功內容包裹在 default 插槽中
      <async-show></async-show>
    </template>
    <template #fallback> // 異步組件未加載時顯示 fallback里的內容
      <h1>loading !!!!</h1>
    </template>
  </Suspense>
`
})

當然 Suspense 組件也支持多個異步組件的插入,并且它會等待所有異步組件都返回才將其顯示出來,不過此時我們需要在其根上包一層,如下栗子:

const app = Vue.createApp({
  template: `
    <Suspense >
      <template #default>
        <div>
          <async-show></async-show>
          <async-common-item></async-common-item>
        </div>
      </template>
      <template #fallback>
        <h1>loading !!!!</h1>
      </template>
    </Suspense>
  `
})

Provide / Inject


通常,當我們需要從父組件向子組件傳遞數據時,我們使用 props。想象一下這樣的結構:有一些深度嵌套的組件,而深層的子組件只需要父組件的部分內容。在這種情況下,如果仍然將 prop 沿著組件鏈逐級傳遞下去,可能會很麻煩。

對于這種情況,我們可以使用一對 provideinject。無論組件層次結構有多深,父組件都可以作為其所有子組件的依賴提供者。這個特性有兩個部分:父組件有一個 provide 選項來提供數據,子組件有一個 inject 選項來開始使用這些數據。

上面兩段話摘自官網,說的很明白,基礎的用法其實和 vue2 中差不多,但是我們知道 vue2 中無法實現數據的響應式監聽,但是 vue3 中我們使用 composition API 就可以完成對應響應式變化的監聽。我們先來回顧一下 vue2 中的基礎用法:

父組件像孫子組件傳遞固定值
const app = Vue.createApp({
  provide: {
    count: 1
  },
  template: `
    <child />
  `
})
app.component('child', {
  template: `
    <child-child></child-child>
  `
})
app.component('child-child', {
  inject: ['count'],
  template: `
    <div>{{count}}</div>
  `
})

如果我們想使用 provide 傳遞數據 data 中的值時,我們就不能用上面這種寫法,我們需要將 provide 轉換為返回對象的函數。栗子如下:

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  provide() {
    return {
      count: this.count
    }
  }
})
父組件像孫子組件動態傳值

如果此時我們新增一個按鈕改變父組件中 count 的值,子組件是無法繼續監聽到改變后的值的。此時如果我們想對祖先組件中的更改做出響應,我們需要為 providecount 分配一個組合式API computed

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  methods: {
    handleClick() {
      this.count++
    }
  },
  provide() {
    return {
      count: Vue.computed(() => this.count)
    }
  },
  template: `
    <child />
    <button @click="handleClick">增加</button>
  `
})
app.component('child', {
  template: `
  <child-child></child-child>
`
})
app.component('child-child', {
  inject: ['count'],
  template: `
  <div>{{count.value}}</div>
`
})

當然此時我們還沒有用到 Composition API 來實現,我們在使用 Composiiton API 來重構下上面的代碼:

const { ref, provide, inject } = Vue
const app = Vue.createApp({
  setup() {
    let count = ref(1)
    const handleClick = () => {
      count.value++
    }
    provide('count', count)
    return { handleClick }
  },
  template: `
    <child />
    <button @click="handleClick">增加</button>
  `
})
app.component('child', {
  template: `
  <child-child></child-child>
`
})
app.component('child-child', {
  setup() {
    let count = inject('count')
    return { count }
  },
  template: `
  <div>{{count}}</div>
`
})

是不是感覺非常簡單,那么問題來了,剛剛我們使用了 provide / inject 實現了父組件和子孫組件中的數據傳遞,如果兩個毫無關聯的組件,那么我們應該如何建立數據通訊呢?除了 vuex 你最先能想到什么,在 2.x 中,Vue實例可用于觸發通過事件觸發 API 強制附加的處理程序 ($on$off$once),這用于創建 event hub,以創建在整個應用程序中使用的全局事件偵聽器,因為我前面寫過相關文章,關于 vue2 的知識就不在這里過多贅述了,詳情可以點擊 Vue 常見 API 及問題。

但是在 vue3 中廢棄了 $on, $off,為什么會廢棄呢,可以參考文章解讀Vue3中廢棄組件事件進行解讀。官方推薦我們使用第三方庫 mitt 進行全局事件的綁定和監聽。大體用法其實和原來差不多,這里就不過多贅述了。

Mixin


Mixin 應該算 vue2 中用的比較多的,用法其實和以前大體相差不大,我在 Vue 常見 API 及問題 中記錄過 Mixin 的基礎用法,官網寫的也挺詳細的,當然現在關于組件公共邏輯的抽離其實更推薦使用 組合式 API 。這里還是簡單總結下 Mixin 的幾個特點:

1、混入過程中,組件 datamethods、優先級高于 mixin datamethods 優先級。
2、生命周期函數,先執行 mixin 里面的,在執行組件里面的。
3、自定義的屬性,組件中的屬性優先級高于 mixin 屬性優先級。

什么叫自定義屬性呢?我們來看個小栗子:

const app = Vue.createApp({
  number: 3,
  template: `
    <div>{{number}}</div>
  `
})

我們直接定義了一個屬性 number,它既不在 data 中,也不在 setup 中,而是直接掛載在 app 上,那么它就是 app 上的自定義屬性。此時我們無法在模板中直接使用 this 訪問到這個 number 屬性,必須要通過 this.$options 才能訪問到它:

const app = Vue.createApp({
  number: 3,
  template: `
    <div>{{this.$options.number}}</div>
  `
})

如果我們此時在 mixin 中也定義一個 number 屬性:

const myMixin = {
  number: 1
}
const app = Vue.createApp({
  mixins: [myMixin],
  number: 3,
  template: `
    <div>{{this.$options.number}}</div>
  `
})

前面我們說過,mixin 的優先級低于組件優先級,所以此時肯定輸出的是 3,但是如果我們希望 mixin 的優先級高于組件優先級,我們就可以使用 app.config.optionMergeStrategies 自定義選項合并策略:

const myMixin = {
  number: 1
}
const app = Vue.createApp({
  mixins: [myMixin],
  number: 3,
  template: `
  <div>{{this.$options.number}}</div>
`
})
// 接收兩個參數,配置優先返回第一個參數,如找不到在返回第二個參數
app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
  return mixinValue || appValue
}

自定義指令


我們先假想一個使用場景,如果我們希望在頁面加載的時候自動獲取 input 框的焦點事件,我們一般會這樣寫:

const app = Vue.createApp({
  template: `
    <input ref="input" />
  `,
  mounted() {
    this.$refs.input.focus()
  }
})

假如另一個組件中又有一個 input ,那么我們就又需要在那個組件的 dom 元素節點處定義 ref,然后在組件的生命周期中調用一遍 this.$refs.input.focus() 。如果我們可以定義一個全局的 autofocus 事件,只要遇到 input 我們就通過給定的指令直接觸發那應該怎么辦呢?此時我們就可以用到自定義指令了:

const app = Vue.createApp({
  template: `
    <input ref="input" v-focus />
  `
})
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

通過過 app.directive 我們定義了一個全局的 focus 指令,指令的使用只需要在前面加上 v- 即可;當然指令也和組件一樣,有著生命周期,我們在 mounted 的時候可以拿到使用指令的 dom 元素節點,然后操作這個節點完成對應的功能。當然上面我們使用 app.directive 將指令定義到了全局,日常開發中我們可能更多的是使用局部指令:

// 定義指令集合,因為可以是多個指令,所以是復數
const directives = {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}
const app = Vue.createApp({
  directives,
  template: `
    <input ref="input" v-focus />
  `
})
動態指令參數

例如我們想通過一個指令實時改變 input 框的位置,那么此時我們寫下代碼:

const app = Vue.createApp({
  template: `
    <input class="input" ref="input" v-pos />
  `
})
app.directive('pos', {
  mounted(el) {
    el.style.top = '200px'
  }
})

上面這個栗子雖然我們每次使用 v-pos 都會改變輸入框的 top 值,但是如果我們希望這個值不是固定的 200 ,而是指令中傳給我們的數字,那該如何進行改造呢?

官網的文檔資料告訴了我們,指令中的參數可以是動態的,例如,在 v-mydirective:[argument]="value" 中,argument 參數可以根據組件實例數據進行更新!這使得自定義指令可以在應用中被靈活使用。光看文字可能有點糊,我們來使用實際的栗子:

// 場景一:指令接收傳值
const app = Vue.createApp({
  template: `
  <input class="input" ref="input" v-pos="100" />
`
})
app.directive('pos', {
  mounted(el, binding) {
    // binding.value 是我們傳遞給指令的值——在這里是 200
    el.style.top = binding.value + 'px'
  }
})

其實上面的小栗子還有個缺點,就是我們將 v-pos 的值定義在 data 中,但是我們實時改變 data 中的值,頁面并不會產生對應的響應式變化。那是因為我們指令注冊的過程中 mounted 生命周期只會執行一遍,所以如果我們希望對應變化的產生就可以使用 updated 生命周期:

const app = Vue.createApp({
  data() {
    return {
      top: 100
    }
  },
  template: `
    <input class="input" v-pos="top" />
  `
})
app.directive('pos', {
  mounted(el, binding) {
    el.style.top = binding.value + 'px'
  },
  // 通過 updated 監聽指令值的實時變化
  updated(el, binding) {
    el.style.top = binding.value + 'px'
  }
})
const vm = app.mount('#root')

當然如果我們在 mountedupdated 時觸發相同行為,而不關心其他的鉤子函數。那么你可以通過將這個回調函數傳遞給指令來實現:

app.directive('pos', (el, binding) => {
  el.style.top = binding.value + 'px'
})

如果應用場景升級,我們不僅希望它只是是在 top 上的偏移,而是通過我們指定傳入的方向值進行偏移,那么應該如何實現呢?這時使用動態參數就可以非常方便地根據每個組件實例來進行更新。

// 場景二:動態指令參數
const app = Vue.createApp({
  template: `
  <input class="input" ref="input" v-pos:[direction]="100" />
`,
  data() {
    return {
      direction: 'bottom'
    }
  }
})
app.directive('pos', {
  mounted(el, binding) {
    // binding.arg 是我們傳遞給指令的參數
    const direction = binding.arg || 'top'
    el.style[direction] = binding.value + 'px'
  }
})

你可以試著使用自定義組件完成一個這樣的功能?

插件


我們在 vue 項目中經常會使用別人寫好的插件,例如 vue-routervue-touch 等,那么我們如何自己編寫一個插件呢?看官網的介紹:插件是自包含的代碼,通常向 Vue 添加全局級功能。它可以是公開 install() 方法的 object ,也可以是 function 。光看這句話可能有點懵,其實就傳達給了我們兩點訊息:

1、編寫插件可以是一個對象寫法,也可以是一個函數寫法
2、插件有一個公開的 install() 默認方法,它接收 vue 實例和你自定義的屬性兩個形參。

舉個栗子:

// 對象寫法:
const myPlugin = {
  install(app, options) {
    console.log(app, options) // vue 實例,{name: "cc"}
  }
}
app.use(myPlugin, { name: 'cc' })

//函數寫法:
const myPlugin = (app, options) => {
  console.log(app, options)
}
app.use(myPlugin, { name: 'cc' })

插件一般怎么寫呢?我們使用插件的時候額外的參數會放到 options 中,而 app 是使用這個插件的時候 vue 對應的實例。我們既然能得到實例,我們就可以對其做很多拓展,例如:

const app = Vue.createApp({
  template: `
    <child></child>
  `
})
// 子組件就可以通過 inject 接收到我們寫的插件里的 `name`
app.component('child', {
  inject: ['name'],
  template: `<div>{{name}}</div>`
})
// 自己寫插件,在上面通過 `provide` 拓展一個 name 屬性
const myPlugin = (app, options) => {
  app.provide('name', 'cc')
}
app.use(myPlugin, { name: 'cc' })

官網栗子中給出了 app.config.globalProperties 這個語法,其實就是對 vue 全局的屬性做一些拓展,比如我們想在全局上加入 sayHello 這樣一個屬性,我們一般會使用 app.config.globalProperties.$sayHello 這樣去寫,在屬性名前加入 $ 符號代表這是我們自己在 vue 全局添加的一個私有屬性,更方便我們管理。此時我們就可以在組件中直接訪問到這個全局私有屬性:

app.config.globalProperties.$sayHello = 'hello cc'
// 子組件直接通過 this 使用 $sayHello 屬性
app.component('child', {
  inject: ['name'],
  template: `<div>{{name}}</div>`,
  mounted() {
    console.log(this.$sayHello)
  }
})

結合官網,我們是否可以簡單的寫一個小插件,例如表單中的 input 框輸入檢測,對輸入的值進行一些基礎的校驗,如下栗子:

當然,這個簡單的小栗子肯定難不倒聰明的我們,其實我們可以使用一個全局 mixin 就可以完成這個功能:

const app = Vue.createApp({
  data() {
    return {
      name: 'cc',
      age: '18'
    }
  },
  template: `
    <div>
      姓名: <input type="text" v-model="name" />
      <span class="hint" v-if="this.$options.rules.name.error">
        {{this.$options.rules.name.message}}
      </span>
    </div>
    <div>
      年齡: <input type="number" v-model="age" />
      <span class="hint" v-if="this.$options.rules.age.error">
        {{this.$options.rules.age.message}}
      </span>
    </div>
  `,
  rules: {
    name: {
      validate: name => name.length > 3,
      error: false,
      message: '用戶名最少為4個字符'
    },
    age: {
      validate: age => age > 20,
      error: false,
      message: '年齡不能小于 20 歲'
    }
  }
})
// 校驗插件
const validatorPlugin = (app, options) => {
  app.mixin({
    created() {
      const rules = this.$options.rules
      for (let key in rules) {
        let item = rules[key]
        this.$watch(key, (value) => {
          if (!item.validate(value)) {
            item.error = true
          } else {
            item.error = false
          }
        }, {
          immediate: true
        })
      }
    }
  })
}
app.use(validatorPlugin)

我們在組件中定義了 rules 屬性,所以我們可以通過 this.$options.rules 直接訪問到這個屬性,然后我們通過 watch 監聽 nameage 的變化,通過回調函數來校驗值是否滿足條件,當然判斷的過程中我們知道 watch 是有惰性的,所以我們在 watch 的配置中要加上 immediate: true ,這樣就可以在頁面加載完成時立即執行。這樣我們就完成了一個迷你版的 input 校驗功能。

自定義 v-model

vue2 中自定義 v-model 的實現及雙向綁定的原理我已經寫過對應的文章了,Vue 中如何自定義 v-model 及雙向綁定的實現原理 ,老版本的 v-model 有幾個痛點:

1、比較繁瑣,要添加一個 model 屬性
2、組件上只能有一個 v-model,如果組件上出現多個 v-model,實現就比較困難
3、對初學者比較不友好,看的云里霧里

所以在 vue3 中對 v-model 也是進行了大刀闊斧的改革,在 vue3 中實現 v-model 不需要再給組件添加一個 model 屬性,只需要:

1、在組件的 props 中添加一個 modelValue 的屬性
2、更新值的時候組件中的 emit 時有一個 update:modelValue 的方法

我們直接通過一個栗子來認識 vue3 中的自定義 v-model

const app = Vue.createApp({
  data() {
    return {
      count: 1
    }
  },
  template: `
    <div>{{count}}</div>
    <child v-model="count"></child>
  `
})
app.component('child', {
  // props 中默認的 modelValue 接收父組件中 count 的值
  props: {
    modelValue: String
  },
  template: `
    <div @click="handleClick">{{modelValue}}</div>
  `,
  methods: {
    handleClick() {
      // 組件更新值的時候使用規定的 `update: modelValue` 方法
      this.$emit('update:modelValue', this.modelValue + 3)
    }
  }
})

前面說過,vue3 中可以使用多個 v-model,那么我們在來看看多個 v-model 的應用:

const app = Vue.createApp({
  data() {
    return {
      count: 1,
      age: 18
    }
  },
  template: `
    <div>父組件的值</div>
    <div>{{count}}</div>
    <div>{{age}}</div>
    <div>子組件 v-model 綁定的值</div>
    <child v-model="count" v-model:age="age"></child>
  `
})

app.component('child', {
  props: {
    modelValue: Number,
    age: Number
  },
  template: `
    <div @click="handleClick">{{modelValue}}</div>
    <input :value="age" @input="handleInput" type="number" />
  `,
  methods: {
    handleClick() {
      this.$emit('update:modelValue', this.modelValue + 3)
    },
    handleInput(event) {
      this.$emit('update:age', +(event.target.value))
    }
  }
})

當我們在 v-model 后面不接入任何參數時,就可以直接在子組件中使用默認的 modelValue 與父組件中 v-model 的值進行綁定,而當我們在 v-model:age 傳入 age 參數之后,對應的子組件的 props 中也需要改成 age,而更新值的時候組件中的 emit 中的方法也要改成對應的 update: age 。其實新版本中的 v-model 使用更簡單更方便,同時可以綁定多個互不干擾。

非 Prop 的 Attribute


官網給出的定義為一個非 propattribute 是指傳向一個組件,但是該組件并沒有相應 propsemits 定義的 attribute。常見的示例包括 classstyleid 屬性。咋看這段解釋可能有點懵,其實我們可以通過一些栗子來看問題

Attribute 繼承

當組件返回單個根節點時,非 prop attribute 將自動添加到根節點的 attribute 中。例如下列栗子:

const app = Vue.createApp({
  template: `
    <child type="number" class="parent"></child>
  `
})

app.component('child', {
  template: `
    <div class="child">
      <input />
    </div>
  `
})

被渲染的 child 組件實際代碼結構如下:

// class 和 type 都被渲染到根節點上去了
<div class="child parent" type="number">
  <input>
</div>
禁用 Attribute 繼承

如果你不希望組件的根元素繼承 attribute,你可以在組件的選項中設置 inheritAttrs: false。例如:禁用 attribute 繼承的常見情況是需要將 attribute 應用于根節點之外的其他元素。

通過將 inheritAttrs 選項設置為 false,你可以訪問組件的 $attrs property,該 property 包括組件 propsemits property 中未包含的所有屬性 (例如,classstylev-on 監聽器等)。還是上面的栗子,我們需要 child 組件中的 input 去渲染對應的 classtype ,我們就可以將代碼改寫一下:

app.component('child', {
  inheritAttrs: false,
  template: `
    <div class="child">
      <input v-bind="$attrs" />
    </div>
  `
})

此時我們再從瀏覽器中查看 DOM 元素節點就可以看到如下結構:

<div class="child">
  <input type="number" class="parent">
</div>
多個根節點上的 Attribute 繼承

如果我們的子組件存在多個根節點怎么辦,例如:

const app = Vue.createApp({
  template: `
    <child class="child"></child>
  `
})
app.component('child', {
  template: `
    <div class="header" >
      <input />
    </div>
    <div class="main" v-bind="$attrs">main</div>
    <div class="footer">footer</div>
  `
})

如果我們不在其中一個根組件使用 v-bind = "$attrs" 控制臺就會給我們報錯,我們在其中一個根節點上使用之后父組件上對應的 attribute 就會被繼承到這個根組件上。

當然我們也可以禁止掉根節點上的繼承,直接在 header 結構下的 input 框加入 v-bind = "$attrs" 即可。如下栗子:

app.component('child', {
  inheritAttrs: false,
  template: `
    <div class="header" >
      <input v-bind="$attrs" />
    </div>
    <div class="main">main</div>
    <div class="footer">footer</div>
  `
})

查漏補缺


我們知道在 vue2 模板中可以通過在 DOM 結構中指定 ref 屬性,然后在邏輯代碼中通過 this.$refs. 去操作 DOM,那么在 vue3 中我們應該如何操作 DOM 元素呢?

我們先來一個簡單的場景,判斷點擊的 dom 元素是否在 id = 'index'dom 結構中,場景代碼如下:

// 判斷點擊的 dom 元素節點是不是在 index 中
const app = Vue.createApp({
    template: `
      <div id="index">
        <div id="index-list">555</div>
      </div>
      <div id="demo">666</div>
    `,
  })

此時我們就要進行 DOM 元素節點判斷,結合 setup 我們應該如何去使用 ref 來獲取 dom 元素節點呢?代碼比較簡單就直接上結果了:

const app = Vue.createApp({
  template: `
    <div id="index" ref="index">
      <div id="index-list">555</div>
    </div>
    <div id="demo">666</div>
  `,
  setup() {
    const { ref, onMounted } = Vue
    const index = ref(null)
    onMounted(() => {
      document.addEventListener('click', function (e) {
        if (index.value.contains(e.target)) {
          console.log('點擊的是 index 里面的元素')
        } else {
          console.log('點擊的不是 index 里面的元素')
        }
      })
    })
    return { index }
  }
})

因為 setup 的執行是在 beforeCreatecreated 之間,所以我們如果想拿到對應的 dom 元素節點,最好在其內部的生命周期中進行獲取。

vue3 中相較 vue2 大體的改動和日常開發中經常會遇到的問題基本都已經整理的差不多了,由于 vue3 代碼基本都是 ts 寫的,所以學習 ts 其實已經迫在眉睫的。結尾綜合做個小栗子吧:

其實很簡單,就是一個 form 表單提交驗證,不過封裝基本用的是 vue3 + ts ,小伙伴可以自己獨立實現一個類似 element-ui 中的表單效驗插件,其實組件開發更多的是學習思路以及代碼的擴展性,優雅性。最近 github 經常打不開,源代碼就放在 gitee 上了。本文多為自己學習筆記記錄,如有錯誤,歡迎指正!!!

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

推薦閱讀更多精彩內容