# 1-1 Vue.js 簡介

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.0 指南 (opens new window)

Vue (音同View) 是一套以視圖層為基礎發展的 JavaScript 漸進式框架。

與其他前端框架/函式庫不同的是,Vue.js 的目標是透過簡單的 API 提供開發者實作資料綁定與操作網頁上的元件,同時也因為 Vue.js 的核心把焦點關注在狀態與畫面的同步層級上,遂能夠輕易地與其他 JavaScript 函式庫、前端開發工具鍊等整合使用,成為一套完整的前端開發方案。

# Vue.js 的發展簡史

Vue.js 最初源於 2013 年,當時只是作者尤雨溪 (Evan You) 在 Google Creative Lab 的創意技術人員(Creative Technologist)工作時的業餘專案。

尤雨溪在 Google 工作一段時間後,結合了過去開發 AngularJS 的經驗,開發出 Vue.js 這一套框架, 並在 2014 年 2 月在 Hacker News 與 Reddit 等網站對外正式發表了這個 Vue.js 0.8 的版本。

隨著時間過去,Vue.js 也吸引到知名 PHP 框架 Laravel 的作者 Taylor Otwell 在 Twitter 上為 Vue.js 打起免費廣告, 並在後續版本中被納入 Laravel 官方指定的入門套件,這使得 Vue.js 逐漸在技術圈更加廣為人知。

Laravel 作者對 Vue 的推崇 "來自 Laravel 作者對 Vue 的推崇": https://twitter.com/taylorotwell/status/1029744172339789825 (opens new window)

後來,Vue.js 1.0 版正式發佈於 2015 年,這個時期的 Vue.js 就像是輕量版的 AngularJS(v1) ,將 template 直接在瀏覽器解析。 同年,vue-router、vuex、vue-cli 等周邊生態工具鍊也相繼發佈。

2016 年 10 月發佈 Vue.js 2.0 版本 (代號: Ghost in the Shell) 。 由於這個版本借鑑了 React 的 Virtual DOM 更新機制,將 template 編譯為 render function,並回傳 Virtual DOM 物件,再由瀏覽器輸出渲染,也提供了 SSR (伺服器端渲染) 的可能性,在維持 V1 時期的良好開發體驗與增強執行效率的平衡下,使得 Vue.js 正式站穩腳步,躋身主流前端框架之列。

而眾所期待的 Vue.js 3.0 (版本代號:One Piece) 在經過長期的 RFC 社群討論後,也已於 2020/09/18 正式推出。 此版本底層核心由 TypeScript 重寫,除了有九成以上 API 與 2.x 相容外,效能變得更快、編譯後的檔案更小之外,也加入了不少好用的 API,為 Vue.js 帶來了新的可能性。

# Vue.js - 漸進式框架

Vue.js Logo

