Vue.js 進階使用
邱俊霖
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
- Navigation Guards
- Meta Fields
- Transitions
- Data Fetching
- Scroll Behavior
- Lazy Loading
router.push(location, onComplete?, onAbort?)
router.replace(location, onComplete?, onAbort?)
Vuex
單一數據源
SSOT
( Single Source of Truth )

Vuex
-
Vuex 的狀態存儲是響應式的。
-
不能直接改變 store 中的狀態。
-
改變 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 // -> 1computed: {
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
- getter( state, getters, rootState, rootGetters )
- mutation( state, payload )
- action(context, payload)
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
- Vue.use( VueRouter );
- Lazy Loading Routes
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
- Vue.use( Vuex )
- Application Structure
├── 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