2022/03/07

Vue.js Essentials

Vue 是一個建立 UI 的 progressive framework。核心 library 只處理 view layer,易於跟其他 project/library 整合。

Vue.js 是 Evan You 在 2013 的業餘專案,因在 google 工作的 AngularJS 經驗,開發了 Vue.js,後來因為 Laravel 的作者 Taylor Otwell 的推崇,使得 Vue.js 越來越知名。2015 年推出 1.0 正式版,類似 AngularJS v1,2016年10月推出 2.0版,參考 React 的 Virtual DOM 機制,將 template 改為 render function,並回傳 virtual DOM。2020/9/18 推出 3.0版,底層改用 TypeScript 重寫,提高效能。

Vue 核心只有「宣告式渲染 declarative rendering」與「元件系統 component system」,傳統的 JQuery 是「指令式渲染」,用 js 指令修改 html 元件內容,Vue 會在 JS 的資料物件狀態異動時,直接同步更新 html。

傳統的網頁是 DOM Model,但 Vue Component 合併了 html, js 與 css,每個元件有自己的 template, code,將元件組合起來就是 component tree,也可編排成為網頁。

progressive framework 的意思是,Vue 以兩個核心概念為基礎,不斷提出相關的工具,例如前端路由 Vue Router,狀態管理 Vuex,Vue-CLI 內建整合了 webpack。developer 可根據專案的需求,漸進地採用 Vue 的眾多整合工具。最基本只需要 Vue.js 核心即可。

安裝

只需要在 html 引用 js 或是 下載 相關的 js

<script src="https://unpkg.com/vue@next"></script>

但文件中提到這種引用方式是開發階段使用

如果是在nodejs 使用 Vue,index.js 可得知是透過 process.env.NODE_ENV 判斷是不是用 prod

'use strict'

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./dist/vue.cjs.prod.js')
} else {
  module.exports = require('./dist/vue.cjs.js')
}

一文弄懂如何在 Vue 中配置 process.env.NODE_ENV

如果要使用特定版本的 Vue.js,可改用這個方式

<!-- 開發版 -->
<script src="https://unpkg.com/vue@3.2.10/dist/vue.global.js"></script>
<!-- production -->
<script src="https://unpkg.com/vue@3.2.10/dist/vue.global.prod.js"></script>

在 nodejs 可用 npm 安裝

npm install vue@next

或使用 Vue CLI

npm install -g @vue/cli

HelloWorld

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hello Vue</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
</head>

<body>

  <div id="app">
    {{ message }}
  </div>

  <div id="app2">
    {{ message }}
  </div>

  <!-- <script src="https://unpkg.com/vue@3.2.10"></script> -->
  <!-- <script src="https://unpkg.com/vue@3.2.10/dist/vue.global.js"></script> -->
  <script src="https://unpkg.com/vue@3.2.10/dist/vue.global.prod.js"></script>

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

    // mount
    vm.mount('#app');

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

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

    // mount
    vm2.mount('#app2');
  </script>

</body>

</html>

如果把 script 程式寫在 head 裡面,必須用 DOMContentLoaded event listener 判斷是否 DOM ready

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

document.addEventListener("DOMContentLoaded", () =>
  // DOM ready
  vm.mount('#app');
});

Introduction

DeclarativeRendering

Vue 會直接以宣告方式,利用 template 語法,將資料顯示在 DOM 元件裡面

  <div id="counter">
    Counter: {{ counter }}
  </div>

  <script>
    const Counter = {
      data() {
        return {
          counter: 0
        }
      },
      mounted() {
        setInterval(() => {
          this.counter++
        }, 1000)
      }
    }

    Vue.createApp(Counter).mount('#counter')
  </script>

也可以處理 element attributes

<div id="bind-attribute">
  <span v-bind:title="message">
    Hover your mouse over me for a few seconds to see my dynamically bound
    title!
  </span>
</div>

<script type="text/javascript">
const AttributeBinding = {
  data() {
    return {
      message: 'You loaded this page on ' + new Date().toLocaleString()
    }
  }
}

