Vue

VUE

Vue 官网

Vue API

安装

Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。

使用npm安装:

1
npm install vue 

创建项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
vue create app_name
# 选手动选择特性

# Babel,编译工具,必选
# TypeScript,可选
# PWA Support,让页面接近原生应用,可选
# Router,必选
# Vuex,必选
# CSS Pre-processors,必选
# Linter / Formatter,代码检测和格式化,可选
# Unit Testing,必选
# E2E Testing,端到端测试,可选

# 使用 History Mode
# 是

# CSS 预处理器
# SCSS/SASS

# ESLint 风格
# 标准
# 保存时检测

# 单元测试
# Jest

# 配置保存位置
# 保存到各自的位置

创建好的APP目录如下:

  • app_name
    • node_modules/
    • public/
      • favicon.ico
      • index.html
    • src/
      • assets/ 资源文件
        • logo.png
      • components/ 公共组件
        • HelloWorld.vue
      • router/ 前端路由
        • index.js
      • store/ 应用数据 state
        • index.js
      • views/ 页面目录
        • About.vue
        • Home.vue
      • App.vue 根 APP
      • main.js 项目入口
    • tests/
      • unit
        • example.spec.js
    • .browserslistrc
    • .gitignore
    • babel.config.js
    • jest.config.js
    • package-lock.json
    • package.json
    • README.md

Vue 实例 生命周期

  1. new Vue()
  2. 初始化:事件,声明周期
  3. beforeCreate()
  4. 初始化,注入,校验
  5. created
  6. 是否指定 el
  7. 是否指定 template
  8. 将template/el翻译渲染
  9. beforeMount
  10. 创建vm.$el,并替换el
  11. mounted
  12. 挂载完毕
  13. data被修改
  14. beforeUpdate
  15. 重新渲染
  16. updated
  17. beforeDestroy
  18. 解除绑定,销毁子组件
  19. destroyed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>

<style scoped lang="scss">
h1 {
margin: 40px 0 0;
}
</style>

模板

插值:

1
2
{{ msg }}
{{ message.split('').reverse().join('') }}

指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
<span v-html="rawHtml"></span>
<span v-once>这个将不会改变: {{ msg }}</span>

<div v-bind:id="dynamicId"></div>
<button :disabled="isButtonDisabled">Button</button>

<a v-on:click="doSomething">...</a>
<a @click="doSomething">...</a>
<form @submit.prevent="onSubmit">...</form>

<!-- 2.6.0 新增 -->
<!-- 这里的 attributeName 会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。 -->
<a v-bind:[attributeName]="url"> ... </a>

Class与Style:

1
2
3
4
5
6
7
8
<div :class="{ active: isActive }"></div>
<div
class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>

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

条件渲染:

1
2
3
4
5
6
7
<p v-if="seen">现在你看到我了</p>
<h1 v-else-if="ok">Oh no</h1>
<h1 v-else>Oh no</h1>

<h1 v-show="ok">Hello!</h1>

<!-- v-if是惰性的,v-show则会直接渲染,不论条件如何 -->

列表渲染:

1
2
3
4
5
6
7
<li v-for="item in items" :key="item.id">
{{ item.message }}
</li>

<li v-for="(item, index) in items" :key="item.id">
{{ item.message }}
</li>

监听事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<button @click="counter += 1">Add 1</button>
<button @click="greet">Greet</button>
<button @click="say('hello', $event)">Greet</button>
<!-- 鼠标事件 -->
<input @click.left="submit">
<input @click.right="submit">
<input @click.middle="submit">
<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form @submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input @keyup.enter="submit">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default {
name: 'HelloWorld',
methods: {
greet: function (event) {
alert('Hello ' + this.name + '!')
if (event) { // `event` 是原生 DOM 事件
alert(event.target.tagName)
}
},
say: function (message, event) {
alert(message)
}
}
}

表单输入绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- 输入框 -->
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
<!-- 复选框 -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<!-- 单选框 -->
<div id="example-4">
<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>
<!-- 选择框 -->
<div id="example-5">
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
</div>

<!-- 修饰符 -->
<input v-model.lazy="msg" >
<input v-model.number="age" type="number">
<input v-model.trim="msg">

组件

1
2
3
4
5
6
7
8
// 组件的data必须是一个函数
data: function () {
return {
count: 0
}
},
// 一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。
props: ['title'],

子组件可以通过emit触发事件:

