2022/03/28

Vue Router

以下為 Vue Router 的一個例子,Vue Router 的用途,是能夠在單一網頁頁面中,在 browser 不重新載入到新的 url 的狀況下,能夠增加 url history 並調整頁面內容狀態的功能,也就是能夠實現 SPA (single page application) 的功能。

傳統的網頁,會向 web application server 要求打開一個 url 網址,server 會回傳整個網頁的 html 內容。後來為了在不轉向到新的 url 的條件下,並調整網頁的內容,就發生了 web service,server 會從某個網址回傳 XML 或 JSON,網頁透過 AJAX 方式提取資料更新網頁內容。SPA 是更進一步,可在不重新發出新的 url request 到 server 的條件下,更新網頁內容並增加 url 瀏覽歷程,也就是增加了單一網頁頁面的顯示狀態。

Vue router 實例:

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- <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 src="https://unpkg.com/vue-router@4.0.11"></script> -->
  <!-- <script src="https://unpkg.com/vue-router@4.0.11/dist/vue.global.js"></script> -->
  <script src="https://unpkg.com/vue-router@4.0.11/dist/vue-router.global.prod.js"></script>
<!--
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> -->

</head>

<body>

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- use the router-link component for navigation. -->
    <!-- specify the link by passing the `to` prop. -->
    <!-- `<router-link>` will render an `<a>` tag with the correct `href` attribute -->
    <router-link to="/">Go to Home</router-link>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- route outlet -->
  <!-- component matched by the route will render here -->
  <router-view></router-view>
</div>

