Vue 学习笔记 -- 2. Vue 组件化编程与脚手架

Vue 学习笔记 – 2. Vue 组件化编程与脚手架

2.1 组件与模块化
  • 模块
    • 向外提供特定功能的 js 程序,一般就是一个 js 文件。
    • 为什么:js 文件很多很复杂。
    • 作用:复用 js,简化 js 的编写,提高 js 的运行效率
  • 模块:
    • 用来实现局部(特定)功能效果的代码集合(html/css/js/image……)
    • 为什么:一个界面的功能很复杂。
    • 作用:复用编码,简化项目编码,提高运行效率
  • 模块化:当应用中的 js 都以模块来编写,那这个应用就是一个模块化的应用。
  • 组件化:当应用中的功能都是多组件的方式来编写,那这个应用就是一个组件化的应用。

在这里插入图片描述
在这里插入图片描述

2.2 非单文件组件
2.2.1 基本使用
<body>
<!-- 
Vue 中使用组件的三大步骤:定义组件(创建组件);注册组件;使用组件(编写组件标签)。
如何定义一个组件:
  使用 Vue.extend(options) 创建,其中 options 和 new Vue(options) 时传入的几乎一样,区别如下:
      el 不要写 —— 最终所哟逇组件都要经过一个 vm 管理,有 vm 中的 el 决定服务哪个容器;
      data 必须写成函数式 —— 避免组件被复用时,数据存在引用关系。
      备注:使用 template 可以配置组件结构。
  如何注册组件:
      局部注册:依靠 new Vue 时传入 components 选项;
      全局注册:依靠 Vue.component('组件名', 组件)。
  编写组件标签:例 <school></school>

  几个注意点:
      关于组件名:
      一个单词组成:第一种写法(首字母小写):school; 第二种写法(首字母大写):School.
      多个单词组成:第一种写法(kebab-case):my-school; 
					第二种写法(CamelCase):MySchool(需要Vue脚手架支持)
      备注:组件名尽可能避免 HTML 中已有元素标签; 可以使用 name 配置项指定组件在开发者工具中的名称。
      关于组件标签:
        第一种写法:<school></school>
        第二种写法:<school/>
        备注:不用脚手架时,<school/>会导致后续组件不能渲染。
      一个简写方式:const school = Vue.extend(options) => const school = options
  -->
  <div id="root"> 
    <!-- 第三步:编写组件标签 -->
    <hello></hello>
    <school></school>
    <hr>
    <student></student>
  </div>
  <hr>
  <div id="root2">
    <hello></hello>
  </div>
</body>
<script type="text/javascript" >
    Vue.config.productionTip = false; // 组织 Vue 在启动时生成生产提示
    // 第一步:创建 school 组件
    const school = Vue.extend({
      template:`
        <div>
          <h2>学校名称: {{name}}</h2>
          <h2>学校地址: {{address}}</h2>
        </div> `,
      data(){
        return {
          name: '南昌大学',
          address: '江西南昌'
        }
      }
    })
    // 第一步:创建 student 组件
    const student = Vue.extend({
      template:`
        <div>
          <h2>学生姓名: {{name}}</h2>
          <h2>学生年龄: {{age}}</h2>
        </div> `,
      data(){
        return {
          name: '凌宸',
          age: 21
        }
      }
    })
    // 第一步:创建 hello 组件
    const hello = Vue.extend({
      template:`
        <div>
          <h2>你好啊!{{name}}</h2> 
        </div> `,
      data(){
        return {name:'凌宸'}
      }
    })
    // 第二步:注册组件(全局注册)
    Vue.component('hello', hello)
    new Vue({
      el: '#root',
      data:{
        msg: '你好啊!'
      },
      // 第二步:注册组件(局部注册)
      components:{
        school,
        student
      }
    })
    new Vue({
      el: '#root2',
    })
</script>
2.2.2 组件的嵌套
<body>
  <div id="root"> 
    <app></app> 
  </div>
</body>
<script type="text/javascript" >
    Vue.config.productionTip = false; 
    // 创建 student 组件
    const student = Vue.extend({
      template:`
        <div>
          <h2>学生姓名: {{name}}</h2>
          <h2>学生年龄: {{age}}</h2>
        </div> `,
      data(){
        return {
          name: '凌宸',
          age: 21
        }
      }
    }) 
    // 创建 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 = {
      template:`<h1>{{msg}}</h1>`,
      data(){return {msg:'你好啊'}}
    }
    // 创建 app 组件
    const app = {
      template:`
        <div>
          <hello></hello>
          <school></school>
        </div>`,
      components:{
        hello, 
        school
      }
    }
    // 创建 vm
    new Vue({
      el: '#root',  
      components:{ app }
    })