1
2
3
4
5
6
7
8
9
<!-- 子组件 -->
<button @click="$emit('enlarge-text')">
Enlarge text
</button>
<!-- 父组件 -->
<blog-post
...
@enlarge-text="onEnlargeText"
></blog-post>

也可以使用v-model:

1
2
3
4
5
6
7
<!-- 子组件 -->
<custom-input
:value="searchText"
@input="searchText = $event"
></custom-input>
<!-- 父组件 -->
<custom-input v-model="searchText"></custom-input>
1
2
3
4
5
6
7
8
9
10
// 子组件
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})

组件可以使用插槽:

组件注册

1
2
3
4
5
6
7
8
9
10
11
12
13
// 全局注册
Vue.component('my-component-name', {
// ... 选项 ...
})
// 局部注册
var ComponentA = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
1
2
3
4
5
6
7
8
9
10
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
components: {
ComponentA,
ComponentC
},
// ...
}

Prop

驼峰命名法在使用时应写为短横线分割命名。

类型:

1
2
3
4
5
6
7
8
9
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})

Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})

绑定原生事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})

Slot

1
2
3
4
5
6
7
8
9
10
<a
v-bind:href="url"
class="nav-link"
>
<slot>Default</slot>
<slot name="sname">Default</slot>
</a>
<!-- 使用 -->
<template v-slot:sname>
<template #sname>

动态组件

keep-alive 可以保存状态,防止重新渲染。

1
2
3
<keep-alive>
<a v-bind:is="currentTabComponent"></a>
</keep-alive>

异步组件

1
2
3
4
5
6
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})

访问元素 & 组件

1
2
3
4
5
6
// 访问根
this.$root
// 访问父级
this.$parent.map || this.$parent.$parent.map
// 访问子级
this.$refs.usernameInput

计算属性与侦听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default {
name: 'HelloWorld',
data: {
message: 'Hello',
firstName: 'Foo',
},
// 计算属性,依赖不发生变化,就不会重新计算,只会返回缓存
computed: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
},
// 或
computed: {
reversedMessage: {
get: function() {
return ""
},
set: function(newValue) {
// ...
}
}
},
// 侦听属性,当需要在数据变化时执行异步或开销较大的操作时可用。
watch: {
firstName: function (newValue, oldValue) {
// ...
}
}
}

过渡动画

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:

  • 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
  • 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
  • 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)
1
2
3
4
5
6
7
8
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade" :duration="{ enter: 500, leave: 800 }">
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#demo',
data: {
show: true
}
})
1
2
3
4
5
6
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}

过渡类名有:

1
2
3
4
5
6
.v-enter
.v-enter-active
.v-enter-to
.v-leave
.v-leave-active
.v-leave-to

也可以使用关键帧动画:

1
2
3
4
5
6
<div id="example-2">
<button @click="show = !show">Toggle show</button>
<transition name="bounce">
<p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus.</p>
</transition>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

也可以自定义过渡类名:

1
2
3
4
5
6
.enter-class
.enter-active-class
.enter-to-class
.leave-class
.leave-active-class
.leave-to-class

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">

<div id="example-3">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>

也可以使用钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"

v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
new Vue({
el: '#example-4',
data: {
show: false
},
methods: {
beforeEnter: function (el) {
el.style.opacity = 0
el.style.transformOrigin = 'left'
},
enter: function (el, done) {
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
Velocity(el, { fontSize: '1em' }, { complete: done })
},
leave: function (el, done) {
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
Velocity(el, {
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0
}, { complete: done })
}
}
})

设置初始渲染的过渡

1
2
3
4
5
6
7
8
<transition
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class" (2.1.8+)
appear-active-class="custom-appear-active-class"
>
<!-- ... -->
</transition>

过渡模式:

1
2
3
<transition name="fade" mode="out-in">
<!-- ... the buttons ... -->
</transition>

mode:
in-out
out-in

列表过渡:

1
2
3
4
5
6
7
8
9
10
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="flip-list-demo" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
1
2
3
4
5
6
7
8
9
10
11
new Vue({
el: '#flip-list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9]
},
methods: {
shuffle: function () {
this.items = _.shuffle(this.items)
}
}
})
1
2
3
.flip-list-move {
transition: transform 1s;
}

状态过渡:

1
2
3
4
5
6
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>