<script>
// 1. Define route components.
// These can be imported from other files
const Home = { template: '<div>Home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. Define some routes
// Each route should map to a component.
// We'll talk about nested routes later.
const routes = [
  { path: '/', component: Home },
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. Create the router instance and pass the `routes` option
// You can pass in additional options here, but let's
// keep it simple for now.
const router = VueRouter.createRouter({
  // 4. Provide the history implementation to use. We are using the hash history for simplicity here.
  history: VueRouter.createWebHashHistory(),
  routes, // short for `routes: routes
})

// 5. Create and mount the root instance.
const app = Vue.createApp({})
// Make sure to _use_ the router instance to make the
// whole app router-aware.
app.use(router)

app.mount('#app')

// Now the app has started!
</script>
</body>

</html>

以下是點擊了 foo 以後,網頁的 DOM 產生的資料。 foo 的部分,會自動加上這兩個 css class class ="router-link-exact-active router-link-active"

<div id="app" data-v-app="">
   <h1>Hello App!</h1>
   <p>
     <a href="#/" class="">Go to Home</a>
     <a href="#/foo" class="router-link-active router-link-exact-active" aria-current="page">Go to Foo</a>
     <a href="#/bar" class="">Go to Bar</a>
   </p>
   <div>foo</div>
</div>

<router-link> 相關屬性

to

<!-- 直接填寫文字字串 -->
<router-link to="/home">Home</router-link>
<!-- 結果 -->
<a href="/home">Home</a>


<!-- 使用 v-bind,省略 path -->
<router-link v-bind:to="'/home'">Home</router-link>

<!-- 不寫 v-bind 也可以,就像綁定別的屬性一樣 -->
<router-link :to="'/home'">Home</router-link>

<!-- 同上 -->
<router-link :to="{ path: '/home' }">Home</router-link>

<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

<!-- 帶查詢參數,下面的結果為 /register?plan=private -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">Register</router-link>

replace

如果不希望在 browser 留下 url history,可加上 replace

<router-link to="/home" replace>Home</router-link>

當點擊時,會呼叫 router.replace() 而不是 router.push()

active-class

設定當 link 啟用時,DOM 節點使用的 css class

<style>
   ._active{
      background-color : red;
   }
</style>
<p>
   <router-link v-bind:to = "{ path: '/route1'}" active-class = "_active">Router Link 1</router-link>
   <router-link v-bind:to = "{ path: '/route2'}">Router Link 2</router-link>
</p>

aria-current-value

預設為 "page",可能的值

'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' (string)

當 link 為 active 時,傳送給 aria-current 屬性的值

custom

預設為 "false"

決定 <router-link> 是不是"不要"產生到 <a> 裡面。如果使用 v-slot 產生 custom router link,預設要包裝在 <a> 裡面,如果增加 custom 屬性,就取消這個限制

<router-link to="/home" custom v-slot="{ navigate, href, route }">
  <a :href="href" @click="navigate">{{ route.fullPath }}</a>
</router-link>

會 render 為

<a href="/home">/home</a>
<router-link to="/home" v-slot="{ route }">
  <span>{{ route.fullPath }}</span>
</router-link>

會 render 為

<a href="/home"><span>/home</span></a>

exact-active-class

當 link 被精確匹配時,要啟用的 css class

<router-link> 的 v-slot

可產生自訂的 html tag,記得一定要加上 custom

ex:

<router-link
  to="/foo"
  custom
  v-slot="{ href, route, navigate, isActive, isExactActive }"
>
  <NavLink :active="isActive" :href="href" @click="navigate">
    {{ route.fullPath }}
  </NavLink>
</router-link>
  • href: resolved url,類似 a tag 的 href
  • route: resolved normalized location
  • navigate: 驅動 navigation 的 function,必要時,會自動 prevent events
  • isActive: 如果被 apply active class ,就會是 true
  • isExactActive: 如果被 apply exact active class,就會是 true

會 render 為

<navlink active="true" href="#/foo">/foo</navlink>

ex:

    <ul>
      <router-link
        to="/foo"
        custom
        v-slot="{ href, route, navigate, isActive, isExactActive }"
      >
        <li
          :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']"
        >
          <a :href="href" @click="navigate">{{ route.fullPath }}</a>
        </li>
      </router-link>
    </ul>

會 render 為

<ul>
  <li class="router-link-active router-link-exact-active"><a href="#/foo">/foo</a></li>
</ul>

Dynamic Route Matching

Vue 需要將某個 pattern 對應到一個 component 的方法,例如不同 userid 的 user 資料

$route.params 可用來協助對應 route 的 pattern 上的參數

const User = {
  template: '<div>User {{ $route.params.id }}</div>',
}

// these are passed to `createRouter`
const routes = [
  // dynamic segments start with a colon
  { path: '/users/:id', component: User },
]
pattern matching $route.params
/users/:id /users/john { id: 'john' }
/users/:id/posts/:postid /users/john/posts/123 { id: 'john', posted: '123' }

Reacting to Params Changes

因為 /users/john/users/mary 這兩個路徑會使用同一個 component,比較有效率的方法,是不重建 component,直接更新內容,但這樣也不會呼叫到 component 的 lifecycle hooks

const User = {
  template: '<div>User {{ $route.params.id }}</div>',
  created() {
    this.$watch(
      () => this.$route.params,
      (toParams, previousParams) => {
        // react to route changes...
        console.log("created toParams=", toParams, ", previousParams=",previousParams);
      }
    )
  },
  // navigation guard,可在這邊檢查並取消 navigation
  async beforeRouteUpdate(to, from) {
    // react to route changes...
    // this.userData = await fetchUser(to.params.id)
    console.log("beforeRouteUpdate to=", to, ", from=",from);
    this.userData = to.params.id
  },
}

Catch /404 Not Found Route

const routes = [
  // will match everything and put it under `$route.params.pathMatch`
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  // will match anything starting with `/user-` and put it under `$route.params.id`
  { path: '/user-:id(.*)', component: User },
]

如果直接在 brwoser 網址連結到

1-2-param.html#/not

就會是用 NotFound 這個 Component 處理

如果直接在 brwoser 網址連結到

1-2-param.html#/user-test/12345

會收到 $route.params.id 的值為 test/12345

Routes' Matching Syntax

大部分的 application 使用 static route 以及類似 /users/:userId 這樣的 route

custom regex

如果要在 url 分辨 orderId 及 productName,最簡單的方法就是加上 /o /p 靜態的部分用來區別

const routes = [
  // matches /o/3549
  { path: '/o/:orderId' },
  // matches /p/books
  { path: '/p/:productName' },
]

如果 orderId 跟 productName 可用數字/字串區分,可將 route 改為

const routes = [
  // /:orderId -> matches only numbers
  { path: '/:orderId(\\d+)' },
  // /:productName -> matches anything else
  { path: '/:productName' },
]

repeatable params

如果是 /first/second/third 這樣的 route

* 為 0 or more, + 為 1 or more

const routes = [
  // /:chapters -> matches /one, /one/two, /one/two/three, etc
  { path: '/:chapters+' },
  // /:chapters -> matches /, /one, /one/two, /one/two/three, etc
  { path: '/:chapters*' },
]

可用以下方式,將 array 參數轉換為 path

// given { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// produces /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// produces /a/b

// given { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// throws an Error because `chapters` is empty
const routes = [
  // only match numbers
  // matches /1, /1/2, etc
  { path: '/:chapters(\\d+)+' },
  // matches /, /1, /1/2, etc
  { path: '/:chapters(\\d+)*' },
]

optional param

? 代表 0 or 1

const routes = [
  // will match /users and /users/posva
  { path: '/users/:userId?' },
  // will match /users and /users/42
  { path: '/users/:userId(\\d+)?' },
]

Nested Routes

在 application 裡面常見到 nested components

ex:

/user/johnny/profile                  /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+
<div id="app">
 <h1>Nested Views</h1>
  <p>
    <router-link to="/users/eduardo">/users/eduardo</router-link>
    <br />
    <router-link to="/users/eduardo/profile"
      >/users/eduardo/profile</router-link
    >
    <br />
    <router-link to="/users/eduardo/posts">/users/eduardo/posts</router-link>
  </p>
  <router-view></router-view>
</div>

<script>

const User = {
  template: '<h2>User {{ $route.params.username }}</h2><router-view></router-view>',
}
const UserHome = {
  template: '<div>Home</div>',
}
const UserProfile = {
  template: '<div>UserProfile</div>',
}
const UserPosts = {
  template: '<div>UserPosts</div>',
}

const routes = [
  {
    path: '/users/:username',
    component: User,
    children: [
      // UserHome will be rendered inside User's <router-view>
      // when /users/:username is matched
      { path: '', component: UserHome },

      // UserProfile will be rendered inside User's <router-view>
      // when /users/:username/profile is matched
      { path: 'profile', component: UserProfile },

      // UserPosts will be rendered inside User's <router-view>
      // when /users/:username/posts is matched
      { path: 'posts', component: UserPosts },
    ],
  }
]

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
})

