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

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

2.1 组件与模块化

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

Vue 学习笔记 -- 2. Vue 组件化编程与脚手架_第1张图片
Vue 学习笔记 -- 2. Vue 组件化编程与脚手架_第2张图片

2.2 非单文件组件

2.2.1 基本使用
<body>

  <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:`
        

学校名称: {{name}}

学校地址: {{address}}

`
, data(){ return { name: '南昌大学', address: '江西南昌' } } }) // 第一步:创建 student 组件 const student = Vue.extend({ template:`

学生姓名: {{name}}

学生年龄: {{age}}

`
, data(){ return { name: '凌宸', age: 21 } } }) // 第一步:创建 hello 组件 const hello = Vue.extend({ template:`

你好啊!{{name}}

`
, 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:`
        

学生姓名: {{name}}

学生年龄: {{age}}

`
, data(){ return { name: '凌宸', age: 21 } } }) // 创建 school 组件 const school = Vue.extend({ name:'school', template:`

学校名称: {{name}}

学校地址: {{address}}

`
, data(){ return { name: '南昌大学', address: '江西南昌' } }, // 局部注册 components:{ student } }) // 创建 hello 组件 const hello = { template:`

{{msg}}

`
, data(){return {msg:'你好啊'}} } // 创建 app 组件 const app = { template:`
`
, components:{ hello, school } } // 创建 vm new Vue({ el: '#root', components:{ app } })
script>
2.2.3 VueComponent
<body>

    <div id="root">
        <school>school>
        <hello>hello>
    div>
body>
<script type="text/javascript" >
    Vue.config.productionTip = false; 
    // 创建 school 组件
    const school = Vue.extend({ 
      template:`
        

学校名称: {{name}}

学校地址: {{address}}

`
, data(){ return { name: '南昌大学', address: '江西南昌' } }, }) console.log(school) // 创建 hello 组件 const hello = { template:`

{{msg}}

`
, data(){return {msg:'你好啊'}} } // 创建 vm new Vue({ el: '#root', components:{ school, hello } })
script>
2.2.4 一个重要的内置关系

Vue 学习笔记 -- 2. Vue 组件化编程与脚手架_第3张图片

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 标签需要是:
      //  	<%= htmlWebpackPlugin.options.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);
      使用方式:
        打标识: 

.......

获取: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 配置


<template>
  <div>
    <h1>{{msg}}h1>
    <h2>学生姓名: {{name}}h2>
    <h2>学生性别: {{sex}}h2>
    <h2>学生年龄: {{myAge}}h2>  
    <button @click="myAge ++">点我年龄加 1button>
  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')

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')

2.11 scoped 样式

  • 作用:让样式在局部生效,防止冲突。用法如下:

2.12 TodoList 案例

  • 组件化编码流程:

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

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

  • props传过来的若是对象类型的值,修改对象中的属性时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>

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

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handlerCheck(todo.id)"/>
      
      
      <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>

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

<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>localStorageh1>
    <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 样例用本地存储改写, 下面展示修改部分代码:

<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指向会出问题!


<template>
  <div  class="student"> 
    <h2>学生姓名: {{name}}h2> 
    <h2>学生年龄: {{age}}h2>  
    <h2>当前的 n 值为 {{number}} h2>
    <button @click="add">点我 n + 1button>
    <button @click="sendStudnetName" >点我发送学校名给 Appbutton>
    <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>

<template>
  <div class="app">
    <h1>{{msg}}h1>
    
    <School :getSchoolName="getSchoolName"/>

    
    
    <Student v-on:lingchen="getStudentName" @click.native="show"/>
    
    
  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)"/>
      
      
      <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)"/>
      
      
      <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>

你可能感兴趣的:(Vue,学习之路,vue.js,前端)