2019年2月8日 星期五

Vue 學習筆記 (二) : MVVM 架構與雙向資料綁定

配置好 Vue 的執行環境後, 接著進行 Vue 的簡單雙向資料綁定測試. 本系列之前的測試文章參考 :

關於前端網頁框架 Vue
Vue 學習筆記 (一) : 環境配置

本測試參考了下列書籍 :
  1. 一次搞懂熱門前端框架 (旗標)
  2. The Majesty of Vue (Packt)
  3. Vue.js 2 Cookbook (Packt)
  4. Vue.js 2.x by Example (Packt)
Vue 在網頁技術的 MVC 架構中扮演 V (View) 視圖與 M (Model) 模型之間的中介互動角色, 稱為 ViewModel 層, 是資料層與頁面顯示層之間的橋樑, 這在前端稱為 MVVM 架構 :
  • Model (資料管理層) 
  • View (畫面顯示層)
  • ViewModel (資料與顯示互動中介層)
MVVM 架構示意圖如下 :




Vue 透過 ViewModel 中介層的資料與畫面更新達成雙向資料綁定功能, 無論是資料 (Model) 或頁面 (View) 發生變動, 另一端都能立即刷新. Vue 的特點就是採用漸進式架構, 核心功能專注於處理模型層與視圖層之中介, 簡單易上手, 不像其他框架之高度複雜性.

使用 Vue 基本上有三個步驟 :

1. 用 script 元素指定 vue.js 來源 (自備或使用 CDN)
2. 在網頁中指定一個具有 id 屬性的元素 (p 或 div) 掛載 (mount) App
3. 用 script 元素撰寫 App 或指定 App 來源

Vue 的中介互動功能是透過 Vue 物件達成的, 因此在 App 中須建立一個 Vue 物件來傳遞資料到網頁中, 或者是接收從網頁傳來的資料, 稱為雙向資料綁定, Vue 物件提供了特定屬性與方法來達成雙向資料綁定 (Two-way binding) 功能.


一. 單向資料綁定 (從 Vue 物件傳送資料到網頁) : 

首先必須在網頁中建立一個模版來掛載 App 並綁定資料來源, 這樣就能從 Vue 物件傳遞資料到網頁中了. 此網頁模版是由具 id 屬性的網頁元素如 p 或 div 構成, 綁定之資料則以兩個大括弧之 Mustache 標籤表示 {{property}}, 這個 property 就是 App 中的變數, 也就是所綁定的資料來源 :

<div id='msg'>
  {{ messge }}
</div>

這個 id 為 msg 的元素是一個容器, 用來掛載 (mount) 應用程式 App, 也是 App 在網頁中的工作區. 對 App 而言, 它只能操作這個容器中的的元素, 不認得網頁中其他任何元素. 換句話說只有所掛載的容器才是 Vue 應用程式的作用區域 (Scope). 注意, Vue 的 App 只能掛載到 html 內部的網頁元素上 (通常掛載 div 元素), 如果掛載到 html 或 body 標籤會產生執行錯誤.

接著是撰寫 Javascript App, 事實上就是建立一個 Vue 物件來操作網頁中此 App 所掛載的容器. 建立 Vue 物件時至少要傳入包含 el 與 data 屬性的物件當參數, 其中 el 用來指定網頁中所掛載容器之 id, 而 data 則定義所綁定的資料變數, 例如 :

var app=new Vue({
  el: '#msg',
  data: {message: 'Hello World!'}
  });

此處 el 屬性值 '#msg' 表示此 App 要掛載到網頁中 id 為 msg 的元素當作容器, 而屬性 data 表示此網頁容器所綁定的變數, 亦即資料來源, 此處定義了一個變數 message 並賦予初始值 'Hello World!', 當 Vue 物件建立時, 這個初始值會立即被傳遞到網頁容器中以 Mustache 標籤所綁定的位置.

屬性 el 指定元素的用法與 jQuery 一樣, 用 "#" 表示存取 id 屬性, 除此之外也可以指定 DOM 物件給 el 屬性 :

var element=document.getElementById('msg');
var app=new Vue({
  el: element,
  data: {message: 'Hello World!'}
  });

建立 Vue 物件後此物件即掛載到指定之網頁容器上, Vue 物件會以自己建立的 Virtual DOM 來取代此容器內原來的網頁元素, 這就是為何 Vue 物件可以將變數之值動態即時注入到所綁定之位置的原因. 完整範例如下 :

測試 1 : Hello World  (本機來源)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="js/vue.js"></script>
  </head>
  <body>
    <div id='msg'>{{message}}</div>
    <script>
  new Vue({el: '#msg', data: {message: 'Hello World!'}}); 
    </script>
  </body>
</html>

瀏覽網頁結果為顯示 "Hello World!".

也可以使用 CDN 來源, 並將應用程式 new Vue() 寫成外部檔案 helloworld.js, 放在 /js 底下, 其內容為 :

