1. 什么是组件化开发
传统方式编写应用的问题
:
- 依赖关系混乱,不好维护
- 代码复用率不高
组件
:实现应用中局部功能代码和资源的集合,组件
是可复用的 Vue 实例, 把一些公共的模块抽取出来,然后写成单独的的工具组件或者页面,在需要的页面中就直接引入即可,提高了代码的复用率
组件化开发思想
:如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
模块化
:当应用中的js都以模块来编写的,那这个应用就是一个模块化的应用。
组件化
:当应用中的功能都是以组件的方式来编写的,那这个应用就是一个组件化的应用
2 非单文件组件
非单文件组件
:一个文件中包含n个组件
单文件组件
:一个文件只包含1个组件
2.1 使用组件的三大步骤
- 定义组件
- 注册组件
- 使用组件
2.2 如何定义组件
使用const school =Vue.extend(options)
(简写成 const school = options,vue底层给了一个判断,当你传入的参数是对象时vue自动调用Vue.extend帮你创建组件对象)创建,其中options和new Vue(options)创建实例时传入的options几乎一样,仅有例外是像el这样跟实例特有的选项。
注意
:
- 不要写el,最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器
- data必须是一个函数 ,这样每个实例都可以维护一份被返回对象的独立拷贝,保证每个模板的数据是相互独立的,避免组件被复用时数据存在引用关系(若data为对象不是函数,当这个组件被两个结构引用,两个结构中的data都指向同一地址,其中一个结构更改了data数据,则另一个结构也会同样更改)
//data为对象形式
let data = {
a: 1,
b: 2,
};
const x1 = data;
const x2 = data;
x1.a = 99;
console.log(x1.a); //99
console.log(x2.a); //99
//data为函数形式
function data1() {
return {
a: 1,
b: 2,
};
}
const x3 = data1();
const x4 = data1();
x3.a = 99;
console.log(x3.a); //99
console.log(x4.a); //1
- 组件模板内容只包含一个根元素div ,单文件组件template下有且只能有一个根元素div
在vue实例中会通过el:'#app’挂载Dom,但vue不知道dom的起始点,所以div标签就标记了挂载Dom元素的起始点,在单文件组件中,template下的元素div其实也是一个遍历起始点,“树"状数据结构,肯定要有个"根” - 组件模板内容可以是模板字符串`` ,ES6 引入新的声明字符串的方式
组件命名方式
:
-
一个单词组成
第一种写法(首字母小写):school
第二种写法(首字母大写):School -
多个单词组成
第一种写法(kebab-case命名):my-school
第二种写法(CamelCase命名):MySchool(在使用Vue脚手架的情况下可用) -
组件名尽可能回避HTML中已有的元素名称,例如h2、H2,也可以使用name配置项指定组件在开发者工具中呈现的名字
const school = Vue.extend({
template: `
<div class="demo">
<h2>学校名称:{
{schoolName}}</h2>
<h2>学校地址:{
{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
data() {
return {
schoolName: '尚硅谷',
address: '北京昌平'
}
}
2.3 如何注册组件
全局注册
:Vue.component(‘组件名’, 组件)
Vue.component('school', school)
局部注册
:new Vue的时候传入components选项,只能在当前vue实例挂载的对象中使用,类似于局部变量,有函数作用域。
//注册方式
const app = new Vue({
el:"#app",
components:{
//局部组件创建
//'school': school
school
}
})
2.4 如何使用组件
第一种写法:直接使用<school></school>
调用组件
第二种写法:<school/>
(在使用Vue脚手架的情况下可用)
案例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>基本使用</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<hello></hello>
<hr>
<h1>{
{msg}}</h1>
<hr>
<!-- 第三步:编写组件标签 -->
<school></school>
<hr>
<!-- 第三步:编写组件标签 -->
<student></student>
</div>
<div id="root2">
<hello></hello>
</div>
</body>
<script type="text/javascript">
//第一步:创建school组件
const school = Vue.extend({
template: `
<div class="demo">
<h2>学校名称:{
{schoolName}}</h2>
<h2>学校地址:{
{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
data() {
return {
schoolName: '尚硅谷',
address: '北京昌平'
}
},
methods: {
showName() {
alert(this.schoolName)
}
},
})
//第一步:创建student组件
const student = Vue.extend({
template: `
<div>
<h2>学生姓名:{
{studentName}}</h2>
<h2>学生年龄:{
{age}}</h2>
</div>
`,
data() {
return {
studentName: '张三',
age: 18
}
}
})
//第一步:创建hello组件
const hello = Vue.extend({
template: `
<div>
<h2>你好啊!{
{name}}</h2>
</div>
`,
data() {
return {
name: 'Tom'
}
}
})
//第二步:全局注册组件
Vue.component('hello', hello)
//创建vm
new Vue({
el: '#root',
data: {
msg: '你好啊!'
},
//第二步:注册组件(局部注册)
components: {
school,
student
}
})
new Vue({
el: '#root2',
})
</script>
</html>
2.5 组件嵌套
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>组件的嵌套</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
</div>
</body>
<script type="text/javascript">
//定义school的student子组件
const student = Vue.extend({
name: 'student',
template: `
<div>
<h2>学生姓名:{
{name}}</h2>
<h2>学生年龄:{
{age}}</h2>
</div>
`,
data() {
return {
name: '尚硅谷',
age: 18
}
}
})
//定义school子组件
const school = Vue.extend({
name: 'school',
template: `
<div>
<h2>学校名称:{
{name}}</h2>
<h2>学校地址:{
{address}}</h2>
<student></student>
</div>
`,
data() {
return {
name: '尚硅谷',
address: '北京'
}
},
//注册组件(局部)
components: {
student
}
})
//定义hello子组件
const hello = Vue.extend({
template: `<h1>{
{msg}}</h1>`,
data() {
return {
msg: '欢迎来到尚硅谷学习!'
}
}
})
//定义app父组件
const app = Vue.extend({
template: `
<div>
<hello></hello>
<school></school>
</div>
`,
components: {
school,
hello
}
})
//创建vm
new Vue({
template: '<app></app>',
el: '#root',
//注册组件(局部)
components: {
app
}
})
</script>
</html>
2.6 VueComponent构造函数
school组件本质是一个名为VueComponent
的构造函数,且不是程序员定义的,是Vue内部Vue.extend函数生成的,我们只需要写<school/>
或<school></school>
,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行new VueComponent(options)
特别注意
:每次调用Vue.extend,返回的都是一个全新的VueComponent(因为Vue.extend在vue内部是函数,data使用函数式是一个道理保证每个模板的数据是相互独立的)
关于this指向
:
① 组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是VueComponent(组件)实例对象
② new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是Vue实例对象
VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)
Vue的实例对象,以后简称为vm
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>VueComponent</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<school></school>
<hello></hello>
</div>
</body>
<script type="text/javascript">
//定义school组件
const school = Vue.extend({
name: 'school',
template: `
<div>
<h2>学校名称:{
{name}}</h2>
<h2>学校地址:{
{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
data() {
return {
name: '尚硅谷',
address: '北京'
}
},
methods: {
showName() {
console.log('showName', this)
}
},
})
//定义hello组件下 test子组件
const test = Vue.extend({
template: `<span>atguigu</span>`
})
//定义hello组件
const hello = Vue.extend({
template: `
<div>
<h2>{
{msg}}</h2>
<test></test>
</div>
`,
data() {
return {
msg: '你好啊!'
}
},
components: {
test
}
})
//创建vm
const vm = new Vue({
el: '#root',
components: {
school,
hello
}
})
</script>
</html>
2.7 一个重要的内置关系
内置关系
:
组件实例对象的原型对象的__proto__属性 全等于 Vue的原型对象
VueComponent.prototype.__proto__ === Vue.prototype
这样组件实例对象vc(vc是尚硅谷老师起的简称,以后就说组件实例对象)就可以访问到Vue原型上的属性和方法(本来VueComponent原型对象的_ _ proto _ 应该指向Object的原型对象,vue强行更改的)
组件实例对象就是小型的实例对象vm,但它没有el配置对象
每一个构造函数都有原型对象prototype,把所有不变的的方法都直接定义在原型对象上,然后从构造函数中new出来的实例对象就可以共享这些方法。
实例对象都会有__proto__属性,指向构造函数的原型对象prototype,之所以实例对象可以使用构造函数原型对象的属性和方法,就是因为对象有__proto__属性的存在。
构造函数.prototype === 实例对象. _ proto _ _
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>一个重要的内置关系</title>
<!-- 引入Vue -->
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<school></school>
</div>
</body>
<script type="text/javascript">
Vue.prototype.x = 99
//定义school组件
const school = Vue.extend({
name: 'school',
template: `
<div>
<h2>学校名称:{
{name}}</h2>
<h2>学校地址:{
{address}}</h2>
<button @click="showX">点我输出x</button>
</div>
`,
data() {
return {
name: '尚硅谷',
address: '北京'
}
},
methods: {
showX() {
console.log(this.x)//99
}
},
})
//创建一个vm
const vm = new Vue({
el: '#root',
data: {
msg: '你好'
},
components: {
school
}
})
//定义一个构造函数
/* function Demo(){
this.a = 1
this.b = 2
}
//创建一个Demo的实例对象
const d = new Demo()
console.log(Demo.prototype) //显式原型属性
console.log(d.__proto__) //隐式原型属性
console.log(Demo.prototype === d.__proto__)
//程序员通过显式原型属性操作原型对象,追加一个x属性,值为99
Demo.prototype.x = 99
console.log('@',d) */
</script>
</html>
3 单文件组件
单文件组件
由三部分组成:组件模板、组件交互、组件样式。
School.vue子组件
<!-- 在vscode中安装vuter 创建.vue文件 在文件里面输入<v 就可以生成单文件组件模板了 -->
<template>
<!-- 组件模板 -->
<!--单文件组件template下有且只能有一个根元素div,template下的元素div其实也是一个遍历起始点,"树"状数据结构,肯定要有个"根"-->
<div class="demo">
<h2>学校名称:{
{
name }}</h2>
<h2>学校地址:{
{
address }}</h2>
<button @click="showName">点我提示学校名</button>
</div>
</template>
<script>
// 组件交互(数据、方法相关代码)
export default {
//此处省略了Vue.extend()
//组件名 (可以不写)
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
methods: {
showName() {
alert(this.name);
},
},
};
</script>
<style>
/* 组件样式 */
.demo {
background-color: pink;
}
</style>
4 Vue 脚手架
4.1 Vue 脚手架安装
Vue CLI
是一个基于 Vue.js 进行快速开发的完整系统。
安装最新版本 vue-cli
npm install -g @vue/cli
安装vue-cli 3.x及以上指定版本
npm install '@[email protected]' -g
检查安装是否成功
vue -V
创建项目
vue create xxx
看项目需求,可以选择vue2和vue3
运行项目
npm run serve
4.2 项目示例
模板项目结构
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放组件
│ │ └── School.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
├── vue.config.js:vue可选的配置文件
代码展示
index.html主页面
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<!--让IE浏览器以最高的渲染级别渲染页面 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- 开启移动端的理想视口 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<!-- 配置页签图标 -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<!-- 配置网页标题 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- 当浏览器不支持js时 noscript中的元素就会被渲染 -->
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 容器 -->
<div id="app"></div>
<!-- 此处不用App.vue vue脚手架默认App.vue在此编译 -->
</body>
</html>
main.js入口文件
/*
该文件是整个项目的入口文件
*/
//引入Vue
import Vue from 'vue';
//引入父组件App
import App from './App.vue';
//关闭vue的生产提示
Vue.config.productionTip = false;
//创建Vue实例对象
new Vue({
//挂载dom元素:该实例为#app标签服务
el: '#app',
//创建App模板,将App组件放到容器中
render: h => h(App),
});
App.vue 父组件
<template>
<!-- 组件模板必须只包含一个根元素,这个根元素为遍历起始点 -->
<div>
<img src="./assets/logo.png" alt="log" />
<!-- 使用子组件 -->
<School></School>
<Student></Student>
</div>
</template>
<script>
//引入子组件
import School from "./components/School.vue";
import Student from "./components/Student.vue";
export default {
name: "App",//可以不写
//注册子组件
components: {
Student, School },
};
</script>
<style>
</style>
School.vue 子组件
<template>
<!-- 组件模板 -->
<div class="demo">
<h2>学校名称:{
{
name }}</h2>
<h2>学校地址:{
{
address }}</h2>
<button @click="showName">点我提示学校名</button>
</div>
</template>
<script>
// 组件交互(数据、方法相关代码)
export default {
//此处省略了Vue.extend()
//组件名
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
methods: {
showName() {
alert(this.name);
},
},
};
</script>
<style>
/* 组件样式 */
.demo {
background-color: pink;
}
</style>
Student.vue 子组件
<template>
<div>
<h2>学生姓名:{
{
name }}</h2>
<h2>学校年龄:{
{
age }}</h2>
</div>
</template>
<script>
export default {
//name: "Student",
data() {
return {
name: "张三",
age: 18,
};
},
};
</script>
<style>
</style>
项目报错
报错原因
:eslint语法检查的时候把命名不规范的代码当成了错误
解决方案
:
- 更改组件名,使其符合Vue推荐的双驼峰或-衔接命名规范,如: StudentName 或者 student-name
- 修改配置项,关闭eslint语法检查
1.在项目的根目录找到(没有就创建)vue.config.js文件
2.在文件中添加如下内容,随后保存文件重新编译即可
module.exports = {
lintOnSave: false, //关闭eslint检查
};
关于不同版本的Vue
vue.js与vue.runtime.xxx.js(main.js中引入的运行版)的区别:
vue.js是完整版的Vue,包含:核心功能 + 模板解析器。
vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
因为vue.runtime.xxx.js没有模板解析器,所以不能使用template这个配置项,需要使用render函数接收到的createElement函数去指定具体内容。render 函数和 template 一样都是创建 html 模板的
vue.config.js配置文件
使用vue inspect > output.js可以查看到Vue脚手架的默认配置。
使用vue.config.js可以对脚手架进行个性化定制,详情见:https://cli.vuejs.org/zh
5 Vue组件小知识
5.1 ref属性
ref
被用来给元素或子组件注册引用信息(id的替代者),应用在html标签上获取的是真实DOM元素或应用在组件标签上是组件实例对象(vc)
使用方式
:
//打标识
<h1 ref="xxx">.....</h1>或 <School ref="xxx"></School>
//获取
this.$refs.xxx
<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)
},
},
};
</script>
5.2 props配置项
props配置项
:让组件接收外部传过来的数据
1.父组件通过传统方式或v-bind动态绑定向子组件传送数据
<!-- App父组件 -->
<template>
<div>
<!-- 传送数据一定要写在父组件的子组件标签上(通过标签属性)-->
<!-- 传统方式传送数据 -->
//<Student name='张三'/>
<!-- 动态绑定传送数据(不限于形式,可能是函数) Student.name会作为表达式自动执行 -->
<Student :name='Student.name'/>
</div>
</template>
<script>
//引入子组件
import Student from "./components/Student.vue";
export default {
name: "App",
components: {
Student },
data() {
return {
Student:{
name:'张三'
}
};
},
};
</script>
2.子组件内部通过props接收父组件传递的数据
props配置项
:让组件接收外部传过来的数据
props传递数据原则:单向数据流,只能父传子
v-bind是不支持使用驼峰标识的,例如cUser
要改成c-User
。
<!-- Student子组件 -->
//第一种方式(只接收)最常用
props:['name']
//第二种方式(限制类型)
props:{
name:String}
//第三种方式(限制类型、限制必要性、指定默认值)
props:{
name:{
type:String, //类型
required:true, //必要性
default:'张三' //默认值
}
}
备注
:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
5.3 mixin(混入)
mixin(混入)
:可以把多个组件共用的配置提取成一个混入对象
使用方式
:
第一步:定义混合
{
data(){
....},
methods:{
....}
....
}
mixin.js
//定义混合
export const mixin = {
methods: {
showName() {
alert(this.name);
},
},
};
第二步:使用混入
全局混入:Vue.mixin(xxx)
main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
import {
mixin} from './mixin'
//全局混入
Vue.mixin(mixin)
//创建vm
new Vue({
el:'#app',
render: h => h(App)
})
局部混入:mixins:[‘xxx’]
School.vue子组件
<template>
<div class="demo">
<h2 @click="showName">学校名称:{
{
name }}</h2>
<h2>学校地址:{
{
address }}</h2>
</div>
</template>
<script>
import {
mixin } from "../mixin";
export default {
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
mixins: [mixin],
};
</script>
<style>
</style>
Student.vue子组件
<template>
<div>
<h2 @click="showName">学生姓名:{
{
name }}</h2>
<h2>学生性别:{
{
sex }}</h2>
</div>
</template>
<script>
import {
mixin} from '../mixin'
export default {
name: "Student",
data() {
return {
name: "张三",
sex: "男",
};
},
//局部混入
mixins:[mixin]
};
</script>
5.4 Vue插件
Vue插件
:用于增强Vue
本质
:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。
定义插件
对象.install = function (Vue, options) {
// 1. 添加全局过滤器
Vue.filter(....)
// 2. 添加全局指令
Vue.directive(....)
// 3. 配置全局混入(合)
Vue.mixin(....)
// 4. 添加实例方法
Vue.prototype.myMethod = function () {
...}
Vue.prototype.myProperty = xxxx
}
使用插件
Vue.use()
实例
plugins.js
export default {
install(Vue) {
//全局过滤器
Vue.filter("mySlice", function (value) {
return value.slice(0, 4);
});
//给Vue原型上添加一个方法(vm和vc就都能用了)
Vue.prototype.hello = () => {
alert("你好啊");
};
},
};
main.js
//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";
//引入插件
import plugins from "./plugins";
//使用插件
Vue.use(plugins);
//创建vm
new Vue({
el: "#app",
render: (h) => h(App),
});
School.vue子组件中使用
<template>
<div>
<h2>学校名称:{
{
name | mySlice }}</h2>
<h2>学校地址:{
{
address }}</h2>
<button @click="test">点我测试一个hello方法</button>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "尚硅谷atguigu",
address: "北京",
};
},
methods: {
test() {
this.hello();
},
},
};
</script>
5.5 scoped样式
scoped样式
:让样式在局部生效,防止冲突。
写法
:
<style scoped>
</style>
5.6 nextTick
语法:this.$nextTick(回调函数)
作用:在下一次 DOM 更新结束后执行其指定的回调。
什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。
6 TodoList案例
6.1 目标功能界面
6.2 界面模块拆分
6.3 css样式文件
base.css
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;
}
header.css
.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);
}
list.css
.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;
}
item.css
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;
}
footer.css
.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;
}
6.4 index.html和main.js
index.html 主页面
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
main.js入口文件
//引入Vue
import Vue from 'vue';
//引入父组件App
import App from './App.vue';
//关闭vue的生产提示
Vue.config.productionTip = false;
//创建Vue实例对象
new Vue({
//挂载dom元素:该实例为#app标签服务
el: '#app',
//创建App模板,将App组件放到容器中
render: h => h(App),
});
6.5 组件文件
App.vue父组件
<template>
<div id="app">
<div class="todo-container">
<div class="todo-wrap">
<!-- :addTodo(父传子)向子组件ToDoHeader传递数据 ,下面同理-->
<ToDoHeader :addTodo="addTodo" />
<ToDoList
:todoList="todoList"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
<ToDoFooter
:todoList="todoList"
:checkAllTodo="checkAllTodo"
:clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
//引入子组件
import ToDoHeader from "./components/ToDoHeader";
import ToDoList from "./components/ToDoList";
import ToDoFooter from "./components/ToDoFooter.vue";
export default {
name: "App",
//注册子组件
components: {
ToDoHeader,
ToDoList,
ToDoFooter,
},
data() {
return {
todoList: [
{
id: "001", title: "抽烟", done: true },
{
id: "002", title: "喝酒", done: false },
{
id: "003", title: "开车", done: true },
],
};
},
methods: {
//添加todo
addTodo(todoObj) {
this.todoList.unshift(todoObj);
},
//勾选或取消todo
checkTodo(id) {
this.todoList.forEach((todo) => {
if (todo.id === id) {
todo.done = !todo.done;
}
});
},
//删除todo
deleteTodo(id) {
if (confirm("你确定删除吗?")) {
this.todoList = this.todoList.filter((todo) => todo.id !== id);
}
},
//全选or取消全选
checkAllTodo(done) {
this.todoList.forEach((todo) => {
todo.done = done;
});
},
//清除所有已经完成的todo
clearAllTodo() {
//done为true的todo就不在列表中展示了
if (confirm("你确定清空吗?")) {
this.todoList = this.todoList.filter((todo) => !todo.done);
}
},
},
};
</script>
<style scoped>
/* import是ES6语法,引进模块,而@import是stylus的语法
@import是在<style>下引进styl者css文件,而在<script>引进样式文件用import*/
/* 引入css样式 */
@import "./assets/css/base.css";
</style>
ToDoHeader.vue子组件
<template>
<div class="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="add"
/>
</div>
</template>
<script>
//引入字符串ID生成器nanoid
import {
nanoid } from "nanoid";
export default {
name: "ToDoHeader",
//接收App父组件传递的数据
props: ["addTodo"],
data() {
return {
title: "",
};
},
methods: {
//添加todo
add() {
//若输入框为空 则返回 不执行下面的操作
if (!this.title) return;
//将用户输入的数据包装成一个todo对象
const todoObj = {
id: nanoid(), title: this.title, done: false };
//通知App组件添加一个todo对象
this.addTodo(todoObj);
//清空输入框
this.title = "";
},
},
};
</script>
<style scoped>
@import "../assets/css/header.css";
</style>
ToDoList.vue子组件
<template>
<ul class="todo-main">
<!-- (item,index) in 数组,若item不使用可以省略 :todo(父传子)向子组件ToDoItem传递数据 -->
<ToDoItem
v-for="todoObj in todoList"
:key="todoObj.id"
:todo="todoObj"
:checkTodo="checkTodo"
:deleteTodo="deleteTodo"
/>
</ul>
</template>
<script>
import ToDoItem from "./ToDoItem";
export default {
name: "ToDoList",
components: {
ToDoItem,
},
//App传来的checkTodo只是在这中转一下,目的是给ToDoItem组件用
props: ["todoList", "checkTodo", "deleteTodo"],
};
</script>
<style scoped>
@import "../assets/css/list.css";
</style>
ToDoItem.vue组件(ToDoList.vue的子组件)
<template>
<li>
<label>
<!-- checked选框是否勾选 -->
<input type="checkbox" :checked="todo.done" @click="checkTodo(todo.id)" />
<!-- 以下代码也可以实现勾选或取消一个todo的功能,但不推荐,因为修改了props,但vue对props里层的数据检测不到,所以没有报错 -->
<!-- <input type="checkbox" v-model="todo.done" /> -->
<span>{
{
todo.title }}</span>
</label>
<button class="btn btn-danger" @click="deleteTodo(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: "ToDoItem",
//接受父组件传递的数据
props: ["todo", "checkTodo", "deleteTodo"],
};
</script>
<style scoped>
@import "../assets/css/item.css";
</style>
ToDoFooter.vue组件
<template>
<!-- 若列表总数量total为0 则不展示footer组件 -->
<div class="todo-footer" v-show="total">
<label>
<!-- checked选框是否勾选 -->
<input type="checkbox" :checked="isAll" @click="checkAll" />
</label>
<span>
<span>已完成{
{
doneTotal }}</span> / 全部{
{
total }}
</span>
<button class="btn btn-danger" @click="clearAllTodo">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: "ToDoFooter",
props: ["todoList", "checkAllTodo", "clearAllTodo"],
computed: {
//统计列表总数量
total() {
return this.todoList.length;
},
//统计列表勾选的数量
doneTotal() {
return this.todoList.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
},
//当勾选的数量与勾选的数量相等时 footer选框勾选
isAll() {
return this.doneTotal === this.total && this.total > 0;
},
},
methods: {
//全选or取消全选
checkAll(e) {
this.checkAllTodo(e.target.checked);
},
},
};
</script>
<style scoped>
@import "../assets/css/footer.css";
</style>
6.6 总结TodoList案例
组件化编码流程:
- 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
- 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
- 一个组件在用:放在组件自身即可
- 一些组件在用:放在他们共同的父组件上(状态提升)
- 实现交互:从绑定事件开始
props适用于:
- 父组件 ==> 子组件 通信
- 子组件 ==> 父组件 通信(要求父先给子一个函数)
使用v-model时要切记:
- v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
- props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
小技巧:
- 一堆数据用数组,每一个数据中的属性太多用对象
- 数据在哪里,操作数据的方法就在哪里
6.7 浏览器的本地储存
- 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
- 浏览器端通过
sessionStorage
和localStorage
属性来实现本地存储机制。 相关API
:
- xxxxxStorage.setItem(‘key’, ‘value’);
该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值 - xxxxxStorage.getItem(‘person’);
该方法接受一个键名作为参数,返回键名对应的值。 - xxxxxStorage.removeItem(‘key’);
该方法接受一个键名作为参数,并把该键名从存储中删除。 - xxxxxStorage.clear()
该方法会清空存储中的所有数据。
备注
:
- SessionStorage存储的内容会随着浏览器窗口关闭而消失。
- LocalStorage存储的内容,需要手动清除才会消失。
- xxxxxStorage.getItem(xxx)如果xxx对应的value获取不到,那么getItem的返回值是null。
- JSON.parse(null)的结果依然是null。
6.8 ToDoList本地存储版本
App.vue
<script>
//引入子组件
import ToDoHeader from "./components/ToDoHeader";
import ToDoList from "./components/ToDoList";
import ToDoFooter from "./components/ToDoFooter.vue";
export default {
name: "App",
//注册子组件
components: {
ToDoHeader,
ToDoList,
ToDoFooter,
},
data() {
return {
//由于todoList是ToDoHeader组件和ToDoList组件都在使用,所以放在App中(状态提升)
//[]空数组 第一次使用时localStorage里面没有数据 使用空数组
//从localStorage中读取数据
todoList: JSON.parse(localStorage.getItem("todoObj")) || [],
};
},
methods: {
//...和之前的数据一样 现在省略
},
watch: {
todoList: {
deep: true,
handler(value) {
//在搜索栏添加的数据是对象形式的(在本项目中) 需要转化为字符串 JSON.Stringify()对象=>字符串 JSON.Parse()字符串=>对象
//向localStorage中添加数据
localStorage.setItem("todoObj", JSON.stringify(value));
},
},
},
};
</script>