const app = Vue.createApp({})
app.use(router)

app.mount('#app')

</script>

Programmatic Navigation

除了用 <router-link> 產生 url anchor tags 以外,也可以用程式處理

在 vue application 中,可用 $router 為 route instance,故要呼叫 this.$router.push

<router-link :to="..."> 就等同於 router.push(...)

// literal string path
router.push('/users/eduardo')

// object with path
router.push({ path: '/users/eduardo' })

// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 如果有 path,就會忽略 params
// with query, resulting in /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// with hash, resulting in /about#team
router.push({ path: '/about', hash: '#team' })

可用 uername 變數

const username = 'eduardo'
// we can manually build the url but we will have to handle the encoding ourselves
router.push(`/user/${username}`) // -> /user/eduardo
// same as
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// if possible use `name` and `params` to benefit from automatic URL encoding
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` cannot be used alongside `path`
router.push({ path: '/user', params: { username } }) // -> /user

replace current location

切換網址,但不會 push 到 history

<router-link :to="..." replace> 等同 router.replace(...)

router.push({ path: '/home', replace: true })
// equivalent to
router.replace({ path: '/home' })

history

// go forward by one record, the same as router.forward()
router.go(1)

// go back by one record, the same as router.back()
router.go(-1)

// go forward by 3 records
router.go(3)

// fails silently if there aren't that many records
router.go(-100)
router.go(100)

named route

為 route 命名,優點:

  • 沒有 hardcoded url
  • 可自動 encode/decode params
  • 填寫 url 不會發生 typo
  • bypass path ranking
const routes = [
  {
    path: '/user/:username',
    name: 'user',
    component: User
  }
]

用以下方式使用 named route

<router-link :to="{ name: 'user', params: { username: 'erina' }}">
  User
</router-link>

router.push({ name: 'user', params: { username: 'erina' } })

named views

有時候,需要一次顯示多個 views,但不是 nested view

/settings/emails                                       /settings/profile
+-----------------------------------+                  +------------------------------+
| UserSettings                      |                  | UserSettings                 |
| +-----+-------------------------+ |                  | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | |  +------------>  | | Nav | UserProfile        | |
| |     +-------------------------+ |                  | |     +--------------------+ |
| |     |                         | |                  | |     | UserProfilePreview | |
| +-----+-------------------------+ |                  | +-----+--------------------+ |
+-----------------------------------+                  +------------------------------+
<div id="app">
  <h1>Nested Named Views</h1>
  <router-view></router-view>
</div>

<script>

const About = {
  template: '<h1>About</h1>',
}
const Home = {
  template: '<div>Home</div>',
}
const UserEmailsSubscriptions = {
  template: '<div><h3>Email Subscriptions</h3></div>',
}
const UserProfile = {
  template: '<div><h3>UserProfile</h3></div>',
}
const UserProfilePreview = {
  template: '<div><h3>UserProfilePreview</h3></div>',
}


const UserSettingsNavTemplate = `
  <div class="us__nav">
    <router-link to="/settings/emails">emails</router-link>
    <br />
    <router-link to="/settings/profile">profile</router-link>
  </div>
  `
// const UserSettingsNav = {
//   template: UserSettingsNavTemplate
// }

const UserSettingsTemplate = `
  <div class="us">
    <h2>User Settings</h2>
    <user-settings-nav />
    <router-view class="us__content" />
    <router-view name="helper" class="us__content us__content--helper" />
  </div>
  `
const UserSettings = {
  template: UserSettingsTemplate
}

const routes = [
    {
      path: '/settings',
      // You could also have named views at tho top
      component: UserSettings,
      children: [
        {
          path: 'emails',
          component: UserEmailsSubscriptions,
        },
        {
          path: 'profile',
          components: {
            default: UserProfile,
            helper: UserProfilePreview,
          },
        },
      ],
    },
  ]

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
})

const app = Vue.createApp({})
app.use(router)

app.component('user-settings-nav', {
  template: UserSettingsNavTemplate
})

app.mount('#app')

</script>

Redirect and Alias

直接在 routes 做 redirect

const routes = [{ path: '/home', redirect: '/' }]

const routes = [{ path: '/home', redirect: { name: 'homepage' } }]

用 redirect function 做 dynamic redirect

const routes = [
  {
    // /search/screens -> /search?q=screens
    path: '/search/:searchText',
    redirect: to => {
      // the function receives the target route as the argument
      // we return a redirect path/location here.
      return { path: '/search', query: { q: to.params.searchText } }
    },
  },
  {
    path: '/search',
    // ...
  },
]

可 redirect 到相對路徑

const routes = [
  {
    // will always redirect /users/123/posts to /users/123/profile
    path: '/users/:id/posts',
    redirect: to => {
      // the function receives the target route as the argument
      // a relative location doesn't start with `/`
      // or { path: 'profile'}
      return 'profile'
    },
  },
]

alias

/ alias 為 /home ,代表瀏覽 /home/ 都是一樣的 component

const routes = [{ path: '/', component: Homepage, alias: '/home' }]

在 nested view 也可以用

const routes = [
  {
    path: '/users',
    component: UsersLayout,
    children: [
      // this will render the UserList for these 3 URLs
      // - /users
      // - /users/list
      // - /people
      { path: '', component: UserList, alias: ['/people', 'list'] },
    ],
  },
]

如果有參數

const routes = [
  {
    path: '/users/:id',
    component: UsersByIdLayout,
    children: [
      // this will render the UserDetails for these 3 URLs
      // - /users/24
      // - /users/24/profile
      // - /24
      { path: 'profile', component: UserDetails, alias: ['/:id', ''] },
    ],
  },
]

Passing Props to Route Components

在 component 使用 $route 時,會跟 route 綁定在一起。可利用 propsoption

可將以下語法

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]

替換為

const User = {
  // make sure to add a prop named exactly like the route param
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]

Boolean mode

props 設定為 true,就表示 route.params 會指定為 component props

Named views

可為每一個 named view 定義 props

const routes = [
  {
    path: '/user/:id',
    components: { default: User, sidebar: Sidebar },
    props: { default: true, sidebar: false }
  }
]

Object mode

const routes = [
  {
    path: '/promotion/from-newsletter',
    component: Promotion,
    props: { newsletterPopup: false }
  }
]

Function mode

const routes = [
  {
    path: '/search',
    component: SearchUser,
    props: route => ({ query: route.query.q })
  }
]

URL /search?q=vue 會將 {query: 'vue'} 以 props 傳給 SearchUser component


History Mode

Hash Mode

在網址後面加上 #,不利於 search engine

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})

HTML5 Mode

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
})

在 url 會看到 https://example.com/user/id ,但實際上如果直接在 browser 對 server 發送 https://example.com/user/id 這個 request,會出現 404 Error

解決方式就是單純地將該網址用 index.html 服務

References

Vue router v4.x

Vue router v4.x API Reference

Vue.js 路由

Vue router 與前端路由管理

2022/03/21

Vue.js Essentials 3

Event Handling

Listening to Evnets

可使用 v-on directive (通常縮寫為 @)監聽 DOM events,並執行某些 js script

v-on:click="methodName"@click="methodName" 都可以

<div id="basic-event">
  <button @click="counter += 1">Add 1</button>
  <p>The button above has been clicked {{ counter }} times.</p>
</div>

<script>
Vue.createApp({
  data() {
    return {
      counter: 0
    }
  }
}).mount('#basic-event')
</script>

Method Event Handlers

如果 js logic 比較複雜,v-on 可改用 method 而不是 attribute

<div id="event-with-method">
  <!-- `greet` is the name of a method defined below -->
  <button @click="greet">Greet</button>
</div>

<script>
Vue.createApp({
  data() {
    return {
      name: 'Vue.js'
    }
  },
  methods: {
    greet(event) {
      // `this` inside methods points to the current active instance
      alert('Hello ' + this.name + '!')
      // `event` is the native DOM event
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
}).mount('#event-with-method')
</script>

Methods in Inline Handlers

可在 inline js statement 使用 method

<div id="inline-handler">
  <button @click="say('hi')">Say hi</button>
  <button @click="say('what')">Say what</button>
</div>

<script>
Vue.createApp({
  methods: {
    say(message) {
      alert(message)
    }
  }
}).mount('#inline-handler')
</script>

如果需要使用原本的 DOM event,可用 $event 傳入

<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<script>
Vue.createApp({
  methods: {
    warn(message, event) {
      // now we have access to the native event
      if (event) {
        event.preventDefault()
      }
      alert(message)
    }
  }
}).mount('#inline-handler')
</script>

Multiple Event Handlers

可用 , 區隔,同時使用多個 methods

<!-- both one() and two() will execute on button click -->
<button @click="one($event), two($event)">
  Submit
</button>

<script>
Vue.createApp({
  methods: {
    one(event) {
      // first handler logic...
    },
    two(event) {
      // second handler logic...
    }
  }
}).mount('#inline-handler')
</script>

Event Modifiers

在 event handler 裡面呼叫 event.preventDefault()event.stopPropagation() 很常見。vue 提供 v-on 使用的 event modifier

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive
<!-- the click event's propagation will be stopped -->
<a @click.stop="doThis"></a>

<!-- the submit event will no longer reload the page -->
<form @submit.prevent="onSubmit"></form>

<!-- modifiers can be chained -->
<a @click.stop.prevent="doThat"></a>

<!-- just the modifier -->
<form @submit.prevent></form>

<!-- use capture mode when adding the event listener -->
<!-- i.e. an event targeting an inner element is handled here before being handled by that element -->
<div @click.capture="doThis">...</div>

<!-- only trigger handler if event.target is the element itself -->
<!-- i.e. not from a child element -->
<div @click.self="doThat">...</div>

注意:順序很重要

@click.prevent.self 會停止所有 clicks

@click.self.prevent 只會停止 clicks on the element itself

<!-- the click event will be triggered at most once -->
<a @click.once="doThis"></a>

.once 可用在 component events,跟其他 modifier 不同。

vue 提供 .passive modifier,跟 addEventListenerpassive option 一樣

<!-- the scroll event's default behavior (scrolling) will happen -->
<!-- immediately, instead of waiting for `onScroll` to complete  -->
<!-- in case it contains `event.preventDefault()`                -->
<div @scroll.passive="onScroll">...</div>

.passive 在改善 mobile device 方面特別有用

注意:.passive.prevent 不能一起使用,因為 .prevent 會被忽略,造成 browser 發生 warning。 passive 會跟 browser 互動,不需要 prevent the event's default behavior

Key Modifiers

在監聽 keyboard events 時,常需要檢查 keys,vue 提供 v-on@ 使用 key modifier

<!-- only call `vm.submit()` when the `key` is `Enter` -->
<input @keyup.enter="submit" />

可使用 KeyboardEvent.key 定義的 key names,要轉為 kebab-case

<!-- 當 $event.key 為 PageDown 時,才會呼叫該 handler -->
<input @keyup.page-down="onPageDown" />

以下為常用的 key 的 aliases

  • .enter
  • .tab
  • .delete (captures both "Delete" and "Backspace" keys)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

System Modifier Keys

可限制某個 modifier key 被按下時,才會驅動 mouse/keyboard event listener

  • .ctrl
  • .alt
  • .shift
  • .meta 在 Mac 是 ⌘,在 Windows 是 ⊞,在 Sun Microsystems keyboard 是 ◆,在特殊的 MIT and Lisp machine keyboard (ex: Knight kryboard, space-cadetkeyboard) 是 META,在 Symbolics keyboards 是 META 或 Meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

modifier keys 通常是用 keyup event,但 keyup.ctrl 只會在按下 ctrl 時被 trigger,不會在 release ctrl 時被 trigger

.exact modifier

.exact 可控制 system modifier 的 exact combination

<!-- this will fire even if Alt or Shift is also pressed -->
<button @click.ctrl="onClick">A</button>

<!-- this will only fire when Ctrl and no other keys are pressed -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- this will only fire when no system modifiers are pressed -->
<button @click.exact="onClick">A</button>

mouse button modifier

  • .left
  • .right
  • .middle

Why Listeners in HTML?

vue 的 event modifier 策略違反了傳統的 "separation of concerns" 規則,因為 handler function 與 expression 跟 ViewModel 綁定,這樣做的優點:

  1. 移除 HTML template 時,就移除了 handler function,很容易維護
  2. 因不需要在 js 手動綁定 event listener,ViewModel 的 code 可單純只有 logic 且為 DOM-free,很容易測試
  3. 當 ViewModel 被刪除時,所有 event listener 也自動被移除,不需要手動移除

Form Input Bindings

Basic Usage

可使用 v-model 在 form input, textarea, select 產生 two-way data binding,根據 input type 自動更新 element。v-model 是根據 user input event 更新 data + 特殊 edge case 的 syntax sugar

注意:v-model 會忽略 fome element 裡面初始的 value, checked, selected attributes,會使用 current active instance data 作為 source of truth,因此要在 js 的 data option 裡面宣告初始值。

v-model 會因為不同的 input element 使用不同的 properties,產生不同 events

  1. text, textarea 使用 value property 及 input event
  2. checkboxes, radiobuttons 使用 checked property 及 change event
  3. select 使用 value 為 prop 及 change event

注意:v-model 在 IME (Chinese, Japanese, Korean..) 語系中,在 IME composition 時,並不會讓 v-model 更新,如果需要處理輸入法的異動更新,要改用 input event listener 及 value binding

  • Text
<input v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>

<script>
Vue.createApp({
  data() {
    return {
      message: ''
    }
  }
}).mount('#v-model-basic')
</script>
  • Multiline Text
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br />
<textarea v-model="message" placeholder="add multiple lines"></textarea>

<script>
Vue.createApp({
  data() {
    return {
      message: ''
    }
  }
}).mount('#v-model-textarea')
</script>

textarea 不能用 interpolation

<!-- bad -->
<textarea>{{ text }}</textarea>

<!-- good -->
<textarea v-model="text"></textarea>
  • Checkbox
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

<script>
Vue.createApp({
  data() {
    return {
      checked: false
    }
  }
}).mount('#v-model-checkbox')
</script>
<div id="v-model-multiple-checkboxes">
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames" />
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
  <label for="mike">Mike</label>
  <br />
  <span>Checked names: {{ checkedNames }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      checkedNames: []
    }
  }
}).mount('#v-model-multiple-checkboxes')
</script>
  • Radio
<div id="v-model-radiobutton">
  <input type="radio" id="one" value="One" v-model="picked" />
  <label for="one">One</label>
  <br />
  <input type="radio" id="two" value="Two" v-model="picked" />
  <label for="two">Two</label>
  <br />
  <span>Picked: {{ picked }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      picked: ''
    }
  }
}).mount('#v-model-radiobutton')
</script>
  • Select
<div id="v-model-select" class="demo">
  <select v-model="selected">
    <option disabled value="">Please select one</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      selected: ''
    }
  }
}).mount('#v-model-select')
</script>

如果 v-model expression 的初始值跟 option 不吻合,<select> element 會以 "unselected" state 被 rendered。在 iOS 會造成 user 無法選擇第一個 element,因為 iOS 不會 fire a change event。

建議用 empty value 增加一個 disabled option,類似上面提供的例子一樣


multiple select

<select v-model="selected" multiple>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<br />
<span>Selected: {{ selected }}</span>

<script>
Vue.createApp({
  data() {
    return {
      selected: ''
    }
  }
}).mount('#v-model-select')
</script>

v-for 實作dynamic options

<div id="v-model-select-dynamic" class="demo">
  <select v-model="selected">
    <option v-for="option in options" :value="option.value">
      {{ option.text }}
    </option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>

<script>
Vue.createApp({
  data() {
    return {
      selected: 'A',
      options: [
        { text: 'One', value: 'A' },
        { text: 'Two', value: 'B' },
        { text: 'Three', value: 'C' }
      ]
    }
  }
}).mount('#v-model-select-dynamic')
</script>

Value Bindings

radio, checkbox, select option 中,v-model binding values 通常是 static string (checkbox 是 booleans)

<!-- `picked` is a string "a" when checked -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` is either true or false -->
<input type="checkbox" v-model="toggle" />

