[譯] Vue.js 優雅地整合第三方 JavaScript

LucasTwilight發表於2019-03-17

摘要:Vue.js 的一個主要的優點是可以很好地與其他程式碼一起工作:也就是說它不僅很容易嵌入到其他的應用程式當中,而且也很容易將非 Vue 程式碼包裝到 Vue 當中。本文討論了 Vue.js 的第二個優勢,包括了三種不同型別的第三方 JavaScript,以及將它們嵌入到 Vue 中的方法。

Vue.js 在過去幾年中實現了非常驚人的使用量增長。它已經從一個鮮為人知的開源庫變成了第二受歡迎的前端框架(僅次於 React.js)。

Vue 使用者增長的一個核心原因是:Vue 是一個漸進式框架 — 它允許你的頁面中部分使用 Vue.js 來進行開發,而不需要一個完整的單頁應用。也允許你只加入一個 script 標籤,而不是使用一個完整的構建系統就可以啟動並且執行。

這種漸進式的哲學讓 Vue.js 的碎片化開發非常簡單,不需要進行大型架構的重寫。然而,有一件事卻經常被忽略,不僅將 Vue.js 嵌入到其他框架編寫的網站中比較容易。在 Vue.js 中嵌入其他程式碼也非常容易。雖然 Vue 會控制 DOM,但是它也預留了一個出口,允許其他非 Vue 的 JavaScript 控制 DOM。

本文將會探討當你想要使用不同型別的第三方 JavaScript,並且想將其嵌入到 Vue 專案中的情況,然後介紹最適合嵌入到 Vue 中的幾種型別的工具和技術。在最後,我們會考慮這些方法的缺點,以及在決定使用它們的時候需要考慮什麼。

本文假設你熟悉 Vue.js 以及元件和指令的概念。如果你正在尋找 Vue 和這些概念的介紹,可以參考 Sarah Drasner 的 introduction to Vue.js series 或者 Vue 官方文件

第三方 JavaScript 型別

我們將主要的三種第三方 JavaScript 型別按照複雜程度排序:

  1. DOM 無關的庫
  2. 元素擴充庫
  3. 元件和元件庫

DOM 無關的庫

第一種第三方 JavaScript 庫是僅提供邏輯方面的功能,並不直接訪問 DOM,比如用於處理時間的 moment.js 或者用於增強函數語言程式設計能力的 lodash 都屬於這種型別。

這些庫很容易整合到 Vue 應用當中,但是可以多種方式來提供合理的訪問方式。這些庫一般都是為了提供實用的程式功能,和其他任何型別的 JavaScript 專案都是相融的。

元素增強庫

元素增強是一種為 DOM 元素新增額外功能的方法,這種方法由來已久。比如可以幫助圖片進行懶載入的 lozad 或者為輸入框提供輸入過濾的 Vanilla Masker

這些庫通常只會一次影響單個元素,他們可能會操縱單個元素,但是不會為 DOM 增加新的元素。

這些工具具有嚴格的用途,並且和其他解決方案進行互動非常簡單。這些庫經常會被引入到 Vue 工程中,防止重複造輪子。

元件和元件庫

這些工具是大型的,並且密集的框架。比如 Datatables.net,或者 ZURB Foundation。這些庫會建立一個完整的互動式元件。通常具有多個可互動元素。

這些庫要麼會直接將這些元素注入到 DOM 中,要麼期望能夠對 DOM 進行高階別的控制。它們通常使用其他的框架或者工具集構建(上面的兩個例子都是基於 jQuery 進行構建的)。

這些工具提供了非常廣泛的功能,並且在沒有大量修改的情況下,將其替換成其他的工具是非常具有挑戰性的,因此,將他們嵌入到 Vue 中的解決方案,對於遷移一個大型應用來說非常關鍵。

如何在 Vue 中使用

DOM 無關的庫

將 DOM 無關的庫整合到 Vue.js 工程中相對簡單一些。如果你在使用 JavaScript 模組,那麼就像在工程中引入其他模組一樣,簡單地使用 import 或者 require 就好了。比如:

import moment from 'moment';