Vue.createApp(AttributeBinding).mount('#bind-attribute')
</script>

UserInput

<div id="event-handling">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">Reverse Message</button>
</div>

<script type="text/javascript">
const EventHandling = {
  data() {
    return {
      message: 'Hello Vue.js!'
    }
  },
  methods: {
    reverseMessage() {
      this.message = this.message
        .split('')
        .reverse()
        .join('')
    }
  }
}

Vue.createApp(EventHandling).mount('#event-handling')
</script>
<div id="two-way-binding">
  <p>{{ message }}</p>
  <input v-model="message" />
</div>

<script type="text/javascript">
const TwoWayBinding = {
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}

Vue.createApp(TwoWayBinding).mount('#two-way-binding')
</script>

Conditionals

<!---- conditional ------>

<div id="conditional-rendering">
  <span v-if="seen">Now you see me</span>
</div>

<script type="text/javascript">
const ConditionalRendering = {
  data() {
    return {
      seen: true
    }
  }
}

Vue.createApp(ConditionalRendering).mount('#conditional-rendering')
</script>

<!---- List ------>
<div id="list-rendering">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>

<script type="text/javascript">
const ListRendering = {
  data() {
    return {
      todos: [
        { text: 'Learn JavaScript' },
        { text: 'Learn Vue' },
        { text: 'Build something awesome' }
      ]
    }
  }
}

Vue.createApp(ListRendering).mount('#list-rendering')
</script>

ComponentSystem

component 就是有 pre-defined option 的 instance

產生 component object 後,定義在 components option 裡面

將 component 定義為,可以接受 prop

用 v-bind 將 todo 傳入 repeated component

<!---- component ------>

<div id="todo-list-app">
  <ol>
    <!--
      Now we provide each todo-item with the todo object
      it's representing, so that its content can be dynamic.
      We also need to provide each component with a "key",
      which will be explained later.
    -->
    <todo-item
      v-for="item in groceryList"
      v-bind:todo="item"
      v-bind:key="item.id"
    ></todo-item>
  </ol>
</div>

<script type="text/javascript">
const TodoList = {
  data() {
    return {
      groceryList: [
        { id: 0, text: 'Vegetables' },
        { id: 1, text: 'Cheese' },
        { id: 2, text: 'Whatever else humans are supposed to eat' }
      ]
    }
  }
}

const app = Vue.createApp(TodoList)

app.component('todo-item', {
  props: ['todo'],
  template: `<li>{{ todo.text }}</li>`
})

app.mount('#todo-list-app')
</script>

Component 類似 W3C Web Components Spec 裡面的 Custom Elements


Application & Component Instances

產生 Application Instance

每一個 Vue Application 都必須要從 application instance 開始,要用 createApp function

const app = Vue.createApp({
  /* options */
})

app instance 是用來註冊 "globals",然後就能在 applicaiton 裡面使用 components

const app = Vue.createApp({})
app.component('SearchInput', SearchInputComponent)
app.directive('focus', FocusDirective)
app.use(LocalePlugin)


//也可用 chaining 語法,因為 function 都會回傳 app instance
Vue.createApp({})
  .component('SearchInput', SearchInputComponent)
  .directive('focus', FocusDirective)
  .use(LocalePlugin)

Root Component

傳給 createApp 的 options 用來設定 root component,該 component 用在 mount application 的 rendering 的起始點。

application 必須被 mounted 到一個 DOM element。ex: <div id="app"></div> 要傳 #app