new Vue({el: '#msg', data: {message: 'Hello World!'}});

例如 :

測試 2 : Hello World  (外部來源)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id='msg'>{{message}}</div>
    <script src="js/helloworld.js"></script>
  </body>
</html>

結果與測試 1 完全一樣.

注意, 載入 helloworld.js 的 script 標籤不可放在 head 標籤內, 因為當 head 部分被載入會立即執行 helloworld.js, 但此時 div 元素尚未建立, 因此資料無法綁定, 會顯示 {{ message }} 而不是 'Hello World!', 例如 :


測試 3 : Hello World  (外部來源)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="js/helloworld.js"></script>
  </head>
  <body>
    <div id='msg'>{{message}}</div>
  </body>
</html>

上例中將應用程式 helloworld.js 放在 head 標籤中是錯誤的, 應該放在所綁定的元素後面才會正確運作, 放在 body 的結尾處最保險.

如果要放在所綁定的元素前面, 可以將 App 寫在自訂函數中, 再用 body 元素的 onload 屬性指定當 body 內元素全部載入後執行此函數即可, 例如 :


測試 4 : Hello World  (使用 onload) [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://unpkg.com/vue/dist/vue.js"></script> 
  </head>
  <body onload='sayhello()'>
    <script>
      function sayhello() {
        new Vue({el: '#msg', data: {message: 'Hello World!'}});
        }
    </script>
    <div id='msg'>{{message}}</div>
  </body>
</html>

本例 App 雖位於綁定元素 id='msg' 的 div 之前, 但是函數 sayhello() 會在 body 內元素都載入後才執行, 因此網頁會顯示 'Hello World!' 之正確結果.

上面提到與 Vue 綁定 id 的 div 元素是 Vue 物件操作的容器, Vue 物件傳遞過來的資料可用 Mustache 標籤放在此容器內任何元素中, 例如 :


測試 5 : 傳遞資料到 div 容器內之 h1 元素 [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id='msg'>
     <h1>{{message}}</h1>
    </div>
    <script>
      new Vue({el: '#msg', data: {message: 'Hello World!'}});
    </script>
  </body>
</html>

可見 Hello World! 受到 h1 標籤控制變粗體了! 這說明 Vue 物件所傳遞的資料可放在容器元素 div 內的任何地方, 不一定要放在 div 本身.


二. 雙向資料綁定

上面範例都是從 Vue 物件傳送資料到網頁, Vue 提供了 v-model 屬性讓網頁元素可以反方向從網頁傳遞資料給 Vue 物件以達成雙向資料綁定功能, 亦即透過設定 v-model 我們告訴 Vue 這個 input 輸入欄位要跟 Vue 物件裡面的哪一個 data 變數綁定在一起. 輸入元素以 input 元素為例 :

<input type='text' v-model='message'>

此 input 元素透過 v-model 屬性與 Vue 物件的 data 屬性中的 message 子屬性綁定在一起, 而 message 若與網頁中 Mustache 標籤 message 綁定的話, 則在 input 中輸入的資料會立刻顯示在網頁上, 例如 :


測試 6 : 雙向資料綁定 (無按鈕)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script> 
  </head>
  <body>
    <div id='msg'>
      <p>{{message}}</p>
      <input type='text' v-model="message">
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {message: 'Hello World!'}
        });
    </script>
  </body>
</html>


網頁初始執行時, p 標籤的初始內容為 Hello World!, 在底下的 input 文字欄位輸入任意文字時, 上面 p 標籤的內容也會立即同步顯示相同內容, 這就是所謂的雙向資料綁定! 因為 input 標籤以 v-model 屬性與 Vue 物件之 data 子物件的 message 屬性綁定, 因此在 input 文字欄位所輸入的任何字串都會同步修改 data 子物件的 message 屬性值; 同時, 此屬性又被 Mustache 樣版引擎投放到 p 標籤的內容中, 因此 p 標籤內容也會同步改變.

注意, p 標籤與 input 標籤都必須放在 id='msg' 的 div 容器內, 這樣雙向資料綁定才會生效, 因為此容器是 Vue 物件的工作空間.

不過若在 input 文字欄位中輸入 HTML 碼的話, p 標籤將如實顯示 HTML 碼, 因為 Mustache 樣版引擎遇到 < 與 > 符號時會自動脫逸 (escapse), 因此不會顯示 HTML 效果.




解決辦法有兩個, 在 Vue 2 以前的版本使用三重大括弧 {{{ }}} 來阻止脫逸; 但在 Vue 2 之後改用 v-html 屬性來呈現 HTML 內容 :

<p v-html='message'></p>

亦即只要將原本的 Mustache 標籤 {{ }} 改用 v-html 取代即可, 綁定方式完全不變, 此方法可顯示 HTML 與純文字內容, 例如 :