Vue.component('my-component', {
  //…
  methods: {
    formatWithMoment(time, formatString) {
      return moment(time).format(formatString);
    },
});
複製程式碼

如果使用全域性 JavaScript,那麼需要在 Vue 工程之前引入這個庫:

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.22/vue.min.js"></script>
<script src="/project.js"></script>
複製程式碼

另外一種常見的分層方法是使用過濾器或者方法將庫中的函式進行包裝,以便在模板中比較方便地訪問。

Vue 過濾器

Vue 的過濾器是一種模式,允許您直接在模板中內嵌應用文字格式。文件中提供了一個示例,你可以建立一個 'capitalize' 過濾器,然後將其應用到模板中,如下所示:

{{myString | capitalize}}
複製程式碼

當匯入與格式有關的庫時,你可能希望能夠直接在過濾器中使用。比如,如果你使用 moment 來格式化工程中的日期,將其轉換為相對時間,我們可以建立一個 relativeTime 過濾器。

const relativeTime = function(value) {
  if (!value) return '';
  return moment(value).fromNow();
}
複製程式碼

然後我們可以使用 Vue.filter 方法來將其全域性新增到所有的 Vue 元件和例項上:

Vue.filter('relativeTime', relativeTime);
複製程式碼

或者將其新增到使用了 filters 選項的特定元件上:

const myComponent = {
  filters: {
    'relativeTime': relativeTime,
  } 
}
複製程式碼

你可以試著在 CodePen 上跑一下這段程式碼:參閱 Smashing Magazine(@smashing-magazine)的這段程式碼片:Vue 整合:相對時間過濾器

元素增強庫

與 DOM 無關的庫相比,元素增強庫的整合稍微複雜一些。如果你不小心,Vue 會和庫產生交叉控制,爭奪 DOM 的控制權。

為了避免這樣的情況發生,你需要將庫掛載到 Vue 的生命週期當中,讓這些庫在 Vue 完成 DOM 操作之後執行,並且正確處理 Vue 觸發的更新操作。

這些事可以在元件內部完成,但是由於這些庫一般都只會接觸一個元素,因此將其封裝到自定義指令(directive)中是更加靈活的方法。

Vue 指令

Vue 指令是一種修飾符,可以為頁面中的元素新增行為。Vue 已經提供了許多你已經熟悉了的內建指令,比如 v-onv-model 以及 v-bind。並且我們還可以建立自定義指令來為元素新增任何型別的行為 — 這正是我們想要實現的。

定義一個自定義指令和定義元件非常相似;使用一組和特定宣告週期鉤子對應的方法建立一個物件,並且通過執行將其新增到全域性:

Vue.directive('custom-directive', customDirective);
複製程式碼

或者通過在元件內部新增 directives 物件來將指令新增到元件本地。

const myComponent = {
  directives: {
    'custom-directive': customDirective,
  } 
}
複製程式碼

Vue 指令鉤子

Vue 指令有針對以下可用於自定義行為的鉤子。雖然可以在單個指令中使用這些鉤子,但是一般情況下只會在一個指令中使用其中的一個到兩個鉤子。這些生命週期鉤子都是可選的,所以請在使用的時候選擇需要的即可。

  • bind(el, binding, vnode):當指令首次繫結到一個元素上的時候,會且僅會被呼叫一次。這是一個進行一次性設定工作的好地方,但是要小心,即使元素存在,也有可能還未被實際掛載到文件中。
  • inserted(el, binding, vnode):當繫結元素插入到父節點中的時候被呼叫。這也不能夠保證文件中存在這個元素,但是這意味著如果你需要引用父節點,那麼是可以引用到的。
  • update(el, binding, vnode, oldVnode):當包含元件的 VNode 更新時呼叫,但是無法保證元件的其他孩子將會更新,並且該指令的值可能已經被更改,也可能還未更改。(你可以通過比較 binding.valuebinding.oldValue 來優化掉不必要的更新)。
  • componentUpdated(el, binding, vnode, oldValue)update 非常類似,但是這個鉤子會在當前節點包含的所有孩子都更新完成後呼叫。如果你的指令的行為依賴於當前節點的對等體,(比如 v-else),那麼可以使用這個鉤子來代替 update
  • unbind(el, binding, vnode)bind 類似,這個鉤子當且僅當指令從元素上解綁的時候被觸發一次。這是一個執行所有解除安裝程式碼的好地方。

這些函式的引數如下:

  • el:指令所繫結的元素;
  • binding:一個包含了指令引數以及值的相關資訊的物件;
  • vnode:Vue 編譯器產出的對應元素的虛擬節點;
  • oldValue:更新之前的虛擬節點,只會在 updatecomponentUpdated 中被傳入。

更多資訊可以在 Vue 自定義指令指南中找到。

在自定義指令中引入 Lozad 庫

讓我們來看一個使用了 lozad 的引入例子,lozad 庫是一種基於 Intersection Observer API 的懶載入庫。使用 lozad 的 API 非常簡單:通過 data-src 來替換圖片的 src 屬性,並且傳遞一個選擇器或者元素到 lozad() 方法中,然後呼叫的物件中的 observe 即可。

const el = document.querySelector('img');
const observer = lozad(el); 
observer.observe();
複製程式碼

我們可以通過指令中的 bind 鉤子來很方便地進行實現。

const lozadDirective = {
  bind(el, binding) {
    el.setAttribute('data-src', binding.value) ;
    let observer = lozad(el);
    observer.observe();
  }
}
Vue.directive('lozad', lozadDirective)
複製程式碼

有了這個,我們可以簡單地將源字串傳遞給 v-lozad 指令,來將圖片轉變為懶載入:

<img v-lozad="'https://placekitten.com/100/100'" />
複製程式碼

這段程式碼片可以在 CodePen 中檢視:參閱 Smashing Magazine(@smashing-magazine)的這段程式碼片:Vue 整合:僅設定 bind 的 Lozad 指令

我們還沒有完成!雖然這樣在初始載入的時候可以工作,但是如果源字串的值是動態的,Vue 會動態改變繫結嗎?可以在上面的 CodePen 中通過點選 “Swap Sources” 按鈕觸發。如果我們只實現 bind,那麼當需要動態改變源字串的話,data-srcsrc 則不會動態改變。

為了實現這樣的效果,我們需要增加 updated 鉤子:

const lozadDirective = {
  bind(el, binding) {
    el.setAttribute('data-src', binding.value) ;
    let observer = lozad(el);
    observer.observe();
  },
  update(el, binding) {
    if (binding.oldValue !== binding.value) {
      el.setAttribute('data-src', binding.value);
      if (el.getAttribute('data-loaded') === 'true') {
        el.setAttribute('src', binding.value);
      }
    }
  }
}
複製程式碼

有了這個就好了!我們的指令現在可以在 Vue 更新的時候觸發 lozad 了。最後的版本可以通過下面的程式碼片檢視:參閱 CodePen 上面 Smashing Magazine(@smashing-magazine)的這段程式碼片:Vue 整合:設定了 update 的 Lozad 指令

元件和元件庫

需要整合的最複雜的第三方 JavaScript 是需要控制整個 DOM 區域的,完整的元件或者元件庫。這些工具希望能夠建立,銷燬並且控制 DOM。

對於這些庫,將他們整合到 Vue 的最好方法是將其包裝到一個專用的元件中,並且大量使用 Vue 的生命週期函式來管理初始化,資料傳入,以及事件處理和回撥。

我們的目標是完全抽象出第三方庫的細節,以便其他的 Vue 程式碼可以像與原生元件互動一樣,和我們包裝的元件進行互動。

元件生命週期鉤子

要包裝更加複雜的元件,我們需要了解元件中可用的所有生命週期鉤子函式,這些鉤子函式有:

  • beforeCreate() 在元件被例項化之前呼叫,很少使用,但是如果需要類似整合分析功能的時候是有用的。
  • created() 在元件被例項化之後,掛載到 DOM 上之前呼叫,在我們需要一次性的,不依賴 DOM 的設定工作的時候非常有用。
  • beforeMount() 在元件掛載到 DOM 之前被呼叫。(也很少使用)
  • mounted() 在元件被掛載到 DOM 之後呼叫。對於呼叫時需要依賴 DOM 或者假設 DOM 存在的庫來說,這是我們最常使用的鉤子函式。
  • beforeUpdate() 在 Vue 即將更新渲染模板的時候呼叫,很少使用,但是同樣地,在整合分析的時候也是有用的。
  • updated() 當 Vue 完成模板更新的時候呼叫。適合任何需要重新例項化的過程。
  • beforeDestroy() 在 Vue 解除安裝一個元件之前呼叫。如果我們需要在第三方元件上呼叫任何銷燬或者解除安裝的方法,這裡是一個完美的地方。
  • destroyed() 當 Vue 完成了一個元件的解除安裝之後呼叫。

一次包裝一個元件,一個鉤子函式

來讓我們看看流行的 jquery-multiselect 庫。目前已經有許多 Vue 寫的多選元件了,但是這個例子是一個很好的組合:複雜到足夠有趣,簡單到足夠理解。

實現一個第三方元件包裝器,首先需要使用到 mounted 鉤子。由於第三方元件可能希望在呼叫第三方庫之前,DOM 就已經存在,因此需要在這裡初始化第三方元件。

例如,開始包裝 jquery-multiselect 的時候,我們會寫如下程式碼:

mounted() { 
  $(this.$el).multiselect();
}
複製程式碼

你可以在 CodePen 中檢視下面程式碼片:參閱 Smashing Magazine(@smashing-magazine)的這段程式碼片:Vue 整合:簡單的多選包裝

這看起來很不錯。如果我們需要在解除安裝的時候呼叫某些方法,我們需要新增 beforeDestroy 鉤子函式,但是這個庫沒有需要我們呼叫的任何解除安裝方法。

將回撥轉換為事件

我們要對這個庫做的下一件事是在使用者選擇某個選項的時候,提供通知 Vue 應用的能力。jquery-multiselect 庫通過 afterSelect 以及 afterDeselect 函式來進行回撥,但是這樣並不適合 Vue,我們讓這些回撥內部觸發事件。我們可以簡單地將回撥函式進行包裝:

mounted() { 
  $(this.$el).multiSelect({
     afterSelect: (values) => this.$emit('select', values),
     afterDeselect: (values) => this.$emit('deselect', values)
   });
}
複製程式碼

然而,如果我們在事件監聽器中插入一個 logger,我們會發現並沒有真正提供到一個類似 Vue 的介面。在每次選擇或者取消選擇的時候,我們會收到一個值已經改變了的列表,但是為了更符合 Vue,我們應該讓列表觸發 change 事件。

我們沒有像 Vue 那樣的方法去設定值。我們應該考慮使用這些工具其實現類似 v-model 的方法,比如 Vue 提供的原生選擇元素

實現 v-model

要在元件上實現 v-model,我們需要實現兩件事:接收一個 value 屬性並且將相應的選項設定為選中,然後在選項改變之後觸發 input 事件並且傳入一個新的陣列。

這裡有四個需要處理的部分:一個特定的初始值,將所有更改傳遞到父元件,處理從外部元件接收到的所有更改,最後處理對於插槽(選項列表)內部內容的所有變更。

讓我們挨個來進行實現。

  1. 通過屬性來進行初始化設定

首先,我們需要讓元件接收一個屬性,並且當我們初始化的時候,告訴多選元件,需要選中哪個。

export default {
  props: {
    value: Array,
    default: [],
  },
  mounted() { 
    $(this.$el).multiSelect();
    $(this.$el).multiSelect('select', this.value);
  },
}
複製程式碼
  1. 處理內部變化

為了處理因為使用者和多選元素的互動所產生的變化,我們可以回到之前探討過的回撥 — 但這次不是那麼簡單了。我們需要考慮原始值以及發生的變化,而不是簡單地將接收到的值傳遞出去。

mounted() { 
  $(this.$el).multiSelect({
    afterSelect: (values) => this.$emit('input', [...new Set(this.value.concat(values))]),
    afterDeselect: (values) => this.$emit('input', this.value.filter(x => !values.includes(x))),
  });
  $(this.$el).multiSelect('select', this.value);
},
複製程式碼

這些回撥函式看起來有些密集,所以讓我們來把它分解一下。

afterSelect 方法將新選擇的值與我們現有的值連線起來,但是為了確保沒有重複,我們採用 Set(保證唯一性)來進行處理。然後將其解構,轉換為陣列。

afterDeselect 方法只是從列表中過濾掉當前取消選擇的值,以便傳遞出去新的列表。

  1. 處理外部觸發的更新

接下來我們需要做的是在 value 屬性更新時,更改 UI 中的選定值。這包括將屬性的宣告式變化轉換到多選可用的必要的變化。最簡單的方式是在 value 屬性上使用觀察者。

watch:
  // don’t actually use this version. See why below
  value() {
    $(this.$el).multiselect('select', this.value);
  }
}
複製程式碼

