视频地址:【尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通】 https://www.bilibili.com/video/BV1Zy4y1K7SH/?p=13&share_source=copy_web&vd_source=b1cb921b73fe3808550eaf2224d1c155
目录
3 Vue脚手架
3.1 初始化脚手架
3.1.1 说明
- Vue脚手架是Vue官方提供的标准化开发工具(开发平台)
- 最新版本是4.x
- 文档:Home | Vue CLI (vuejs.org)
目前脚手架是4版本,脚手架平台用最新版本。目前Vue2还是主流。
Vue CLI,即Vue脚手架,command line interface,即命令行接口工具。
3.1.2 具体步骤
- 第一步:全局安装@vue/cli
- npm install -g @vue/cli
- 第二步:切换到要创建项目的目录,使用命令创建项目
- vue create xxx(项目名称)
- 之后出现配置
- 选择default就行,然后回车,等着就行啦
- 第三步:启动项目
- npm run serve
打开网址http://localhost:8080/,下面就是vue准备的hello world组件。
关闭项目,ctrl+C
1. 如出现下载缓慢请配置 npm 淘宝镜像:npm config set registry https://registry.npm.taobao.org
3.1.3 模板项目的结构
(在块引用里没有别的意思,就想让他们紧凑些)
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源(比如图片、视频等文件)
│ │ └── logo.png
│ │── components: 存放组件(所有程序员写出的组件都存放在这里,除了App.vue之外)
│ │ └── HelloWorld.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git 版本管制忽略的配置(记录不想用git管理的文件)
├── babel.config.js: babel 的配置文件,es6转es5
├── package-lock.json:包版本控制文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
当执行完npm run serve后(在vscode里新建终端,选择当前项目的文件夹,然后输入命令npm run serve),马上执行main.js.
备注
脚手架的高级功能:修改代码后ctrl+s,程序会重新编译,页面会自动刷新
3.1.3.1 main.js
分析main.js,仔细看看下面代码里的注释。
*/
// 引入vue
// 之前是在html文件里通过script标签引入外部js
// 现在es6引入语法引入
// 脚手架已经安装完了vue,在node_modules里
import Vue from 'vue'
// 引入APP组件,是所有组件的父组件。
import App from './App.vue'
// 关闭vue的生产提示
Vue.config.productionTip = false
// 创建vue实例对象--vm
new Vue({
el: '#app',
// 稍后分解
// 功能:将APP组件放入容器中,
render: h => h(App),
})
3.1.3.2 App.vue
然后分析与main.js同级的APP.vue。
在单文件组件里已经写过App.vue,以及School.vue和student.vue了,因此App.vue和components没有什么好讲的。
3.1.3.3 index.html
到此为止,main.js里的id为app的容器还没有看到,在public的index.html里。
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
3.1.3.4 render配置项
main.js中的render配置项。
在main.js中使用render()函数将App组件放入容器中,是由于在脚手架中默认引入的vue不是完整的vue,而是缺少模板解析器的vue。(当引入残缺版Vue还想配置内容,那么就得借助render())
vue.js与vue.runtime.xxx.js的区别:
(1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
(2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
模板解析器是用于解析vue配置项中的template配置项。将vue中的模板解析器去除后,可以节省项目的占用空间(模版解析器占了Vue.js1/3的体积),同时也更加符合逻辑,因为项目构建后已经是浏览器可以解析的html、css、js等文件,不再需要模板解析器.
render()的完整代码
//创建Vue实例对象---vm
new Vue({
// 将App组件放入容器中
// 简写,由于此函数不需要使用this,可以写成箭头函数
// 然后再对箭头函数进行简写
// render: h => h(App),
// 完整写法
render(createElement) {
// render函数接收一个创建页面元素的createElement函数,
// 用于创建页面元素
// render()函数将创建的元素返回,再将其放入容器中
return createElement(App)
}
// 指定vue控制的容器
}).$mount('#app')
其他组件
render(createElement) {
// 创建返回 <h1>hello world</h1>
return createElement('h1', 'hello world')
}
缩写形式
render: h => h(App)
App组件外的其他组件使用模板<template>
标签能够被放入页面,是由于vue脚手架为其配置了模板编译的第三方包。vm里(main.js)的template就不能被第三方包解析。
- 总结
- vue.js与vue.runtime.xxx.js的区别:
(1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
(2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。 - 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容。
- vue.js与vue.runtime.xxx.js的区别:
3.1.3.5 修改默认配置项
1 查看项目的默认配置
Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakc 配置,终端运行:
vue inspect > output.js
在当前项目文件夹下运行此命令。
将vue脚手架默认配置全都整理成一个js文件。
可以看到整个app应用的入口就是在src下的main.js。
output.js只是整理所有的配置项给你展示出来,不能用于修改配置项。
2 修改项目的默认配置
可以修改的配置项以及如何修改可以参考官网:
Vue CLI 官网 配置参考
p64跳过
2.1 修改入口文件名
2.2 关闭代码语法检查
3.2 ref 与 props
从头开始写一个Vue项目
main.js
// 引入Vue
import Vue from 'vue'
// 引入App
import App from './App.vue'
// 关闭Vue生产提示
Vue.config.productionTip = false
// 创建vm
new Vue({
el: '#app',
render: h => h(App)
})
School.vue
<template>
<div class="demo">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
export default {
name:"School",
data() {
return {
name: "尚硅谷",
address: "北京"
}
}
}
</script>
<style>
.demo {
font-size: 10px
}
</style>
App.vue
<template>
<div>
<!-- 自闭合,脚手架可用 -->
<School/>
<School/>
<School/>
</div>
</template>
<script>
import School from "./components/School.vue"
export default {
name: "App",
components: {School},
}
</script>
<style>
</style>
启动,命令 npm run serve
运行结果
3.2.1 ref属性
(之前学过标签属性key。ref也是一种标签属性。)
3.2.1.1 使用id获取元素或子组件
使用id获取标签或子组件的DOM元素。通过给标签加id属性,然后通过document.getElementById('id名')获取元素。
<template>
<div>
<h1 v-text="msg" id="title"></h1>
<button @click="showDOM">点我输出上方的DOM元素</button>
</div>
</template>
<script>
import School from "./components/School.vue";
export default {
name: "App",
components: { School },
data() {
return {
msg: "欢迎学习Vue",
};
},
methods: {
showDOM() {
console.log(document.getElementById("title"));
},
},
};
</script>
<style>
</style>
运行结果
3.2.1.2 ref属性的使用方式
ref属性,可以理解为id的替代者。
1 标识元素或子组件
- 标记 html 标签元素
-
<h1 ref="xxx">.....</h1>
-
- 标记子组件
-
<School ref="xxx"></School>
-
2 获取标识的元素或子组件
this.$refs.xxx
其中,this为被标记的元素或子组件所在的组件实例对象。
3.2.1.3 使用ref标记html标签元素
ref 属性应用在 html 标签元素上,获取的是对应的真实 DOM 元素。
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button @click="showDOM">点我输出上方的DOM元素</button>
</div>
</template>
<script>
import School from "./components/School.vue";
export default {
name: "App",
components: { School },
data() {
return {
msg: "欢迎学习Vue",
};
},
methods: {
showDOM() {
console.log(this.$refs.title);
},
},
};
</script>
<style>
</style>
运行结果
3.2.1.4 使用 ref 属性标记子组件
ref 属性应用在组件标签上,获取的是对应组件实例对象
School组件
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button @click="showDOM">点我输出上方的DOM元素</button>
<School ref="myschool"> </School>
</div>
</template>
<script>
import School from "./components/School.vue";
export default {
name: "App",
components: { School },
data() {
return {
msg: "欢迎学习Vue",
};
},
methods: {
showDOM() {
console.log(this.$refs.title);
console.log(this.$refs.myschool);
},
},
};
</script>
<style>
</style>
运行结果
使用 ref 属性和使用 id 进行对比,使用 ref 属性不用自己操作 DOM 元素,且使用 ref 属性获取子组件时,获取的为整个组件实例对象而不是子组件编译解析后的模板,有利于后期对子组件进行操作。
3.2.2 props配置项
关于配置项,之前就有el、data、methods、watch、computed...
案例
Student.vue
<template>
<div>
<h1>{{ msg }}</h1>
<h2>学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
<h2>学生年龄:{{ age }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
msg: "我是尚硅谷的学生",
name: "张三",
gender: "男",
age: 18,
};
},
};
</script>
App.vue
<template>
<div>
<Student> </Student>
<hr />
<Student> </Student>
<hr />
<Student> </Student>
</div>
</template>
<script>
import Student from "./components/Student.vue";
export default {
name: "App",
components: { Student },
methods: {
showDOM() {},
},
};
</script>
<style>
</style>
main.js不用改。
运行结果
3.2.2.1 props配置项的使用
- 李四也想通过这个形式展示自己的学生信息。
- 可以复用吗
- 可以,通过props配置项完成。
- prop,属性,一堆属性就是props。
完整代码实现
Student.vue
前面跟上面都一样,只是原来data里的name gender 和 age都删掉,props里加上name gender 和 age。
<template>
<div>
<h1>{{ msg }}</h1>
<h2>学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
<h2>学生年龄:{{ age }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
msg: "我是尚硅谷的学生",
};
},
props: ['name', 'gender', 'age']
};
</script>
App.vue
然后在App.vue里,使用Student组件的标签里加上三个属性的具体取值
<template>
<div>
<Student name="李四" gender="女" age="18"> age </Student>
<hr />
</div>
</template>
<script>
import Student from "./components/Student.vue";
export default {
name: "App",
components: { Student },
methods: {
showDOM() {},
},
};
</script>
<style>
</style>
运行结果
3.2.2.2 对age进行运算
1 简单接收 最常用
将传入的age属性改为 :age="18"(App.vue 付款方),然后使用的时候是{{ age + 1}} (Student.vue 收款方)
因为单纯的 age=“18”,模板拿到会将其作为字符串去处理。不会作为表达式运算。但是加上 冒号:就可以了,其实就是v-bind:age="18",也就是数据绑定了。
Student.vue
<template>
<div>
<h1>{{ msg }}</h1>
<h2>学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
<h2>学生年龄:{{ age + 1 }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
msg: "我是尚硅谷的学生",
};
},
props: ['name', 'gender', 'age']
};
</script>
App.vue
<template>
<div>
<Student name="李四" gender="女" :age="18"> age </Student>
<hr />
</div>
</template>
<script>
import Student from "./components/Student.vue";
export default {
name: "App",
components: { Student },
methods: {
showDOM() {},
},
};
</script>
<style>
</style>
2 声明接收
声明接收——接收的同时对数据进行类型限制。
props: {
name:String,
gender:String,
age:Number
}
完整Student.vue
<template>
<div>
<h1>{{ msg }}</h1>
<h2>学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
<h2>学生年龄:{{ age+1 }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
msg: "我是尚硅谷的学生",
};
},
// props: ['name', 'gender', 'age']
props: {
name:String,
gender:String,
age:Number
}
};
</script>
3 复杂接收
接收的同时对数据:进行类型限制type + 默认值的指定default + 必要性的限制required。
不按要求来就会报错,页面不显示。
default和required不会同时出现
props: {
name : {
type:String, //name的类型是字符串
required: true //名字是必要的
},
age : {
type:Number, //name的类型是字符串
default: 99 //不传的话age就是99,默认值是99
},
gender: {
type: Stirng,
required: false
}
}
完整代码
Student.vue
<template>
<div>
<h1>{{ msg }}</h1>
<h2>学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
<h2>学生年龄:{{ age+1 }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
msg: "我是尚硅谷的学生",
};
},
// props: ['name', 'gender', 'age']
// props: {
// name:String,
// gender:String,
// age:Number
// }
props: {
name : {
type:String, //name的类型是字符串
required: true //名字是必要的
},
age : {
type:Number, //name的类型是字符串
default: 99 //不传的话age就是99,默认值是99
},
gender: {
type: Stirng,
required: false
}
}
};
</script>
总结,简单接收最常用。
- 细节
- 1 声明的时候不要瞎声明
- 没有传的属性值不要在props里出现。
- 2 接收到的props不允许修改
- 外部传入的数据不允许改,内部数据可以改
- 能有效果,但是不建议改,不然vue会出现问题
- 如果业务需求要改
- 这里props是优先被接收的,优先被放到vc上,然后给myName赋值
- 这里漏写了实现的方法,不难,methods里写个函数就可以了
- 实现
- 3 props里的prop名字不能与vue、js等的关键词重名
- 不能是key,ref等
- 1 声明的时候不要瞎声明
3.2.2.3 总结
3.3 mixin混入
混入就是复用配置
3.3.1 mixin混入
- mixin配置项,叫混入,一般也叫混合。
- mixin (混入)可以把多个组件共用的配置提取成一个混入对象,实现对组件配置项的复用。
3.3.2 未使用mixin
需求,当点击学生姓名的时候,弹窗展示这个人的名字
School.vue
<template>
<div>
<h2 @click="showName">学校名称:{{ name}}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
methods: {
showName() {
alert(this.name)
}
}
};
</script>
Student.vue
<template>
<div>
<h2 @click="showName">学生名称:{{ name}}</h2>
<h2>学生性别:{{ gender }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "张三",
gender: '男',
};
},
methods: {
showName() {
alert(this.name)
}
}
};
</script>
App.vue
<template>
<div>
<School />
<hr />
<Student> </Student>
<hr />
</div>
</template>
<script>
import Student from "./components/Student.vue";
import School from "./components/School.vue";
export default {
name: "App",
components: { Student, School },
methods: {
showDOM() {},
},
};
</script>
<style>
</style>
结果
问题发现:School和Student组件有同样的method等一样的东西。
3.3.3 mixin的定义与使用
在src文件夹下新建一个mixin.js文件(命名没有要求),
步骤1:将组件里相同的配置项删除,复制在mixin.js里。
mixin.js完整代码
export const mixin = { //分别暴露
methods: {
showName() {
alert(this.name)
}
}
}
步骤2:School组件引入混合mixin
// 引入一个混合
import {mixin} from '../mixin'
之前用了分别暴露,所以这里import后面有{}
步骤3:使用mixin,在vc中使用mixin,与data,name的位置一样。混合必须写在数组里。
mixin: [mixin]
步骤2和3的完整代码
School.vue
<template>
<div>
<h2 @click="showName">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
// 引入一个混合
import {mixin} from '../mixin'
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
mixin: [mixin]
};
</script>
Student.vue
<template>
<div>
<h2 @click="showName">学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
</div>
</template>
<script>
import {mixin} from '../mixin'
export default {
name: "Student",
data() {
return {
name: "张三",
gender: '男',
};
},
mixin: [mixin]
};
</script>
运行结果
在mixin混入中也可以编写组件的其他配置项。mounted()例子,还有data例子
mixin.js
export const mixin = {
methods: {
showName() {
alert(this.name)
}
}
}
export const mixin2 = {
data() {
return {
x: 100,
y: 200
}
},
}
School.vue
<template>
<div class="demo">
<h2>学校:{{name}}</h2>
<h2>地址:{{address}}</h2>
<button @click="showName">showName</button>
</div>
</template>
<script>
// 导入mixin
import {mixin, mixin2} from '../mixin'
export default {
name: 'School',
data() {
return {
name: 'SGG',
address: 'Beijing'
}
},
mixins: [mixin, mixin2]
}
</script>
<style>
</style>
3.3.4 mixin中的配置项与组件的配置项冲突
3.3.4.1 普通配置项
当mixin中的普通配置项与组件的普通配置项发生冲突时,优先使用组件中自己的配置项。
mixin配置见上节。
School.vue
<template>
<div class="demo">
<h2>学校:{{name}}</h2>
<h2>地址:{{address}}</h2>
<button @click="showName">showName</button>
</div>
</template>
<script>
// 导入mixin
import {mixin, mixin2} from '../mixin'
export default {
name: 'School',
data() {
return {
name: 'SGG',
address: 'Beijing',
x: 666
}
},
mixins: [mixin, mixin2]
}
</script>
<style>
</style>
3.3.4.2 生命周期钩子
当mixin中的生命周期函数与组件中的周期函数发生冲突时,会先执行mixin中的生命周期函数,后执行组件自己的生命周期函数。
mixin.js
export const mixin = {
methods: {
showName() {
alert(this.name)
}
}
}
export const mixin2 = {
data() {
return {
x: 100,
y: 200
}
},
}
export const mixin3 = {
mounted() {
console.log('mixin3 mounted')
}
School.vue
<template>
<div class="demo">
<h2>学校:{{name}}</h2>
<h2>地址:{{address}}</h2>
<button @click="showName">showName</button>
</div>
</template>
<script>
// 导入mixin
import {mixin, mixin2, mixin3} from '../mixin'
export default {
name: 'School',
data() {
return {
name: 'SGG',
address: 'Beijing',
x: 666
}
},
mixins: [mixin, mixin2, mixin3],
mounted() {
console.log('School mounted')
}
}
</script>
<style>
</style>
运行结果
3.3.5 总结
- 功能:可以把多个组件共用的配置提取成一个混入对象
- 使用方式:
- 第一步定义混合:
{ data(){....}, methods:{....} .... }
-
第二步使用混入:
- 第一步定义混合:
-
全局混入:Vue.mixin(xxx)
-
局部混入:mixins:['xxx']
3.4 插件
插件,可以增强vue。
3.4.1 插件
Vue中自定义的插件,插件就是包含install方法的一个对象,install的第一个参数是Vue(),第二个以后的参数是插件使用者传递的数据,插件对象中的install方法会被vue自动调用。
使用插件能够增强vue的功能。
3.4.2 插件的定义与使用
步骤1:在src下创建一个plugin.js。往install方法里放入 过滤器、自定义全局指令、混入、往原型上添加方法。
plugin.js
export default {
// 使用插件时,vue会自动将Vue()[vue实例对象的构造函数]传入
// x,y,z 为其他自己传入的参数
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.demo= ()=>{alert('你好啊')}
}
}
步骤2:引入插件
使用插件时,先导入对应的插件,使用Vue.use()
方法使用对应的插件。
这里只需要在main.js里导入和引入插件,不需要在后面的组件里引入混入等等。
main.js
//引入Vue
import Vue from 'vue'
//引入App组件,它是所有组件的父组件
import App from './App.vue'
// 导入插件
import plugins from './plugins.js'
//关闭vue的生产提示
Vue.config.productionTip = false
// 使用插件
Vue.use(plugins)
//创建Vue实例对象---vm
new Vue({
// 将App组件放入容器中
render: h => h(App),
// 指定vue控制的容器
}).$mount('#app')
App.vue (没有变化)
<template>
<div>
<School></School>
<Student></Student>
</div>
</template>
<script>
// 导入子组件
import School from './components/School.vue'
import Student from './components/Student.vue'
export default {
name: 'App',
components: {
School,
Student
}
}
</script>
<style>
</style>
Student.vue (没有变化)
<template>
<div>
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<input type="text" v-fbind:value="name">
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男'
}
},
}
</script>
运行结果
3.4.3 总结
3.5 scoped样式
最开始的案例,跟前面每节的代码其实差不多。
3.5.1 样式冲突问题
给School组件和Student写样式,class类名一样,都是demo,背景色一个是skyblue,一个是orange,会出现样式冲突,结果是什么呢。
School.vue
<template>
<div>
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
}
};
</script>
<style>
.demo {
background-color: skyblue;
}
</style>
Student.vue
<template>
<div>
<h2 class="demo">学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "张三",
gender: "男",
};
}
};
</script>
<style>
.demo {
background-color: orange;
}
</style>
结果
结果以student组件的样式为准。为什么呢。
原因:
APP.vue
import Student from "./components/School.vue";
import School from "./components/Student.vue";
因为App这里的引入的顺序的问题,Student将School里的样式覆盖掉了。
3.5.2 样式冲突解决
给style标签后加一个scoped(scope,作用域,范围,scoped,局部的),意味着style里写的全部样式只负责同一个vue组件template标签里的结构。
School.vue
<template>
<div>
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
}
};
</script>
<style scoped>
.demo {
background-color: skyblue;
}
</style>
Student.vue
<template>
<div>
<h2 class="demo">学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "张三",
gender: "男",
};
}
};
</script>
<style scoped>
.demo {
background-color: orange;
}
</style>
结果
3.5.3 解决的原理
scoped给最外侧的div加一个特殊的标签属性,而且data-v-xxx的xxx是随机生成的。然后通过demo配合标签属性选择器就完成了控制指定的div。
3.5.4 scoped的注意事项
- App组件不适合用scoped
- 1 如果只给App的style里添加一个样式控制,可以用来控制School和Student的样式(前提是School和Student里有相应的类名)
- 2 但是在1的基础上,给App的style加上scoped,那么App的style就不能控制App子组件的样式了。
- 3 一般而言,App里的样式基本上是很多子组件都在用的样式。
- <style lang=" ">
- lang:language的简称。不写lang默认就是css。
- 在 vue中,不仅可以用css写样式,还可以用less,即lang="css" / lang="less"
- 但是vue脚手架不能处理less,可以安装less-loader,指令npm i less-loader。但是要降版本,npm i less-loader@7.
- less可以嵌套写样式。
.demo { background-color: skyblue; .atguigu { font-size: 40px; } }
3.5.5 总结
- scoped样式
- 作用:让样式局部生效,防止冲突
- 写法:<style scoped>
3.6 Todo-list案例
todo-list案例,类似手机里的功能待办事项。
3.6.1 组件化编码流程(通用)
- 实现静态组件:抽取组件,使用组件实现静态页面效果
- 展示动态数据:
- 数据的类型、名称是什么?
- 数据保存在哪个组件?
- 交互——从绑定事件监听开始。
3.6.2 页面组件划分
组件的划分:按照功能点划分。
对于每个要做的事,也作为一个组件。
如果很难给组件起名字,说明可能组件拆分不合理。
3.6.3 静态组件
组件化编码流程步骤1:实现静态组件:抽取组件,使用组件实现静态页面效果。
静态组件:只考虑header、Item、todoList和footer四个组件的结构和样式,不包含交互和动态数据。
Item是List的子组件。
完成静态组件。
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader/>
<MyList/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: { MyHeader, MyList, MyFooter },
methods: {},
};
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
MyHeader.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
</template>
<script>
export default {
name: "MyHeader",
};
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
MyList.vue
<template>
<ul class="todo-main">
<MyItem/>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
MyItem.vue
<template>
<li>
<label>
<input type="checkbox" />
<span>yyyy</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
MyFooter.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span> <span>已完成0</span> / 全部2 </span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter",
};
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
以上就是静态组件。
运行结果。
3.6.4 展示动态数据
组件化编码流程步骤2:展示动态数据。
3.6.4.1 初始化列表
- 数据的类型、名称是什么?
- todoList:用数组存,里面存每个要做的事——对象(一堆数据用数组,每个数据的属性太多了用对象。)
- 数组保存在哪个组件?
- 谁用在谁里面写
步骤1:准备数据,数据在MyList里。
data() {
return {
todos: [
{id:'001',title:'吃饭',done: true},//id是字符串,数字是有尽头的,一般是哈希值
{id:'002',title:'学习',done: false},
{id:'003',title:'工作',done: false},
]
}
}
步骤2:展示,先在MyList里展示。这里将数据传给MyItem展示。
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
分析一下,v-for将每一项传给MyItem。
todo前面加冒号后,后面的todo才是一个表达式,才会去读v-for里的todo。否则就只是给todo属性赋值为“todo”。
步骤3:在MyItem里声明接收todo对象。
props:['todo'],
然后,在MyItem里展示todos,
<span>{{todo.title}}</span>
,且在checkbox的input显示是否done。(是否完成要看todo的done属性,checkbox是否勾选看checked属性,因此用下面的方式动态展示多选框的状态)
<input type="checkbox" :checked="todo.done"/>
本小节结束,完整代码
MyList.vue
<template>
<ul class="todo-main">
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
data() {
return {
todos: [
{id:'001',title:'吃饭',done: true},//id是字符串,数字是有尽头的,一般是哈希值
{id:'002',title:'学习',done: false},
{id:'003',title:'工作',done: false},
]
}
}
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
MyItem.vue
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
props:['todo'],
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
运行结果
可以看到吃饭已经被打勾。
3.6.4.2 添加
步骤1:按下回车添加,因此在MyHeader里的input框中,添加事件,keyup绑定键盘事件,enter修饰符——按下回车走逻辑。
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
步骤2:还是在MyHeader里,配置methods,写add方法。首先要收集input框里的输入,因此用v-model收集。
<input type="text" placeholder="请输入你的任务名称,按回车键确认"
v-model="title" @keyup.enter="add"/>
步骤3:然后初始化title
data() {
return {
title: ''
}
},
步骤4:然后编写add方法。
1 将用户的输入包装成一个todo对象
uuid:用于生成全球唯一的字符串编码。地理位置+电脑网卡Mac地址+等等。变种:nanoid,uuid一定程度上的精简。安装:npm i nanoid。
1.1 引入nanoid
import {nanoid} from 'nanoid'
1.2 包装为一个对象
const todoObj = { id: nanoid(), title: this.title, done: false };
2 将对象添加到todoList中。这里出现问题了。
问题分析:todoList在MyList.vue中,todoObj对象在MyHeader.vue中。想从组件外部往组件内部携带一些数据,可以通过全局事件总线/消息订阅与发布/vuex实现。
怎么处理呢?将todoList数据放到App.vue中。然后MyList.vue要用就从App.vue拿,MyHeader.vue要添加就将数据传给App.vue。示意图如下。
首先将MyList的todos给App.vue。
export default {
name: "App",
components: { MyHeader, MyList, MyFooter },
data() {
return {
todos: [
{id:'001',title:'吃饭',done: true},//id是字符串,数字是有尽头的,一般是哈希值
{id:'002',title:'学习',done: false},
{id:'003',title:'工作',done: false},
]
}
},
methods: {},
};
todos是MyList要用,因此传给MyList
<MyList :todos="todos"/>
MyList接收。props接收的数据出现在了MyList组件的实例对象vc上。只要是vc身上的东西,模板里可以随意使用,因此模板里的todos这里不需要修改。
props:['todos']
然后是MyHeader里的todoObj给App.vue。
(父组件给子组件传一个函数,子组件在合适的时候去调用这个函数,但是函数是在父组件的,因此父组件就能收到参数了)
首先是App.vue传给MyHeader一个receive函数。分为1,先定义函数,
methods: {
receive(x) {
console.log(x)
}
},
2,传给MyHeader这个函数。这里还是通过props传,props可以传一个函数。
<MyHeader :receive="receive"/>
其次是MyHeader收到这个函数,
props:['receive'],
然后在add方法(合适的时候)里调用receive函数。
methods: {
add() {
const todoObj = { id: nanoid(), title: this.title, done: false };
this.receive(todoObj);
// console.log(todoObj);
},
},
本来receive函数的定义是在App.vue中,因此App.vue就能收到函数的参数了。
然后App里将收到的参数添加到todoList中。
methods: {
addTodo(todoObj) {
// console.log(x)
this.todos.unshift(todoObj)
}
这里将receive函数改名为addTodo了(之前的receive函数包括App使用MyHeader标签,以及MyHeader中的receive都改名了)
最好添加完,input里的输入也清空。在MyHeader的add方法里清空,
methods: {
add() {
const todoObj = { id: nanoid(), title: this.title, done: false };
this.addTodo(todoObj);
this.title = ''
// console.log(todoObj);
},
},
如果输入为空(空格也认为是空),不会添加,且会提示。
methods: {
add() {
if (this.title.trim() !== "") {
const todoObj = { id: nanoid(), title: this.title, done: false };
this.addTodo(todoObj);
this.title = "";
// console.log(todoObj);
} else {
alert("输入不能为空");
}
},
},
效果
本小节结束,完整代码
MyList
<template>
<ul class="todo-main">
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
props:['todos']
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
MyHeader
<template>
<div class="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="add"
/>
</div>
</template>
<script>
import { nanoid } from "nanoid";
export default {
name: "MyHeader",
data() {
return {
title: "",
};
},
props: ["addTodo"],
methods: {
add() {
if (this.title.trim() !== "") {
const todoObj = { id: nanoid(), title: this.title, done: false };
this.addTodo(todoObj);
this.title = "";
// console.log(todoObj);
} else {
alert("输入不能为空");
}
},
},
};
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
App
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos"/>
<MyFooter />
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: { MyHeader, MyList, MyFooter },
data() {
return {
todos: [
{id:'001',title:'吃饭',done: true},//id是字符串,数字是有尽头的,一般是哈希值
{id:'002',title:'学习',done: false},
{id:'003',title:'工作',done: false},
]
}
},
methods: {
addTodo(todoObj) {
// console.log(x)
this.todos.unshift(todoObj)
}
},
};
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
运行结果
添加功能实现。
3.6.4.3 勾选
勾选要可以引起数据的变化。
步骤1:绑定事件。在MyItem里绑定事件
做法1:绑定点击事件。@click
<input type="checkbox" :checked="todo.done" @click="handleCheck(todo.id)"/>
做法2:绑定change事件。
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
然后编写handleCheck方法
methods: {
handleCheck(id) {
}
}
步骤2:通知App组件将对应的todo对象的done值取反。
数据在哪里,操作数据的方法就在哪里。
todos在App组件里,那么所有对数据的增删改查操作都应该配置在App里。
//勾选或者取消勾选一个todo
checkTodo(id) {
this.todos.forEach((todo)=>{
if(todo.id === id) {
todo.done = !todo.done;
}
})
}
然后真正要用这个方法的是MyItem,因此要将这个checkTodo方法传给MyList,然后传给MyItem。
先传给MyList
<MyList :todos="todos" :checkTodo="checkTodo"/>
MyList接收
props: ["todos", "checkTodo"],
MyList传给MyItem(这里可以注意一下标签的缩进)
<MyItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
:checkTodo="checkTodo"
/>
MyItem接收到checkTodo方法,并且使用该方法
props:['todo', 'checkTodo'],
methods: {
handleCheck(id) {
// 通知App组件将对应的todo对象的done值取反
this.checkTodo(id);
}
}
功能完成,完整代码如下。
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos" :checkTodo="checkTodo"/>
<MyFooter />
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: { MyHeader, MyList, MyFooter },
data() {
return {
todos: [
{id:'001',title:'吃饭',done: true},//id是字符串,数字是有尽头的,一般是哈希值
{id:'002',title:'学习',done: false},
{id:'003',title:'工作',done: false},
]
}
},
methods: {
addTodo(todoObj) {
// console.log(x)
this.todos.unshift(todoObj)
},
//勾选或者取消勾选一个todo
checkTodo(id) {
this.todos.forEach((todo)=>{
if(todo.id === id) {
todo.done = !todo.done;
}
})
}
},
};
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
MyList.vue
<template>
<ul class="todo-main">
<MyItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
:checkTodo="checkTodo"
/>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
props: ["todos", "checkTodo"],
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
MyItem.vue
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
props:['todo', 'checkTodo'],
methods: {
handleCheck(id) {
// 通知App组件将对应的todo对象的done值取反
this.checkTodo(id);
}
}
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
做法2
将前面的操作都删掉。
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
如果input是checkbox,且v-model绑定的是一个boolean值,那么这个boolean值就可以觉得checkbox勾还是不勾。
<input type="checkbox" v-model="todo.done">
为什么呢?首先初始化列表,来维护勾还是不勾。然后v-model是一个双向数据绑定,勾不勾都会引起todo.done的变化,而todo对象来自App.vue,那么todo.done就会变化。
但是不建议这么写,稍微有点违反原则(修改了props)。因为todo是通过props接收的,但是props只读不能修改数据值。但是呢,对于对象属性的修改,vue监听不到,因此todo这里没有改变。所以再改回去吧。
3.6.4.4 删除
每项都有删除按钮,鼠标悬浮时会有高亮效果(整个li)
步骤1:加高亮效果
li:hover {
background-color: #ddd;
}
步骤2:鼠标悬浮的时候,对应todo显示删除按钮
li:hover button{
display: block;
}
步骤3:点击删除按钮,拿到这件事的id,然后从todos根据id删除。3.1 找到点击按钮,绑定点击事件,
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
3.2 写事件处理函数
删除todo的函数在App里定义,用filter过滤,且将过滤后的数组作为新的todos
// 删除一个todo
deleteTodo(id) {
this.todos = this.todos.filter((todo) => {
return todo.id !== id;
});
},
与checkTodo的操作一样,App将deleteTodo传给MyList,MyList接收后传给MyItem,MyItem使用deleteTodo。
删除todo的操作在MyItem里完成。
handleDelete(id) {
if(confirm('确定删除吗')) {
this.deleteTodo(id);
}
}
以上操作完成,完整代码如下。
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo" />
<MyList
:todos="todos"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
<MyFooter />
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: { MyHeader, MyList, MyFooter },
data() {
return {
todos: [
{ id: "001", title: "吃饭", done: true }, //id是字符串,数字是有尽头的,一般是哈希值
{ id: "002", title: "学习", done: false },
{ id: "003", title: "工作", done: false },
],
};
},
methods: {
addTodo(todoObj) {
// console.log(x)
this.todos.unshift(todoObj);
},
//勾选或者取消勾选一个todo
checkTodo(id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.done = !todo.done;
}
});
},
// 删除一个todo
deleteTodo(id) {
this.todos = this.todos.filter((todo) => {
return todo.id !== id;
});
},
},
};
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
MyList.vue
<template>
<ul class="todo-main">
<MyItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
name: "MyList",
components: { MyItem },
props: ["todos", "checkTodo",'deleteTodo'],
};
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
MyItem.vue
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@change="handleCheck(todo.id)"
/>
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "MyItem",
props: ["todo", "checkTodo", "deleteTodo"],
methods: {
handleCheck(id) {
// 通知App组件将对应的todo对象的done值取反
this.checkTodo(id);
},
handleDelete(id) {
if (confirm("确定删除吗?")) {
this.deleteTodo(id);
}
},
},
};
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: #ddd;
}
li:hover button {
display: block;
}
</style>
3.6.4.5 底部统计
todos是全部的数量,todos里的todo的done值为true的个数是已完成的数量。
3.6.4.5.1 全部的数量
步骤1:App将todos传给MyFooter,
<MyFooter
:todos="todos"/>
且MyFooter声明接收
props: ['todos']
步骤2:全部就是todos.length
<span> <span>已完成0</span> / 全部{{todos.length}} </span>
3.6.4.5.2 已完成的数量
要算的东西,考虑使用计算属性。
vue的原则:模板语法的代码,要求尽可能简单,不要太长。
步骤1:给已完成起个名字
<span>已完成{{doneTotal}}</span>
步骤2:定义计算属性doneTotal
写法1:简单写法
computed: {
doneTotal() {
let i = 0;
this.todos.forEach((todo)=>{
if(todo.done === true) {
i++;
}
return i;
})
}
}
写法2:高端写法,使用reduce(专门用于做条件统计)
MDN->搜索reduce
// 箭头函数return可以不写
return this.todos.reduce((pre,todo)=> pre + (todo.done? 1:0 ),0)
本小节结束,全部代码如下
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo" />
<MyList
:todos="todos"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
<MyFooter
:todos="todos"/>
</div>
</div>
</div>
</template>
MyFooter.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span> <span>已完成{{doneTotal}}</span> / 全部{{todos.length}} </span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter",
props: ['todos'],
computed: {
doneTotal() {
// 箭头函数return可以不写
return this.todos.reduce((pre,current)=> pre + (current.done? 1:0 ),0)
// let i = 0;
// this.todos.forEach((todo)=>{
// if(todo.done === true) {
// i++;
// }
// return i;
// })
}
}
};
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
3.6.4.6 底部交互
完成底部的多选框和清除功能
3.6.4.6.1 多选框状态
如果tod都被勾选,那么底部的多选框要被勾选。如果已完成!==全部,那么多选框就不能被勾选。
步骤1:将全部用计算属性计算出来
<span> <span>已完成{{doneTotal}}</span> / 全部{{total}} </span>
total定义
total() {
return this.todo.length;
},
步骤2:将多选框的状态通过计算属性得出
<input type="checkbox" :checked="isAll"/>
isAll定义为
isAll() {
return this.doneTotal === this.total && this.total > 0;
}
且如果没有任务,那么底部不要显示
<div class="todo-footer" v-show="total">
当total为0,那么v-show为0,则footer不会显示。
步骤2:点击多选框,所有todo都被勾选。
绑定一个change事件。
<input type="checkbox" :checked="isAll" @change="changeAll"/>
编写changeAll函数,告诉存储todo的App,全选/全不选
App里改变todo状态的函数
// 全选/取消全选todo
changeAll(done) {
this.todo.forEach((todo) => {
todo.done = done;
})
}
且传给MyFooter
<MyFooter :todos="todos" :changeAll="changeAll"/>
然后MyFooter接收下
props: ['todos','changeAll'],
且使用changeAll函数
methods: {
changeAll(e) {
this.changeAll(e.target.checked);
}
}
改进方式
将多选框的状态用v-model实现。
<input type="checkbox" v-model="isAll"/>
v-model绑定isAll计算属性,isAll通过get获取到值,通过set修改值
isAll: {
get() {
return this.doneTotal === this.total && this.total > 0;
},
set(value) {
this.changeAll(value);
}
}
3.6.4.6.2 清空数据
步骤1:给清空按钮绑定点击事件
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
步骤2:编写清除所有已完成todos的函数,App里完成
// 清除所有已经完成的tood
clearAll() {
this.todos = this.todos.filter((todo) => {
return todo.done !== true;
});
}
步骤3:App将函数传给MyFooter,MyFooter接收clearAll
<MyFooter
:todos="todos" :changeAll="changeAll" :clear="clearAll"/>
props: ['todos','changeAll','clearAll'],
步骤4:Myfooter去调用
clear() {
this.clearAll();
}
完整代码如下
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo" />
<MyList
:todos="todos"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
<MyFooter
:todos="todos" :changeAll="changeAll" :clearAll="clearAll"/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
import MyFooter from "./components/MyFooter.vue";
export default {
name: "App",
components: { MyHeader, MyList, MyFooter },
data() {
return {
todos: [
{ id: "001", title: "吃饭", done: true }, //id是字符串,数字是有尽头的,一般是哈希值
{ id: "002", title: "学习", done: false },
{ id: "003", title: "工作", done: false },
],
};
},
methods: {
addTodo(todoObj) {
// console.log(x)
this.todos.unshift(todoObj);
},
//勾选或者取消勾选一个todo
checkTodo(id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.done = !todo.done;
}
});
},
// 删除一个todo
deleteTodo(id) {
this.todos = this.todos.filter((todo) => {
return todo.id !== id;
});
},
// 全选/取消全选todo
changeAll(done) {
this.todo.forEach((todo) => {
todo.done = done;
})
},
// 清除所有已经完成的tood
clearAll() {
this.todos = this.todos.filter((todo) => {
return todo.done !== true;
});
}
},
};
</script>
MyFooter
<template>
<div class="todo-footer" v-show="total">
<label>
<input type="checkbox" v-model="isAll" />
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="clear">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "MyFooter",
props: ["todos", "changeAll", "clearAll"],
computed: {
total() {
return this.todos.length;
},
doneTotal() {
// 箭头函数return可以不写
return this.todos.reduce(
(pre, current) => pre + (current.done ? 1 : 0),
0
);
},
isAll: {
get() {
return this.doneTotal === this.total && this.total > 0;
},
set(value) {
this.changeAll(value);
},
},
},
methods: {
clear() {
this.clearAll();
},
},
};
</script>
3.6.5 总结
- 1 组件化编码流程
- 1 拆分静态组件:组件按照功能点拆分,命名不要与html元素冲突
- 2 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用。
- 1 一个组件在用,放在组件自身即可
- 2 一些组件在用,放在他们共同的父组件上
- 3 实现交互,从绑定事件开始
- 2 props适用于
- 父组件==>子组件 通信
- 子组件==>父组件通信(要求父先给子一个函数)
- 3 使用v-model要切记,v-model绑定的值不能是props传过来的值,因为props是不可以被修改的
- 4 props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这么做
3.6.6 todo-List案例-本地存储版
用监视watch实现todos的变化,
App.vue
当todos改变的时候,通过watch添加到浏览器的本地存储。这里启用深度监视,这样todo里的done属性改变,本地存储的数据也会更新。
watch: {
todos: {
deep:true,//深度监视
handler(value) {
localStorage.setItem('todos',JSON.stringify(value));
}
}
}
初始化的时候也要从本地存储里读数据。
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || []
}
}
3.7 浏览器的本地存储
浏览器的本地存储,是JS里的功能,不是Vue的,不需要脚手架。
localStorage
浏览器的本地存储。
举例,唯品会网站,点击搜索皮鞋,然后浏览器整个都关掉。再打开网站和搜索框,结果搜索历史里有皮鞋。
皮鞋 借助本地存储,存到了硬盘上。
查看:浏览器的开发者工具-> Application -> Storage
以上就是浏览器本地存储的应用,就是往用户本地存储点东西。
唯品会可以放,我们能不能放呢?
3.7.1 本地存储的保存
本地存储实现
<body>
<h2>localStorage</h2>
<!-- click事件,原生html要加() -->
<button onclick="saveData()">点我保存一个数据</button>
<script type="text/javascript">
function saveData() {
// window.localStorage.setItem('msg','hello!');
// window上的东西,那么可以省略window不写
localStorage.setItem('msg','hello!');
}
</script>
</body>
1 key和value都是string类型,如果不是就会转换为string类型
2 保存一个对象(对象是直接调对象.toString()存到本地存储的value里)。
let p = { name: '张三', age: 18 };
function saveData() {
// window.localStorage.setItem('msg','hello!');
// window上的东西,那么可以省略window不写
localStorage.setItem('msg', 'hello!');
localStorage.setItem('msg1', p);
}
怎么处理呢,使用JSON.stringify()
localStorage.setItem('msg1', JSON.stringify(p));
这会就对了
这里本质是个字符串,但是浏览器能猜到这是个对象,就解析成对象的形式了。
3.7.2 本地存储的读取
读取对象
function readData() {
const result = localStorage.getItem('msg1');
console.log(JSON.parse(result));
}
以对象的形式显示(普通字符串就直接显示result,对象的话要对result进行JSON.parse())
3.7.3 本地存储的删除
直接调用removeItem()
localStorage.removeItem('msg');
3.7.4 本地存储的清空
清空本地存储。
localStorage.clear();
注意:读取一个没有的结果,是null,JSON.parse(null) 还是null。
3.7.5 sessionStorage
session,会话。sessionStorage对象和localStorage对象中的方法一致,两者不同的是:只要浏览器关闭,那么sessionStorage就会被全部清除掉;
localStorage和sessionStorage统称为WebStorage。
4备注,2 手动清楚,有2种方式,1是调用API,2清空缓存(用户主动对浏览器清空缓存)。
3.8 Vue中的自定义事件
自定义事件,区别于js里的内置事件(比如click、keyup等内置事件)
内置事件给html元素使用,自定义事件给组件使用。
自定义事件是一种组件间通信的方式,适用于:子组件 ===> 父组件
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
案例
要求:在school组件有个按钮,点下按钮,就将学校名传递给app组件。即子组件向父组件传递数据。
3.8.1 自定义属性实现子组件向父组件传递数据
这样的操作以前做过,即通过props传递数据,但是前提是父组件要给子组件提供一个函数。
App.vue
<template>
<div class="app">
<h1>{{msg}}</h1>
<School :getSchoolName="getSchoolName"></School>
<Student></Student>
</div>
</template>
<script>
//导入子组件
import Student from './components/Student'
import School from './components/School'
export default {
name: 'App',
components: { School, Student },
data() {
return {
msg: '你好啊!'
}
},
methods: {
getSchoolName(name) {
console.log('App收到了学校名:', name)
}
}
}
</script>
<style scoped>
.app {
background-color: gray;
padding: 5px;
}
</style>
School.vue
<template>
<div class="school">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="sendSchoolName">把学校名给App</button>
</div>
</template>
<script>
export default {
name:'School',
props:['getSchoolName'],
data() {
return {
name:'SGG',
address:'Beijing',
}
},
methods: {
sendSchoolName(){
// 调用父组件传递过来的方法
// 将数据传递给父组件
this.getSchoolName(this.name)
}
},
}
</script>
<style scoped>
.school{
background-color: skyblue;
padding: 5px;
}
</style>
这样就实现了school组件向app组件传递数据的操作。
3.8.2 自定义事件实现子组件向父组件传递数据
在父组件里给子组件绑定自定义事件
App.vue
<Student v-on:atgugui="getStudentName"> </Student>
<!-- v-on给student组件的实例对象vc上绑定了一个atguigu事件,
如果触发,就会调用getStudentName函数 -->
<!-- 给student组件的实例对象绑定了事件,就要去student那里去触发事件 -->
触发事件的demo函数
methods: {
demo() {
console.log('getStudentName被调用了');
}
},
在子组件里触发自定义事件
通过按钮点击来触发事件
<button @click="sendStudentName">把学生名给学校</button>
在sendStudentName方法内部触发事件。
注意:这里给student组件的实例对象vc绑定了自定义事件atguigu,那么要触发这个事件,就去找student的vc,在student中也就是个this。然后调用this的$emit(emit:发射;爆发)方法,函数的参数是要触发的事件——'atguigu'。
并且将学生的name传给App.vue
methods: {
sendStudentName() {
// 触发student身上的atguigu
this.$emit('atguigu', this.name);
}
}
App.vue里的getStudentName接受student传来的name
methods: {
getStudentName(name) {
console.log('demo被调用了',name);
}
},
示意图
v-on可以简写为@
3.8.3 使用ref绑定自定义事件
使用ref绑定student实例对象
<Student ref="student"/>
触发事件
mounted() {
//app挂载完毕
this.$refs.student.$on('atguigu',this.getStudentName);
}
使用ref绑定自定义事件的优点:灵活性强。
案例:要求App组件挂载完成3秒钟后,给student子组件实例绑定自定义事件。这样就只能用ref实现,而v-on就不能完成。
mounted() {
//app挂载完毕
setTimeout(() => {
this.$refs.student.$on('atguigu',this.getStudentName);
}, 3000);
}
案例2:让自定义事件只能被触发一次。
使用ref,$on改为$once
mounted() {
//app挂载完毕
setTimeout(() => {
this.$refs.student.$once('atguigu',this.getStudentName);
}, 3000);
}
使用v-on
在自定义事件后加入事件修饰符once
<Student v-on:atgugui.once="getStudentName"> </Student>
案例3:接收多个参数。
getStudentName(name,...params) {
console.log('demo被调用了',name);
}
这里第一个参数就是name,剩下的所有参数都会放到数组params中。
...是扩展运算符。
以上两小节没有给出完整代码,可以参考【精选】[Vue]组件自定义事件_vue组件自定义事件_萤火虫的小尾巴的博客-CSDN博客
3.8.4 解绑自定义事件
不用的自定义事件,最好解绑一下。
给谁绑定的自定义事件,就去找谁去解绑。
1 解绑一个自定义事件
在student组件里,给解绑按钮绑定一个点击事件。
<button @click="unbind">解绑atguigu事件</button>
定义unbind事件
methods: {
unbind() {
this.$off('atguigu');
}
}
以上适用于解绑一个自定义事件。
完整代码
App.vue
<template>
<div class="app">
<h1>{{ msg }}</h1>
<School />
<hr />
<Student v-on:atguigu="getStudentName"> </Student>
<hr />
</div>
</template>
<script>
import School from "./components/School.vue";
import Student from "./components/Student.vue";
export default {
name: "App",
components: { School, Student },
data() {
return {
msg: "你好啊",
};
},
methods: {
getStudentName(name) {
console.log("demo被调用了", name);
}
}
};
</script>
<style>
.app {
background-color: gray;
}
</style>
Student.vue
<template>
<div class="student">
<h2>学生名称:{{ name }}</h2>
<h2>学生性别:{{ gender }}</h2>
<button @click="sendStudentName">把学生名给学校</button>
<button @click="unbind">解绑atguigu事件</button>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "张三",
gender: "男",
};
},
methods: {
sendStudentName() {
// 触发student身上的atguigu
this.$emit("atguigu", this.name);
},
unbind() {
this.$off("atguigu");
},
},
};
</script>
<style scoped>
.student {
background-color: orange;
}
</style>
2 解绑多个自定义事件
在$off方法里,传入一个数组,里面有多个事件名。
methods: {
unbind() {
this.$off(['atguigu','atguiguguigu']);
}
}
3 解绑所有自定义事件
methods: {
unbind() {
this.$off();
}
}
销毁后,自定义事件就失效了,但是原生的DOM事件依然可以调用,只是没有响应。(对应生命周期的destroy)
3.8.5 父组件显示子组件传来的数据
1 使用v-on绑定自定义事件
在App.vue中定义变量studentName,用于接收从子组件传来的值
data() {
return {
msg: "你好啊",
studentName: ''
};
},
给变量studentName赋值。
methods: {
getStudentName(name) {
console.log("demo被调用了", name);
this.studentName = name;
}
}
2 使用ref绑定自定义事件
也可以实现。
但是如果将自定义事件触发后的回调函数写在mounted()中,此时的this是触发事件的子组件的实例对象,
但是写成箭头函数就可以了。为什么?如果是箭头函数,但是箭头函数没有自己的this。那就向外找,也就是mounted(),mounted()里写普通函数,this就是当前组件的实例对象。
注意:通过this.$refs.xxx.$on('atguigu',回调)
绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
此外,为子组件绑定内置事件,且不让内置事件被认为是自定义事件,需要使用事件修饰符native
,即可为子组件绑定内置事件。(这里有点)
3.8.6 总结
一种组件间通信的方式,适用于:子组件 ===> 父组件
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
绑定自定义事件:
第一种方式,在父组件中:
<Demo @atguigu="test"/>或 <Demo v-on:atguigu="test"/>
第二种方式,在父组件中:
<Demo ref="demo"/>
......
mounted(){
this.$refs.xxx.$on('atguigu',this.test)
}
若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。
触发自定义事件:this.$emit('atguigu',数据)
解绑自定义事件this.$off('atguigu')
组件上也可以绑定原生DOM事件,需要使用native修饰符。
注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
3.9 全局事件总线
可以实现任意组件间的通信。
p84