Vue

简介

Vue是一套用于构建用户界面的渐进式框架

优点

  • 学习成本低、轻量级的框架、渐进式框架
  • 双向数据绑定、组件化开发
  • 单页面路由、虚拟dom
  • 数据和结构的分离、运行速度快、插件化

缺点

  • 不支持IE8以下
  • 生态系统可能没有Angular和React那么丰富
  • 单页面应用,不利于seo优化
  • 初次加载时耗时多

常用指令

指令 说明 示例
v-if 控制元素渲染,值为true重新加载dom, v-else-if和v-else同理 <div v-if="true">渲染元素</div>
v-show 控制元素显示,值为false相当于css的display:none <div v-show="false">隐藏元素</div>
v-text 元素的InnerText属性,用于填充文本 <div v-text="显示的文本"></div>
v-html 元素的innerHTML属性,用于填充文本或富文本 <div v-html="html"></div>, html在data中声明
v-model 双向绑定,视图层修改值时会同步改变数据层的值 <input v-model="value" />, value在data中声明
v-bind 给元素的属性赋值, v-bind:属性名="常量/变量名" ,简写形式:属性名="变量名" <img v-bind:src="imgUrl"/><img :src="imgUrl"/>
v-on 自定义事件, v-on:事件名="表达式/函数名",简写形式@事件名="表达式" <div v-on:click="clickEvent"></div><div @click="clickEvent"></div>

计算属性和侦听器

计算属性

计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。

1
2
3
4
<div id="app">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
var vm = new Vue({
el: '#app',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})

侦听器

当需要在数据变化时执行异步或开销较大的操作时,使用侦听器是最有用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var vm = new Vue({
el: '#app',
data: {
message: 'Hello'
},
watch: {
message: function (newValue, oldValue) {
this.doSomething();
}
},
methods: {
doSomething() {
// doSomething
}
}
})

修饰符

事件修饰符

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
<!-- 阻止单击事件继续传播 等同于event.stopPropagation() -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 等同于event.preventDefault() -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
<!-- 这个 .passive 修饰符尤其能够提升移动端的性能。 -->
<!-- 不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive 会告诉浏览器你不想阻止事件的默认行为。 -->

其他

1
2
3
4
5
6
7
8
9
10
11
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">

<!-- 如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符: -->
<input v-model.trim="msg">

<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">

<!-- 如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符: -->
<input v-model.number="age" type="number">

生命周期

基本生命周期 说明
beforeCreate 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
created 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 实例被挂载后调用。
beforeUpdate 数据更新时调用。
updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
不常用生命周期 说明
activated 被 keep-alive 缓存的组件激活时调用。
deactivated 被 keep-alive 缓存的组件停用时调用。
errorCaptured 当捕获一个来自子孙组件的错误时被调用。

组件传值

父组件

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
<template>
<div>
<div>父组件</div>
<!-- Child子组件 -->
<Child :msg="parentMsg" @toParent="getMsg" />
</div>
</template>

<script>
import Child from "./Child";
export default {
components: {
Child
},
data() {
return {
parentMsg: "我是父组件消息",
childMsg: ""
};
},
methods: {
// 子组件事件
getMsg(msg) {
this.childMsg = msg;
}
}
};
</script>

子组件

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
<template>
<div>
<div>子组件</div>
<div>我接受到的父组件的消息是:{{msg}}</div>
<div @click="toParent">向父组件发送信息</div>
</div>
</template>

<script>
export default {
// props: ['msg'],
// props: {
// msg: String
// },
props: {
msg: {
type: String,
default: '默认值'
}
},
data() {
return {
childMsg: "我是子组件消息",
};
},
methods: {
toParent() {
this.$emit("toParent", this.childMsg);
}
}
};
</script>

兄弟传值

1
2
3
// main.js
import Vue from 'vue'
Vue.prototype.$eventBus = new Vue();

第一个子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<button @click="toBrother">兄弟传值</button>
</div>
</template>

<script>
export default {
data() {
return {
msg: "兄弟传值"
};
},
methods: {
toBrother() {
this.$eventBus.$emit("toBrother", this.msg);
}
}
};
</script>

第二个子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div>第二个子组件,得到的信息是:{{getMsg}}</div>
</div>
</template>

<script>
export default {
data() {
return {
getMsg: ""
};
}
created() {
this.$eventBus.$on("toBrother", msg => {
this.getMsg = msg;
});
}
};
</script>

Vuex

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
34
35
36
37
38
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.store({
state:{
name: 'helloVueX',
age: 15
},
getters:{
nameInfo(state){
return "姓名:" + state.name
},
fullInfo(state,getters){
return getters.nameInfo + '年龄:' + state.age
}
}
mutations:{
edit(state, payload){
state.name = 'jack'
// 或接收入参payload(入参为多个参数时payload必须是一个对象)
// state.name = payload;
}
},
actions:{
asyncEdit(context,payload){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
context.commit('edit',payload) // 调用mutations中的edit
resolve()
}, 2000)
})
}
}
})

export default store

挂载

1
2
3
4
5
6
7
8
9
10
11
12
// main.js
import Vue from 'vue'
import App from './App'
import store from './store'

Vue.config.productionTip = false

new Vue({
el: '#app',
store,
render: h => h(App)
})