但是,有一個問題!因為觸發 select 同時會我們的 onSelect 處理程式,從而使用更新值。如果我們使用這樣的一個簡單的觀察者,我們會陷入到死迴圈中。

幸運的是,對於我們來說,Vue 能夠讓我們同時訪問到舊的和新的值。我們可以進行比較,只有在值發生變化的時候才觸發 select。在 JavaScript 中,陣列的比較可能會比較棘手,但是對於這個例子,我們可以通過 JSON.stringify 來直接進行比較,因為我們的陣列實際上比較簡單(因為沒有物件)。在考慮到我們還需要取消選擇已經刪除的選項之後,我們最後的觀察者是這樣的:

watch: {
    value(newValue, oldValue) {
      if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
        $(this.$el).multiSelect('deselect_all');
        $(this.$el).multiSelect('select', this.value);
      }
    }
  },
複製程式碼
  1. 將外部更新表現在插槽中

我們還有一件事需要處理:我們的多選元素正在使用通過插槽傳入的選項值。如果這組選項發生了變化,我們需要告訴多選元素進行更新,否則新的選項不會展示出來。幸運的是,我們在多選元件的更新中有一個簡單的 API(refresh 函式和一個明顯的 Vue 鉤子)。這樣就可以簡單地處理這種情況了。