<!-- `selected` is a string "abc" when the first option is selected -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

如果要 dynamic property,可使用 v-bind

  • Checkbox

true-value, false-value 不會影響 input 的 value attribute

<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
// when checked:
vm.toggle === 'yes'
// when unchecked:
vm.toggle === 'no'
  • Radio
<input type="radio" v-model="pick" v-bind:value="a" />
// when checked:
vm.pick === vm.a
  • Select Options
<select v-model="selected">
  <!-- inline object literal -->
  <option :value="{ number: 123 }">123</option>
</select>
// when selected:
typeof vm.selected // => 'object'
vm.selected.number // => 123

Modifiers

  • .lazy

v-model 預設會在每一次 input event 發生時,同步 input data,可增加 lazy modifier,修改為 change event 後同步資料

<!-- synced after "change" instead of "input" -->
<input v-model.lazy="msg" />
  • .number

如果想讓 user input 自動 typecast 為 number,可用 .number

<input v-model.number="age" type="number" />

如果 input value 無法被 parseFloat() parsing 時,會回傳原始 value

  • .trim

自動 trim space

<input v-model.trim="msg" />

v-model with Components

vue component 可產生 reusable inputs with customized behavior


Components Basics

以下為 vue component 的 example,通常在 vue application,會使用 Single File Component,而不是 string template。