</script>
2.2.3 VueComponent
<body>
<!--  
关于 VueComponent:
   school 组件本质是一个名为 VueComponent 的构造函数,且不是程序员定义的,是 Vue.extend 生成的。
   我们只需要写 <school></school> 或 <school/> ,Vue 解析时会帮我们创建 school 组件的实例对象。
      即 Vue 帮我们执行的:new VueComponent(options)。
   特别注意:每次调用 Vue.extend ,返回的都是一个全新的 VueComponent。
   关于 this 的指向:
       组件配置中:data(),methods中的函数,watch中的函数,computed中的函数 
				它们的 this 均是VueComponent实例对象。
       new Vue() 配置中:data(),methods中的函数,watch中的函数,computed中的函数 
				它们的 this 均是Vue实例对象。
   VueComponent的实例对象,以后简称 vc (也可称为 组件实例对象)。
   Vue 的实例对象简称为 vm。
-->
    <div id="root">
        <school></school>
        <hello></hello>
    </div>
</body>
<script type="text/javascript" >
    Vue.config.productionTip = false; 
    // 创建 school 组件
    const school = Vue.extend({ 
      template:`
        <div>
          <h2>学校名称: {{name}}</h2>
          <h2>学校地址: {{address}}</h2> 
        </div> `,
      data(){
        return {
          name: '南昌大学',
          address: '江西南昌'
        }
      }, 
    }) 
    console.log(school)
    // 创建 hello 组件
    const hello = {
      template:`<h1>{{msg}}</h1>`,
      data(){return {msg:'你好啊'}}
    }
    // 创建 vm
    new Vue({
      el: '#root',  
      components:{ school, hello }
    })
</script>
2.2.4 一个重要的内置关系

在这里插入图片描述

2.3 单文件组件
2.3.1 School.vue
<template>
  <div class="demo">
    <h2>学校名称: {{name}}</h2>
    <h2>学校地址: {{address}}</h2> 
    <button @click="showName">点我提示学校名</button>
  </div>
</template>

<script>
  export default {
    name:'School',
    data() {
      return {
        name:'南昌大学',
        address:'江西南昌'
      }
    },
    methods: {
      showName(){ alert(this.name) }
    },
  } 
</script>

<style>
  .demo{ background-color: pink; }
</style>
2.3.2 Student.vue
<template>
  <div >
    <h2>学生姓名: {{name}}</h2>
    <h2>学生年龄: {{age}}</h2>  
  </div>
</template>

<script>
  export default {
    name:'Student',
    data() {
      return {
        name:'凌宸',
        age:21
      }
    }, 
  } 
</script>
2.3.3 App.vue
<template>
  <div>
    <school />
    <student/>
  </div>
</template>

<script>
  // 引入组件
  import School from './School' 
  import Student from './Student'

  export default {
    name:'App', 
    components: { School, Student }
  }
</script> 
2.3.4 main.js
import App from './App.vue'

new Vue({
    el: '#root',
    components:{App}
})
2.3.5 index.html
<body>
    <!-- 准备一个容器 -->
    <div id="root">
      <App></App>
    </div>
    <script type="text/javascript" src="../js/vue.js"></script>
    <script type="text/javascript" src="./main.js"></script>
</body> 
</html>

PS:将这5个文件放入同一级目录,然后运行index.html 文件,浏览器是没有效果的。需要在 Vue 的脚手架中运行。

2.4 安装 Vue-CLI
  • CLI (@vue/cli) 是一个全局安装的 npm 包,提供了终端里的 vue命令。它可以通过 vue create快速搭建一个新项目,或者直接通过 vue serve 构建新想法的原型。

  • 将 npm 配置为淘宝镜像。

    npm config set registry https://registry.npm.taobao.org
    
  • 全局安装@vue/cli (仅第一次执行)。

    npm install -g @vue/cli
    
  • 切换到需要创建项目的目录,使用命令创建项目。

    vue create xxxx
    
  • 根据提示进入 xxxx 目录,启动项目,并完成访问,查看 HelloWorld 项目。

    npm run serve
    
2.5 模板项目结构
├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

关于不同版本的 Vue:
    vue.js 与 vue.runtime.xxx.js 的区别:
      vue.js 是完整版的 Vue,包含:核心功能 + 模板解析器。
      vue.runtime.xxx.js 是运行版的 Vue,只包含核心功能,没有模板解析器。
    因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 配置项,
      需要 render 函数接受的 createElement 函数去指定具体内容。