updated() {
  $(this.$el).multiSelect('refresh');
},    
複製程式碼

你可以在 CodePen 上檢視到這個元件的最終版本:參閱 Smashing Magazine(@smashing-magazine)的這段程式碼片:Vue 整合:具有 v-model 的多選包裝器

缺點和其他考慮因素

現在我們已經瞭解了在 Vue 中使用第三方 JavaScript 是多麼簡單了,是時候討論一下這些方法的缺點,以及何時使用它們了。

效能影響

在 Vue 中使用是為了 Vue 編寫的第三方 JavaScript 的主要缺點之一就是效能 ——— 特別是在引用由其他框架構建的元件以及元件庫的時候。在使用者與我們的應用程式互動之前,瀏覽器會需要下載和解析額外的 JavaScript。

比如,如果使用上述的多選元件,需要引入全部的 jQuery 程式碼。這使得使用者需要下載兩倍於現在的框架程式碼,僅僅是為了這樣一個元件!顯然,使用原生的 Vue.js 元件會更好。

此外,當第三方使用的 API 和 Vue 的宣告方式大相徑庭的時候,你可能會發現自己的程式需要大量額外的執行時間。同樣使用多選的示例,我們不得不每次更換插槽的值的時候,重新整理整個元件(需要檢視一大堆的 DOM),而 Vue 原生的元件可以通過虛擬 DOM 來使其更新更加高效。

