知识点自测
- this指向=》调用者
let obj = {
fn: function(){
// this指向此函数的调用者
},
fn () {
// this指向当前函数的调用者 (如果都是在vue里, this指向的都是vue实例对象)
},
fn: () => {
// this指向外层函数作用域this的值
}
}
obj.fn();
axios().then(res => {
// 这里的this的值是多少哦?
})
- =作用
let a = 10;
let b = a;
b = 20; // 基础类型, 单纯的值的赋值
let a = {name: "哈哈"};
let b = a; // a变量的值是引用类型, a里保存的是对象在堆的内存地址, 所以b和a指向同一个对象 (引用类型=是内存地址的赋值)
b.name = "刘总";
今日学习目标
- 能够理解vue组件概念和作用
- 能够掌握封装复用组件能力
- 能够使用组件之间通信
- 能够完成todo案例?
vue组件化开发
什么是组件化开发
组件是可复用的 Vue 实例, 封装标签, 样式和JS代码
.vue组件分类:
- 页面组件
- 页面下的功能组件
组件化开发 :一个页面(.vue)可能有一个或多个组件(.vue)组成完整的页面功能
- 封装的思想,把页面上
可重用的部分封装为组件,从而方便项目的 开发 和 维护
一个页面, 可以拆分成一个个组件,一个组件就是一个整体, 每个组件可以有自己独立的 结构(template) 样式(style) 和 行为(script) (html, css和js)
为什么用组件化开发?
以前做过一个折叠面板, 如何实现多个折叠面板?
方案1: 复制代码,不同的部分, 用不同的isShow变量
<template>
<div id="app">
<h3>案例:折叠面板</h3>
<div>
<div class="title">
<h4>芙蓉楼送辛渐</h4>
<span class="btn" @click="isShow = !isShow">
{{ isShow ? '收起' : '展开' }}
</span>
</div>
<div class="container" v-show="isShow">
<p>寒雨连江夜入吴, </p>
<p>平明送客楚山孤。</p>
<p>洛阳亲友如相问,</p>
<p>一片冰心在玉壶。</p>
</div>
</div>
<div>
<div class="title">
<h4>芙蓉楼送辛渐</h4>
<span class="btn" @click="isShow1 = !isShow1">
{{ isShow1 ? '收起' : '展开' }}
</span>
</div>
<div class="container" v-show="isShow1">
<p>寒雨连江夜入吴, </p>
<p>平明送客楚山孤。</p>
<p>洛阳亲友如相问,</p>
<p>一片冰心在玉壶。</p>
</div>
</div>
<div>
<div class="title">
<h4>芙蓉楼送辛渐</h4>
<span class="btn" @click="isShow2 = !isShow2">
{{ isShow2 ? '收起' : '展开' }}
</span>
</div>
<div class="container" v-show="isShow2">
<p>寒雨连江夜入吴, </p>
<p>平明送客楚山孤。</p>
<p>洛阳亲友如相问,</p>
<p>一片冰心在玉壶。</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false,
isShow1: false,
isShow2: false
}
}
}
</script>
<style>
body {
background-color: #ccc;
}
#app {
width: 400px;
margin: 20px auto;
background-color: #fff;
border: 4px solid blueviolet;
border-radius: 1em;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
padding: 1em 2em 2em;
}
h3 {
text-align: center;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #ccc;
padding: 0 1em;
}
.title h4 {
line-height: 2;
margin: 0;
}
.container {
border: 1px solid #ccc;
padding: 0 1em;
}
.btn {
/* 鼠标改成手的形状 */
cursor: pointer;
}
</style>
上面的代码:不利于维护
小结
- 现代前端开发均会使用组件化的开发思路
- 组件化开发有利于解决代码重复、冗余等问题
vue组件-封装使用
目标
掌握组件封装使用的基本步骤
为啥要封装组件
- 复用。一次封装,多次使用
- 代码整理,方便维护。
步骤
- 定义组件
- 注册组件
- 使用组件
案例
定义一个名为MyCom的组件,并在App.vue中使用它
目录
├── App.vue # 在App.vue内部,导入并使用组件
└── MyCom.vue
- 创建组件: MyCom.vue
- 引入并注册组件
// 局部注册组件
// 进入到当前组件内部
// 1. 导入组件
import 组件名 from './组件文件.vue'
// 2. 局部注册
export default {
components: {
组件名: 组件名
}
}
- 使用组件。在当前页面中,当做标签来使用。
注意:
组件名不能与现有的html标签名一致。
小结
- 每一个组件都是封闭的。它有自己的template, script,style
- 组件之间可以相互引用使用。
vue组件-用scoped实现组件的私有样式
目标
解决多个组件样式名相同, 冲突问题
问题说明
默认组件style 中定义的样式是全局=》存在相同名字覆盖的情况
解决方案
局部样式:在style标签上加上scoped属性
<stype scoped>
h2 {} // 样式只会在当前组件内生效
</style>
原理
- 在style上加入scoped属性, 就会在此组件的标签上加上一个随机生成的data-v开头的属性
- 而且必须是当前组件的元素或者子组件的根元素, 才会有这个自定义属性
小结
style上加scoped, 组件内的样式只在当前vue组件生效;相反,样式就是全局的
vue组件-/deep/深度作用选择符
问题导入
当父子组件都使用了scoped的情况下,如何在父组件中控制子组件的样式?
解决方案
父组件的选择器 /deep/ .子组件的选择器
父组件
<template>
<div class="box">
<h1 class="red">父组件</h1>
<hr />
+ <Child />
</div>
</template>
<script>
import Child from '@/components/Child'
export default {
components: {
Child,
},
}
</script>
<style scoped>
.red {
color: blue;
}
+ .box /deep/ h2 {
+ color: lawngreen;
+ }
</style>
子组件:
<template>
<div>
<h2>子组件</h2>
<p class="red">
<span>123</span>
</p>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.red {
color: red;
}
</style>
小结
父组件中控制子组件元素或类名,覆盖样式=》需要在前边加上 /deep/
注意⚠️:默认子组件的根元素,会带上父组件的data-v-hash属性,所以可以直接控制
vue封装组件-实操-改造折叠面板
目标
封装并使用组件
思路
哪部分template, scripte, style 复用, 就把哪部分封装到组件内,
操作
1. 创建组件
在components下创建一个文件:Pannel.vue
<template>
<div>
<div class="title">
<h4>芙蓉楼送辛渐</h4>
<span class="btn" @click="isShow = !isShow">
{{ isShow ? "收起" : "展开" }}
</span>
</div>
<div class="container" v-show="isShow">
<p>寒雨连江夜入吴,</p>
<p>平明送客楚山孤。</p>
<p>洛阳亲友如相问,</p>
<p>一片冰心在玉壶。</p>
</div>
</div>
</template>
<script>
export default {
name: "Pannel",
data() {
return {
isShow: false,
};
},
};
</script>
<style scoped>
.title {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #ccc;
padding: 0 1em;
}
h4 {
line-height: 2;
margin: 0;
}
.container {
border: 1px solid #ccc;
padding: 0 1em;
}
.btn {
/* 鼠标改成手的形状 */
cursor: pointer;
}
</style>
总结: 封装标签+样式+js - 组件都是独立的, 为了复用
2.注册使用
在页面中使用:
<template>
<div id="app">
<h3>案例:折叠面板</h3>
+ <Pannel></Pannel>
</div>
</template>
<script>
+ import Pannel from './components/Pannel.vue'
export default {
components: {
+ Pannel: Pannel // key随便定义的组件名(也是一会使用的自定义标签名, 不能与已知的 标签重名)
}
}
</script>
<style lang="less">
body {
background-color: #ccc;
#app {
width: 400px;
margin: 20px auto;
background-color: #fff;
border: 4px solid blueviolet;
border-radius: 1em;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
padding: 1em 2em 2em;
h3 {
text-align: center;
}
}
}
</style>
组件使用总结:
- (创建)封装html+js+css到独立的.vue文件中
- (导入注册)组件文件 => 得到组件配置对象
- (使用)当前页面当做标签使用
vue组件通信
背景
- 一个页面有多个组件构成
- 每个组件之间的数据是相互独立的
问题: 如何在组件之间做通讯?
因为每个组件的变量和值都是独立的=》 如果想获取对方页面中定义的变量应该怎么做?
组件通信先暂时关注父传子(数据从父组件传递给子组件) , 子传父(数据从子组件传递给父组件)
-
父: 使用其他组件的vue文件
-
子: 被引入到这个vue文件的组件(嵌入)
vue组件通信_父传子-理论示例
目标
掌握父传子的用法
父子组件
如果一个组件A在组件B中被导入使用,称组件B是父组件,组件A是子组件
格式
示例代码
父组件
<template>
<div style="border:1px solid #ccc; margin:5px;padding:5px">
<h1>父组件</h1>
<!-- 1. 父传。自定义属性 -->
<MyCom :abc="userName" :list="hobby"/>
</div>
</template>
<script>
// 导入->注册->使用
import MyCom from './MyCom.vue'
export default {
data(){
return {
userName: '小花',
hobby: ['vue','react']
}
},
components: { MyCom }
}
</script>
<style>
</style>
子组件
<template>
<div style="border:1px solid #ccc; margin:5px;padding:5px">
<h2>子组件</h2>
<!-- 使用 -->
{{abc}}
<p>
{{list[0]}}
</p>
<button @click="fn">打印</button>
</div>
</template>
<script>
export default {
// 2.子接
props: ['abc', 'list'],
methods: {
fn(){
console.log(this, this.abc)
}
}
}
</script>
<style>
</style>
示意图
案例-示例
需求
封装一个商品组件MyProduct.vue - 外部传入具体要显示的数据, 如下图所示
步骤
- 创建组件components/MyProduct.vue - 准备标签
- 组件内再props定义变量, 用于接收外部传入的值
props属性名建议都小写,因为标签里的属性只能小写/把变量驼峰转成-连接 - App.vue中引入注册组件, 使用时, 传入具体数据给组件显示
components/MyProduct.vue - 准备模板标签
<template>
<div class="my-product">
<h3>标题:</h3>
<p>价格: 元</p>
<p></p>
</div>
</template>
<script>
export default {
}
</script>
<style>
.my-product {
width: 400px;
padding: 20px;
border: 2px solid #000;
border-radius: 5px;
margin: 10px;
}
</style>
完整代码
components/MyProduct.vue
<template>
<div class="my-product">
<h3>标题: {{ title }}</h3>
<p>价格: {{ price }}元</p>
<p>{{ info }}</p>
</div>
</template>
<script>
export default {
props: ['title', 'price', 'info'] // 声明属性, 等待接收外部传入的值
}
</script>
<style>
.my-product {
width: 400px;
padding: 20px;
border: 2px solid #000;
border-radius: 5px;
margin: 10px;
}
</style>
App.vue中使用并传入数据
<template>
<div>
<!--
加: 后面是vue变量数据
不加: 后面认为是字符串
-->
<MyProduct title="超级好吃的口水鸡" price="50" :info="msg"></MyProduct>
<MyProduct :title="'超级难吃的榴莲'" :price="20" :info="msg"/>
</div>
</template>
<script>
import MyProduct from './components/MyProduct.vue'
export default {
data(){
return {
msg: '开业大酬宾, 全场8折'
}
},
components: {
MyProduct
}
}
</script>
小结
组件封装复用的标签和样式, 而具体数据要靠外面传入
vue组件通信_父向子-循环复用
目标
对子组件使用v-for循环,把数据循环分别传入给组件内显示
数据
list: [
{ id: 1, proname: "超级好吃的棒棒糖", proprice: 18.8, info: '开业大酬宾, 全场8折' },
{ id: 2, proname: "超级好吃的大鸡腿", proprice: 34.2, info: '好吃不腻, 快来买啊' },
{ id: 3, proname: "超级无敌的冰激凌", proprice: 14.2, info: '炎热的夏天, 来个冰激凌了' },
],
参考代码
<template>
<div>
<MyProduct v-for="obj in list"
:title="obj.proname"
:price="obj.proprice"
:info="obj.info"
:key="obj.id"></MyProduct>
</div>
</template>
<script>
import MyProduct from "./components/MyProduct_13.1";
export default {
data() {
return {
list: [
{ id: 1, proname: "超级好吃的棒棒糖", proprice: 18.8, info: '开业大酬宾, 全场8折' },
{ id: 2, proname: "超级好吃的大鸡腿", proprice: 34.2, info: '好吃不腻, 快来买啊' },
{ id: 3, proname: "超级无敌的冰激凌", proprice: 14.2, info: '炎热的夏天, 来个冰激凌了' },
],
};
},
components: {
MyProduct,
},
};
</script>
<style>
</style>
vue单向数据流-不要修改props
在vue中需要遵循单向数据流原则
- 在父传子的前提下,父组件的数据发生会通知子组件自动更新
- 子组件内部,不能直接修改父组件传递过来的props => props是只读的
示例代码
<template>
<div style="border:1px solid #ccc; margin:5px;padding:5px">
<h1>31-vue单向数据流-父组件</h1>
<MyCom
:name="name"
:hobby="hobby"/>
<button @click="fn">改数据</button>
</div>
</template>
<script>
// 导入
import MyCom from './MyCom.vue'
export default {
data(){
return {
name: '小花',
hobby:['vue', 'react']
}
},
components: { MyCom },
methods: {
fn(){
this.name = '小花花'
this.hobby.push('小程序')
}
}
}
</script>
<style>
</style>
子
<template>
<div style="border:1px solid #ccc; margin:5px;padding:5px">
<h2>子组件</h2>
<p>
name: {{name}}
</p>
<p>
hobby: {{hobby}}
</p>
<button @click="fn">修改props</button>
</div>
</template>
<script>
export default {
props: ['name', 'hobby'],
methods: {
fn(){
// 直接去修改props ===> 改了父组件传来的数据
// 这里打破 单向数据流的规则,vue能捕获到错误
// this.name = '小花花'
// 这里打破 单向数据流的规则,vue能不能捕获到错误
// hobby是引用数据类型,push并没有修改 数组的地址
this.hobby.push('小程序')
}
}
}
</script>
<style>
</style>
图示
特殊说明
说明:父组件传给子组件的是一个对象,子组件修改对象的属性,是不会报错的,对象是引用类型, 互相更新;但不能改变引用地址
小结
props的值不能重新赋值, 但是引用类型可以子改父
vue组件通信_子传父
目标
掌握子传父的用法
子传父是指:从子组件内部把数据传出来给父组件使用或者修改父组件数据
语法
- 父组件中:< 子组件 @自定义事件名1="父methods函数1" @自定义事件名2="父methods函数2" />
- 子: this.$emit("自定义事件名1", 传值1) ---> 执行父methods里函数代码
示例
<template>
<div style="border:1px solid #ccc; margin:5px;padding:5px">
<h1>32-子传父</h1>
<!-- 1. 添加事件监听 -->
<!-- 当子组件发生了abc事件要执行fn函数 -->
<MyCom @abc="fn"/>
</div>
</template>
<script>
// 导入
import MyCom from './MyCom.vue'
export default {
components: { MyCom },
methods: {
fn(obj){
console.log('fn-子组件发生了abc事件',obj)
}
}
}
</script>
<style>
</style>
子组件
<template>
<div style="border:1px solid #ccc; margin:5px;padding:5px">
<h2>子组件</h2>
<button @click="fn">触发abc事件</button>
</div>
</template>
<script>
export default {
methods: {
fn(){
console.log('子组件click')
// 2. 触发abc事件
this.$emit('abc',{name:'小花'})
}
}
}
</script>
图示
小结
自定义事件 + $emit
子传父案例
需求
课上例子, 砍价功能, 子组件点击实现随机砍价-1功能
思路
价格数据是定义在父组件中的,而发起砍一刀的操作按钮是在子组件中定义的,这就是经典的子传父操作。
参考代码
components/MyProduct.vue
<template>
<div class="my-product">
<h3>标题: {{ title }}</h3>
<p>价格: {{ price }}元</p>
<p>{{ info }}</p>
<p>
<button @click="kanFn">砍价</button>
</p>
</div>
</template>
<script>
export default {
props: ['index', 'title', 'price', 'info'], // 声明属性, 等待接收外部传入的值
methods: {
kanFn(){
this.$emit('subprice', this.index, 1)
}
}
}
</script>
<style>
.my-product {
width: 400px;
padding: 20px;
border: 2px solid #000;
border-radius: 5px;
margin: 10px;
}
</style>
App.vue
<template>
<div>
<MyProduct
v-for="(obj, index) in list"
:title="obj.proname"
:price="obj.proprice"
:info="obj.info"
:key="obj.id"
:index="index"
@subprice="fn"
></MyProduct>
</div>
</template>
<script>
import MyProduct from "./components/MyProduct.vue";
export default {
data() {
return {
list: [
{
id: 1,
proname: "超级好吃的棒棒糖",
proprice: 18.8,
info: "开业大酬宾, 全场8折",
},
{
id: 2,
proname: "超级好吃的大鸡腿",
proprice: 34.2,
info: "好吃不腻, 快来买啊",
},
{
id: 3,
proname: "超级无敌的冰激凌",
proprice: 14.2,
info: "炎热的夏天, 来个冰激凌了",
},
],
};
},
components: {
MyProduct,
},
methods: {
fn(index, price) {
this.list[index].proprice = price;
},
},
};
</script>
<style>
</style>
小结
父自定义事件和方法, 等待子组件触发事件给方法传值
案例-todos
案例-创建工程和组件
目标: 新建工程, 准备好所需的一切
预先准备: 把styles的样式文件准备好.index.css
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}
.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: 12px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
z-index: 9999
}
.toggle-all + label:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #4d4d4d;
}
.todo-list li.completed label {
color: #cdcdcd;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
base.css
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
根据需求: 我们定义3个组件准备复用
components/TodoHeader.vue - 复制标签和类名
<template>
<header class="header">
<h1>todos</h1>
<input id="toggle-all" class="toggle-all" type="checkbox" >
<label for="toggle-all"></label>
<input
class="new-todo"
placeholder="输入任务名称-回车确认"
autofocus
/>
</header>
</template>
<script>
export default {
}
</script>
components/TodoMain.vue - 复制标签和类名
<template>
<ul class="todo-list">
<!-- completed: 完成的类名 -->
<li class="completed" >
<div class="view">
<input class="toggle" type="checkbox" />
<label>吃饭</label>
<button class="destroy"></button>
</div>
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>睡觉</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</template>
<script>
export default {
}
</script>
components/TodoFooter.vue - 复制标签和类名
<template>
<footer class="footer">
<span class="todo-count">总计:<strong>数量值</strong></span>
<ul class="filters">
<li>
<a class="selected" href="javascript:;" >全部</a>
</li>
<li>
<a href="javascript:;">未完成</a>
</li>
<li>
<a href="javascript:;" >已完成</a>
</li>
</ul>
<button class="clear-completed" >清除已完成</button>
</footer>
</template>
<script>
export default {
}
</script>
App.vue中引入和使用
<template>
<section class="todoapp">
<!-- 除了驼峰, 还可以使用-转换链接 -->
<todo-header></todo-header>
<todo-main></todo-main>
<todo-footer></todo-footer>
</section>
</template>
<script>
import TodoHeader from "./components/TodoHeader";
import TodoMain from "./components/TodoMain";
import TodoFooter from "./components/TodoFooter";
import "./styles/base.css"
import "./styles/index.css"
export default {
components: {
TodoHeader,
TodoMain,
TodoFooter,
},
};
</script>
注意:页面组件根元素添加类名todoapp
循环展示任务
目的: 把待办任务, 展示到页面TodoMain.vue组件上
App.vue
<todo-main :list="list"></todo-main>
export default {
data() {
return {
list: [
{ id: 100, name: "吃饭", isDone: true },
{ id: 201, name: "睡觉", isDone: false },
{ id: 103, name: "打豆豆", isDone: true },
],
};
}
};
TodoMain.vue
<template>
<ul class="todo-list">
<!-- completed: 完成的类名 -->
<li :class="{ completed: item.isDone }" v-for="item in list" :key="item.id">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.isDone" />
<label>{{ item.name }}</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</template>
<script>
export default {
props: ["list"]
};
</script>
<style>
</style>
添加功能
目标: 在顶部输入框输入要完成的任务名, 敲击回车, 完成新增功能
TodoHeader.vue
<template>
<header class="header">
<h1>todos</h1>
<input id="toggle-all" class="toggle-all" type="checkbox" >
<label for="toggle-all"></label>
<input
class="new-todo"
placeholder="输入任务名称-回车确认"
v-model="name"
@keydown.enter="down"
/>
</header>
</template>
<script>
export default {
data(){
return {
name: ""
}
},
methods: {
down(ev){
this.$emit("add", this.name)
this.name = ""
}
}
}
</script>
App.vue
<todo-header @add="addFn"></todo-header>
methods: {
// ...省略了原来的方法
addFn(name){
this.list.push({
id: Date.now(),
name,
isDone: false
})
}
}
删除功能
目标: 实现点x, 删除任务功能
App.vue - 传入自定义事件等待接收要被删除的id或索引
<todo-main :list="list" @del="delFn"></todo-main>
methods: {
delFn(id) {
// 把id过滤掉
this.list = this.list.filter((item) => item.id !== id);
},
},
TodoMain.vue - 把id传回去实现删除(想好数据在哪里, 就在哪里删除)
<button class="destroy" @click="del(item.id)"></button>
methods: {
del(id) {
// console.log(id)
this.$emit("del", id);
},
},
注意⚠️:通过子传父删除,以免影响后续不同状态下数据的删除操作
底部统计
目的: 显示现在任务的总数
TodoFooter.vue
<template>
<footer class="footer">
+ <span class="todo-count">总计:<strong>{{list.length}}</strong></span>
<ul class="filters">
<li>
<a class="selected" href="javascript:;">全部</a>
</li>
<li>
<a href="javascript:;">未完成</a>
</li>
<li>
<a href="javascript:;">已完成</a>
</li>
</ul>
<button class="clear-completed">清除已完成</button>
</footer>
</template>
<script>
export default {
+ props: ['list']
}
</script>
<style>
</style>
App.vue - 传入数据
<todo-footer :list="list"></todo-footer>
数据切换(难点)
目的: 点击底部切换数据,显示对应状态的任务列表
需求:
- 父组件中定义切换状态数据=》
全部 未完成 已完成 - 当前状态传递给子组件foot,根据切换状态高亮显示按钮
- 显示对应状态的任务列表数据
父组件App.vue
<todo-main
+ :list="showArr"
/>
<todo-footer :list="list"
+ :condition="conStr"
+ @changeCondition="changeFn"
/>
<script>
export default{
data(){
return {
// ...其他代码省略
+ conStr: "all" // all(全部) completed(完成) incompleted(未完成)
}
},
methods: {
// ...其他省略
+ changeFn(str){ // 数据筛选-类型切换
+ this.conStr = str;
+ }
},
+ computed: { // 计算不同状态的列表数据
showArr(){
if (this.conStr === 'completed'){
return this.list.filter(obj => obj.isDone)
} else if (this.conStr === 'incompleted'){
return this.list.filter(obj => !obj.isDone)
} else {
return this.list
}
}
+ }
}
</script>
子组件TodoFooter.vue
<template>
<footer class="footer">
<span class="todo-count">总计:<strong>{{list.length }}</strong></span>
<ul class="filters">
<li>
<a :class="{selected: condition === 'all'}" href="javascript:;" @click="changeFn('all')">全部</a>
</li>
<li>
<a :class="{selected: condition === 'incompleted'}" href="javascript:;" @click="changeFn('incompleted')">未完成</a>
</li>
<li>
<a :class="{selected: condition === 'completed'}" href="javascript:;" @click="changeFn('completed')">已完成</a>
</li>
</ul>
<button class="clear-completed" @click="$emit('clearCompleted')">清除已完成</button>
</footer>
</template>
<script>
export default {
props: ['condition'],
methods: {
changeFn(str){
this.$emit("changeCondition", str)
}
}
}
</script>
清空已完成
目的: 点击右下角按钮- 把已经完成的任务删除了
App.vue - 先传入一个自定义事件-因为得接收TodoFooter.vue里的点击事件
<todo-footer :list="list"
:condition="conStr"
@changeCondition="changeFn"
+ @clearCompleted="clearFn"
/>
<script>
clearFn(){ // 清空已完成
+ this.list = this.list.filter(obj => !obj.isDone)
}
</script>
TodoFooter.vue=》通知父组件删除已完成数据
<button class="clear-completed" @click="$emit('clearCompleted')">清除已完成</button>
数据缓存
目的: 新增/修改状态/删除 后, 马上把数据同步到浏览器本地存储
需求:
- list默认值从本地取/没有给空数组
- list发生任何变化都更新本地缓存
App.vue
<script>
export default {
data() {
return {
+ list: JSON.parse(localStorage.getItem("todoList")) || [],
conStr: "all" // all(全部) completed(完成) incompleted(未完成)
};
},
components: {
TodoHeader,
TodoMain,
TodoFooter,
},
methods: {
delFn(id) {
// 把id过滤掉
this.list = this.list.filter((item) => item.id !== id);
},
addFn(name){
this.list.push({
id: Date.now(),
name,
isDone: false
})
},
changeFn(str){ // 数据筛选-类型切换
this.conStr = str;
},
clearFn(){ // 清空已完成
this.list = this.list.filter(obj => !obj.isDone)
}
},
+ watch: { // 监测状态改变, 也要同步到本地
list: {
deep: true,
handler(listArr){
localStorage.setItem("todoList", JSON.stringify(this.list));
}
}
+ },
computed: {
showArr(){
if (this.conStr == 'completed'){
return this.list.filter(obj => obj.isDone)
} else if (this.conStr == 'incompleted'){
return this.list.filter(obj => !obj.isDone)
} else {
return this.list
}
}
}
};
</script>
全选功能
目标: 点击左上角v号, 可以设置一键完成, 再点一次取消
提示: 根据全选框的值,遍历所有的对象, 修改他们的完成状态属性的值和全选框的值保持一致
子组件TodoHeader.vue
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="all">
<label for="toggle-all"></label>
props: ['list'],
computed: {
all: {
set(val){
this.$emit("isAll", val)
},
get(){
return this.list.every(obj => obj.isDone)
}
}
},
父组件App.vue
<todo-header :list="list" @add="addFn" @isAll="changeAllFn"></todo-header>
changeAllFn(bool){ // 全选改变事件
this.list.forEach(obj => obj.isDone = bool);
}
拓展-全局注册
全局入口在main.js, 在new Vue之上注册
语法:
import Vue from 'vue'
import 组件对象 from 'vue文件路径'
Vue.component("组件名", 组件对象)
main.js
import Vue from 'vue'
import App from './App.vue'
+ import Pannel from './components/Pannel' // 引入组件文件对象
+ Vue.component("PannelCom", Pannel) // 组件名开头大写驼峰(推荐) => 全局注册一个组件
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
全局注册PannelCom组件名后, 就可以当做标签在任意template里用
单双标签都可以, 运行后, 会把这个自定义标签当做组件解析, 使用组件里封装的标签替换到这个位置
在页面中使用:
<template>
<div id="app">
<h3>案例:折叠面板</h3>
<PannelCom></PannelCom>
<!-- or -->
<PannelCom />
</div>
</template>
总结
- 组件分类=》1.页面组件 2. 页面下功能 (.vue格式)
- 组件化开发是什么 =》一个页面由多个.vue文件组成,完成一个完整的页面效果
- 组件创建和复用 =》1. 全局 (main.js) 2. 局部
- 掌握组件通信的主要方式
-
- 父传子 =》父组件:提供数据 =》:传递数据名字="变量" | 子组件:接收props:['传递数据名字']
- 子传父(单向数据流)=》父组件:提供自定义事件=》@语义化事件名="callback" | 子组件:通知父组件修改
this.$emit('语义化事件名', data,data2)
收集的那些个面试题
喜欢小狗狗吗
目标: 封装Dog组件, 用来复用显示图片和标题的
效果:
参考代码
components/Dog1.vue
<template>
<div class="my_div">
<img
src="https://scpic.chinaz.net/files/pic/pic9/202003/zzpic23514.jpg"
alt=""
/>
<p>这是一个孤独可怜的狗</p>
</div>
</template>
<script>
export default {};
</script>
<style>
.my_div {
width: 400px;
border: 1px solid black;
text-align: center;
float: left;
}
.my_div img {
width: 100%;
}
</style>
在App.vue中使用
<template>
<div>
<Dog></Dog>
<Dog/>
</div>
</template>
<script>
import Dog from './components/Dog1'
export default {
components: {
Dog
}
}
</script>
<style>
</style>
总结: 重复部分封装成组件, 然后注册使用
点击文字变色
目标: 修改Dog组件, 实现组件内点击变色
提示: 文字在组件内, 所以事件和方法都该在组件内-独立
图示:
正确代码(先不要看)
components/Dog2.vue
<template>
<div class="my_div">
<img
src="https://scpic.chinaz.net/files/pic/pic9/202003/zzpic23514.jpg"
alt=""
/>
<p :style="{backgroundColor: colorStr}" @click="btn">这是一个孤独可怜的狗</p>
</div>
</template>
<script>
export default {
data(){
return {
colorStr: ""
}
},
methods: {
btn(){
this.colorStr = `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`
}
}
};
</script>
<style>
.my_div {
width: 400px;
border: 1px solid black;
text-align: center;
float: left;
}
.my_div img {
width: 100%;
}
</style>
卖狗啦
目标: 把数据循环用组件显示铺设
数据:
[
{
dogImgUrl:
"http://nwzimg.wezhan.cn/contents/sitefiles2029/10146688/images/21129958.jpg",
dogName: "博美",
},
{
dogImgUrl:
"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1224576619,1307855467&fm=26&gp=0.jpg",
dogName: "泰迪",
},
{
dogImgUrl:
"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2967740259,1518632757&fm=26&gp=0.jpg",
dogName: "金毛",
},
{
dogImgUrl:
"https://pic1.zhimg.com/80/v2-7ba4342e6fedb9c5f3726eb0888867da_1440w.jpg?source=1940ef5c",
dogName: "哈士奇",
},
{
dogImgUrl:
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563813435580&di=946902d419c3643e33a0c9113fc8d780&imgtype=0&src=http%3A%2F%2Fvpic.video.qq.com%2F3388556%2Fd0522aynh3x_ori_3.jpg",
dogName: "阿拉斯加",
},
{
dogImgUrl:
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563813454815&di=ecdd2ebf479568453d704dffacdfa12c&imgtype=0&src=http%3A%2F%2Fwww.officedoyen.com%2Fuploads%2Fallimg%2F150408%2F1-15040Q10J5B0.jpg",
dogName: "萨摩耶",
},
]
图示
参考代码
components/Dog3.vue
<template>
<div class="my_div">
<img
:src="imgurl"
alt=""
/>
<p :style="{backgroundColor: colorStr}" @click="btn">{{ dogname }}</p>
</div>
</template>
<script>
export default {
props: ['imgurl', 'dogname'],
// ...其他代码省略
};
</script>
App.vue引入使用把数据循环传给组件显示
<template>
<div>
<Dog v-for="(obj, index) in arr"
:key="index"
:imgurl="obj.dogImgUrl"
:dogname="obj.dogName"
></Dog>
</div>
</template>
<script>
import Dog from './components/Dog3'
export default {
data() {
return {
// 1. 准备数据
arr: [
{
dogImgUrl:
"https://img-pre.ivsky.com/img/tupian/pre/201605/30/pomeranian-001.jpg",
dogName: "博美",
},
{
dogImgUrl:
"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1224576619,1307855467&fm=26&gp=0.jpg",
dogName: "泰迪",
},
{
dogImgUrl:
"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2967740259,1518632757&fm=26&gp=0.jpg",
dogName: "金毛",
},
{
dogImgUrl:
"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fs9.rr.itc.cn%2Fr%2FwapChange%2F20165_6_11%2Fa0teml39607703025596.png&refer=http%3A%2F%2Fs9.rr.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1616048681&t=228c337babe1aed1e95a3e689693130f",
dogName: "哈士奇",
},
{
dogImgUrl:
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563813435580&di=946902d419c3643e33a0c9113fc8d780&imgtype=0&src=http%3A%2F%2Fvpic.video.qq.com%2F3388556%2Fd0522aynh3x_ori_3.jpg",
dogName: "阿拉斯加",
},
{
dogImgUrl:
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563813454815&di=ecdd2ebf479568453d704dffacdfa12c&imgtype=0&src=http%3A%2F%2Fwww.officedoyen.com%2Fuploads%2Fallimg%2F150408%2F1-15040Q10J5B0.jpg",
dogName: "萨摩耶",
},
],
};
},
components: {
Dog
}
};
</script>
选择喜欢的狗
目标: 用户点击狗狗的名字, 在右侧列表显示一次名字
参考代码
components/Dog4.vue
this.$emit("love", this.dogname);
App.vue
<template>
<div>
<Dog
v-for="(obj, index) in arr"
:key="index"
:imgurl="obj.dogImgUrl"
:dogname="obj.dogName"
@love="fn"
></Dog>
<hr />
<p>显示喜欢的狗:</p>
<ul>
<li v-for="(item, index) in loveArr" :key="index">{{ item }}</li>
</ul>
</div>
</template>
<script>
import Dog from "./components/Dog4";
export default {
data() {
return {
loveArr: []
}
},
// ...中间省略代码
methods: {
fn(dogName) {
this.loveArr.push(dogName)
},
},
};
</script>