一、组件基础
1.组件初印象
1.概念:
组件是把页面上可重用的部分封装成组件,方便项目的开发的维护
2.本质:
组件是有HTML结构,css样式,js业务逻辑的HTML自定义标签(下面详解)
上述这个描述可能存在一点点抽象,vue组件这个描述与我们之前学过的div用大盒子装载是一个意思的。我们拿官网的图来给兄弟姐妹们讲解:
上述我们之前一般是用三个大的div(Header、Main、Aside)来封装我们的页面的再细分下去其他模块结构,现在我们用vue组件表示来就是再Root这个根组件中用引入对应的组件标签就可以了
比如上述结构可以用标签这样写:
记住组件的本质:组件是有HTML结构,css样式,js业务逻辑的HTML自定义标签
这样是不是对组件有点感觉了呢!那我们来看看我们的组件是怎么组成的
2.组件组成
一个组件(.vue文件)由三个标签组成:
小提示 : 快速生成组件三大部分的快捷键:
- 标签,这里写组件的html结构
- 标签,这里写组件的js代码
- 标签,这里写组件的css代码
接下来我们来看个实际的例子
(小提示:按下<可快速生成一个.vue后缀的结构)
下面这张图兄弟姐妹们仔细看,对我们学vue很重要呦
组件 = (页面)盒子 = (html)自定义标签(HTML+CSS+JS) = (代码).vue文件 = (内存)vue实例
3.组件的分类与使用
我们的组件分为两种:①局部组件 ②全局组件
1.局部组件
就是在当前的.vue后缀文件中生效,出了当前.vue后缀的文件就失效。
下面我们来看看我们怎么使用我们的组件:
三大步:①导入 ②注册 ③使用
1.导入局部组件 : 在scrip标签中导入 import 组件名 from '组件路径' 2.挂载组件 : 在export default里面写一个属性components export default { components: { "标签名": 组件名 } } 3.使用组件 : 像标签一样使用即可,组件可以理解为一个自定义标签 <组件名></组件名>
全局|局部组件案例:
下面我创建两个.vue文件
1.App.vue
<template> <!-- 1.组件html代码,组件默认会把template里面的根元素作为挂载点因此下面不需要写el --> <!-- 为什么组件data必须是一个函数 :因为组件是需要复用的,如果组件data是一个对象,那么组件在复用的时候就会使用相同的对象地址。一旦在一个地方被修改,其他的也会跟着修改。 如果组件data是一个函数,组件在每一次复用的时候就会调用这个函数得到一个全新的对象,这样就可以做到在复用的时候每个组件之间的数据都是独立的,互不影响。 --> <div> <h1>我是App.vue</h1> <!-- 1.组件的使用 --> <MyPens></MyPens> </div> </template> <script> // 1.组件业务js导入 import MyPens from '@/components/MyPens.vue' export default { // 2.组件的注册 components: {MyPens}, // 存放vue的实例对象 data() { return { // 在这里写data数据.必须是函数!!!,{}本质是new出来了一个 } }, methods: {}, computed: {}, } </script> <style> /* 3.组件的样式 */ </style>
2.MyPens.vue
<template> <div> 存放HTML结构,不写标签就会报错 <hr> {{ msg }} </div> <!-- 1.组件中的最外层父元素只能有一个,不能添加两个平级父元素 正确: <div> <div></div> <div> 错误: <div></div> <div></div> --> </template> <script> // 2.js代码,写组件js业务逻辑 export default { //之前vue实例的代码写在这里:可以放data,methods,计算属性、侦听器等 //注意哟,组件里面的data是一个函数,返回值就是之前vue实例中的data对象 data(){ return{ msg:'我是初映CY,我是放在MyPens中的' } }, } </script> <style> /* 3.css:写组件样式 */ </style>
上述我们写了两个.vue分别为App.vue与MyPens.vue,我们在App.vue中引入我们MyPens.vue中的文件。
2.全局组件
使用方法与局部组件一样,唯一区别就是该组件的注册是在main
1.组件分类:
1.局部组件 2.全局组件
1.局部组件:
注册方法与局部组件注册一致,但是注册不在需要的.vue文件中注册,而是在main.js文件中注册
main.js(核心代码如下)
import MyButton from "@/components/MyButton.vue"//文件路径 Vue.component('MyButton',MyButton)//注册全局
下面我们引入下我们的组件到别的文件中
我们引入了到App与MyPens文件中,我们打开App.vue
可以看到我们的全局组件引入成功啦,以上就是全局与局部组件的使用方法。
【额外补充】
①浏览器默认打开加载App.vue文件
问题引入:为什么我们浏览器打开的默认是App.vue的文件呢?其实是因为我们在main.js中确定了App.vue是跟根文件入口,我们项目打开默认走的就是App.vue
这是我们的main.js文件现在的情况:
当我们修改了框起来的配置之后的样子:
当我们按照这个配置重新运行的时候页面加载的是:(在终端使用命令npm run serve)
可以发现我们成功的将页面的默认加载文件给更改了,改成了MyPens.vue。
②提高子组件css样式的权重
问题引入:当我们的子组件与父(根组件)有重名的的时候,我们是优先加载哪一个的css样式呢?
当我们MyPens.vue与App.vue都有个大div类名为color,并且我们都设置了相同的css样式:
同类名:
同css样式:
我们在浏览器中查看效果:
发现了我们子与父发生了同名的css样式,我们优先加载的是父(根)组件的。但是我们需要子属性的咋办?使用scoped属性即可
可见当我们在子组件中加载了scoped属性之后,我们子组件的样式就不会被覆盖掉,而是子显示子组件的样式,父显示父组件的样式。
二、组件传值
1.父传子
父传:
父中用v-bind来传数据
如:(v-bind:"属性名" =属性值 该指令可简写为:属性名="属性值")
<子组件> :arr="list"//这两种写法均可,推荐使用这种 v-bind:id="item.id" <子组件>
子收:
子组件中声明props来接收
props写的有两种:
①不声明数据类型写法(不推荐使用)
props:["属性名","属性名"]
如:
props:["name","price","id"]
注意点:是用数组包裹属性名
②声明数据类型写法(推荐使用)
对象名: { type: 数据类型, //注意前面需要加:解析不然是字符串类型 default: xxx, //默认值,可以不用写默认值 },
如:
props: { price: { type: Number, //注意前面需要加:解析不然是字符串类型 default: 100, //默认值 } }
2.子传父
子传:
this.$emit('方法名', 传递的数据)
如:
this.$emit('price', this.id)
注意点:方法名的书写需要引号
父收:
<子组件> @子组件定义的方法名='新方法名' </子组件>
如:
我们在父组件中导入组件并且讲方法名命名为dochange
<myGoods @price="doChange" ></myGoods>
在methods:{}中重新定义我们的方法dochange(此处是子组件传递过来的数据),这样我们父子传值就完成了。
methods: { // 接收MyGoods传来的值 3.根据定义事件来写属于我们的方法 doChange(id) { //看子文件中传入的事件对象 console.log(id) this.list.forEach((element) => { if (element.id === id) { element.price-- } }) }, },
父子组件案例:
子组件MyGoods.vue
<template> <div class="box"> <!-- 2.获取data中的值 --> <p>商品名称:{{ name }}</p> <p>商品价格:{{ price }}</p> <p>商品编号:{{ id }}</p> <button @click="doClick">点我来一🔪</button> </div> </template> <script> export default { // 1.父传子(单项数据流):子组件中声明props,会平铺到数据中 // props:["name","price","id"] // 上述写法不可传指定类型值,因此我们优化下写法: props: { name: String, price: { type: Number, //注意前面需要加:解析不然是字符串类型 default: 100, //默认值 }, id: { type: [String, Number], required: true, //必传 }, }, methods: { // 2.&emit:子传父 自定义事件 doClick() { // price是我们自定义的方法,this.id是传入的实参 this.$emit('price', this.id) // this.price-- //子组件与父组件绑定了,当只在子组件修改的时候,与父组件不同步因此会报错。一般处理数据都是子传父 // console.log(this.price); }, }, } </script> <style scoped> div { border: 2px solid rgb(6, 58, 199); } </style>
父组件MyGoods.vue
<template> <div> <h1>我是父组件</h1> <!-- 3.使用组件 --> <myGoods v-for="item in list" :key="item.id" :name="item.name" :price="item.price" :id="item.id" @price="doChange" ></myGoods> </div> </template> <script> // 1.导入组件 import MyGoods from '@/components/MyGoods.vue' export default { components: { MyGoods }, //2. 注册事件 data() { return { list: [ { name: '苹果手机', price: 8888, id: 1, }, { name: '小米手机', price: 5888, id: 2, }, { name: '华为手机', price: 7888, id: 3, }, ], } }, methods: { // 接收MyGoods传来的值 3.根据定义事件来写属于我们的方法 doChange(id) { //看子文件中传入的事件对象 console.log(id) this.list.forEach((element) => { if (element.id === id) { element.price-- } }) }, }, } </script> <style> div { border: 2px solid rgb(234, 226, 12); } </style>
好了兄弟姐妹们,先看下实际的效果:
可以看到我们的传值是完全OK的,父组件App.vue里面的数据与我们子文件MyGoods里面的数据是一摸一样的。可得知这两者数据相同并且得到了修改。
下面我们来尝试下,我们父传子之后,我们子不传递给父的情况:
这张图就灵活的显示了当我们在子组件中不传递数据给我们的父组件时,我们只是在我们的页面修改数据,这样就出现了一个小小的BUG,我们页面显示的(子组件数据)与我们App.vue中的数据并不是同步更新的,且当我们点击 来一刀 的时候右上交也会有报错提示。所以可以得出一个结论:当我们需要修改父中的数据我们需要传递过去给父改,我们的props是单项传输之读取的,需要$emit()传过去。