第二章 Vue组件化编程
模块与组件、模块化与组件化
模块
1.理解:向外提供特定功能的js程序,一般就是一个js文件
2.为什么:js文件很多很复杂
3.作用:复用js,简化js的编写,提高js运行效率
组件
1.理解:用来实现局部(特定)功能效果的代码集合(html/css/js/image…..)
2.为什么:一个界面的功能很复杂
3.作用:复用编码,简化项目编码,提高运行效率
类似于thymleaf模板中的封装html页面的做法,将一个网页的头部封装层一块砖,需要的时候直接调用就可以了。vue中的组件也就是一个html、css、js等集合。相当于一个盒子,放进去一块砖。
模块化
当应用中的js都以模块来编写的,那这个应用就是一个模块化的应用。
组件化
当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用,。
Vue中使用组件的三大步骤:
一、定义组件(创建组件)
二、注册组件
三、使用组件(写组件标签)
一、如何定义一个组件?
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别;
区别如下:
1.el不要写,为什么? ——— 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
2.data必须写成函数,为什么? ———— 避免组件被复用时,数据存在引用关系。
备注:使用template可以配置组件结构。
二、如何注册组件?
1.局部注册:靠new Vue的时候传入components选项
2.全局注册:靠Vue.component('组件名',组件)
三、编写组件标签:
<school></school>
非单文件组件使用
- 模板编写没有提示
- 没有构建过程,无法将ES6转换成ES5
- 不支持组件的CSS
- 真正开发中几乎不用
1.创建组件:使用Vue.extend创建(可省略不写)
const school = Vue.extend({
template:`
<div class="demo">
<h2>学校名称:{{schoolName}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
// el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
data(){
return {
schoolName:'尚硅谷',
address:'北京昌平'
}
},
methods: {
showName(){
alert(this.schoolName)
}
},
})
2.注册组件:局部和全局两种。
new Vue({
el:'#root',
data:{
msg:'你好啊!'
},
//第二步:注册组件(局部注册)
components:{
school,(这里可以放组件的名字,如果组件的名字就是你想要的名字可以这样简写)
student
}
})
//第二步:全局注册组件
Vue.component('hello',hello)
3.使用组件:直接到vue对象容器中编写组件标签
<div id="root">
<hello></hello>
<hr>
<h1>{{msg}}</h1>
<hr>
<!-- 第三步:编写组件标签 -->
<school></school>
<hr>
<!-- 第三步:编写组件标签 -->
<student></student>
</div>
组件使用的几个注意点
1.关于组件名:
一个单词组成:
第一种写法(首字母小写):school
第二种写法(首字母大写):School
多个单词组成:
第一种写法(kebab-case命名):my-school
第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
备注:
(1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
(2).可以使用name配置项指定组件在开发者工具中呈现的名字。
2.关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>
备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。
3.一个简写方式:
const school = Vue.extend(options) 可简写为:const school = options
组件的嵌套
在组件中注册其他组件,并将标签写在template标签里。
const school = Vue.extend({
name:'school',
template:`
<div>
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<student></student>
</div>
`,
data(){
return {
name:'尚硅谷',
address:'北京'
}
},
//注册组件(局部)
components:{
student
}
})
关于VueComponent
1.school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
2.我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。
3.特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!
4.关于this指向:
(1).组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
(2).new Vue(options)配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。
5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。
Vue的实例对象,以后简称vm。
vue(类似于后端的类)然后它的实例对象的__proto__(隐式原型对象)指向的就是vue本身的原型对象,然后这个原型对象的这个属性执行的是object基类
vueComponent实例对象可以找到这个方法原型对象,也可以找到vue的原型对象,多层找值。
1.一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
2.为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。
单文件组件开发方法
单文件组件是一.vue结尾的。运行需要脚手架环境。
单文件组件就是一个文件中只有一个组件,将html功能模块化。
首先创建两个vue组件:两者类似,这里只展示一个。组件的创建有三个板块,注意template下面只能放一个标签,最好用div括起来。
<template>
<div class="demo">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
</template>
<script>
export default { //这里是简写创建方式,export default是默认的暴露方式
//暴露之后才能被其他的引入。
name:'School',
data(){
return {
name:'尚硅谷',
address:'北京昌平'
}
},
methods: {
showName(){
alert(this.name)
}
},
}
</script>
<style>
.demo{
background-color: orange;
}
</style><template>
<div class="demo">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
</template>
<script>
export default {
name:'School',
data(){
return {
name:'尚硅谷',
address:'北京昌平'
}
},
methods: {
showName(){
alert(this.name)
}
},
}
</script>
<style>
.demo{
background-color: orange;
}
</style>
然后使用一个总的组件将其余的注册:
<template>
<div>
<School></School>
<Student></Student>
</div>
</template>
<script>
//引入组件,默认暴露的引入方式,有三种暴露方式,也有对应的引入方式
import School from './School.vue'
import Student from './Student.vue'
export default {
name:'App',
components:{
School,
Student //注册组件
}
}
</script>
一个js来创建vue对象然后注册总的组件:
import App from './App.vue'
new Vue({
el:'#root',
template:`<App></App>`,
components:{App},
})
最后在编写一个html页面运行。
<body>
<!-- 准备一个容器 -->
<div id="root"></div>
<script type="text/javascript" src="../js/vue.js"></script>
<script type="text/javascript" src="./main.js"></script>//加载这个需要el容器,也需要vue.js所以放在这里
</body>
</html>
第三章 脚手架
初始化脚手架
全局安装vue
npm install -g @vue/cli
使用指令创建项目
vue create xxxx
第三步:启动项目(可以在vscode中启动)
npm run serve
如出现下载缓慢请配置npm淘宝镜像:npmconfigsetregistryhttps://registry.npm.taobao.org
Vue脚手架隐藏了所有webpack相关的配置,若想查看具体的webpakc配置,请执行:vueinspect>output.js
src分析脚手架
脚手架是单文件组件开发的形式
脚手架在配置文件中固定了开始执行的main.js文件
/*
该文件是整个项目的入口文件
*/
//引入Vue
import Vue from 'vue'
//引入App组件,它是所有组件的父组件
import App from './App.vue'
//关闭vue的生产提示
Vue.config.productionTip = false
/*
关于不同版本的Vue:
1.vue.js与vue.runtime.xxx.js的区别:
(1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
(2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
2.因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用
render函数接收到的createElement函数去指定具体内容。
*/
//创建Vue实例对象---vm
new Vue({
el:'#app',
//render函数完成了这个功能:将App组件放入容器中
render: h => h(App),
// render:q=> q('h1','你好啊')
// template:`<h1>你好啊</h1>`,
// components:{App},
})
分析这个文件可以知道,这里引入的vue不是完整的vue.js。使不包含模板解析的js。因为脚手架会解析vue文件,所以基本上用不到vue的模板解析部分,这里的render函数代替了需要的模板引擎部分,所以不能写模板注册。
ref属性
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
<School ref="sch"/>
</div>
</template>
<script>
//引入School组件
import School from './components/School'
export default {
name:'App',
components:{School},
data() {
return {
msg:'欢迎学习Vue!'
}
},
methods: {
showDOM(){
console.log(this.$refs.title) //真实DOM元素
console.log(this.$refs.btn) //真实DOM元素
console.log(this.$refs.sch) //School组件的实例对象(vc)
}
},
props属性
<div>
<h1>{{msg}}</h1>
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>学生年龄:{{myAge+1}}</h2>
<button @click="updateAge">尝试修改收到的年龄</button>
</div>
</template>
<script>
export default {
name:'Student',
data() {
console.log(this)
return {
msg:'我是一个尚硅谷的学生',
myAge:this.age
}
},
methods: {
updateAge(){
this.myAge++
}
},
//简单声明接收
// props:['name','age','sex']
//接收的同时对数据进行类型限制
/* props:{
name:String,
age:Number,
sex:String
} */
//接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
props:{
name:{
type:String, //name的类型是字符串
required:true, //name是必要的
},
age:{
type:Number,
default:99 //默认值
},
sex:{
type:String,
required:true
}
}
}
</script>
app组件传递数据
<div>
<Student name="李四" sex="女" :age="18"/>
</div>
可以使用这个方法来对data中的数据传入,传入有三种方式,直接传递数据或者对数据做一些详细的限制。如果想对传入进来的参数修改之后在放入页面,可以使用另一个数据来接收这个传递进来的数据,然后对这个数据修改之后存放。
在app传递数据的时候,前面不加冒号代表里面是个字符串,加上冒号代表是个表达式。
mixin混合用法
可以将多个组件中共同的部分提取出来单独定义成一个js,然后使用多个组件引入。
export const hunhe = {
methods: {
showName(){
alert(this.name)
}
},
mounted() {
console.log('你好啊!')
},
}
export const hunhe2 = {
data() {
return {
x:100,
y:200
}
},
}
<script>
import {hunhe,hunhe2} from '../mixin'
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男'
}
},
mixins:[hunhe,hunhe2]
}
</script>
引入之后会将两者的data值合并,如果有相同的属性则以内部的为准(就近原则)。、
自定义插件
混合封装一些相同的方法引入到需要的地方,实现复用。
自定义插件可以定义一些全局的设置,然后引入到需要的vue里面。
export default {
install(Vue,x,y,z){
console.log(x,y,z)
//全局过滤器
Vue.filter('mySlice',function(value){
return value.slice(0,4)
})
//定义全局指令
Vue.directive('fbind',{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
})
//定义混入
Vue.mixin({
data() {
return {
x:100,
y:200
}
},
})
//给Vue原型上添加一个方法(vm和vc就都能用了)
Vue.prototype.hello = ()=>{alert('你好啊')}
}
}
import plugins from './plugins'
//应用(使用)插件
Vue.use(plugins,1,2,3)
这样vue对象全局都可以使用插件中的配置了。
scoped属性
使用组件的时候,每个组件都有单独的css样式,app组件会将所有的组件都整合到一起,这样模板解析之后所有的css都在同一个页面,那样就会造成css冲突,所以就出现了这个属性,可以为css样式生成一个随机的值。
<style scoped>
.title{
color: red;
}
</style>
todoList案例
最终需要完成的是这样,可以添加任务,删除任务。将添加的任务写到本地的cookie中。
使用vue的组件完成。首先可以将这些模块分为:MyHeader、MyList、MyItem、MyFooter四个部分,然后由app来统领。
配置网页结构
首先是配置mylist和myitem两个组件的包含关系;需要实现的功能:显示数据,勾选改变app中的值,删除引起app中的值改变。
然后就是配置头部myheader组件;需要的功能:给app中的数据添加值
最后配置底部;需要实现的功能:勾选,显示,清除完成的任务。
最后就是app组件 ,需要完成与其他组件的交互,很多其他组件的功能部分写在这里。
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :recever="recever"/>
<MyList :todo="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
<MyFooter :todo="todos" :checkAll="checkAll" :a="a"/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter.vue'
export default {
name:'App',
components:{MyHeader,MyList,MyFooter},
data() {
return {
//由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
todos:JSON.parse(localStorage.getItem('todos')) || []
}
},
watch:{
todos(value){
localStorage.setItem('todos',JSON.stringify(value))
}
},
methods:{
recever(x){
this.todos.unshift(x)
},
checkTodo(id){
this.todos.forEach((todo)=>{
if(todo.id==id)todo.done=!todo.done
})
},
deleteTodo(id){
this.todos=this.todos.filter((todo)=>{
return todo.id!==id
})
},
checkAll(x){
if(x){
this.todos.forEach((todo)=>{
todo.done=true
})
}else{
this.todos.forEach((todo)=>{
todo.done=false
})
}
},
a(){
console.log(this.todos)
this.todos=this.todos.filter((todo)=>{
return todo.done==false
})
console.log(this.todos)
}
}
}
</script>
浏览器本地存储
function saveData(){
localStorage.setItem('msg','hello!!!')
localStorage.setItem('msg2',666)
localStorage.setItem('person',JSON.stringify(p))
}
function readData(){
console.log(localStorage.getItem('msg'))
console.log(localStorage.getItem('msg2'))
const result = localStorage.getItem('person')
console.log(JSON.parse(result))
// console.log(localStorage.getItem('msg3'))
}
function deleteData(){
localStorage.removeItem('msg2')
}
function deleteAllData(){
localStorage.clear()
}
</script>
使用localStroage就可以
会话的存储(关闭浏览器就删除了)
<script type="text/javascript" >
let p = {name:'张三',age:18}
function saveData(){
sessionStorage.setItem('msg','hello!!!')
sessionStorage.setItem('msg2',666)
sessionStorage.setItem('person',JSON.stringify(p))
}
function readData(){
console.log(sessionStorage.getItem('msg'))
console.log(sessionStorage.getItem('msg2'))
const result = sessionStorage.getItem('person')
console.log(JSON.parse(result))
// console.log(sessionStorage.getItem('msg3'))
}
function deleteData(){
sessionStorage.removeItem('msg2')
}
function deleteAllData(){
sessionStorage.clear()
}
</script>
组件的自定义事件:两种实现方式
自定义事件使用方式:首先需要到组件上绑定自定义事件名称和回调函数(两种方式)。
然后在组件中定义一个方法来触发这个自定义事件,传递参数this.$emit('atguigu',this.name,666,888,900)
第一种方式,将自定义事件绑定到组件上
app组件中的自定义事件
<Student @atguigu="getStudentName" @demo="m1"/>、
student组件中的一个方法,用于触发组件上绑定的事件,使用$emit()可以实现绑定
sendStudentlName(){
//触发Student组件实例身上的atguigu事件
this.$emit('atguigu',this.name,666,888,900)
// this.$emit('demo')
// this.$emit('click')
},
app中自定义事件的方法
getStudentName(name,...params){
console.log('App收到了学生名:',name,params)
this.studentName = name
},
m1(){
console.log('demo事件被触发了!')
},
第二种方式,使用$on来绑定,与组件中的定义无关
<Student ref="student" @click.native="show"/>
mounted() {
this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
// this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
},
全局事件总线(GlobalEventBus)
1. 一种组件间通信的方式,适用于<span style="color:red">任意组件间通信</span>。
全局事件总线就是类似于自定义事件,但是自定义事件只能绑定在父子关系内。全局事件总线就是在vue上找一个代理,这个代理可以被所有的组件找到。并且这个代理上面有 $on $off $emit.
这样就可以在任意的链各个组件之间绑定事件了。
2. 安装全局事件总线:
new Vue({
el:'#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线
},
})
3. 使用事件总线:
1. 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的<span style="color:red">回调留在A组件自身。</span>
methods(){
demo(data){......}
}
mounted() {
this.$bus.$on('xxxx',this.demo)
}
2. 提供数据:```this.$bus.$emit('xxxx',数据)```
4. 最好在beforeDestroy钩子中,用$off去解绑<span style="color:red">当前组件所用到的</span>事件。
消息订阅与发布(pubsub)
1. 一种组件间通信的方式,适用于<span style="color:red">任意组件间通信</span>。
2. 使用步骤:
1. 安装pubsub:```npm i pubsub-js```
2. 引入: ```import pubsub from 'pubsub-js'```
3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的<span style="color:red">回调留在A组件自身。</span>
methods(){
demo(data){......}
}
......
mounted() {
this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
}
```
4. 提供数据:```pubsub.publish('xxx',数据)```
5. 最好在beforeDestroy钩子中,用```PubSub.unsubscribe(pid)```去<span style="color:red">取消订阅。</span>
nextTick
1. 语法:```this.$nextTick(回调函数)```
2. 作用:在下一次 DOM 更新结束后执行其指定的回调。
3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。
使用场景:在这个方法中,改变todo的值之后,需要等待vue将模板重新解析之后将输入框显示到网页上之后在执行获取焦点的方法。这样就需要调用这个api才能行。
handleEdit(todo){
if(todo.hasOwnProperty('isEdit')){
todo.isEdit = true
}else{
// console.log('@')
this.$set(todo,'isEdit',true)
}
this.$nextTick(function(){
this.$refs.inputTitle.focus()
})
},
Vue封装的过度与动画
1. 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。
1. 准备好样式:
- 元素进入的样式:
1. v-enter:进入的起点
2. v-enter-active:进入过程中
3. v-enter-to:进入的终点
- 元素离开的样式:
1. v-leave:离开的起点
2. v-leave-active:离开过程中
3. v-leave-to:离开的终点
2. 使用```<transition>```包裹要过度的元素,并配置name属性:
```vue
<transition name="hello">
<h1 v-show="isShow">你好啊!</h1>
</transition>
```
3. 备注:若有多个元素需要过度,则需要使用:```<transition-group>```,且每个元素都要指定```key```值。
css的写法直接略过,调用第三方动画的写法:首先安装这个库,然后引入,然后就可以用了。
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__swing"
leave-active-class="animate__backOutUp"
>
<h1 v-show="!isShow" key="1">你好啊!</h1>
<h1 v-show="isShow" key="2">尚硅谷!</h1>
</transition-group>
<script>
import 'animate.css'
..........
</script>