相信各位朋友在進入 Vue.js 官網 (https://vuejs.org) 的時候, 都會看到大大的標題「The Progressive JavaScript Framework」,漸進式 JavaScript 框架。 那麼,漸進式框架是什麼呢?

在談漸進式框架以前,就必須先從 Vue.js 的核心開始說起。

前面提到,最初 Vue.js 只是作者尤雨溪將它作為 UI library 的 side-project (業餘專案) 來發起。 說穿了, Vue.js 最主要的核心功能有兩個部分:「宣告式渲染」與 「元件系統」 。

# 指令式渲染 vs. 宣告式渲染

宣告式渲染目標是希望以「宣告」的方式在 HTML (也就是模板) 來表現 JavaScript 裡面的狀態 (資料),當狀態被更新的時候, Vue.js 就會根據資料或狀態的內容,同步更新 HTML 將我們的資料渲染出來。

過去傳統我們所熟悉的 jQuery 或原生 JavaScript 在操作網頁畫面的時候,大多是以直接操作 DOM 物件為主的方式來處理的。 舉個簡單的例子,假設今天有個 input 輸入框與一個 h1 的標籤:

<h1 id="message"></h1>
<input name="message" type="text" value="Hello JavaScript!">
1
2

此時希望能夠做到,當使用者在輸入框輸入文字的同時,也能同步更新 h1 標籤的文字內容,用 JavaScript 我們可以這樣做:

document.querySelector('#message').innerText = 
  document.querySelector('[name="message"]').value;

document.querySelector('[name="message"]')
  .addEventListener('input', (el) => { 
    document.querySelector('#message').innerText = el.target.value; 
  });
1
2
3
4
5
6
7

將指定的輸入框透過 addEventListener 監聽 input 的事件,並在事件觸發的時候去更新 h1 標籤內的文字。

那麼如果有多個標籤文字要連動更新呢? 就可能會變成這樣:

document.querySelector('[name="message"]')
  .addEventListener('input', (el) => { 
    document.querySelector('#message1').innerText = el.target.value; 
    document.querySelector('#message2').innerText = el.target.value; 
    document.querySelector('#message3').innerText = el.target.value; 
    document.querySelector('#message4').innerText = el.target.value; 
    // 下略...
  });
1
2
3
4
5
6
7
8

像這種一個指令一個動作,以「操作 DOM 為基礎」的模式,我們通常稱為「指令式程式設計」 (Imperative programming) ,或叫指令式渲染。

DOM與事件關係圖-1 我們都知道 JavaScript 是個以事件驅動 (event-driven) 的程式語言,它與網頁元素間的互動也是圍繞在事件中聯繫,當網頁內容的事件與需要處理的元素數量很少時,傳統的指令式可以很直覺地對 DOM 進行操作。

這種模式的好處是開發簡單直覺,但容易陷入「見樹不見林」的迷思,一旦專案的規模日漸增長,就容易寫出義大利麵式的程式碼。

從前人們沒有狀態集中管理的觀念,後端把資料吐在 HTML 上,我們就透過 JavaScript 直接把資料從 DOM 取出,處理完再寫回 DOM 上。 像這樣把 DOM 當做是資料結構來處理資料的方式顯然有很大的問題,當某個 DOM 被刪除的時候,是否意味著這個資料將永久消失再也拿不到了?

所以,當時多數人會做的事情,就是把資料往某個全域變數丟。 優點就是彈性大,缺點就是彈性太大,在網頁的任何地方都可以存取,於是除錯的時候就會不知道從何下手。 於是後來才又發展出 MVC / MVP / MVVM ... 等等模式的框架,而 Vue.js 就是屬於 MVVM 的類型之一,這個我們稍後再談。

DOM與事件關係圖-2 當專案的規模隨著業務需求增長時,網頁裡需要管理的元素越來越多,事件也越來越複雜,就容易陷入混亂,增加未來維護程式的困難。


那麼宣告式渲染又是什麼呢? 讓我們將前面的例子用 Vue.js 來改寫:

<div id="demo">
  <h1>{{ message }}</h1>
  <input v-model="message">
</div>
1
2
3
4
// for Vue 2.x
const vm = new Vue({
  data: {
    message: 'Hello Vue.js!'
  }
}).$mount('#demo');
1
2
3
4
5
6
// for Vue 3.0
const vm = Vue.createApp({
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}).mount('#demo');
1
2
3
4
5
6
7
8

同樣地,雖然內容有些不同,在 HTML 裡我們一樣可以看出有 h1 標籤與 input 的文字輸入框, 而 JavaScript 裡頭明顯少了直接對 DOM 操作的語法,取而代之的是一個透過 new 建立的 Vue 實體。

Vue.js 會將大括號 {{ }} 的內容,對應至 Vue.js 實體的 message 狀態,並且將之即時渲染至畫面上。

而 Vue.js 就是以這幾行簡單的語法,即可完成前面的 input 事件綁定與動態更新。 更甚者,未來若要在網頁增添元素對應 message,也只需要更改 HTML 內模板,無需調整 JavaScript 的程式碼,如:

<div id="demo">
  <h1>{{ message }}</h1>
  <h1>{{ message }}</h1>
  <h1>{{ message }}</h1>

  <input v-model="message">
</div>
1
2
3
4
5
6
7

只需要這樣,就可以一次更新三組 h1 標籤,而不需要像過去一樣將所有 DOM 找出,一一修改它們的 innerText。 語法看不懂沒關係,這裡先大略介紹概念,後面還會有詳細說明。

DOM與事件關係圖-3

如上圖, Vue.js 操作網頁的邏輯,其實就是將資料/狀態統一由 JavaScript 的物件來維護管理。 網頁上的事件都是針對物件裡的狀態來進行操作,當狀態被修改了以後, Vue.js 就會自動對模板指定的內容來進行更新同步,這也就是人們常說的 MVVM 模式。

# Vue.js 與 MVVM 模式

MVVM 分別由 Model、View 以及 ViewModel 這三個部分所組成,此一概念已知最早由微軟在 2005 年所提出。 Model 層與 View 層分別代表著狀態與畫面兩個部分,與大家所熟知的 MVC 的前兩項是一樣的。不同的是,ViewModel 層會自動將 Model 層與 View 層的狀態自動同步。

VUE與MVVM

而 Vue.js 正是採用了此一策略,將 DOM 的事件監聽與狀態的資料綁定封裝起來, 當使用者透過操作 View 或觸發網頁上的事件,ViewModel 層的 Vue.js 就會將狀態回存至 Model (由某個 JavaScript 物件表示)。 若 Model 裡的狀態被修改了,Vue.js 也會同步更新網頁模板 (View) 的對應內容。

# Vue.js 與元件系統

在眾多 MVVM 框架解決了狀態與畫面同步的問題後,下一個課題是網頁應用的元件化。 將應用的元件化之後,代表同樣邏輯、同樣模板的東西可以重複拿來使用。

以網頁來說,網頁系統的根本是 DOM 模型,那麼一個一個的 DOM 單元 (Node) 可以組合成 DOM-Tree 的模式, 那麼 Vue.js 的元件系統則是將此一概念再加以簡化,最小單位不一定如網頁般只能是 Node 的節點,它也可以是組合好的元件單元,就像我們平常玩的樂高積木一樣。

Vue.js與元件系統

元件 (Component) 的最小單位可以只是節點,也可以是好幾個節點組合而成的元件,甚至元件內還可有元件。 每個元件都有屬於自己的模板、程式邏輯,以及樣式,而整個網頁由這些元件構成起來,便成了元件樹 (Component tree)。

Vue.js與元件系統

觀念雖好,但也帶來了新的需求,像是元件之間的狀態傳遞與管理、單頁式的路由,甚至是如何與其他前端建構工具的結合...等問題。 還好 Vue.js 本身足夠優秀,由於使用的人夠多,再加上開源的特性,自然吸引了眾多開發者願意跳進去為它擴充開發工具,建立起一整套完整屬於 Vue.js 的前端生態圈。

# Vue.js 與前端生態圈

Vue.js 生態圈

起初 Vue.js 想做的事並沒有像這張圖的規模這麼多,甚至只有圖中左側最核心的兩個圓,這是 Vue.js 的特色,也是刻意為之的設計。

相較於 Angular 那樣大而全的框架,Angular 在設計過程中,設計團隊將開發者可能會遇到的多數問題都預先考慮進去, 這樣的好處是在開發專案時,這個框架就可以很快速地幫助你解決大多數面臨的難題與需求,像是需要處理前端路由、Ajax 的函式庫等等,不太需要向外去尋求第三方套件的輔助, Angular 都能自己一套搞定,這類集中式的設計確保了整體生態系統的一致性。

而 Vue.js 選擇走向類似 React 那樣小而美的道路,但兩者其實並沒有優劣之分,一切都是設計者的權衡取捨。

像 Vue.js 這類小型職責範圍 (Small-Scope) 的框架,它的優勢是在剛開始開發的時候,所需要知道的背景知識與概念不需要像 Angualr 或 React 那樣, 只要有基本的 HTML、CSS 以及 JavaScript 的知識,就可以快速上手。

而當專案規模擴大時,Vue.js 也提供了足夠的彈性,你可以任意套用第三方的工具,也可以考慮自己刻一個, 然後依循 Vue.js 提供的機制,如 Props、Events 等,整合至現有的 Vue.js 專案中。

更甚者,Vue.js 團隊也不斷提出相關的工具練,如前端路由有 Vue router,狀態管理有 Vuex, 建構工具鍊有 Vue-CLI 內建整合了 webpack , 甚至是伺服器端渲染 (SSR) 等等議題,官方團隊也試圖為開發者們提供了相關的解決方案。

漸進式框架代表著框架允許開發者以「漸進」的方式來挑選你所需的特性。 當專案的規模不需要控制前端路由、不需要大型集中狀態的管理,那麼你只需要載入 Vue.js 核心就夠。

沒用過 node? 不知道 npm / yarn 是什麼? 沒關係,你甚至可以像過去使用 jQuery 方式, 透過 CDN 引入一個 <script> 標籤,然後直接開始你的 Vue.js 「輕前端」之旅。

這也是為什麼我認為 Vue.js 是如此適合初學者使用。

專案開發有它本身的複雜度,當然框架以及其生態圈也有適合它場景的複雜度。

現代前端框架這麼多,我相信剛入行的朋友多數都會有選擇恐慌症,覺得我非得找到大家都說那個最強最好用的前端框架,再投入開發。 總覺得學了一個框架,結果這個框架在社群不是主流派別,豈不是虧大。 但綜觀前端技術的發展歷史,其實不難發現,那些新的技術、新的工具都是圍繞著問題而生。

現有的工具不夠好用,綁手綁腳寫起來不夠爽,於是有人就自幹了一套新的解法,如果這個解法可以解決大家也遇到的問題,那麼竄紅起來也就只是時間的問題。 如果你還沒用過 Vue.js,我也推薦你試試,相信它不會讓你失望的。

備註

「輕前端」 一詞概念係由黑暗執行緒大大發起 (來源 (opens new window)),為降低技能門檻,都避免提及 npm、webpack 等安裝編譯機制,單純靠 <script> 引用相關程式。 在專案新舊技術轉換期,特別適合引入 Vue.js 漸進式框架的概念。

# Vue.js 3.0 新增的特性

Vue.js 自 3.0 (版本代號:One Piece) 版本開始,除了底層核心由 TypeScript 重寫,也新增了不少特性,這裡簡單列出幾點給讀者們參考:

  • 引進了 "Fragment" 特性,不再限制元件必須只能是單一根節點
  • 狀態的響應式偵測由 Object.defineProperty 改為 Proxy API,執行時的效能更好,也解決了過去 Vue 2.x 在物件、陣列更新偵測的問題
  • 模板編譯:靜態節點優化,新的 Virtual DOM 更新時只需要遍歷動態的節點,更新時與實際 DOM 數量脫鉤,減少不必要的效能浪費
  • 內建新增 "Teleport"、"Suspense" 等功能型元件,擴充了更多可能性
  • 新的 Composition API 提供了另一種對程式碼、邏輯甚至是狀態的共用管理方式,擺脫過去 mixins 的混亂
  • 新增 setupref 等語法糖,開發時的體驗更好

除此之外,Vue.js 開源社群也為周邊生態圈不停地努力, 像是眾所週知建立 Vue 專案的 scaffolding (鷹架、腳手架) Vue-CLI (opens new window) 、 瀏覽器裡的 Vue 開發/除錯工具 Vue-Devtools (opens new window)、 VSCode 的知名套件 vetur (opens new window) ...等等, 都是由 Vue 核心開發團隊與眾多社群開發者所貢獻,為了提供在 Vue.js 上開發體驗更加流暢舒適的工具。

# 安裝與起步

要開始使用 Vue.js,你不一定需要先懂 npm/yarn 等等的套件管理工具。 以初學的角度來說,最簡單的方式就是在你的 HTML 底部加上一行:

<!-- Vue 2.x -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
1
2

或者,如果你想要嚐鮮 Vue 3.0,那麼可以改為引入:

<!-- Vue 3.0 -->
<script src="https://unpkg.com/vue@next"></script>
1
2

就可以開始你的 Vue.js 之旅了!

另外,若你已是前端開發的熟手,也可以透過 vite (opens new window) :

$ npm init vite-app hello-vue3
# 或 yarn create vite-app hello-vue3
1
2

以及大家都很熟悉的 vue-cli :

$ npm install -g @vue/cli
# 或 yarn global add @vue/cli

$ vue create hello-vue3
# 選擇 vue 3 preset
1
2
3
4
5

來建立你的 Vue.js 專案。

備註

關於透過 npm/yarn 與 Vue-CLI 詳細的專案建置方式, 為了不增加讀者負擔,這個部分會留待在後續 Vue-CLI 相關章節詳細解說。

# Hello Vue!

載入了 Vue.js 的 js 檔案之後,讓我們開始一個最簡單的 Hello World 範例。 我們只需要在 HTML 準備一個對應的節點,這裡我們以一個 id="app"div 來示範:

<div id="app">
  {{ message }}
</div>
1
2
3

Vue.js 的模板語法,是在 HTML 以兩個大括號 {{ }} 來表示,大括號的內容是 Vue.js 實體物件內的狀態。

Vue.js 實體物件是什麼? 我們在 <script> 標籤內新增一段程式碼:

// Vue 2.x
const vm = new Vue({
  data: {
    message: 'Hello Vue!'
  }
});

vm.$mount('#app');
1
2
3
4
5
6
7
8

在升級至 Vue 3.0 後,可以寫成新的 Composition API Style:

// Vue 3.0 with Composition API Style
const { createApp, ref } = Vue;

const vm = createApp({
  setup () {
    const message = ref('Hello Vue 3.0!');
    return {
      message
    }
  }
});

// mount
vm.mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13
14

當然,要維持過去 Vue 2.x 的 Options-base 風格也是沒問題的:

// Vue 3.0 with options-base style
const vm = Vue.createApp({
  data () {
    return {
      message: 'Hello Vue 3.0!'
    }
  }
});

// mount
vm.mount('#app');
1
2
3
4
5
6
7
8
9
10
11

這裡我們分別列出 Vue 2.x 與 3.x 的寫法給讀者參考。

小提醒

有關 Vue 3.0 Composition API 的用法後面會有專門章節討論。 如果沒有特別提及,本書多數案例會採用傳統 Vue.js 2.x / 3.x 都相容的 Options-base 風格來做解說。

不管是 2.x 的 new Vue({...}) 或是 3.0 的 Vue.createApp({...}),這裡我們先建立一個 Vue 的實體物件,並且將這個物件指定至 vm 中。

在這個物件 data 內的部分,就是 Vue.js 所儲存的狀態 (資料) 了,而生成後的 Vue 實體,則是透過 mount 與網頁的 HTML 結合, 也就是這個 Vue.js 實體物件 (根元件) 可以控制的範圍,

看到這裡,相信各位讀者對 Vue.js 應該有了初步的認識。 現在我們已經成功透過 Vue.js 控制網頁內容了。 在下個章節中,我會繼續來介紹 Vue.js 實體物件裡的各種重要屬性。

小提醒

Vue.js 也有與其他 JavaScript Library 一樣的特性,若你將 JavaScript 程式寫在 <head> ... </head> 的時候,可能會發現完全沒有作用。 這是由於瀏覽器在執行這段程式碼還未解析 <body> DOM 內容的緣故,此時需要加上 DOMContentLoaded 事件來等待 DOM 載入完成,方能正確執行。

const vm = Vue.createApp({
  // 略
});

document.addEventListener("DOMContentLoaded", () => {
  // DOM Ready!
  vm.mount('#app');
});
1
2
3
4
5
6
7
8
Last Updated: 4/8/2021, 11:38:55 AM