菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
0
0

Vue组件化开发+Vue脚手架

原创
05/13 14:22
阅读数 179

1. 什么是组件化开发

传统方式编写应用的问题:
● 依赖关系混乱,不好维护
● 代码复用率不高

组件:实现应用中局部功能代码和资源的集合,组件是可复用的 Vue 实例, 把一些公共的模块抽取出来,然后写成单独的工具组件或者页面
在需要的页面中就直接引入即可,提高了代码的复用率。

组件化开发思想:如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
图片.png
图片.png

模块化:当应用中的js都以模块来编写的,那这个应用就是一个模块化的应用。
组件化:当应用中的功能都是以组件的方式来编写的,那这个应用就是一个组件化的应用

2 非单文件组件

非单文件组件:一个文件中包含n个组件

2.1 使用组件的三大步骤

● 定义组件
● 注册组件
● 使用组件

2.2 如何定义组件

使用const school =Vue.extend(options) (简写成 const school = options,vue底层给了一个判断,当你传入的参数是对象时,vue自动调用Vue.extend帮你创建组件对象)创建,其中options和new Vue(options)创建实例时传入的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(遍历起始点)
● 组件模板内容可以是模板字符串`` (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>

图片.png

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>

图片.png

2.7 一个重要的内置关系

内置关系:

组件实例对象的原型对象的__proto__属性 全等于 Vue的原型对象
VueComponent.prototype.proto === Vue.prototype

这样组件实例对象vc就可以访问到Vue原型上的属性和方法(本来VueComponent原型对象的__ proto __应该指向Object的原型对象,vue强行更改的)
组件实例对象就是小型的实例对象vm,但它没有el配置对象
每一个构造函数都有原型对象prototype,把所有不变的的方法都直接定义在原型对象上,然后从构造函数中new出来的实例对象就可以共享这些方法。
实例对象都会有__proto__属性,指向构造函数的原型对象prototype,之所以实例对象可以使用构造函数原型对象的属性和方法,就是因为对象有__proto__属性的存在。

构造函数.prototype === 实例对象.__ proto __
图片.png

<!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 单文件组件

传统组件的问题与解决方案
问题:
1.全局定义的组件必须保证组件的名称不重复
2.字符串模板缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的
3.不支持 CSS 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
4.没有构建步骤限制,只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器(如:Babel)

解决方案:
针对传统组件的问题,Vue 提供了一个解决方案 —— 使用 Vue 单文件组件。
单文件组件:一个文件只包含1个组件
单文件组件的组成结构
● template 组件模板区域
● script 组件交互区域(业务逻辑)
● style 组件样式区域

<template>
	<!-- 这里用于定义Vue组件的模板内容 --> 
</template> 

<script> 
	// 这里用于定义Vue组件的业务逻辑 
	export default { 
	    // 私有数据
		data: () { return {} },  
		// 处理函数
		methods: {}  
		// ... 其它业务逻辑 
	} 
</script> 

//加scoped 组件私有
<style scoped> 
	/* 这里用于定义组件的样式 */ 
</style>

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 项目基础架构,是一个基于 Vue.js 进行快速开发的完整系统。

安装最新版本 vue-cli

npm install -g @vue/cli

安装vue-cli 3.x及以上指定版本

npm install  '@vue-cli@3.x.x' -g

检查安装是否成功

vue -V 

创建项目

vue create xxx

看项目需求,可以选择vue2和vue3
图片.png

运行项目

npm run serve

4.2 项目示例

模板项目结构
图片.png

├── 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>

图片.png
项目报错
图片.png
报错原因: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:'张三' //默认值
	}
}

//以对象形式列出所有 prop,这些 property 的名称和值分别是 prop 各自的名称和类型:
props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

备注: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>

图片.png

5.5 scoped样式

scoped样式:让样式在局部生效,防止冲突。
写法:

<style scoped>
</style>

5.6 nextTick

语法:this.$nextTick(回调函数)
作用:在下一次 DOM 更新结束后执行其指定的回调。
什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

6 TodoList案例

6.1 目标功能界面

图片.png

6.2 界面模块拆分

图片.png
图片.png

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适用于:
● 父组件 ==> 子组件 通信
● 子组件 ==> 父组件 通信(要求父先给子一个函数)
图片.png

使用v-model时要切记:
● v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
● props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

小技巧:
● 一堆数据用数组,每一个数据中的属性太多用对象
● 数据在哪里,操作数据的方法就在哪里

6.7 浏览器的本地储存

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

发表评论

0/200
0 点赞
0 评论
收藏