Vue.js 進階使用

邱俊霖

GitHub

Vue.js

  • Vue Router
  • Vuex
  • Single File Component

Vue Router

Start

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 組件來導航. -->
    <!-- 通過傳入 `to` 屬性指定鏈接. -->
    <!-- <router-link> 默認會被渲染成一個 `<a>` 標籤 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的組件將渲染在這裡 -->
  <router-view></router-view>
</div>
// 定義路由的組件。
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

const test = {
  template: `<div>test<foo></foo></div>`,
  components: {
    'foo': Foo
  }
};

// 定義路由
// 每個路由應該映射一個組件。其中"component" 可以只是一個組件配置對象。
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar },
  { path: '/test', component: test }
]

// 創建 router 實例,然後傳 `routes` 配置
const router = new VueRouter({
  routes // routes: routes
})

// 創建和掛載根實例。
// 記得要通過 router 配置參數注入路由,
// 從而讓整個應用都有路由功能
const app = new Vue({
  router
}).$mount('#app')

// 現在,應用已經啟動了!

當 <router-link> 對應的路由匹配成功,將自動設置 class 屬性值 .router-link-active

// 給 active link 樣式
.router-link-active {
  background: blue;
}

Parameter

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

const router = new VueRouter({
  routes: [
    // 動態路徑參數 以冒號開頭
    { path: '/user/:id', component: User }
  ]
})
<router-link to="/user/Deleav">User1</router-link>
<router-link to="/user/DMoon">User2</router-link>
const User = {
  template: '...',
  watch: {
    '$route': function(to, from) {
      // 對路由變化作出響應...
      console.log( to.path );
    }
  }
}

當使用路由參數時,原來的組件實例會被復用,比起重新渲染,重複使用效能較高。但這也意味著組件的生命週期鉤子不會再被調用。

Reacting to Params Changes

Nested Routes

/user/foo/profile                     /user/foo/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+
<div id="app">
  <router-view></router-view>
</div>
const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})

Nested Routes

最頂層的 router-view

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User,
      children: [
        {
          // UserProfile will be rendered inside User's <router-view>
          // when /user/:id/profile is matched
          path: 'profile',
          component: UserProfile
        },
        {
          // UserPosts will be rendered inside User's <router-view>
          // when /user/:id/posts is matched
          path: 'posts',
          component: UserPosts
        }
      ]
    }
  ]
})
const User = {
  template: `
    <div class="user">
      <h2>User {{ $route.params.id }}</h2>
      <router-view></router-view>
    </div>
  `
}

Children Option

Declarative Programmatic
<router-link :to="..."> router.push(...)

Programmatic Navigation

// literal string
router.push('home')

// object
router.push({ path: 'home' })

// named route
router.push({ name: 'user', params: { userId: 123 }})

// with query, resulting in /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

// with hash, resulting in /home#content
router.push({ path: 'home', hash: 'content' })
Declarative Programmatic
<router-link :to="..." replace> router.replace(...)

Programmatic Navigation

// literal string
router.replace('home')

// object
router.replace({ path: 'home' })

// named route
router.replace({ name: 'user', params: { userId: 123 }})

// with query, resulting in /register?plan=private
router.replace({ path: 'register', query: { plan: 'private' }})

// with hash, resulting in /home#content
router.replace({ path: 'home', hash: 'content' })
<!-- literal string -->
<router-link to="home">Home</router-link>
<!-- renders to -->
<a href="home">Home</a>

<!-- javascript expression using v-bind -->
<router-link v-bind:to="'home'">Home</router-link>

<!-- Omitting v-bind is fine, just as binding any other prop -->
<router-link :to="'home'">Home</router-link>

<!-- same as above -->
<router-link :to="{ path: 'home' }">Home</router-link>

<!-- named route -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

<!-- with query, resulting in /register?plan=private -->
<router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>

Named Views

<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: Foo,
        a: Bar,
        b: Baz
      }
    }
  ]
})

注意要使用 components (帶 s)

同層級多 router-view

Redirect and Alias

const router = new VueRouter({
  routes: [
    { path: '/b', component: ComponentB },
    { path: '/a', redirect: '/b' }
  ]
})
const router = new VueRouter({
  routes: [
    { path: '/b', component: ComponentB },
    { path: '/a', redirect: to => {
      // 方法接收 目標路由 作為參數
      // return 重定向的 字符串路徑/路徑對象
      return '/b'
    }}
  ]
})