Component 是 reusable instances with a name

<div id="components-demo">
  <button-counter></button-counter>
</div>

<script>
// Create a Vue application
const app = Vue.createApp({})

// Define a new global component called button-counter
app.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button v-on:click="count++">
      You clicked me {{ count }} times.
    </button>`
})

app.mount('#components-demo')
</script>

component 就是 reusable instance,故能夠使用data, computed, watch, methods, and lifecycle hooks

Reusing Components

每一個 component 都有各自的 instance,獨立的 count

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

Organizing Components

在 app 裡面會使用 tree of nested components

例如會有 components for header, sidebar, content area

為了在 templates 裡面使用 component,必須要先向 Vue 註冊,有兩種註冊類型:global 與 local。

component method 是 global component

const app = Vue.createApp({})

// global component
app.component('my-component-name', {
  // ... options ...
})

Passing Data to Child Components with Props

props 是可以跟 component 註冊的 custom attribute

例如 blog post component,可用 props 提供 component 接受的 list of props

'title'變成該 component 的 property,然後就能在 template 裡面使用

<div id="blog-post-demo" class="demo">
  <blog-post title="My journey with Vue"></blog-post>
  <blog-post title="Blogging with Vue"></blog-post>
  <blog-post title="Why Vue is so fun"></blog-post>
</div>

<script>
const app = Vue.createApp({})

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-post-demo')
</script>

<div id="blog-posts-demo">
  <blog-post
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
  ></blog-post>
</div>

<script>
const App = {
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

const app = Vue.createApp(App)

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-posts-demo')
</script>

可使用 v-bind 做 dynamic pass props,在一開始不知道有多少 content 資料的時候很有用。

Listening to Child Components Events

ex: 要為 blogpost 增加 accessibility feature,把文字放大

增加 postFontSize data property

<div id="blog-posts-events-demo" class="demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
       v-for="post in posts"
       :key="post.id"
       :title="post.title"
       @enlarge-text="postFontSize += 0.1"
    ></blog-post>
  </div>
</div>

<script>
const app = Vue.createApp({
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue'},
        { id: 2, title: 'Blogging with Vue'},
        { id: 3, title: 'Why Vue is so fun'}
      ],
      postFontSize: 1
    }
  }
})

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button @click="$emit('enlargeText')">
        Enlarge text
      </button>
    </div>
  `
})

