[譯] Vue.js 優雅地集成第三方 JavaScript

摘要:Vue.js 的一個主要的優勢是能夠很好地與其餘代碼一塊兒工做:也就是說它不只很容易嵌入到其餘的應用程序當中,並且也很容易將非 Vue 代碼包裝到 Vue 當中。本文討論了 Vue.js 的第二個優點,包括了三種不一樣類型的第三方 JavaScript,以及將它們嵌入到 Vue 中的方法。javascript

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

Vue 用戶增加的一個核心緣由是:Vue 是一個漸進式框架 — 它容許你的頁面中部分使用 Vue.js 來進行開發,而不須要一個完整的單頁應用。也容許你只加入一個 script 標籤,而不是使用一個完整的構建系統就能夠啓動而且運行。html

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

本文將會探討當你想要使用不一樣類型的第三方 JavaScript,而且想將其嵌入到 Vue 項目中的狀況,而後介紹最適合嵌入到 Vue 中的幾種類型的工具和技術。在最後,咱們會考慮這些方法的缺點,以及在決定使用它們的時候須要考慮什麼。vue

本文假設你熟悉 Vue.js 以及組件和指令的概念。若是你正在尋找 Vue 和這些概念的介紹,能夠參考 Sarah Drasner 的 introduction to Vue.js series 或者 Vue 官方文檔java

第三方 JavaScript 類型

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

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

DOM 無關的庫

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

這些庫很容易集成到 Vue 應用當中,可是能夠多種方式來提供合理的訪問方式。這些庫通常都是爲了提供實用的程序功能,和其餘任何類型的 JavaScript 項目都是相融的。android

元素加強庫

元素加強是一種爲 DOM 元素添加額外功能的方法,這種方法由來已久。好比能夠幫助圖片進行懶加載的 lozad 或者爲輸入框提供輸入過濾的 Vanilla Maskerios

這些庫一般只會一次影響單個元素,他們可能會操縱單個元素,可是不會爲 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索