[TOC] ## 1-1 Vue.js 簡介 ### 漸進式 ![image](https://hackmd.io/_uploads/B1NsF03xyg.png) 紅色部分是最核心的特色,可以依照專案需求漸進式套用其他需要的東西 ### 指令式渲染 vs. 宣告式渲染 原生JS以「操作 DOM 為基礎」的模式為指令式渲染 取得元素,操控元素,一個指令一個動作 很多元素的時候會很亂,無法管理 ![image](https://hackmd.io/_uploads/rJhi5Chl1g.png) Vue.js則是將資料/狀態統一由 JS 的**物件**來維護管理,事件改變物件內的狀態,狀態被修改後再去更新模板的內容,也就是MVVM模式 ```html <div id="demo"> <h1>{{ message }}</h1> <input v-model="message"> </div> ``` ```javascript // 建立一個Vue的物件實體,並且將這個物件指定至變數vm之中。 const vm = Vue.createApp({ data() { return { message: 'Hello Vue!' } } }).mount('#demo'); // 宣告後馬上掛載到指定的網頁節點 // 也可以之後再掛載 vm.mount('#demo'); ``` 這邊的`{data(){...}}`是稱為`options`的物件參數 透過`mount`,表示`vm`這個 Vue.js 實體物件 (根元件) 可控制的範圍為`#demo`這個節點 ### MVVM 模式 ![image](https://hackmd.io/_uploads/BkLd6A3ekl.png) 將 DOM 的事件監聽與狀態的資料綁定封裝起來 (ViewModel, VM) 使用者操作 View (V) 透過 VM 內的 Vue.js 把狀態回存至 Model (M) (某個物件,以上面的程式碼來說就是 `vm` ?) Model 被更新後 VM 內的 Vue.js 也會同步更新網頁模板 (V) ### DOMContentLoaded 如果程式碼寫在`<head>`裡面會因為瀏覽器還沒解析到`<body>`內的DOM而出錯,可以利用`DOMContentLoaded`事件等到DOM完全載入後再執行 ```javascript const vm = Vue.createApp({ // 略 }); document.addEventListener("DOMContentLoaded", () => { // DOM Ready! vm.mount('#app'); }); ``` ## 1-2 Vue.js 的核心: 實體 ### 把實體掛載至 DOM 方法一:在`options`設定`el`屬性 (3.0不能用) ```javascript // for Vue 2.x const vm = new Vue({ el: '#app' }); ``` 方法二:用`$mount` (2.x) 或 `mount` (3.0) 方法 ```javascript // 新增節點然後加入至 body const el = document.createElement('div'); document.body.appendChild(el); // Vue 2.x vm.$mount(el); // Vue 3.0 vm.mount('#app'); ``` `el`屬性和`mount()`設定的節點可以是 CSS 選擇器,也可以是 DOM 物件 :heavy_check_mark:可以用`vm.mount(el);`嗎 可以! #### 測試結果 :::spoiler ```javascript import { createApp, ref, } from "https://unpkg.com/vue@3/dist/vue.esm-browser.js"; const el = document.createElement("div"); el.setAttribute("id", "child"); el.textContent = "{{ name }}已陣亡"; document.body.appendChild(el); const vm = createApp({ data() { return { name: "天兔", }; }, mounted() { console.log("呀哈"); }, }); vm.mount(el); ``` >![image](https://hackmd.io/_uploads/rkkyQAFM1e.png) ::: #### 測試失敗紀錄 :::spoiler 在實作的時候一直失敗,看到官方文件這句話 >![image](https://hackmd.io/_uploads/rkKZakMz1l.png) 而檢查檔案中有使用到.mount()的地方只有main.js ![image](https://hackmd.io/_uploads/Sk_P6JzGye.png) 所以猜測,在App.vue裡面集合了所有要拿來製作實體的東西(?),然後一次在App.vue中用createApp()創造這個實體,並且掛載上去 ::: ### 定義狀態 在 Vue 實體的定義的狀態,就是透過 `data` 屬性來儲存。 `data` 屬性是function的形式 ```javascript const vm = Vue.createApp({ // 實體所回傳的狀態會以物件 key-value 的形式 // 且在 Vue 3.0 開始,data 將強制以 function 的形式出現 data () { return { name: '008JS' } } }); ``` ### 模板語法 採用的是 Mustache 語法 > [從 Mustache 的 render 函式了解模板系統如何解析並渲染傳入的資料](https://uu9924079.medium.com/從-mustache-的-render-函式了解模板系統如何解析並渲染傳入的資料-e32f4ca4dd50) 利用`{{ }}`把定義的狀態輸出到html上 ```html <div id="app"> {{ name }} 好棒棒! </div> ``` ![image](https://hackmd.io/_uploads/SJkZzL1byg.png) 理解:也就是在data內存放變數,可以利用這個變數直接在html上渲染 Vue.js 會自動將 data 內的屬性加上 getter 與 setter 的特性,以便監控狀態的更新 (響應式更新)。 在 Vue.js 的實體當中,以底線 _ 或錢字號 $ 作為開頭的屬性,不會被加上 getter 與 setter 的特性,命名時避免使用_和$作開頭 定義的狀態要怎麼更新?透過 `vm.$data.XXX` 來操作內部狀態。(注意 vm.mount('#app') 之後才會產生對應的 $data) ```javascript const vm = Vue.createApp({ // 實體所回傳的狀態會以物件 key-value 的形式 // 且在 Vue 3.0 開始,data 將強制以 function 的形式出現 data () { return { name: '008JS' } } }).mount('#app'); // 建立實體的同時要掛載到DOM上!!! vm.$data.name = "30cm"; // 才能更改 ``` 如果建立實體的同時沒有掛載到DOM上,就無法取得$data ```javascript const vm = createApp({ data() { return { name: "天兔", }; }, mounted() { console.log("呀哈"); }, }); vm.mount(el); vm.$data.name = "小八"; ``` >![image](https://hackmd.io/_uploads/BklQORYMke.png) ```javascript const vm = createApp({ data() { return { name: "天兔", }; }, mounted() { console.log("呀哈"); }, }).mount(el); vm.$data.name = "小八"; ``` >![image](https://hackmd.io/_uploads/SyTBdCFzyx.png) 在`{{ }}`內也可以進行變數之間的運算,最後將算出來的值輸出到畫面上 `{{ }}`可以自定義更改,例如改成`%{ }%`,設定`delimiters`屬性。但這個屬性只支援在「瀏覽器」即時編譯的版本中使用。 ```javascript const vm = Vue.createApp({ delimiters: [`%{`,`}%`], data () { return { name: '008JS' } } }); ``` ### 共用 data 的汙染 定義在Vue實體外的data,可以讓不同的Vue實體共用這個data的設定,但更改實體A的data也會同時改變實體B的狀況,建議使用淺拷貝或深拷貝`JSON.parse(JSON.stringify(...))`的方式引入實體A和實體B ### template 模板屬性 除了直接用`{{ }}`寫在html,也可以使用`template`屬性 ```javascript const vm = Vue.createApp({ template: `<div>{{ greeting }} 好棒棒!</div>`, data () { return { greeting: 'Hello Vue.js!' } } }).mount('#app'); ``` ## 1-3 資料加工與邏輯整合 ### methods 方法 重複的程式片段包成function,function可以放在`methods`屬性,並作為一個物件的屬性名稱。 ```javascript const vm = Vue.createApp({ data () { return { price: 100, quantity: 10, } }, methods: { subtotal: function () { // 小心不要寫成箭頭函式 return this.price * this.quantity; // 記得要加this } // 但ES6的方法可以簡寫成這樣 sum () { return this.price * this.quantity; } } }).mount('#app'); ``` > [ES6的方法簡寫](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions) 在模板中呼叫這個方法就可以使用了 ```html <div id="app"> 總額是{{ subtotal() }}元 </div> ``` ### computed 和methods很像,使用上不用小括號執行 ```javascript const vm = Vue.createApp({ data () { return { price: 100, quantity: 10, } }, computed: { subtotal: function () { return this.price * this.quantity; } } }).mount('#app'); ``` ```html <div id="app"> <!-- 不用小括號 --> 總額是{{ subtotal }}元 </div> ``` 和methods的差別在於,computed只會計算一次並把結果暫存起來,直到它裡面的屬性(price, quantity之類的)被更新,才會再次執行 所以執行效率上computed比較快,但缺點是computed中的function無法帶入參數(因為不用小括號) :heavy_check_mark:當我們此時嘗試要去更新 data 裡的 message 屬性時,會觸發 methods 的呼叫,而 computed 卻不會再次執行。 #### 測試結果 :::spoiler 原本書上html是寫下面這樣 ```html <div id="test1"> <p>總金額共{{ subtotalComputed }}元</p> <p>總金額共{{ subtotalMethods() }}元</p> </div> ``` 搭配以下的js,在最後更新 data 裡的 message 屬性 ```javascript const vm = createApp({ data() { return { message: "Hello Vue!", }; }, computed: { subtotalComputed: function () { console.log("computed"); return 100 * 100; }, }, methods: { subtotalMethods() { console.log("methods"); return 100 * 100; }, }, }).mount("#test1"); vm.$data.message = "Hey"; ``` 但是都只有呼叫一次而已 ![image](https://hackmd.io/_uploads/H10LX9Z7yl.png) 所以我把html加上一行要取用message的地方 ```html <div id="test1"> <p>總金額共{{ subtotalComputed }}元</p> <p>總金額共{{ subtotalMethods() }}元</p> {{ message }} </div> ``` 就可以看到methods被執行了兩次 ![image](https://hackmd.io/_uploads/rkriX9ZX1x.png) ::: ### computed / methods 的使用時機比較 幣值轉換 ```html <!-- 使用methods --> <!-- 要綁v-on,雖然現在還看不懂v-on是啥 --> <div id="app"> <p>1 日幣 = 0.278 台幣</p> <div>台幣 <input type="text" v-model="twd" v-on:input="twd2jpy"></div> <div>日幣 <input type="text" v-model="jpy" v-on:input="jpy2twd"></div> </div> <!-- 使用computed --> <div id="app"> <p>1 日幣 = 0.278 台幣</p> <div>台幣 <input type="text" v-model="twd"></div> <div>日幣 <input type="text" v-model="jpy"></div> </div> ``` >之後會提到 v-model 是幹嘛的 >各位讀者只要知道這裡放上 v-model ,使用者輸入的內容會跑進 Vue 實體對應的屬性即可。 ```javascript // 使用methods,每一種轉換就要寫一個function const vm = Vue.createApp({ data () { // 每一個幣值就要記一個 return { twd: 0.278, jpy: 1, } }, methods: { // 要寫兩個function twd2jpy () { this.jpy = Number.parseFloat(Number(this.twd) / 0.278).toFixed(3); }, jpy2twd () { this.twd = Number.parseFloat(Number(this.jpy) * 0.278).toFixed(3); }, }, mounted () { // 這個component掛載的時候會執行的function this.twd2jpy(); } }).mount('#app'); // 使用computed,重點是從台幣轉換成各種幣值,也就是以台幣為基準點來轉換 const vm = Vue.createApp({ data () { // 紀錄台幣就好 return { twd: 0.278 // 這個型別是string } }, computed: { jpy: { get () { return Number.parseFloat(Number(this.twd) / 0.278).toFixed(3); }, set (val) { this.twd = Number.parseFloat(Number(val) * 0.278).toFixed(3); } } } }).mount('#app'); ``` #### 自行測試 把各種幣別統一管理在methods裡面,再用`this.XXX`取用這個method ```javascript methods: { getExchangeRate(currency) { let rate = 1; switch (currency) { case "jpy": rate = 0.22; break; case "usd": rate = 31.5; break; } return rate; }, }, computed: { jpy: { get() { return Number(Number(this.twd) / this.getExchangeRate("jpy")).toFixed(2); }, set(value) { this.twd = Number(Number(value) * this.getExchangeRate("jpy")).toFixed(2); }, }, usd: { get() { return Number(Number(this.twd) / this.getExchangeRate("usd")).toFixed(2); }, set(value) { this.twd = Number(Number(value) * this.getExchangeRate("usd")).toFixed(2); }, }, }, ``` :question:在輸入時會一直強制變成小數點兩位的狀態 ## 1-4 Vue.js 的黑魔法: 指令 ### 屬性綁定 - `v-bind` 屬性也用變數的方式去渲染 ```javascript const vm = createApp({ data() { return { isDisabled: true, }; }, }).mount(#app); ``` ```html <button v-bind:disabled="isDisabled">click me!</button> ``` 渲染出來就是`disabled`這個屬性是`isDisabled`屬性的值`true` ![image](https://hackmd.io/_uploads/B1xlDT-7yg.png) ### 表單綁定 - `v-model` 要是表單元素才可以透過`v-model`進行雙向綁定,例如 `<input>`、`<textarea>` 以及 `<select>` 雙向綁定理解:輸入的值會設定成`v-model`的變數的值。 #### `<input type="radio">` 原本同一組radio要使用同一個`name`才能綁在一起 ```html <div> <input type="radio" name="contact" id="contactChoice1" value="email" /> <label for="contactChoice1">Email</label> <input type="radio" name="contact" id="contactChoice2" value="phone" /> <label for="contactChoice2">Phone</label> <input type="radio" name="contact" id="contactChoice3" value="line" /> <label for="contactChoice3">Line</label> </div> ``` 現在可以設定同一個`v-model`,讓radio綁成一組 ```html <div> <input type="radio" id="contactChoice1" value="email" v-model="contact" /> <label for="contactChoice1">Email</label> <input type="radio" id="contactChoice2" value="phone" v-model="contact" /> <label for="contactChoice2">Phone</label> <input type="radio" id="contactChoice3" value="line" v-model="contact" /> <label for="contactChoice3">Line</label> </div> ``` 並且在實體中設定,就會預設選擇設定的那個value ```javascript const vm = createApp({ data() { return { contact: "line", }; }, }).mount("#app"); ``` ![image](https://hackmd.io/_uploads/ryQLaH2Qkl.png) #### 自己亂測試 設定成同一個`v-model`之後,即使`name`不一樣也會綁在一起 設定成同一個`name`,即使`v-model`不一樣也會綁在一起 #### `<input type="checkbox">` 和radio的差別是實體中設定要是陣列,如果不是陣列就會全選 #### `<input type="select">` 他的範例壞掉惹,但程式碼是對的 把「請選擇」的value=""刪掉好像也沒關係 ```html <select v-model="selected"> <option disabled>請選擇</option> <option>台北市</option> <option>新北市</option> <option>基隆市</option> </select> <p>Selected: {{ selected }}</p> ``` 讓一開始選項停在「請選擇」 ```javascript const vm = createApp({ data() { return { selected: "請選擇", }; }, }).mount("#app"); ``` ### v-model 與修飾子 #### `.lazy` 從監聽input事件改為change事件,當使用者離開輸入框焦點才會觸發事件 #### `.number` input輸入的型別從字串改為數字,如果想取得的是數字就可以自動幫忙轉型並作運算 #### `.trim` 自動刪除輸入前後的空白 ### 模板綁定 - v-text / v-html / v-once / v-pre #### `v-text` 跟{{ }}有類似的效果,但設定在標籤上的話就會把這個標籤的文字(我猜是text-content)(書上寫innerHTML)換成變數的內容 #### `v-html` 把標籤的innerHTML換成變數的內容 #### `v-once` 只會渲染一次 #### `v-pre` 不使用指令 如果想要顯示{{ }}大括號本身的話就要用這個指令 `<div v-html="rawContent" v-pre></div>` 這樣也不會顯示rawContent的值 ### v-bind 樣式綁定 class/style ```css .error { color: red; } ``` 綁class`error`,語法會判斷屬性(error)的值(isError)的truthiness,是true就加上這個屬性成為class [官方文件](https://vuejs.org/guide/essentials/class-and-style.html) ```html <input type="text" v-model.trim="title" v-bind:class="{error: isError}" /> <input type="text" v-model.trim="title" v-bind:class="{ error: title.length > 10 }" /> ``` 把邏輯移到computed ```javascript computed: { isError: function () { return this.title.length > 10; }, }, ``` style中的css推薦使用駝峰式,如果用烤肉串式要用引號括起來 ```html <input type="text" v-model.trim="title" v-bind:style="{ fontSize: fontSize }" /> ``` ```javascript data() { return { fontSize: "30px", }; }, ``` 放進array,例如跟瀏覽器支援相關的,會使用最後一個瀏覽器有支援的 ```html <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div> ```