app.mount('#blog-posts-events-demo')
</script>

可用 emits option 檢查 all the events that a component emits

app.component('blog-post', {
  props: ['title'],
  emits: ['enlargeText']
})
  • Emitting a value with an Event

可在 $emit 增加第二個參數

<button @click="$emit('enlargeText', 0.1)">
  Enlarge text
</button>
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

或是 event handler 為 method,可在第一個參數傳入 value

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
methods: {
  onEnlargeText(enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}
  • Using v-model on Components

custom events 可產生 custom inputs 跟 v-model 一起使用

<input v-model="searchText" />

跟上面一樣

<input :value="searchText" @input="searchText = $event.target.value" />

如果用在 component,就跟這個一樣

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>

在 component 裡面的 <input> 必須滿足

  1. bind value property 到 modelValue prop
  2. input,新的 value 會產生 update:modelValue event
app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})

現在就可以使用 v-model

<custom-input v-model="searchText"></custom-input>

另一個在 component 實作 v-model 的方法,是用 computed properties 定義 getter, setter,get 要回傳 modelValue property,set 要產生相關 event

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
})

Content Distribution with Slots

可傳送 content 給 component

<div id="slots-demo" class="demo">
  <alert-box>
    Something bad happened.
  </alert-box>
</div>

<script>
const app = Vue.createApp({})

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