2.6 vue.config.js 配置文件
  • vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。你也可以使用 package.json 中的 vue 字段,但是注意这种写法需要你严格遵照 JSON 的格式来写。典例如下:
module.exports = {
  pages: {
    index: {
      // page 的入口
      entry: 'src/index/main.js',
      // 模板来源
      template: 'public/index.html',
      // 在 dist/index.html 的输出
      filename: 'index.html',
      // 当使用 title 选项时,template 中的 title 标签需要是:
      //  	<title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Index Page',
      // 在这个页面中包含的块,默认情况下会包含
      // 提取出来的通用 chunk 和 vendor chunk。
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    },
    // 当使用只有入口的字符串格式时,
    // 模板会被推导为 `public/subpage.html`
    // 并且如果找不到的话,就回退到 `public/index.html`。
    // 输出文件名会被推导为 `subpage.html`。
    subpage: 'src/subpage/main.js'
  },
  lintOnSave: false // 关闭语法检查
}
2.7 ref 属性
<template>
  <div>
    <h1 v-text="msg" ref="title"></h1>
    <button @click="showDOM" ref="btn">点我展示上方的 DOM 信息</button>
    <school ref="sch"/>
  </div>
</template>

<script>
  // 引入组件
  import School from './components/School' 
  /*
    ref 属性:
      被用来给元素或子组件注册引用信息(id 的替代者);
      应用在 html 标签上获取的是真实 DOM 元素,应用在组件标签上是组件实例对象(vc);
      使用方式:
        打标识: <h1 v-text="msg" ref="xxx">.......</h1> 或 <School ref="xxx"/>
        获取:this.$refs.xxx
  */
  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 组件的实例对象
      }
    },
  }
</script>
2.8 props 配置
<!--
    1. 功能:让组件接收外部传过来的数据
    2. 传递数据: <Demo name="xxx"/> 
    3. 接收数据:
        1. 第一种方式(只接收): props:['name'] 
        2. 第二种方式(限制类型): props:{name:String} 
        3. 第三种方式(限制类型、限制必要性、指定默认值):
            props:{
                name:{
                type:String, //类型
                required:true, //必要性
                default:'老王' //默认值
                }
            }
	备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,
		若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
-->
<template>
  <div>
    <h1>{{msg}}</h1>
    <h2>学生姓名: {{name}}</h2>
    <h2>学生性别: {{sex}}</h2>
    <h2>学生年龄: {{myAge}}</h2>  
    <button @click="myAge ++">点我年龄加 1</button>
  </div>
</template>

<script>
  export default {
    name:'Student',
    data() {
      return {
        msg:'我是南昌大学的一名学生',
        myAge:this.age 
      }
    } ,
    // 简单接受
    // props:['name', 'sex', 'age'], 
    // 接受的同时对数据进行类型限制。
    /*props:{
      name:String,
      age:Number,
      sex:String
    }*/
    // 接受的同时对数据进行类型限制,必要性限制,默认值指定。
    props:{
      name:{
        type:String,
        required:true,
      },
      age:{
        type:Number,
        default:99
      },
      sex:{
        type:String,
        required:true
      }
    }
  } 
</script>
2.9 mixin 混合
// mixin.js
export const mixin = {
    methods:{
        showName(){
          alert(this.name)
        }
    }
}
<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>
import Vue from 'vue'
import App from './App.vue'
import {mixin} from './mixin'
// 全局配置 混合
Vue.config.productionTip = false 
Vue.mixin(mixin) 
new Vue({
  render: h => h(App),
}).$mount('#app')
<!-- 
    1. 功能:可以把多个组件共用的配置提取成一个混入对象 
    2. 使用方式: 
        第一步定义混合: 
        {
            data(){....},
            methods:{....}
            ....
        }  
        第二步使用混入:
 			全局混入: Vue.mixin(xxx) 
            局部混入: mixins:['xxx'] 