使用

  • state
    1
    2
    3
    4
    5
    6
    <template>
    <div id='app'>
    <!-- 组件中使用 -->
    name: {{ $store.state.name }}
    </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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import {mapState, mapMutations, mapActions, mapGetters} from 'vuex'
    export default {
    name: 'HelloWorld',
    data () {
    return {
    msg: 'Welcome to Your Vue.js App'
    }
    },
    computed: {
    // ...mapGetters({
    // // 把 `this.getData` 映射为 `this.$store.getters.fullInfo`
    // getData: 'fullInfo'
    // }),
    ...mapGetters([
    'fullInfo',
    ]),
    getData(){
    return this.$store.getters.fullInfo
    }
    },
    methods:{
    // ...mapMutations({
    // editName: 'edit' // 将 `this.editName()` 映射为 `this.$store.commit('edit')`
    // }),
    // ...mapActions({
    // asyncEditName: 'asyncEdit', // 将 `this.asyncEditName()` 映射为 `this.$store.dispatch('asyncEdit')`
    // }),
    ...mapMutations([
    'edit' // 将 `this.edit()` 映射为 `this.$store.commit('edit')`
    ]),
    ...mapActions([
    'asyncEdit' // 将 `this.asyncEdit()` 映射为 `this.$store.dispatch('asyncEdit')`
    ]),
    getName() {
    console.log(this.$store.state.name);
    },
    getGetterName() {
    console.log(this.$store.getters.fullInfo);
    },
    editName() {
    this.$store.commit('edit', 'rose');
    // this.$store.commit('edit', {age:15,name: 'rose'});
    },
    asyncEditName() {
    this.$store.dispatch('asyncEdit', {age:15})
    }
    }
    }

路由

1
2
3
4
5
6
7
8
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>

<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 定义 (路由) 组件。
const Foo = { template: '<div>foo</div>' }
import Bar from '@/components/Bar'

// 2. 定义路由
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
const app = new Vue({
router
}).$mount('#app')

动态路由

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
const User = {
template: '<div>User</div>'
}

const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
// 访问路由为 http://xxx/user/12345
{ path: '/user/:id', component: User },
// 访问路由为 http://xxx/user/jack/post/12345
{ path: '/user/:username/post/:post_id', component: User },
// 捕获所有路由
{ path: '*', component: User },
// 捕获404 Not found 路由
// 访问路由如 http://xxx/user-jack
{ path: '/user-*', component: User },
// 重定向路由
{ path: '/a', redirect: { name: 'foo' }},
// 或
{ path: '/a', redirect: '/b' },
// 路由懒加载
{ path: '/foo', component: () => import('./Foo.vue') },
// 把某个路由下的所有组件都打包在同个异步块 (chunk) 中
{ path: '/foo', component: () => import(/* webpackChunkName: "group-foo" */ './Foo.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
29
30
31
32
33
34
35
36
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`
}
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 <router-view> 中
{
path: '',
component: UserHome
},
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
})

路由API

编程式导航

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

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

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

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

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

    // 如果提供了 path,params 会被忽略 , 你需要提供路由的 name 或手写完整的带有参数的 path
    const userId = '123'
    router.push({ name: 'user', params: { userId }}) // -> /user/123
    router.push({ path: `/user/${userId}` }) // -> /user/123
  2. router.replace(location, onComplete?, onAbort?)
    router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 在浏览器记录中前进一步,等同于 history.forward()
    router.go(1)

    // 后退一步记录,等同于 history.back()
    router.go(-1)

    // 前进 3 步记录
    router.go(3)

    // 如果 history 记录不够用,那就默默地失败呗
    router.go(-100)
    router.go(100)

导航守卫

1
2
3
4
5
6
7
8
9
10
11
12
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
// to: Route: 即将要进入的目标 路由对象
// from: Route: 当前导航正要离开的路由
// next: Function // next() 继续执行路由 next(false)中断当前的导航 next('/') 或者 next({ path: '/' }) 跳转到一个不同的地址
if (to.name !== 'Login' && !Token) {
next({ name: 'Login' })
} else {
next()
}
})

组件内的守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 如在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的 Foo 组件,此时被调用
this.name = to.params.name
next()
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}

常见bug

  1. 打包后白屏

    1
    Refused to apply style from 'http://127.0.0.1:5500/public/assets/css/app.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
    1
    2
    3
    4
    // vue.config.js中添加
    module.exports = {
    publicPath: process.env.NODE_ENV === 'production' ? './' : './'
    }
  2. 子组件改变父组件的数据,父组件元素绑定的值不更新

    • 使用 $forceupdate()
    • 使用$emit('update:value',newValue)
    • 在父组件methods中改变,子组件触发父组件中的方法
  3. v-for绑定的数据更新,视图不更新

    • 原因:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

    • 当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue

    • 当你修改数组的长度时,例如: vm.items.length = newLength

    • 解决办法:

      1
      2
      3
      Vue.setVue.set(example1.items, indexOfItem, newValue) 
      或 example1.items.splice(indexOfItem, 1, newValue)
      example1.items.splice(newLength)
    • 使用了前两种方法依旧不更新视图
      包裹一层标签,并使用v-ifv-show操作

  4. 修改data数据后视图层没有同步更新

    使用

    1
    2
    3
    this.$nextTick(()=>{
    this.username = 'jack'
    })

如果有任何建议或者补充,欢迎在下方评论区留言!