const RootComponent = {
  /* options */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')

mount 不會回傳 application,而是回傳 root component instance,通常會命名為 vm (view model)。

真實的 application 會是 nested, reusable component

Root Component
└─ TodoList
   ├─ TodoItem
   │  ├─ DeleteTodoButton
   │  └─ EditTodoButton
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

每個 component 都有自己的 component instance,所有 component instance 共享一個 application instance

Component Instance Properties

data() 裡面定義的 properties 會透過 component instance 外顯

const app = Vue.createApp({
  data() {
    return { count: 4 }
  }
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

有多個 component options,可被 component 的 template 使用

methods, props, computed, inject, setup

Vue 有提供以 $ 開頭的 built-in properties $attrs$emit ,避免跟 user-defined property name 重複

Lifecycle Hooks

每個 component instance 會經過 setup data observation, compile the template, mount to the DOM, update DOM when data changes 的處理過程。也會執行 lifecycle hooks functions,可增加自訂的處理流程。

// created 是 instance 被產生時會被呼叫
Vue.createApp({
  data() {
    return { count: 1 }
  },
  created() {
    // `this` points to the vm instance
    console.log('count is: ' + this.count) // => "count is: 1"
  }
})

created, mounted, updated, unmounted

該 function 都可使用 this,代表 current active component instance

不要在 option peoperty 使用 ES6 的 arrow function,因為 arrow function 缺少了 this

Lifecycle Diagram


Template Syntax

使用 HTML-based template syntax,可用 html browser 直接瀏覽

Vue 會編譯 template 為 Virtual DOM render functions,加上 reactivity system。Vue 可自行判斷 minimal number of components to re-render,及 minimal amount of DOM manipulation

Interpolations

  • Text

最基本是用雙括號 mustache syntax,該標籤會自動被 msg property 的值取代

<span>Message: {{ msg }}</span>

也可以用 one-time interpolation

<span v-once>This will never change: {{ msg }}</span>
  • raw html

double mustaches 將 data 以純文字的方式處理,可用 v-html 將 data 以 html 處理

<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

但動態 render html 可能會遇到 XSS 問題

  • Attributes

mustanches 不能用在 html attributes 裡面,要用 v-bind

<div v-bind:id="dynamicId"></div>

如果 bound value 為 null 或 undefined,則該 attribute 不會 included 到 rendered element 裡面

當 isButtonDisabled 確實有值的時候,就會出現 disabled attribute

<button v-bind:disabled="isButtonDisabled">Button</button>
  • Using JS Expressions

可在 data binding 使用 js 語法,但限制只能有單一 expression

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>


<!-- this is a statement, not an expression: -->
{{ var a = 1 }}
<!-- flow control won't work either, use ternary expressions -->
{{ if (ok) { return message } }}

Directives

directives 是以 v- 開頭的特殊 attributes,除了 v-for v-on 以外,都是一個單一 js expression。directive 可 reactively 在該 expression 改變時,對 DOM 套用變化。

ex: v-if 可動態根據 seen 決定要 remove/insert <p>

<p v-if="seen">Now you see me</p>
  • Arguments

directive 可以在 name 後面加上 : 參數

ex: href 是 element 的 href attribute

<a v-bind:href="url"> ... </a>

ex: listens to DOM event

<a v-on:click="doSomething"> ... </a>
  • Dynamic Arguments

可在 directive argument 使用 js expression,但要用 square bracket []

ex: [attributeName] 會用 js expression 運算一次,如果 component instance 有 data property: attributeName,值為 href,會讓以下程式跟 v-bind:href 一樣

<a v-bind:[attributeName]="url"> ... </a>

ex: 如果有 eventName 的值為 focusv-on:[eventName] 就等同 v-on:focus

<a v-on:[eventName]="doSomething"> ... </a>
  • Modifiers

modifier 是 dot . 後面的 postfix,代表 directive 應該 bound in some special way

ex: .prevent modifier 讓 v-on directive 在 triggered event 呼叫 event.preventDefault()

<form v-on:submit.prevent="onSubmit">...</form>

Shorthands

v- prefix 用來識別 vue 專用的 attributes,當使用 vue 開發 single-page application 時,v- 變得不重要了,因此 vue 提供特殊的縮寫

  • v-bind shorthand
<!-- full syntax -->
<a v-bind:href="url"> ... </a>

<!-- shorthand -->
<a :href="url"> ... </a>

<!-- shorthand with dynamic argument -->
<a :[key]="url"> ... </a>
  • v-on shorthand
<!-- full syntax -->
<a v-on:click="doSomething"> ... </a>

<!-- shorthand -->
<a @click="doSomething"> ... </a>

<!-- shorthand with dynamic argument -->
<a @[event]="doSomething"> ... </a>
  • caveats

dynamic argument expression 有語法限制,因為某些特殊的字元 ex: spaces, quotes,不能用在 html attribute names

ex:

<!-- This will trigger a compiler warning. -->
<a v-bind:['foo' + bar]="value"> ... </a>

建議在這種狀況,改用 computed property

在使用 in-DOM template 時,要避免命名為大寫字元,因為 browser 會自動把 attribute name 轉成小寫字元

<!--
This will be converted to v-bind:[someattr] in in-DOM templates.
Unless you have a "someattr" property in your instance, your code won't work.
-->
<a v-bind:[someAttr]="value"> ... </a>

JS expression 只能使用 globalsWhitelist.ts 列出的 global function,無法使用 user 自訂的 global fuctions

import { makeMap } from './makeMap'

const GLOBALS_WHITE_LISTED =
  'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
  'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
  'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'

export const isGloballyWhitelisted = /*#__PURE__*/ makeMap(GLOBALS_WHITE_LISTED)

Data Properties and Methods

Data Properties

component 的 data option 是 function,Vue 會在產生 new component instance 時,呼叫這個 function。該 function 會回傳一個 物件,然後 vue 會儲存到 $data

const app = Vue.createApp({
  data() {
    return { count: 4 }
  }
})

const vm = app.mount('#app')

console.log(vm.$data.count) // => 4
console.log(vm.count)       // => 4

// Assigning a value to vm.count will also update $data.count
vm.count = 5
console.log(vm.$data.count) // => 5

// ... and vice-versa
vm.$data.count = 6
console.log(vm.count) // => 6

這些 instance properties 只會在產生 instance 時,同時被加入。因此要確認所有資料都有在 data function 裡面回傳的物件中。必要時,要用 null, undefined 或其他值,先定義 property

後續還是可以增加 component instance 的 property,不放在 data 裡面,但無法自動被 vue reactivity system 處理

vue 使用 $ prefix 為 built-in API 的 prefix,並使用 _ prefix 為 internal properties,要避免在 data 裡面使用這兩個字元

Methods

使用 methods option 增加 component instance 的 methods

const app = Vue.createApp({
  data() {
    return { count: 4 }
  },
  methods: {
    increment() {
      // `this` will refer to the component instance
      this.count++
    }
  }
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

vm.increment()

console.log(vm.count) // => 5

vue 會自動 bind this 為 component instance,要避免在 method 使用 arrow function,因為 arrow function 沒有 this

methods 可在 component 的 template 呼叫

ex: 按下按鈕時,呼叫 increment

<button @click="increment">Up vote</button>

可直接在 template 呼叫 method,這樣寫比使用 computed property 好

ex: 可在支援 js expression 的地方,呼叫 method toTitleDate, formateDate

<span :title="toTitleDate(date)">
  {{ formatDate(date) }}
</span>

Debouncing and Throttling

vue 不內建支援 debouncing or throttling,但可使用其他 library ex: Lodash

<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js"></script>
<script>
  Vue.createApp({
    methods: {
      // Debouncing with Lodash
      click: _.debounce(function() {
        // ... respond to click ...
      }, 500)
    }
  }).mount('#app')
</script>

但這樣寫會讓所有 component 共用 debounced function,為了讓 component 各自獨立,可在 created lifecycle hook 加上 debounced function

app.component('save-button', {
  created() {
    // Debouncing with Lodash
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // Cancel the timer when the component is removed
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... respond to click ...
    }
  },
  template: `
    <button @click="debouncedClick">
      Save
    </button>
  `
})

ref: Debounce & Throttle — 那些前端開發應該要知道的小事(一)

References

Vue Guide

重新認識 Vue.js

沒有留言:

張貼留言