app.mount('#slots-demo')
</script>

slot 裡面就放了 Something bad happened.

Dynamic Components

<div id="dynamic-component-demo" class="demo">
  <button
     v-for="tab in tabs"
     v-bind:key="tab"
     v-bind:class="['tab-button', { active: currentTab === tab }]"
     v-on:click="currentTab = tab"
   >
    {{ tab }}
  </button>

  <component v-bind:is="currentTabComponent" class="tab"></component>
</div>

<script>
const app = Vue.createApp({
  data() {
    return {
      currentTab: 'Home',
      tabs: ['Home', 'Posts', 'Archive']
    }
  },
  computed: {
    currentTabComponent() {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  }
})

app.component('tab-home', {
  template: `<div class="demo-tab">Home component</div>`
})
app.component('tab-posts', {
  template: `<div class="demo-tab">Posts component</div>`
})
app.component('tab-archive', {
  template: `<div class="demo-tab">Archive component</div>`
})

app.mount('#dynamic-component-demo')
</script>

DOM Template Parsing Caveats

如果想直接在 DOM 撰寫 template,vue 會從 DOM 取得 template string,這樣可能會發生 browser 在 native HTML parsing 的警告

某些 html element 有限制裡面可以放的 element,ex: ul, ol, table, select

當這樣寫的時候,會產生警告

<table>
  <blog-post-row></blog-post-row>
</table>

解決方式,用 is ,裡面一定要用 "vue:" 為 prefix

<table>
  <tr is="vue:blog-post-row"></tr>
</table>

html attribute name 為 case-insensitive

如果在 in-DOM template 使用 camelCased prop name, event handler parameters,需要改為 kebab-cased (hyphen-delimited)

// camelCase in JavaScript

app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
<!-- kebab-case in HTML -->

<blog-post post-title="hello!"></blog-post>

References

Vue Guide

重新認識 Vue.js

2022/03/14

Vue.js Essentials 2

Computed Properties and Watchers

Computed Properties

in-template expression 很方便好用,但不適合寫太多 code logic

<div id="computed-basics">
  <p>Has published books:</p>
  <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
</div>

<script type="text/javascript">
Vue.createApp({
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  }
}).mount('#computed-basics')
</script>

為了簡化上面的 code,要使用 computed properties

<div id="computed-basics2">
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</div>

<script type="text/javascript">
Vue.createApp({
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  },
  computed: {
    // a computed getter
    publishedBooksMessage() {
      // `this` points to the vm instance
      return this.author.books.length > 0 ? 'Yes' : 'No'
    }
  }
}).mount('#computed-basics2')
</script>

因為 vm.publishedBooksMessage 相依於 vm.author.books,如果修改了 books,publishedBooksMessage 會自動更新

  • Computed Caching vs Methods

剛剛的 sample,也可以用 method 呼叫改寫

<p>{{ calculateBooksMessage() }}</p>
// in component
methods: {
  calculateBooksMessage() {
    return this.author.books.length > 0 ? 'Yes' : 'No'
  }
}

兩種寫法的結果一樣

差異是,computed properties 會根據 reactive dependencies 被 cached。

computed property 只會在 reactive dependencies 被修改時,重新 re-evaluate。

只要 author.books 沒有被修改,使用 publishedBooksMessage 都會直接 return,而不會運算該 function

因為 Date.now() 不是 reactive dependency,所以 now 這個 computed property 永遠不會更新

computed: {
  now() {
    return Date.now()
  }
}

如果是 method,就會在 re-render 時,重複呼叫該 function

如果不需要 caching,就用 method 寫法

  • Computed Setter

computed properties 預設為 getter-only,如果需要時,可增加 setter