-->
2.10 插件
export default {
    install(Vue){
      // 全局过滤器
      Vue.filter('mySlice', function(value){
        console.log(value)
        return value.slice(0, 4)
      })
      // 定义全局指令
      Vue.directive('big', function(element, binding){
        element.innerText = binding.value * 10
      })
      // 定义混合
      Vue.mixin({
        data(){
          return {
            x: 100,
            y: 200
          }
        }
      })
      // 给 Vue 原型上添加一个方法
      Vue.prototype.hello = () => {alert('你好啊!')}
    }
}
import Vue from 'vue'
import App from './App.vue' 
// 引入插件
import plugin from './plugin'
Vue.config.productionTip = false 
Vue.use(plugin)
new Vue({
  render: h => h(App),
}).$mount('#app')
<!-- 
    1. 功能:用于增强Vue
    2. 本质:包含install方法的一个对象,install的第一个参数是Vue,
			第二个以后的参数是插件使用者传递的数据。
    3. 定义插件:
        对象.install = function (Vue, options) {
            // 1. 添加全局过滤器
            Vue.filter(....)

            // 2. 添加全局指令
            Vue.directive(....)

            // 3. 配置全局混入(合)
            Vue.mixin(....)

            // 4. 添加实例方法
            Vue.prototype.$myMethod = function () {...}
            Vue.prototype.$myProperty = xxxx
        }
    4. 使用插件:Vue.use()
-->
2.11 scoped 样式
  • 作用:让样式在局部生效,防止冲突。用法如下:
<style scoped>
  .demo{
    background-color: skyblue;
  }
</style>
2.12 TodoList 案例
  • 组件化编码流程:

    • 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
    • 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
      • 一个组件在用:放在组件自身即可。
      • 一些组件在用:放在他们共同的父组件上(状态提升)。
    • 实现交互:从绑定事件开始。
  • props适用于:

    • 父组件 ==> 子组件 通信。
    • 子组件 ==> 父组件 通信(要求父先给子一个函数)。
  • 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

  • props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

<!-- MyHeader.vue -->
<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',
    props:['addTodo'],
    data(){
      return {title:''}
    },
    methods: {
      add(e){
        // 数据校验
        if(!this.title.trim()) return alert('输入数据不能为空')
        // 将用户输入封装为 todo 对象
        const todo = {id:nanoid(), title:this.title, done:false}
        // console.log(todo) 
        this.addTodo(todo) // 调用 App 中的 addTodo 方法完成添加
        // 清空输入框
        this.title = ''
      }
    },
  }
</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 
      v-for="t in todos" 
      :key="t.id" 
      :todo="t" 
      :checkTodo="checkTodo"
      :deleteTodo="deleteTodo"
    />  
  </ul>
</template>

<script>
  // 引入 MyItem 组件
  import MyItem from './MyItem'  

  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="handlerCheck(todo.id)"/>
      <!--如下代码也能实现功能,但不推荐使用,因为修改了props,违背了原则-->
      <!-- <input type="checkbox" :checked="todo.done" v-model="todo.done"/> -->
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" @click="handlerDelete(todo.id)">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem',
    // 接受传来的 todo 对象
    props:['todo', 'checkTodo', 'deleteTodo'],
    methods: {
      // 勾选 or 取消勾选
      handlerCheck(id){
        // 通知 App 组件中对应 id 的 done 值 取反
        this.checkTodo(id)
      },
      // 删除一个 todo 项
      handlerDelete(id){
        if(confirm('确定删除吗?')){
          this.deleteTodo(id)
        }
      }
    },
  }
</script>

