B站吴悠老师的vue课程学习笔记
概述
Vue是一个渐进式JS框架。什么是框架呢?与库又有什么区别?
库会以工具的方式(比如属性或方法)来帮助我们完成一些特定的功能,封装好一些函数供开发者使用,比如jQuery就是一个常用的JS库。而框架比较复杂,有很多自己的语法,因此需要学习框架的使用方式。
Vue将网页开发中最常见的DOM操作内置到了框架之中,可以使我们专注于数字逻辑处理而避免了繁琐的dom操作,大大降低开发难度。同时使用MVVM模式,将数据和页面呈现相分离,降低代码耦合度,支持组件化开发。
组件是web页面上抽出来一个个包含模板html、功能js、样式css的单元,好的组件具备封装性、正确性、扩展性、复用性
Vue2核心语法
响应式数据与插值表达式
在使用js编写的传统页面中,如果想要修改某个页面上的值,需要先获取要修改的dom,再通过textContent方法改值,每次修改都需要加一行修改代码,dom操作非常的繁琐。
在vue中这项操作变得方便快捷。只需要创建一个vue,绑定好要修改的类,然后在data的return里直接设置要修改的值。页面会同步响应,无需再手动进行dom操作。后续也可以在控制台通过vue实例访问甚至修改这个变量,页面上也会同步显示。对了,别忘了在html标签中使用两个大括号绑定插值表达式,这个和模板字符串有点类似。
还有methods函数属性,可以在其中定义一些函数,在页面上调用,调用时记得加括号
<body>
<div id="app">
<!-- 插值表达式 -->
<p>{{ title }}</p>
<p>{{ content }}</p>
<!-- 插值表达式也可以进行一些逻辑运算 -->
<p>{{1 +2 + 3}}</p>
<p>{{ 1 > 2 ? '对' : '错' }}</p>
<p>{{ output() }}</p>
</div>
<script src="./vue.min.js"></script>
<script>
const vm = new Vue({
// 1. 响应式数据与插值表达式
// 在vue内部对数据做操作会自动更新到视图中
el: '#app', // el属性用于设置vue的生效位置,是选择器的语法
data () { // data用于声明响应式数据
return { // 可以直接通过vue实例访问
title: '这是标题',
content: '这是内容'
}
},
methods: { // methods函数属性
output () {
return '标题为:' + this.title + ',内容为:' + this.content
}
}
})
</script>
</body>
计算属性
如果在页面中频繁调用method函数属性里面的方法,会造成冗余的运算,这时候就可以用computed计算属性,它与函数属性类似,不过具有缓存的功能,只有在响应式数据变化时才会重新计算,所以如果都设置控制台输出,分别调用三次methods和三次computed,methods会输出三次,而computed只会输出一次。注意,调用computed里的方法时不加括号。
// 2. computed计算属性:具有缓存性,响应式数据变化时才会重新计算
computed: {
outputContent () {
console.log('computed执行了')
return '标题为:' + this.title + ',内容为:' + this.content
}
},
监听器
如果想要在响应式数据出现变化时进行一些其他的操作,就可以使用watch监听器
// 3. watch监听器:监听某个数据是否有变化,传入两个值,分别是新值和旧值
watch: { // 比如监听title数据
title (newValue, oldValue) {
console.log(newValue, oldValue);
}
}
指令
有几类可以绑定在标签中的指令
<!-- 4. 指令v- -->
<!-- 内容指令 -->
<p v-text="htmlContent"></p> <!-- 会覆盖标签原始内容 -->
<p v-html="htmlContent"></p> <!-- 可以解析html标签 -->
<!-- 渲染指令 -->
<!-- for循环,item是值,key是键,index是索引 -->
<p v-for="item in arr">这是数组:{{item}}</p>
<p v-for="(item, key,index) in obj">这是对象:{{item}}{{key}}{{index}}</p>
<p v-if="bool">标签内容</p> <!-- 条件判断元素销毁 -->
<p c-show="bool">标签内容</p> <!-- 元素显示,如果是false,display属性会变成none -->
<!-- 属性指令 -->
<p v-bind:title="title">鼠标悬停显示标题内容</p>
<pv :title="title">这是内容</p> <!-- 简写 -->
<!-- 事件指令 -->
<button v-on:click="output">按钮</button>
<button @click="output">按钮</button> <!-- 简写 -->
<!-- 表单指令:v-model实现双向数据绑定 -->
<input type="text" v-model="inputValue">
<p v-text="inputValue"></p>
<!-- 下拉框,v-model会忽略元素表单的value、checked、selected属性的初始值,使用vue实例的数据作为数据来源。并且,v-model的表达式未设置值时,select元素将被渲染为未选中,可能导致用户无法选择第一个选项。因此建议设置一个值为空的禁用选项 -->
<select v-model="selected">
<option value="" disabled>请选择--</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
修饰符
用来实现与指令相关的一些操作,用来简化代码和dom操作
<!-- 5. 修饰符 -->
<input type="text" v-model.trim="inputValue"> <!-- 去除前后空格 -->
<!-- 还有一些事件修饰符 -->
Vue CLI
Vue CLI是Vue基于Webpack打造的脚手架工具,内置了很多模板和工具,可以快速进行Vue的项目搭建
具体的安装过程就不赘述了,网上有很多教程,但是在安装vue/cli时记得使用镜像,不然会等一两个小时还装不好
常用的指令有几种
- 创建项目:
vue create 项目名
,然后进入项目中 - 进入脚手架内部的静态资源服务器:
npm run serve
- 进行代码打包:
npm run build
然后会出现dist目录 - 进入指定目录下的静态资源服务器:
npm i serve -g
然后serve 目录名
组件化开发
组件听起来陌生,但是组件的思想我们早就已经接触过。如果一个程序中有很多冗余的代码来完成相似的任务,我们可以把这种功能抽出来封装成函数,在需要的时候直接调用就可以了。组件也是类似的功能,本质上是自定义的模板。比如需要写很多相似的切换按钮或者列表结构时,就可以自己写一些新标签作为模板,然后就可以把这些标签当作html标签拿来用,可以更好地完成前端页面功能的封装。而且组件可以多重嵌套,这样代码的复用度就会很高,也便于代码的维护。
<body>
<!-- view层 -->
<div id="app">
<!-- 使用自定义组件 -->
<!-- ite是在vm的items中取出的每一项数据,通过v-bind绑定到组件的item属性上 -->
<my-component v-for="ite in items" v-bind:item="ite"></my-component>
</div>
<!-- module层 -->
<script src="./vue.min.js"></script>
<script>
// 自定义组件
Vue.component("my-component", { // 组件的名字
props: ["item"], // 接收vue传递的参数,否则无法在组件中使用
template: "<li>{{ item }}</li>" // 组件的模板
})
var vm = new Vue({
el: "#app",
data() {
return {
items: ["java", "python", "linux"]
}
}
})
</script>
</body>
实例
每个组件文件(.vue)都由三个部分组成,template,script,style。
template是结构也就是模板;script是逻辑,其中的中的export default是暴露的接口;style是样式。
- src目录中的App.vue是根组件,其他子组件要先在根组件中导入路径并在compones中引入后才能使用,使用时用html标签的格式写在template中。子组件也是用.vue文件的形式写在src的components目录中。
- 以helloworld为例,当打包时看到在某一个组件的结构里面存在一个helloworld标签,它就会明白这不是HTML内置的标签,就会去找有没有这样一个组件,就找到了helloworld.vue中的配置对象(export default),然后根据这个对象进行new vue操作。
- App.vue这种根组件也有类似的结构,但是它没有父组件,是在入口处也就是main.js中的render函数进行组件的实例化创建。
- vue组件在使用时就相当于一个自定义HTML标签,但其实每一个vue组件都是一个独立的vue实例,会在编译环节进行结构生成并替换掉自定义的标签。所以vue的实例属性在组件中也是可以使用的,不过el属性只能在根组件中进行设置,内部子组件无需el属性。
组件通信方式
在开发当中,组件和组件之间是有关联性的,体现在数据的交互上,父组件可以给子组件传数据,子组件也可以给父组件传数据,也可以双向传输。
父组件传子组件
父组件在template标签中引用了来自子组件的helloworld标签,并在标签内部自定义了几个属性,想要把这些属性传输给子组件
在子组件的export default中通过props进行处理,里面对父组件传来的每个数据进行设置,包括类型、默认值、是否必填等等。
也可以传输响应式数据,只需要在父组件标签添加属性时加上:,并且在export default中设置data就行,与前面学过的语法是一样的
下面是实例,为了避免啰嗦,style标签我就不粘上来了
父组件App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<!-- 引入组件使用html标签,给子组件设置属性msg、count和响应式数据xiangying -->
<h1>父组件中接收到的数据: {{ childData }}</h1>
<HelloWorld
msg="Welcome to Your Vue.js App"
count="10"
:xiangying="Parentxiangying"
></HelloWorld>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: { // 引入组件
HelloWorld
},
data () { //设置响应式数据
return {
Parentxiangying: 1000,
}
}
}
</script>
子组件HelloWorld.vue
<template>
<div class="hello">
在子组件中展示从父组件中传来的值
<h1>{{ msg }}</h1>
<p>props中的count{{ count }}</p>
<p>props中的响应式数据{{ xiangying }}</p>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
// 组件通信
// 1. 父传子:通过 props 进行处理
props: {
msg: String,
count:{
type: [String, Number],
default: 100, // 默认值,适用于可选项
required: true // 是否必填,如果是true但不填控制台会报错
},
// 也可以设置响应式数据
xiangying: Number
}
</script>
子组件传父组件
这个有点复杂,需要通过自定义事件处理。
我们想把子组件的数据传输到父组件,就要在子组件编写一个事件,比如点击按钮。在子组件的template中加上一个按钮<button @click="handler">按钮</button>
,并在export default中声明按钮中的函数,比如这个我们是点击按钮使得childCount属性++(别忘了在data中设置这个响应式数据),并且用$emit
传输到父组件,参数是自定义事件名和要传递的数据。
父组件内部在HelloWorld标签中添加上这个事件属性。这里我们为了直观,新设置一个参数childData并在页面中展示出来(别忘了在data中设置这个响应式数据),然后在methods中编写这个父组件的方法。
最后效果就是点击页面上子组件的按钮,变化的数据就会传到父组件的标签中并显示出来
总之就是子组件自己编了一个child-count-change事件,由父组件做监听,通过子组件内部调用this.$emit来触发父组件的监听事件并传值,从而触发父组件的函数
父组件App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<h1>父组件中接收到的数据: {{ childData }}</h1>
<HelloWorld
@child-count-change="handlers"
></HelloWorld>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: { // 引入组件
HelloWorld
},
data () { //设置响应式数据
return {
childData: 0
}
},
methods: {
handlers (childCount) {
this.childData = childCount
}
}
}
</script>
子组件HelloWorld.vue
<template>
<div class="hello">
<button @click="handler">按钮</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
// 2. 子传父:通过自定义事件处理
// 子组件自己编的事件名为child-count-change,由父组件做监听
// 通过子组件内部调用this.\$emit来触发父组件的监听事件并传值,从而触发父组件的函数
data () {
return {
childCount: 0
}
},
methods: {
handler () {
this.childCount++
// 自定义事件名,要传递的数据
this.$emit('child-count-change', this.childCount)
}
}
}
</script>
同级之间传递
可以通过父组件来中转,也可以通过EentBus(通过一个额外的vue实例来做存储),如果两个组件离得很远,就可以使用Vuex,具体在下节讲解
组件插槽
如果父组件在使用子组件的时候想自己定义一些东西,就可以使用插槽,在父组件引用的双标签中间写一些东西,就可以在子组件的slot标签的位置呈现出来。本质上就是父子传值,将父组件中定义的数据通过插槽的形式传输到子组件中。
插槽有几种形式,首先就是默认插槽,子组件的slot不加任何修饰,父组件的双标签中的内容分就会查到这个默认位置,如果双标签中没有内容,就会显示slot中的默认内容;其次是具名插槽,如果有好几个插槽,想指定某个特定位置,在slot插槽中加上name="插槽名"
,在父组件的双标签中加上<template v-slot:插槽名>
,或者简写成#name
,就可以把父组件的特定内容传到子组件特定位置的插槽;最后是作用域插槽,适用于想在父组件中使用子组件值的情况,在slot中写上:name="子组件中的值名"
,要注意传递的是整个对象,要在父组件中写清楚<template #插槽名="dataObj"> {{ dataObj.name }}
,或者用解构的写法,<template #header="{ name }"> {{ name }}
父组件App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld
msg="Welcome to Your Vue.js App"
count="10"
:xiangying="Parentxiangying"
@child-count-change="handlers"
></HelloWorld>
<hr>
<HelloWorld>
我是默认插槽,对应普通的slot
<template v-slot:header>我是header插槽,使用特定插槽需要在template标签中加上相应的name,v-slot:name可简写成#name</template>
</HelloWorld>
<hr>
<HelloWorld>
<template #header="dataObj">作用域插槽,子组件给父组件传的值是整个对象,使用时需要在注明是对象的哪个值,或者在传值时加上大括号{{ dataObj.childcount }}</template>
</HelloWorld>
</div>
</template>
子组件HelloWorld.vue
<template>
<div>
<slot name="header" :childcount="childCount">我是hander插槽的默认内容</slot>
<br>
<slot>我是默认插槽的默认内容,如果父组件中没有自定义插槽内容,就显示我</slot>
</div>
</template>
吴悠老师讲的例子可能比较复杂,这里加上一个秦疆老师的例子帮助理解。另外,数据存储在Vue实例中,如果想在组件中删除数据,会涉及到参数传递与事件分发,与前面讲过的是一样的用法
<body>
<!-- view层 -->
<div id="app">
<!-- 使用组件和插槽,只不过插槽是用组件的形式插进来的 -->
<todo>
<todo-title slot="todo-title" :title="title"></todo-title>
<todo-items slot="todo-items" v-for="(item, index) in todoItems"
:item="item" :index="index" v-on:remove="removeItems(index)"></todo-items>
<!-- 前端的remove事件绑定到vue的removeItems方法 -->
</todo>
</div>
<!-- module层 -->
<script src="./vue.min.js"></script>
<script>
// todo父组件,规定了框架,里面有两个插槽,使用反斜杠换行
Vue.component("todo", {
template: '<div>\
<slot name = "todo-title"></slot>\
<ul>\
<slot name = "todo-items"></slot>\
</ul>\
</div>'
})
// 两个子组件具名插槽,列表的名字和列表项,使用props传递数据
Vue.component("todo-title", {
props:["title"],
template: '<div>{{ title }}<div>'
})
Vue.component("todo-items", {
props:["item", "index"],
// 只能绑定当前组件的方法
template: '<li>{{ index }}--{{ item }}<button @click="remove">删除</button></li>',
methods: {
remove(index) {
this.$emit("remove", index) // 自定义事件分发,绑定到前端的remove事件
}
}
})
var vm = new Vue({
el: '#app',
data: {
title: '前端三件套',
todoItems: ['html', 'css', 'javascript']
},
methods: {
removeItems(index) {
console.log("删除了"+this.todoItems[index])
this.todoItems.splice(index, 1) // 从数组中索引为index的元素开始删除几个元素
}
}
})
</script>
</body>
Axios
Axios是一个可以在浏览器和nodejs使用的异步通信框架,用来实现ajax的异步通信,与jQuery相比减少了频繁的dom操作。跟vue一样,需要下载或者引入后使用。axios提供了很多钩子,方便在生命周期的不同阶段获取数据。
使用的格式也比较固定: axios.get().then(response => ())
<body>
<!-- view层 -->
<div id="app">
<div>{{ info.name }}</div>
<div>{{ info.address.country }}</div>
<a v-bind:href="info.url">点我</a>
</div>
<!-- module层 -->
<script src="./vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var vm = new Vue({
el: '#app',
// data: 是属性,data(){}是方法
data() {
return {
// 请求的返回参数格式必须和json字符串格式一致
info:{
name: null,
sex: null,
address: {
street: null,
city: null,
country: null
},
url: null
}
}
},
mounted() { // 钩子函数,页面加载完成后执行,格式是固定的axios.get().then(response => ()),这里是把读到的数据返回给info
axios.get('../data.json').then(Response => (this.info = Response.data))
}
})
</script>
</body>
VueRouter和Vuex
vue-router
是vue的官方路由插件,用于构建单页面应用。传统的页面应用是用超链接来实现页面切换和跳转的,但vue是单页面应用,相当于只有一个index.html的页面,不能用a标签。在vue-router单页面应用中,页面的切换是路径之间的切换,也就是组件的切换。路由模块的本质就是建立起url和页面之间的映射关系,做页面导航。
基本使用方法
- 先定义要展示的子组件(路由),在其script中写上默认导出属性
- 再在index.js也就是主页面中导入子组件的路径,并在routes中加上这个路由配置信息比如path、name、component
- 最后在父组件中的写上router-link,绑定路由链接
VidoeView.vue
<template>
<h3>一级组件视频信息</h3>
</template>
<script>
export default {
name: 'VideoView'
}
</script>
index.js
import VideoView from '../views/VideoView.vue'
const routes = [
{
path: '/video',
name: 'video',
component: VideoView
}
]
App.vue
<template>
<div id="app">
<nav>
<!-- vue router提供的两个组件,router-link和router-view -->
<!-- router-link用来定义路由链接,router-view用来渲染当前路由匹配到的组件 -->
<router-link to="/">Home</router-link> |
<!-- 也可以通过name属性绑定路由链接 -->
<router-link :to="{name: 'video'}">Video</router-link>
</nav>
<router-view/>
</div>
</template>
动态路由
如果我们想打开不同的视频,这些视频的地址几乎一样,只有编号不同,就可以使用动态路由。
在index.js的routes中的路径后面加上:id,并写上props: true,这样就可以在在子组件内部访问这个动态路由;子组件中通过来使用动态路由<p>视频id为:{{ id }}</p>
;App.vue的router有两种写法,<router-link :to="{name: 'video', params: { id: 30 }}">Video</router-link>
或者<router-link to="/video/30">Video</router-link>
嵌套路由
如果想查看某个视频的点赞量、弹幕等参数,也就是一级功能里还有二级功能,就需要二级的路由子组件。
在index.js中要嵌套的父路由中加一个children属性,是一个数组,里面写要嵌套的子路由,写法和普通路由类似,就是path、name、component;别忘了在文件头引入新的子路由,可以把子路由写在一个文件夹里。接下来就是写这两个子路由,定义样式和默认导出参数。最后在父路由中加上子路由的router-link和router-view,写法和App.vue中的一样。
index.js
import VideoView from '../views/VideoView.vue'
import VideoInfo1 from '../views/video/VideoInfo1.vue'
import VideoInfo2 from '../views/video/VideoInfo2.vue'
const routes = [
{
path: '/video/:id', // 动态路由
name: 'video',
component: VideoView,
children: [ // 子路由,数组,形式和普通路由类似
{ path: 'info1', name: 'video-info1', component: VideoInfo1 },
{ path: 'info2', name: 'video-info2', component: VideoInfo2 }
],
props: true // 这样就可以在组件内部访问到动态路由的值
}
]
VideoInfo1.vue
<template>
<div class="video-info1">
<h3>二级组件:点赞情况分析</h3>
</div>
</template>
<script>
export default {
name: 'VideoInfo1'
}
</script>
VideoInfo2.vue
<template>
<div class="video-info1">
<h3>二级组件:互动情况分析</h3>
</div>
</template>
<script>
export default {
name: 'VideoInfo2'
}
</script>
VideoView.vue
<template>
<div class="video">
<h3>一级组件:视频信息</h3>
<p>视频id为:{{ id }}</p>
<router-link :to="{name: 'video-info1', params: { id: 30 }}">点赞信息</router-link>
<router-link :to="{name: 'video-info2', params: { id: 30 }}">互动信息</router-link>
<router-view></router-view>
</div>
</template>
编程式导航
前面我们学的都是通过点击router-link来切换页面,那么如果我们想要主动地跳转页面比如登陆状态过期了跳转到首页,该怎么办呢?
在要跳转的页面的默认导出属性中加一个生命周期钩子create(组件创建完毕后执行),里面有一些操作,用router写具体要跳转的页面或者执行的函数,也可以写在计时器里
<script>
export default {
name: 'VideoInfo1',
created () { // 当组件创建完毕以后会执行的生命周期钩子
setTimeout(() => { // 3秒后跳转到首页
// router是用来进行路由操作的工具
this.$router.push({ name: 'home' })
}, 3000)
}
}
</script>
路由传参与导航守卫
除了编程式导航以外还有一些搭配的操作,比如希望在跳转路由的时候进行数据的传递,就要在在上面的基础上通过query加入要传递的参数,并在跳转的目标页面也写create并使用route接收相应的数据
<script>
export default {
name: 'VideoInfo1',
created () { // 当组件创建完毕以后会执行的生命周期
setTimeout(() => {
// router是用来进行路由操作的工具
// 3秒后跳转到info2并进行数据传递
this.$router.push({ name: 'video-info2', query: { someData: 'info1传递的数据' } })
}, 3000)
}
}
</script>
VideoInfo2.vue
<script>
export default {
name: 'VideoInfo2',
created () {
// route是用来接收数据的
// 在控制台打印接收的数据
console.log(this.$route.query)
}
}
</script>
导航守卫:给所有导航统一做设置,比如进度条。在index.js中写router.beforeEach,三个参数是to,from,和next(接下来干什么)
index.js
// 全局导航守卫,在每次导航触发之前会进行这个触发
router.beforeEach((to, from, next) => {
console.log('路由触发了')
next()
})
Vuex
是一个全局的数据存储工具,提供了五个功能,写在store的index.js当中
- state:用来存储全局的数据,在其他组件中都可以访问,可以写成函数的形式
- mutations:修改全局数据,类似于函数,通过commit提交,必须是同步的
- actions:用来做异步包装
- getters:具有缓存性,相当于计算属性,只有数据变化的是时候才会触发
- modules:模块,便于数据管理避免混乱,在使用时注明是a中的数据还是b中的数据
index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state () { // 在全局存数据,其他组件都可以访问,可以写成函数方式
return {
loginStatus: '用户已经登陆',
count: 0
}
},
getters: { // 具有缓存性,相当于计算属性,只有数据变化的是时候才会触发
len (state) {
console.log('getters执行了')
return state.loginStatus.length
}
},
mutations: { // 修改全局数据,类似于函数,通过commit提交,必须是同步的
changeCount (state, num) {
state.count += num
console.log('mutation执行了,count值为', state.count)
}
},
actions: { // 用来做异步包装
delaychangeCount (store, num) {
setTimeout(() => {
store.commit('changeCount', num)
}, 3000)
}
},
modules: { // 模块,便于数据管理避免混乱,在使用时注明是a中的数据还是b中的数据
a: {
state,
mutations
},
b: {
state,
mutations
}
}
})
VideoInfo2.vue
<script>
export default {
name: 'VideoInfo2',
created () {
// route是用来接收数据的
// 在控制台打印接收的数据
// console.log(this.$route.query)
console.log(this.$store.state.loginStatus)
this.handler()
},
methods: {
handler () { // 全局状态往往在主逻辑中的某个位置,要用的时候才使用
// 报错不知为何TypeError: Cannot read properties of undefined (reading 'commit')
this.$sotre.commit('changeCount', 1)
this.$sotre.commit('changeCount', 2)
this.$store.dispatch('delayChangeCount', 10)
this.$sotre.commit('changeCount', 3)
console.log(this.$store.getters.len) // 虽然调用多次,但是数据没有变化,只会执行一次
console.log(this.$store.getters.len)
console.log(this.$store.getters.len)
}
}
}
</script>