【Vue】Vue全家桶(二)Vue组件化开发+Vue脚手架

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 浏览器的本地储存

  1. 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
  2. 浏览器端通过 sessionStoragelocalStorage 属性来实现本地存储机制。
  3. 相关API
  • xxxxxStorage.setItem(‘key’, ‘value’);
    该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值
  • xxxxxStorage.getItem(‘person’);
    该方法接受一个键名作为参数,返回键名对应的值。
  • xxxxxStorage.removeItem(‘key’);
    该方法接受一个键名作为参数,并把该键名从存储中删除。
  • xxxxxStorage.clear()
    该方法会清空存储中的所有数据。
  1. 备注
  • 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>

猜你喜欢

转载自blog.csdn.net/Better_Xing/article/details/116913205
今日推荐