<style>
  /*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>
<!-- MyFooter.vue -->
<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="deleteAll">清除已完成任务</button>
  </div>
</template>

<script>
  export default {
    name:'MyFooter',
    props:['todos', 'checkAllTodo', 'deleteAllTodo'],
    computed:{
      total(){
        return this.todos.length
      },
      doneTotal(){
        return this.todos.reduce((pre,todo) => pre + (todo.done ? 1 : 0) , 0)
      },
      isAll:{
        get(){
          return this.total === this.doneTotal && this.total !== 0
        },
        set(value){
          this.checkAllTodo(value)
        }
      }
    },
    methods: {
      deleteAll(){
        if(confirm('确定清楚所有已完成项目吗?')){
          this.deleteAllTodo()
        }
      }
    },
  }
</script>

<style>
  /*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>
<!-- 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" :checkAllTodo="checkAllTodo" 
          :deleteAllTodo="deleteAllTodo" />
      </div>
    </div>
</div>
</template>

<script>
  // 引入组件 
  import MyHeader from './components/MyHeader'  
  import MyFooter from './components/MyFooter'  
  import MyList from './components/MyList'   

  export default {
    name:'App',
    components: { MyHeader, MyFooter, MyList} ,
    data(){
      return {
        todos: [
          {id:'001', title: '学习 Vue', done: false},
          {id:'002', title: '学习 Java', done: true},
          {id:'003', title: '学习 C++', done: false}
        ]
      }
    },
    methods: {
      // 添加一个 todo 对象
      addTodo(todo){
        // console.log(todo)
        this.todos.unshift(todo)
      },
      // 勾选 or 取消勾选一个 todo
      checkTodo(id){
        this.todos.forEach((todo) => {
          if(todo.id === id) todo.done = !todo.done
        })
      },
      // 删除一个 todo 项
      deleteTodo(id){
        this.todos = this.todos.filter(todo => todo.id !== id)
      },
      // 全选 or 取消全选
      checkAllTodo(done){
        this.todos.forEach(todo => todo.done = done)
      },
      // 清楚所有已经完成的 todo
      deleteAllTodo(){
        this.todos = this.todos.filter(todo => !todo.done)
        console.log(this.todos)
      }
    },
  }
</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;border-radius: 4px;
    box-shadow: inset 0 1px 0 rgba(255,255,255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  }
  .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>
// main.js
import Vue from 'vue'
import App from './App.vue'  
Vue.config.productionTip = false  

new Vue({
  render: h => h(App),
}).$mount('#app')
2.13 webStorage
  • 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)。

  • 浏览器端通过 Window.sessionStorage 和 Window.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。

  • 以 localStorage 为例:

<body> 
    <h1>localStorage</h1>
    <button onclick="saveData()" >点我保存一个数据</button>
    <button onclick="getData()" >点我读取一个数据</button>
    <button onclick="deleteData()" >点我删除一个数据</button>
    <button onclick="deleteAllData()" >点我清空一个数据</button>
</body>
<script type="text/javascript" > 
    let p = {name:'张三', age:18}
    function saveData(){
        localStorage.setItem('msg', '你好啊')
        localStorage.setItem('p', JSON.stringify(p))
    }

    function getData(){
        console.log(JSON.parse(localStorage.getItem('p')))
    }

    function deleteData(){
        localStorage.removeItem('msg')
    }

    function deleteAllData(){
        localStorage.clear()
    }
</script> 
2.14 TodoLIst 本地存储
  • 将 TodoLIst 样例用本地存储改写, 下面展示修改部分代码:
<!-- 增加对 todos 的深度监视,并在获取 data 时先从本地存储中获取,若本地为空,则指定为空数组 -->
<script>
  // 引入组件 
  import MyHeader from './components/MyHeader'  
  import MyFooter from './components/MyFooter'  
  import MyList from './components/MyList'   

  export default {
    name:'App',
    components: { MyHeader, MyFooter, MyList} ,
    data(){
      return {todos:JSON.parse(localStorage.getItem('todos')) || [] }
    },
    methods: { .... },
    watch:{
      todos:{
        deep:true,
        handler(value){
          localStorage.setItem('todos', JSON.stringify(value))
        }
      }
    }
  }
</script>
2.15 组件自定义事件
  • 一种组件间通信的方式,适用于:子组件 ===> 父组件

  • 使用场景: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指向会出问题!

<!-- Student.vue -->
<template>
  <div  class="student"> 
    <h2>学生姓名: {{name}}</h2> 
    <h2>学生年龄: {{age}}</h2>  
    <h2>当前的 n 值为 {{number}} </h2>
    <button @click="add">点我 n + 1</button>
    <button @click="sendStudnetName" >点我发送学校名给 App</button>
    <button @click="unbind">点我解绑lingchen事件</button>
    <button @click="death">点我销毁 Student 实例</button> 
  </div>
</template>

<script> 
  export default {
    name:'Student',
    data() {
      return { 
        name:'张三', 
        age: 18,
        number: 1
      }
    }, 
    methods: {
      add(){
        console.log('add被调用了')
        this.number ++
      },
      sendStudnetName(){
        // 触发 Studnet 的实例对象 vc 上绑定的 lingchen 事件
        this.$emit('lingchen', this.name)
      },
      unbind(){
        this.$off('lingchen') // 解绑一个自定义事件
        // this.$off(['lingchen', 'atguigu']) // 解绑多个自定义事件
        // this.$off() // 解绑所有的自定义事件
      },
      death(){
        this.$destroy() 
          // 销毁了当前 Student 组件的实例对象,销毁后所有的 Student 实例的自定义事件都不奏效。
      }
    },
  } 
</script>
<style scoped>
  .student{background-color: skyblue;padding: 5px; margin-top: 30px;}
</style>
<!-- App.vue -->
<template>
  <div class="app">
    <h1>{{msg}}</h1>
    <!-- 通过父组件给子组件传递函数类型的 props 实现:子给父传递数据 -->
    <School :getSchoolName="getSchoolName"/>

    <!-- 通过父组件给子组件绑定自定义事件实现:子给父传递数据(第一种写法 使用 v-on 或 @) -->
    <!-- <Student v-on.once:lingchen="getStudentName"/> -->
    <Student v-on:lingchen="getStudentName" @click.native="show"/>
    <!-- 通过父组件给子组件绑定自定义事件实现:子给父传递数据(第二种写法 使用 ref -->
    <!-- <Student ref="student"/> -->
  </div>
</template>

<script>
  // 引入组件 
  import Student from './components/Student' 
  import School from './components/School'
  export default {
    name:'App',
    components: { Student, School} ,
    data(){
      return {
        msg:'你好啊!'
      }
    },
    methods: {
      getSchoolName(name){
        console.log("App 收到了学校名:", name)
      },
      getStudentName(name){
        console.log("App 收到了学生名:", name)
      },
      show(){
        alert('组件使用原生事件 click')
      }
    },
    mounted() {
    // this.$refs.student.$on('lingchen', this.getStudentName) // 绑定自定义事件
    // this.$refs.student.$once('lingchen', this.getStudentName)// 绑定自定义事件(一次性)
    },
  }
</script>

<style>
  .app{ background-color: gray; padding: 5px; }
</style>
2.16 TodoLIst 自定义事件
  • 修改 App.vue 中的 MyHeader 和 MyFooter 组件标签。
<MyHeader @addTodo="addTodo"/> 
<MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @deleteAllTodo="deleteAllTodo"/>
  • 在 MyHeader 组件中添加触发 addTodo 事件的逻辑。
<script>
  import {nanoid} from 'nanoid'
  export default {
    name:'MyHeader', 
    // props:['addTodo'] // 不使用 props
    data(){
      return {title:''}
    },
    methods: {
      add(e){
        // 数据校验
        if(!this.title.trim()) return alert('输入数据不能为空')
        // 将用户输入封装为 todo 对象
        const todo = {id:nanoid(), title:this.title, done:false}
        // console.log(todo)
        // this.addTodo(todo) 
        this.$emit('addTodo', todo) // 触发 addTodo 事件
        // 清空输入框
        this.title = ''
      }
    },
  }
</script>
  • 在 MyFooter 组件中添加触发 checkAllTodo 和 deleteAllTodo 事件的逻辑。
<script>
  export default {
    name:'MyFooter',
    props:['todos'], // 不再接受 checkAllTodo 和 deleteAllTodo 
    computed:{
      .....,
      isAll:{
        get(){
          return this.total === this.doneTotal && this.total !== 0
        },
        set(value){
          // this.checkAllTodo(value)
          this.$emit('checkAllTodo', value) // 触发 checkAllTodo
        }
      }
    },
    methods: {
      deleteAll(){
        if(confirm('确定清楚所有已完成项目吗?')){
          // this.deleteAllTodo()
          this.$emit('deleteAllTodo') // 触发 deleteAllTodo
        }
      }
    },
  }
</script>
2.17 全局事件总线
  • 一种组件间通信的方式,适用于任意组件间通信。

  • 安装全局事件总线:

new Vue({
	......
	beforeCreate() {
		Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
	},
    ......
}) 
  • 使用事件总线:

  • 接收数据:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在A组件自身。

methods(){
  demo(data){......}
}
......
mounted() {
  this.$bus.$on('xxxx',this.demo)
}
  • 提供数据:this.$bus.$emit(‘xxxx’,数据) 。

  • 最好在 beforeDestroy 钩子中,用 $off 去解绑当前组件所用到的事件。

  • 典例

// main.js
import Vue from 'vue'
import App from './App.vue'  
Vue.config.productionTip = false  

new Vue({
  render: h => h(App),
  beforeCreate(){
    Vue.prototype.$bus = this // 安装全局事件总线 $bus 就是当前的 vm
  }
}).$mount('#app')
<template>
  <div  class="student"> 
    <h2>学生姓名: {{name}}</h2> 
    <h2>学生年龄: {{age}}</h2>   
    <button @click="sendStudentName">点我发送学生姓名给 School 组件</button>
  </div>
</template>

<script> 
  export default {
    name:'Student',
    data() {
      return { 
        name:'张三', 
        age: 18, 
      }
    }, 
    methods: {
      sendStudentName(){
        this.$bus.$emit('hello', this.name)
      }
    }
  } 
</script> 
<template>
  <div class="school"> 
    <h2>学校名称: {{name}}</h2>
    <h2>学校地址: {{address}}</h2>   
  </div>
</template>

<script> 
  export default {
    name:'School', 
    data() {
      return { 
        name:'南昌大学',
        address:'江西南昌' 
      }
    }, 
    methods: {
      getStudentName(studnetName){
        console.log('我是School组件,我拿到了数据:', studnetName)
      }
    },
    mounted() {
      this.$bus.$on('hello', this.getStudentName) // 绑定事件
    },
    beforeDestroy() {
      this.$bus.$off('hello') // 解绑事件
    }
  } 
</script>  
2.18 TodoLIst 全局事件总线
  • 修改 App.vue 中的模板中的 MyList 组件标签。并在添加 mounted 和 beforeDestroy 生命周期钩子。
<MyList :todos="todos" />

 	mounted() {
      this.$bus.$on('checkTodo', this.checkTodo)
      this.$bus.$on('deleteTodo', this.deleteTodo)
    },
    beforeDestroy() {
      this.$bus.$off('checkTodo')
      this.$bus.$off('deleteTodo')
    },
  • xiugai1
<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handlerCheck(todo.id)"/>
      <!--如下代码也能实现功能,但不推荐使用,因为修改了props,违背了原则-->
      <!-- <input type="checkbox" :checked="todo.done" v-model="todo.done"/> -->
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" @click="handlerDelete(todo.id)">删除</button>
  </li>
</template>

<script>
  export default {
    name:'MyItem',
    // 接受传来的 todo 对象
    props:['todo'],
    methods: {
      // 勾选 or 取消勾选
      handlerCheck(id){
        // 通知 App 组件中对应 id 的 done 值 取反
        // this.checkTodo(id)
        this.$bus.$emit('checkTodo', id) // 
      },
      // 删除一个 todo 项
      handlerDelete(id){
        if(confirm('确定删除吗?')){
          // this.deleteTodo(id)
          this.$bus.$emit('deleteTodo', id)
        }
      }
    },
  }
</script>
2.19 消息的发布与订阅
  • 安装 pubsub-js 。
npm install pubsub-js
  • 在 School 和Student 组件中引入 pubsub。
import pubsub from 'pubsub-js'
  • 修改 全局事件总线小节 中 School.vue ,修改如下:
    mounted() {
      // this.$bus.$on('hello', this.getStudentName) // 绑定事件
      // 订阅消息
      this.subId = pubsub.subscribe('hello', this.handlerHello)
    },
    beforeDestroy() {
      // this.$bus.$off('hello') // 解绑事件
      // 取消订阅
      pubsub.unsubsribe(this.subId)
    }
  • 修改 Student.vue 中的 sendStudentName 方法如下:
 sendStudentName(){
    // this.$bus.$emit('hello', this.name)
    pubsub.publish('hello', this.name)
}
2.20 TodoList pubsub
  • 修改 App 组件中的 mounted 和 beforeDestroy 生命周期钩子如下:
mounted() {
    // this.$bus.$on('checkTodo', this.checkTodo)
    // this.$bus.$on('deleteTodo', this.deleteTodo)
    this.checkSubId = pubsub.subscribe('checkTodo', this.checkTodo)
    this.deleteSubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
},
beforeDestroy() {
    // this.$bus.$off('checkTodo')
    // this.$bus.$off('deleteTodo')
    pubsub.unsubscribe(checkSubId)
    pubsub.unsubscribe(deleteSubId)
},
  • 修改 MyItem 组件中的 handlerCheck 和 handlerDelete 函数。
// 勾选 or 取消勾选
  handlerCheck(id){
    // 通知 App 组件中对应 id 的 done 值 取反
    // this.checkTodo(id)
    // this.$bus.$emit('checkTodo', id)
    pubsub.publish('checkTodo', id)
  },
  // 删除一个 todo 项
  handlerDelete(id){
    if(confirm('确定删除吗?')){
      // this.deleteTodo(id)
      // this.$bus.$emit('deleteTodo', id)
      pubsub.publish('deleteTodo', id)
    }
  }
2.21 nextTick
  • 语法: this.$nextTick(回调函数) 。

  • 作用:在下一次 DOM 更新结束后执行其指定的回调。

  • 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

  • 给 TodoList 添加编辑功能。

  • 修改 MyItem 组件如下:

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handlerCheck(todo.id)"/>
      <!--如下代码也能实现功能,但不推荐使用,因为修改了props,违背了原则-->
      <!-- <input type="checkbox" :checked="todo.done" v-model="todo.done"/> -->
      <span v-show="!todo.isEdit">{{todo.title}}</span>
      <input type="text" 
        v-show="todo.isEdit" 
        :value="todo.title"
        @blur="handlerBlur(todo, $event)"
        ref="inputTitle"
      >
    </label>
    <button class="btn btn-danger" @click="handlerDelete(todo.id)">删除</button>
    <button v-show="!todo.isEdit" class="btn btn-edit" @click="handlerEdit(todo)">编辑</button>
  </li>
</template>

<script>
  import pubsub from 'pubsub-js'
  export default {
    name:'MyItem',
    // 接受传来的 todo 对象
    props:['todo'],
    methods: {
      // 勾选 or 取消勾选
      handlerCheck(id){
        // 通知 App 组件中对应 id 的 done 值 取反
        // this.checkTodo(id)
        // this.$bus.$emit('checkTodo', id)
        pubsub.publish('checkTodo', id)
      },
      // 删除一个 todo 项
      handlerDelete(id){
        if(confirm('确定删除吗?')){
          // this.deleteTodo(id)
          // this.$bus.$emit('deleteTodo', id)
          pubsub.publish('deleteTodo', id)
        }
      },
      // 编辑
      handlerEdit(todo){
        if(todo.hasOwnProperty('isEdit')){
          todo.isEdit = true; 
        }else{
          this.$set(todo, 'isEdit', true)
        }
        // 获取焦点
        this.$nextTick(function(){
          this.$refs.inputTitle.focus()
        })
      },
      // 失去焦点回调(真正执行修改逻辑)
      handlerBlur(todo, e){
        todo.isEdit = false
        console.log('@@@@',todo.id, e.target.value)
        if(!e.target.value.trim()) return alert('输入不能为空!')
		this.$bus.$emit('updateTodo',todo.id,e.target.value)  
      }
    },
  }
</script>
  • App 组件中添加关于编辑按钮的 样式:
 .btn-edit {
    color: #fff;
    background-color: skyblue;
    border: 1px solid rgb(35, 154, 200);
    margin-right: 5px;
  }
2.22 Vue封装的过度与动画
  • 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。

  • 写法:

  • 准备好样式:

    • 元素进入的样式:

      1. v-enter:进入的起点
      2. v-enter-active:进入过程中
      3. v-enter-to:进入的终点
    • 元素离开的样式:

      1. v-leave:离开的起点
      2. v-leave-active:离开过程中
      3. v-leave-to:离开的终点
  • 使用 包裹要过度的元素,并配置 name 属性:

<transition name="hello">
	<h1 v-show="isShow">你好啊!</h1>
</transition>
  • 备注:若有多个元素需要过度,则需要使用:,且每个元素都要指定 key 值。
  • 样例1:
<template>
  <div>
      <button @click="isShow = !isShow">显示/隐藏</button>
      <transition name="hello">
        <h1 v-show="isShow">你好啊!凌宸</h1>
      </transition>
  </div>
</template>

<script>
    export default {
      name:'Test',
      data(){ return { isShow:true } }
    }
</script>

<style>
  h1{ background-color: orange; } 
  .hello-enter-active{ animation: lingchen 1s linear; }
  .hello-leave-active{ animation: lingchen 1s linear reverse; }
  @keyframes lingchen {
    from{ transform: translateX(-100%); }
    to{ transform: translateX(0px); }
  }
</style>
  • 样例2
<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition-group name="hello" appear>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition-group>
	</div>
</template>

<script>
	export default {
		name:'Test',
		data() { return { isShow:true } },
	}
</script>

<style scoped>
	h1{ background-color: orange; }
	/* 进入的起点、离开的终点 */
	.hello-enter,.hello-leave-to{ transform: translateX(-100%); }
	.hello-enter-active,.hello-leave-active{ transition: 0.5s linear; }
	/* 进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{ transform: translateX(0); }
</style>
2.23 TodoList 动画
  • 给 TodoList 在新增和删除只之时增加动画效果。
  • 修改 MyList 组件中的 template 标签中的内容, 并于 style 标签中增加 动画效果的样式。
<template>
  <ul class="todo-main">
    <transition-group name="todo" appear>
      <MyItem  v-for="t in todos" :key="t.id"  :todo="t"  />  
    </transition-group>
  </ul>
</template>

<style>
  ... (其他原有样式)
  .todo-enter-active{ animation: lingchen 1s linear; }
  .todo-leave-active{ animation: lingchen 1s linear reverse; }
  @keyframes lingchen {
    from{ transform: translateX(100%); }
    to{ transform: translateX(0px); }
  }
</style>
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lingchen0522

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值