Redirect

Alias

const router = new VueRouter({
  routes: [
    { path: '/b', component: ComponentB, alias: '/a' }
  ]
})

Redirect vs Alias?

HTML5 History Mode

default 是 hash mode,如果不想要很醜的 hash mode 可以使用 history mode

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

Advanced

router.push(location, onComplete?, onAbort?)
router.replace(location, onComplete?, onAbort?)

Vuex

單一數據源

SSOT

( Single Source of Truth )

Vuex

  1. Vuex 的狀態存儲是響應式的。

  2. 不能直接改變 store 中的狀態

  3. 改變 store 中的狀態的唯一途徑就是提交(commit) mutations。

Start

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
console.log(store.state.count);
store.commit('increment');
console.log(store.state.count);

Outline

  • State
  • Getters
  • Mutations
  • Actions
  • Modules

State

Single State Tree

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

Getting Vuex State into Vue Components

透過 computed 屬性獲取 Vuex state

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}

Inject Store into child components

透過 store 選項注入 store instance 到所有子組件

const app = new Vue({
  el: '#app',
  // 把 store 對象提供給 “store” 選項,這可以把 store 的實例注入所有的子組件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

且子組件能通過 this.$store 訪問到。

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

mapState

computed: mapState({
  // 箭頭函數可使代碼更簡練
  count: state => state.count,

  // 傳字符串參數 'count' 等同於 `state => state.count`
  countAlias: 'count',

  // 為了能夠使用 `this` 獲取局部狀態,必須使用常規函數
  countPlusLocalState (state) {
    return state.count + this.localCount
  }
})
computed: mapState([
  // 映射 this.count 為 store.state.count
  'count'
])

Object Spread Operator

computed: {
  localComputed () { /* ... */ },
  // 使用對象展開運算符將此對象混入到外部對像中
  ...mapState({
    // ...
  })
}

ES6

ES5

computed: Object.assign({
  localComputed () { /* ... */ },
},
mapState({
  // 使用 Object.assign() 合併 object
  // ...
}))

Getters

有時候我們需要從 store 的 state 中獲取一些處理過的狀態,例如對列表進行過濾:

Getters

// component or instance
computed: {
  doneTodos () {
    return this.$store.state.todos.filter(todo => todo.done)
  }
}

若很多組件都會使用到,就要寫很多次,這時可以利用 getters ( 可認為是 store 的 computed 屬性 )

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Getters

  • 可以在 store.getters 裡取得所有的 getters
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
  • Getters 也可以接受其他 getters 作為第二個參數:
getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

Use

mapGetters

mapGetters 輔助函數僅僅是將 store 中的 getters 映射到局部計算屬性:

computed: {
// 使用對象展開運算符將 getters 混入 computed 對像中
  ...mapGetters([
    'doneTodosCount',
    'anotherGetter',
    // ...
  ])
}
mapGetters({
  // 映射 this.doneCount 為 store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})

Mutations

Mutations

更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 變更狀態
      state.count++
    }
  }
})

你不能直接調用一個 mutation handler,這個選項更像是事件註冊。

store.commit('increment')

Payload

你可以向 store.commit 傳入額外的參數,即 mutation 的 payload:

mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多數情況下,payload 應該是一個 Object:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

Mutations 需遵守 Vue 的響應規則

  • 最好提前在你的 store 中初始化好所有所需屬性。

  • 在 Object 上新增屬性時,應該以新 Object 替換舊的 Object

  • 更新 Array 使用可被 Vue 響應的函式

使用常量替代 Mutation 事件類型

  • Bullet One
  • Bullet Two
  • Bullet Three
const SOME_MUTATION = 'SOME_MUTATION'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我們可以使用 ES2015 風格的計算屬性命名功能來使用一個常量作為函數名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

Mutation 必須是同步函數

提交 Mutation 是「唯一」可變更 state 的方法,我們必須讓他是「可預期」且「不可變」的。

異步函數就讓 Actions 來處理

在組件中提交 Mutations

methods: {
  ...mapMutations([
    'increment' // 映射 this.increment() 为 this.$store.commit('increment')
  ]),
  ...mapMutations({
    add: 'increment' // 映射 this.add() 为 this.$store.commit('increment')
  })
}
  • 使用 this.$store.commit('xxx')
  • 或使用 mapMutations

Actions

Actions