何時使用

利用第三方庫可以大幅減少你的開發時間,並且通常意味著你可以使用你還沒有能力去構建出來的,有著良好維護和測試的元件。

對於那些沒有較大依賴關係的庫,特別是沒有大量 DOM 操作的庫,沒有理由必須要為了使用 Vue 特定的庫,而放棄更加通用的庫。因為 Vue 可以很方便的引入其他第三方 JavaScript,所以你只需要根據你的功能和效能需求,選擇最合適的工具,而沒有必要去特別關注 Vue 特有的庫。

對於更為廣泛的元件框架,有三種需要將其引入的主要情況:

  1. 專案原型:在這種情況下,迭代速度的需求遠遠超過使用者效能;只需要使用所有能讓你工作效率提升的東西。
  2. 遷移現有的站點:如果你需要將現有的站點遷移到 Vue,可以通過 Vue 來將現有的東西進行優雅地包裝,這樣就可以逐步地抽出舊的程式碼,而不用進行一次大爆炸似的重寫。
  3. 當 Vue 元件功能尚不可用的時候:如果你需要完成特定的,或者具有挑戰性的需求的時候,存在第三方庫支援,但是 Vue 還沒有特定的元件,請務必考慮用 Vue 來包裝現有的庫。

當第三方使用的 API 和 Vue 的宣告方式大相徑庭的時候,你可能會發現自己的程式需要大量額外的執行時間。

現有的一些例子

前兩個模式在開源生態環境中使用範圍非常廣泛,所以有非常多的例子可以去參考。由於包裝整個元件更像是一種權宜之計/遷移解決方案,我們在外部找不到那麼多例子,但是還有有一些現有的例子,我曾經在客戶要求下使用了這種方法。下面是三種模式的一些簡單的例子:

  1. Vue-moment 包裝了 moment.js 庫,並且提供了一系列的 Vue 過濾器;
  2. Awesome-mask 包裝了 vanilla-masker 庫並且提供了過濾輸入的指令;
  3. Vue2-foundation 在 Vue 元件內部包裝了 ZURB Foundation 元件。

結論

Vue.js 的受歡迎程度還沒有放緩的跡象,框架的漸進式策略贏得了很多的信任。漸進式策略意味著個人可以逐漸地接入使用,而無需進行大規模的重寫。

正如我們看到的那樣,這種漸進式也在向另外的方向發展。正如你可以在其他應用程式中嵌入 Vue 一樣,也可以在 Vue 內部嵌入其他的庫。

需要一些尚未移植到 Vue 元件的功能嗎?把它拉進來,把它包起來,你會覺得物超所值的。

SmashingMag 上的進一步閱讀:

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章