<div id="animated-number-demo">
<input v-model.number="number" type="number" step="20">
<p>{{ animatedNumber }}</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Vue({
el: '#animated-number-demo',
data: {
number: 0,
tweenedNumber: 0
},
computed: {
animatedNumber: function() {
return this.tweenedNumber.toFixed(0);
}
},
watch: {
number: function(newValue) {
TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue });
}
}
})

Router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
router
}).$mount('#app')

// 现在,应用已经启动了!

组件内访问路由器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Home.vue
export default {
computed: {
username() {
// 我们很快就会看到 `params` 是什么
return this.$route.params.username
}
},
methods: {
goBack() {
window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/')
}
}
}

路由的动态匹配

一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。

1
2
3
4
5
6
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
})

响应路由参数的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const User = {
template: '...',
watch: {
'$route' (to, from) {
// 对路由变化作出响应...
}
}
}
// 或
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}

捕获 404

1
2
3
4
5
6
7
8
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}

当使用一个通配符时,$route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分。

正则匹配

使用 path-to-regexp 作为引擎。

嵌套路由

使用children参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User,
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
})

此时,基于上面的配置,当你访问 /user/foo 时,User 的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由。如果你想要渲染点什么,可以提供一个 空的 子路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const router = new VueRouter({
routes: [
{
path: '/user/:id', component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 <router-view> 中
{ path: '', component: UserHome },

// ...其他子路由
]
}
]
})

编程式导航

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码this.$router.push来实现。点击 <router-link :to="..."> 等同于调用 router.push(...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 字符串
router.push('home')

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

// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})

// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效,如果提供了 path,params 会被忽略
router.push({ path: '/user', params: { userId }}) // -> /user

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

跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

router.go(n)

这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。

命名路由

1
2
3
4
5
6
7
8
9
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
1
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

命名视图

有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。

1
2
3
<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>
1
2
3
4
5
6
7
8
9
10
11
12
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})

嵌套命名视图

1
2
3
4
5
6
7
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar/>
<router-view/>
<router-view name="helper"/>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}

重定向和别名

重定向:

1
2
3
4
5
6
7
8
9
10
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' },
{ path: '/a', redirect: { name: 'foo' }},
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})

别名:

1
2
3
4
5
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})

路由组件传参

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

使用 props 将组件和路由解耦:

1
2
3
4
5
6
7
8
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})

History 模式

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

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

不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。

后台nginx配置:

1
2
3
location / {
try_files $uri $uri/ /index.html;
}

懒加载

首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

1
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })

第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):

1
import('./Foo.vue') // 返回 Promise

结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

1
const Foo = () => import('./Foo.vue')

在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:

1
2
3
4
5
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})

导航守卫

导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。参数或查询的改变并不会触发进入/离开的导航守卫。

全局前置守卫:

1
2
3
4
5
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
// ...
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标 路由对象
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next(‘/‘) 或者 next({ path: ‘/‘ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

全局解析守卫

在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

全局后置钩子

和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

1
2
3
router.afterEach((to, from) => {
// ...
})

路由独享守卫

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

组件内守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}

完整的导航解析流程:
导航被触发。
在失活的组件里调用离开守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

路由元信息

定义路由的时候可以配置 meta 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})

过渡效果:

1
2
3
<transition :name="transitionName">
<router-view></router-view>
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}

const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}
1
2
3
4
5
6
7
watch: {
'$route' (to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}

获取数据

导航后获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>

<div v-if="error" class="error">
{{ error }}
</div>

<div v-if="post" class="content">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default {
data () {
return {
loading: false,
post: null,
error: null
}
},
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.post = null
this.loading = true
// replace getPost with your data fetching util / API wrapper
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}

导航前获取数据

通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export default {
data () {
return {
post: null,
error: null
}
},
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}
}

Vuex

State

从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

1
2
3
4
5
6
7
8
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}

Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):

1
2
3
4
5
6
7
8
9
10
11
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
1
2
3
4
5
6
7
8
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}

Getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}

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)
}
}
})

Mutation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})

store.commit('increment')
store.commit('increment', 10)

Action

可以异步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

store.dispatch('increment')

Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 的状态

SSR

服务端渲染

安装

1
npm install vue vue-server-renderer --save

渲染示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})

// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()

// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})

// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})

Vue Cli

文档

配置

配置文件

vue.config.js文件:

1
2
3
4
5
6
module.exports = {
// 配置根目录位置
publicPath: process.env.NODE_ENV === 'production'
? '/production-sub-path/'
: '/'
}