Action 類似於 mutation,不同在於:

  • Action 提交的是 mutation,而不是直接變更狀態。
  • Action 可以包含任意異步操作。

Actions

Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Argument destructuring

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

分發 Actions

store.dispatch('increment')

action 內可執行異步操作

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

支援 payload

store.dispatch('incrementAsync', {
  amount: 10
})

Sample

actions: {
  checkout ({ commit, state }, products) {
    // 把當前購物車的物品備份起來
    const savedCartItems = [...state.cart.added]
    // 發出結賬請求,然後樂觀地清空購物車
    commit(types.CHECKOUT_REQUEST)
    // 購物 API 接受一個成功 callback 和一個失敗 callback
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失敗操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

在組件中分發 Actions

  • 使用 this.$store.dispatch('xxx')
  • 或使用 mapActions
methods: {
  ...mapActions([
    'increment' // 映射 this.increment() 為 this.$store.dispatch('increment')
  ]),
  ...mapActions({
    add: 'increment' // 映射 this.add() 為 this.$store.dispatch('increment')
  })
}

如何知道 Action 結束了?

store.dispatch 可以處理被觸發的 action 的回調函數返回的Promise

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}
store.dispatch('actionA').then(() => {
  // ...
})

並且返回的仍舊是 Promise,所以現在你可以

Mutation 是不能處理 Promise 的

所以不要用 Mutation 做非同步的事情

Modules

使用單一狀態樹,導致應用的所有狀態集中到一個很大的對象。但是,當應用變得很大時,store 對象會變得臃腫不堪。

Modules

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態

Module Local State

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      // state 模塊的局部狀態
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

mutations 和 getter 第一個參數為 module 的區域狀態

Module Local State

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

action 的 context.state 是區域狀態,根節點的狀態是 context.rootState

Module Local State

Namespace

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // module assets
      state: { ... }, // module state is already nested and not affected by namespace option
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // nested modules
      modules: {
        // inherits the namespace from parent module
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // further nest the namespace
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

map 函式

computed: {
  ...mapState('some/nested/module', [
    'a', 
    'b'
  ]), 
  ...mapGetters('some/nested/module', [
    'getA', 
    'getB'
  ])
},
methods: {
  ...mapMutations('some/nested/module', [
    'foo', 
    'bar'
  ]),
  ...mapActions('some/nested/module', [
    'foo',
    'bar'
  ])
}

其他使用 namespace 的方式

// types.js

// 定義 getter、action、和 mutation 的名稱為常量,以模塊名 `todos` 為前綴
export const DONE_COUNT = 'todos/DONE_COUNT'
export const FETCH_ALL = 'todos/FETCH_ALL'
export const TOGGLE_DONE = 'todos/TOGGLE_DONE'
// import * as types from '../types'

// 使用添加了前綴的名稱定義 getter、action 和 mutation
const todosModule = {
  state: { todos: [] },

  getters: {
    [types.DONE_COUNT] (state) {
      // ...
    }
  },

  actions: {
    [types.FETCH_ALL] (context, payload) {
      // ...
    }
  },

  mutations: {
    [types.TOGGLE_DONE] (state, payload) {
      // ...
    }
  }
}

Dynamic Module Registration

// register a module `myModule`
store.registerModule('myModule', {
  // ...
})

// register a nested module `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
<input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}

Form Handling

<input v-model="message">
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

Single File Components

Single File Components

  • Single File Components
  • vue-cli
  • vue-cli templage
    • webpack
    • browserify

Vue-router

const Foo = r => require.ensure([], () => r(require('./Foo.vue')), 'group-foo')
const Bar = r => require.ensure([], () => r(require('./Bar.vue')), 'group-foo')
const Baz = r => require.ensure([], () => r(require('./Baz.vue')), 'group-foo')

Grouping

const Hello = () => import('./components/Hello');
const Login = () => import('./components/Login');

個別加載

require.ensure 是 webpack 提供的 code spliting api

Vuex

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── types.js          # 根級別的 type
    ├── actions.js        # 根級別的 action
    ├── mutations.js      # 根級別的 mutation
    └── modules
        ├── cart          # 購物車模塊
        │   ├── index.js
        │   └── types.js
        └── products      # 產品模塊
            ├── index.js
            └── types.js

Server side rendering

Why?

  • SEO
  • Clients with a Slow Internet
  • Clients with an Old (or Simple No) JavaScript Engine

Vue.js 進階使用

By 邱俊霖

Vue.js 進階使用

  • 593