測試 7 : 雙向資料綁定 (無按鈕 HTML 版)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script> 
  </head>
  <body>
    <div id='msg'>
      <p v-html='message'></p>
      <input type='text' v-model="message">
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {message: 'Hello World!'}
        });
    </script>
  </body>
</html>


上面的兩個範例是在 input 欄位輸入資料時立即反映在 p 標籤內, 如果加一個確定按鈕, 需按下按鈕才會更新資料該怎麼做? Vue 為按鈕元素提供了 v-on 事件處理屬性來與 Vue 物件的 methods 屬性綁定, 只要將事件處理函數指定給 methods 屬性就可以了 :

<input type='button' v-on:click='doSomething' value='確定'>

此按鈕中以 v-on 屬性指定了按鈕的 click 事件處理函數為 doSomething(), 此函數需在 Vue 物件中定義並以物件形式指派給 methods 屬性才能達成事件綁定功能 :

methods: {doSomethong : function() {//操作網頁顯示的程式碼}}

例如 :


測試 8 : 雙向資料綁定 (有按鈕 HTML 版)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://vuejs.org/js/vue.js"></script> 
  </head>
  <body>
    <div id='msg'>
      <p v-html='message'></p>
      <input type='text' v-model='input_msg' size='30'>
      <input type='button' v-on:click='doSomething' value='確定'>
    </div>
    <script>
      new Vue({
        el: '#msg',
        data: {
          message: 'Hello World!',
          input_msg: ''
          },
        methods: {
          doSomething: function() {
            this.message='您輸入了 : ' + this.input_msg;
            }
          }
        });
    </script>
  </body>
</html>

輸入 <b style='color:blue'>Tony</b> 結果如下 :



此處雙向資料綁定定義於 Vue 物件的 data 子物件的兩個屬性中, 其中 message 用來傳遞資料至網頁, 而 input_msg 則用來接收網頁傳遞過來的資料. 事件處理則是透過按鈕元素的 v-on 屬性定義 click 事件處理函數為 doSomething(), 當使用者按下 '確定' 按鈕時會呼叫 Vue 物件 methods 屬性鎖定義的 doSomethong() 函數處理, 在此函數中, Vue 物件會讀取 input_msg 從網頁接收的資料 this.input_msg 串上前置字串後設定 this.message 屬性, Mustache 樣版引擎會立刻將其輸出到網頁中的 p 標籤中.

其實雙向資料綁定用 jQuery 來做也是輕而易舉的, 我參考了 "The Majesty of Vue" 這本書的 2-3 節範例, 將上面測試 7 的雙向資料綁定功能改以 jQuery 來實做如下所示 :


測試 9 : 雙向資料綁定 (jQuery 版無按鈕)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> 
  </head>
  <body>
    <div id='msg'>
      <p id='message'></p>
      <input type='text' id='input_msg' size='30'>
    </div>
    <script>
      $('#message').html('Hello World!');
      $('#input_msg').on('keyup', function() {
        var input_msg=$('#input_msg').val();
        $('#message').html('您輸入了 : ' + input_msg);
        });
    </script>
  </body>
</html>

這裡使用 jQuery 物件的 on() 方法給輸入欄位 input_msg 掛載 keyup 事件處理函數, 每輸入一個字元就會呼叫此匿名函數, 利用 jQuery 物件的 val() 取得 input_msg 的內容, 前面串上前置字串後呼叫 html() 設定 p 元件的 innerHTML 屬性以顯示 HTML 內容, 功能與上面測試 7 完全一樣.

上面測試 8 Vue 有按鈕版若使用 jQuery 來做如下所示 :


測試 10 : 雙向資料綁定 (jQuery 版有按鈕)  [原始碼]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> 
  </head>
  <body>
    <div id='msg'>
      <p id='message'></p>
      <input type='text' id='input_msg' size='30'>
      <input type='button' id='button1' value='確定'>
    </div>
    <script>
      $('#message').html('Hello World!');
      $('#button1').click(function() {
        var input_msg=$('#input_msg').val();
        $('#message').html('您輸入了 : ' + input_msg);
        });
    </script>
  </body>
</html>

按鈕在 jQuery 要呼叫 click() 來處理按鈕事件, 其餘跟測試 9 是一樣的. 比較兩者作法, jQuery 程式色彩較強, 且須熟悉 DOM 架構, 因為 jQuery 是直接操作 DOM 的. 而 Vue 好像只是在設定屬性而已, 而且 Vue 不直接操作 DOM, 而是隱性地與 Virtual DOM 互動, 據說執行起來比較快.

參考 :

Vue.js 30天隨身包 系列
Day08 - [Directives] 資料綁定(Data Binding)

2019-12-20 補充 :

Vue/React/Angular 都具有雙向資料綁定功能, 但採用的方法不同, 下面這篇文章有詳細演示 :

前端數據綁定之謎

看過之後覺得還是 Vue 最簡潔.

沒有留言 :