原文連結:Vue.js — Considerations and Tricks
Vue.js 是一個很棒的框架。然而,當你開始構建一個大型 JavaScript 專案的時候,你將對 Vue.js 感到一些困惑。這些困惑並不是來自框架本身,相反 Vue.js 團隊會經常調整一些重要設計策略。
相對於 React 和 Angular,Vue.js 面向一些不同水平的開發者。它更加的友好,不管是對初學者還是經驗豐富的老手。它並不隱藏一些 DOM 操作,相反它與 DOM 配合的很好。
這篇文章更像是一個目錄,列舉了我在 Vue.js 的初學路上遇到一些問題和技巧。理解這些關鍵性的設計技巧,有助於我們構建大型的 Web 應用。
寫這篇文章的時候是 2018 年 5 月 18 日,下面這些技巧依然是有效的。但是框架升級,或者瀏覽器底層或者 JS API 發生改變時,他們可能會變得不是那麼有用。
譯者注:儘管 Vue.js 3 即將到來,但是下面的技巧大部分是有用的,因為 3 的版本並不會改變一些上層 API ,最大的特性可能是底層資料 Observer 改有 proxy 實現,以及原始碼使用 typescript 構建。
1、為什麼 Vue.js 不使用 ES Classes 的方式編寫元件
如果你使用過類似於 Angular 的框架或者某些後端 OOP 語言後,那麼你的第一個問題可能是:為什麼不使用 Class 形式的元件?
Vue.js 的作者在 GitHub issues 中很好的回答了這個問題: Use standard JS classes instead of custom syntax?
為什麼不使用 Class 這裡有三個很重要的原因:
- ES Classes 不能夠滿足當前 Vue.js 的需求,ES Classes 標準還沒有完全規範化,並且總是朝著錯誤的方向發展。如果 Classes 的私有屬性和裝飾器(當前已進入 Stage 3)穩定後,可能會有一定幫助。
- ES Classes 只適合於那些熟悉面嚮物件語言的人,它對哪些不使用複雜構建工具和編譯器的人不夠友好。
- 優秀的 UI 元件層次結構一般都是元件的橫向組合,它並不是基於繼承的層次結構。而 Classes 形式顯然更擅長的是後者。
譯者注:But,Vue.js 3.0 將支援基於 Class 的元件寫法,真香。
2、如何構建自己的抽象元件?
如果你想構建自己的抽象元件(比如 transition、keep-alive),這是一個比構建大型 web 應用更加瘋狂地想法,這裡有一些關於這個問題的討論,但是並沒有什麼進展。
Any plan for docs of abstract components?
譯者注:在 Vue.js 內部元件(transition、keep-alive)中,使用了一個 abstract 屬性,用於宣告抽象元件,這個屬性作者並不打算開放給大家使用,所以文件也沒有提及。但是如果你要使用也是可以的,那麼你必須深入原始碼探索該屬性有何作用。
但是不要害怕,如果你可以很好地理解 slots ,你就可以構建自己的抽象元件了。這裡有一篇很好的部落格介紹了要如何做到這一點。
Writing Abstract Components with Vue.js
譯者注:下面是《在 Vue.js 中構建抽象元件》的簡單翻譯
抽象元件與普通元件一樣,只是它不會在介面上顯示任何 DOM 元素。它們只是為現有元件新增額外的行為。
就像很多你已經熟悉的 Vue.js 的內建元件,比如:`<transition>`、`<keep-alive>`、`<slot>`。
現在展示一個案例,如何跟蹤一個 DOM 已經進入了可視區域 ,讓我們使用 IntersectionObserver API 來實現一個解決這個問題的抽象元件。
(完整程式碼在這裡:[vue-intersect](https://github.com/heavyy/vue-intersect))
複製程式碼
// IntersectionObserver.vue
export default {
// 在 Vue 中啟用抽象元件
// 此屬性不在官方文件中, 可能隨時發生更改,但是我們的元件必須使用它
abstract: true,
// 重新實現一個 render 函式
render() {
// 我們不需要任何包裹的元素,只需要返回子元件即可
try {
return this.$slots.default[0];
} catch (e) {
throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.');
}
return null;
},
mounted () {
// 建立一個 IntersectionObserver 例項
this.observer = new IntersectionObserver((entries) => {
this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]);
});
// 需要等待下一個事件佇列,保證子元素已經渲染
this.$nextTick(() => {
this.observer.observe(this.$slots.default[0].elm);
});
},
destroyed() {
// 確保元件移除時,IntersectionObserver 例項也會停止監聽
this.observer.disconnect();
}
}
複製程式碼
讓我們看看如何使用它?
複製程式碼
<intersection-observer @intersect-enter="handleEnter" @intersect-leave="handleLeave">
<my-honest-to-goodness-component></my-honest-to-goodness-component>
</intersection-observer>
複製程式碼
但是在這樣做之前,請你三思。我們一般依賴 mixins 和一些純函式來解決一些特殊場景的問題,你可以將 mixins 直接看做一個抽象元件。
How do I extend another VueJS component in a single-file component? (ES6 vue-loader)
3、我不太喜歡 Vue.js 的單檔案元件,我更希望 HTML、CSS 和 JavaScript 分離。
沒有人阻止你這樣做,如果你是個注重分離的哲學家,喜歡把不同的東西放在不同檔案,或者討厭編輯器對 .vue
檔案的不穩定行為,那麼你這麼做也是可以的。你要做的很簡單:
<!--https://vuejs.org/v2/guide/single-file-components.html -->
<!-- my-component.vue -->
<template src="./my-component.html"></template>
<script src="./my-component.js"></script>
<style src="./my-component.css"></style>
複製程式碼
這麼做,就會出現下一個問題:我的元件總是需要 4 個檔案(vue + html + css + js)嗎?我能不能擺脫 .vue
檔案? 答案是肯定的,你可以使用 vue-template-loader
。
我的同事還為此寫了一篇很棒的教程:
Using vue-template-loader with Vue.js to Compile HTML Templates
4、 函式式元件
感謝 React.js 讓函式式元件很流行,這是因為他們無狀態、易於測試。然而它們也存在一些問題。
譯者注:不瞭解 Vue.js 函式式元件的可以先在官方文件檢視:官方文件
4.1 為什麼我不能對功能元件使用基於 Class 的 @Component 裝飾器?
再次回到 Classes,它只是一種用於儲存本地狀態的資料結構。如果函式式元件是無狀態的,那麼使用 @Component 裝飾器就是無意義的。
這裡有關於這個的討論:
How to create functional component in @Component?
4.2 外部類和樣式不應用於函式式元件
函式式元件不能像普通元件那樣,繫結具體的類和樣式,必須在 render 函式中手動應用這些繫結。
DOM class attribute not rendered properly with functional components
class attribute ignored on functional components
4.3 函式式元件總是會重複渲染?
TLDR:在函式式元件中使用有狀態元件時務必要小心
Functional components are re-rendered when props are unchanged.
函式式元件相當於直接呼叫元件的 Render 函式,這意味著你應該:
避免在 render 函式中直接使用有狀態元件,因為這會在每次呼叫 render 函式時建立不同的元件例項。
如果函式式元件是葉子元件,會更好地利用它們。 需要注意的是,同樣的行為也適用於 React.js。
4.4 如何在Vue.js 函式式元件中觸發一個事件?
在從函式式元件中觸發一個事件並不簡單。不幸的是,文件中也沒有提到這一點。函式式元件中不可用 $emit
方法。stack overflow 上有人討論過這個問題:
How to emit an event from Vue.js Functional component?
5、Vue.js 的透明包裹元件
元件包裹一些DOM元素,並且公開了這些DOM元素的事件,而不是根DOM的節點例項。
例如:
<!-- Wrapper component for input -->
<template>
<div class="wrapper-comp">
<label>My Label</label>
<input @focus="$emit('focus')" type="text"/>
</div>
</template>
複製程式碼
這裡我們真正感興趣的是 input
節點,而不是 div
根節點,因為它主要是為了樣式和修飾而新增的。使用者可能對這個元件的幾個輸入事件感興趣,比如 blur
、focus
、click
、hover
等等。這意味著我們必須重新繫結每個事件。我們的元件如下所示。
<!-- Wrapper component for input -->
<template>
<div class="wrapper-comp">
<label>My Label</label>
<input type="text"
@focus="$emit('focus')"
@click="$emit('click')"
@blur="$emit('blur')"
@hover="$emit('hover')"
/>
</div>
</template>
複製程式碼
實際上這是完全沒必要的。簡單的解決方案是使用 Vue 例項上的屬性 vm.$listeners
將事件重新繫結到所需DOM 元素上:
<!-- Notice the use of $listeners -->
<template>
<div class="wrapper-comp">
<label>My Label</label>
<input v-on="$listeners" type="text"/>
</div>
</template>
<!-- Uses: @focus event will bind to internal input element -->
<custom-input @focus="onFocus"></custom-input>
複製程式碼
6、為什麼你不能在 slot 上繫結和觸發事件
我經常看到有些開發人員,在 slot 上進行事件的監聽和分發,這是不可能的。
元件的 slot 由呼叫它的父元件提供,這意味著所有事件都應該與父元件相關聯。嘗試去傾聽這些變化意味著你的父子元件是緊密耦合的,不過有另一種方法可以做到這一點,Evan You解釋得很好:
Is it possible to emit event from component inside slot #4332
7、slot 中的 slot(訪問孫輩slot)
在某些時候,可能會遇到這種情況。假設有一個元件,比如 A ,它接受一些 slot 。遵循組合的原則,使用元件 A 構建另一個元件 B 。然後你把 B 用在 C 中。
那麼現在問題來了: 如何將 slot 從 C 元件傳遞到 A 元件?
要回答這個問題,首先取決你使用何種方式構建元件? 如果你是用 render 函式,那就很簡單。你只需要在元件 B 的 render 函式中進行如下操作:
// Render function for component B
function render(h) {
return h('component-a', {
// Passing slots as they are to component A
scopedSlot: this.$scopedSlots
}
}
複製程式碼
但是,如果你使用的是基於模板的方式,那麼就有些糟糕了。幸運的是,在這個問題上有了進展:
feat(core): support passing down scopedSlots with v-bind
希望這篇文章讓你對 Vue.js 的設計思路有了更深入的瞭解,併為你提供了一些在高階場景中的技巧。