// ...
computed: {
  fullName: {
    // getter
    get() {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set(newValue) {
      const names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

當呼叫 vm.fullName = 'John' 時,會呼叫 setter,並更新 vm.firstName, vm.lastName

Watchers

vue 提供 watch option 可以 react to data change

如果修改 data 會發生 asynchronous or expensive operations 時很有用

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question" />
  </p>
  <p>{{ answer }}</p>
</div>

<!-- Since there is already a rich ecosystem of ajax libraries    -->
<!-- and collections of general-purpose utility methods, Vue core -->
<!-- is able to remain small by not reinventing them. This also   -->
<!-- gives you the freedom to use what you're familiar with.      -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script>
  const watchExampleVM = Vue.createApp({
    data() {
      return {
        question: '',
        answer: 'Questions usually contain a question mark. ;-)'
      }
    },
    watch: {
      // whenever question changes, this function will run
      question(newQuestion, oldQuestion) {
        if (newQuestion.indexOf('?') > -1) {
          this.getAnswer()
        }
      }
    },
    methods: {
      getAnswer() {
        this.answer = 'Thinking...'
        axios
          .get('https://yesno.wtf/api')
          .then(response => {
            this.answer = response.data.answer
          })
          .catch(error => {
            this.answer = 'Error! Could not reach the API. ' + error
          })
      }
    }
  }).mount('#watch-example')
</script>
  • computed vs wated property

當有一些 data,需要根據其他 data 動態改變時,可能會 overuse watch,尤其是有 AngularJS 背景的開發者,要先考慮使用 computed property 而不是 watch callback

<div id="demo">{{ fullName }}</div>

<script>
const vm = Vue.createApp({
  data() {
    return {
      firstName: 'Foo',
      lastName: 'Bar',
      fullName: 'Foo Bar'
    }
  },
  watch: {
    firstName(val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName(val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
}).mount('#demo')
</script>

watch 的版本會比較精簡

const vm = Vue.createApp({
  data() {
    return {
      firstName: 'Foo',
      lastName: 'Bar'
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}).mount('#demo')

Class & Style Bindings

另一個常見需求是修改 element 的 class list 與 inline styles,因為都是 attributes,可以用 v-bind 修改。但會遇到很多 string 連接的問題。

vue 提供 classsytle,以 object/array 方式處理

Binding html classes

  • Object Syntax

可傳送物件到 :class 這是 v-bind:class 的縮寫

ex: 根據 data property: isActiveTruthy 決定 active class

<div :class="{ active: isActive }"></div>

ex: 可以有多個欄位,也可跟既有的 class 並存

<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

以下的物件

data() {
  return {
    isActive: true,
    hasError: false
  }
}

會 render 為

<div class="static active"></div>

ex: 可以包裝為一個物件

<div :class="classObject"></div>
data() {
  return {
    classObject: {
      active: true,
      'text-danger': false
    }
  }
}

ex: 可以使用 computed property

<div :class="classObject"></div>
data() {
  return {
    isActive: true,
    error: null
  }
},
computed: {
  classObject() {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}
  • Array Syntax

可用 array 傳入 :class

<div :class="[activeClass, errorClass]"></div>
data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}

會 render 為

<div class="active text-danger"></div>

ex: 可以用 ternary expression 處理 class toggle

<div :class="[isActive ? activeClass : '', errorClass]"></div>

ex: 如果有多個 conditional class,可以在 array 裡面用 object syntax

<div :class="[{ active: isActive }, errorClass]"></div>
  • with Components

在單一 root component 使用 class 時,既有的 classes 不會被 overwritten

<div id="app">
  <my-component class="baz boo"></my-component>
</div>

<script>
const app = Vue.createApp({})

app.component('my-component', {
  template: `<p class="foo bar">Hi!</p>`
})
</script>

會 render 為

<p class="foo bar baz boo">Hi</p>

如果是多個 root element,必須指定哪一個 component 接收這些 class,可使用 $attrs component property

<div id="app">
  <my-component class="baz"></my-component>
</div>

<script>
const app = Vue.createApp({})

app.component('my-component', {
  template: `
    <p :class="$attrs.class">Hi!</p>
    <span>This is a child component</span>
  `
})
</script>

Binding inline styles

  • Object Syntax

可使用 camelCase or kebab-case (use quotes with kebab-case) 的 css propery names

<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

<script>
data() {
  return {
    activeColor: 'red',
    fontSize: 30
  }
}
</script>

比較好的寫法是 bind sytle object

<div :style="styleObject"></div>

<script>
data() {
  return {
    styleObject: {
      color: 'red',
      fontSize: '13px'
    }
  }
}
</script>
  • Array Syntax
<div :style="[baseStyles, overridingStyles]"></div>
  • Auto-prefixing

如果在 :style 使用需要 Vendor Prefix 的 css property,vue 會自動加上適當的 prefix,Vue 會在 runtime 自動判斷是否有被目前的 browser 支援。

如果沒有支援,就會自動測試多個 prefix variants,嘗試找到支援的 css property

  • Multiple Values

可提供多個 prefixed values 給某個 style property

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

Conditional Rendering

v-if

在 directive expression 回傳 truth value 時,才會被 rendered

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

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