刘俊凯 发表于 2024-10-19 00:16:13

第 3 章:使用 Vue 脚手架

1. 初始化脚手架

1.1 阐明


[*]Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)。
[*]最新的版本是 5.x。
[*]文档: https://cli.vuejs.org/zh/
1.2 详细步骤



[*]第一步(仅第一次执行):全局安装@vue/cli。
npm install -g @vue/cli
[*]第二步:切换到你要创建项目的目次,然后使用命令创建项目
vue create xxxx(项目名称)
[*]第三步:启动项目
npm run serve
备注:

[*]如出现下载缓慢请配置 npm 淘宝镜像:
npm config set registry https://registry.npm.taobao.org
[*]. Vue 脚手架隐藏了所有 webpack 相干的配置,若想查看详细的 webpakc 配置,请执行:
vue inspect > output.js


[*]待解决,cli安装失败报错
C:\Users\Administrator>npm config set registry https://registry.npm.taobao.org

C:\Users\Administrator>npm install -g @vue/cli
npm error code CERT_HAS_EXPIRED
npm error errno CERT_HAS_EXPIRED
npm error request to https://registry.npm.taobao.org/@vue%2fcli failed, reason: certificate has expired

npm error A complete log of this run can be found in: C:\Users\Administrator\AppData\Local\npm-cache\_logs\2024-09-26T06_33_01_984Z-debug-0.log
1.3 模板项目的结构

├── 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:包版本控制文件
1.4 关于不同版本的Vue


[*]vue.js与vue.runtime.xxx.js的区别:
[*]vue.js是完备版的Vue,包罗:核心功能 + 模板解析器。
[*]vue.runtime.xxx.js是运行版的Vue,只包罗:核心功能;没有模板解析器。

[*]由于vue.runtime.xxx.js没有模板解析器,以是不能使用template这个配置项,需要使用render函数接收到的createElement函数去指定详细内容。
1.4.1 render 函数的应用

/*
        该文件是整个项目的入口文件
*/
//引入Vue
import Vue from 'vue'
//引入App组件,它是所有组件的父组件
import App from './App.vue'
//关闭vue的生产提示
Vue.config.productionTip = false

//创建Vue实例对象---vm
new Vue({
        el:'#app',
        //render函数完成了这个功能:将App组件放入容器中
render: h => h(App),// es6 箭头函数简写语法
// 完整写法
//   render: function(createElement){
//                 return createElement('h1','你好啊')
//   }
        // render:q=> q('h1','你好啊')

        // template:`<h1>你好啊</h1>`,
        // components:{App},
})
1.5 vue.config.js配置文件


[*]使用vue inspect > output.js可以查看到Vue脚手架的默认配置。
[*]使用vue.config.js可以对脚手架举行个性化定制,详情见:https://cli.vuejs.org/zh
2. ref


[*]被用来给元素或子组件注册引用信息(id的替代者)
[*]应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
[*]使用方式:
[*]打标识:<h1 ref="xxx">.....</h1>或 <School ref="xxx"></School>
[*]获取:this.$refs.xxx

[*]作用:用于给节点打标识
[*]读取方式:this.$refs.xxxxxx
2.1 ref 的现实应用

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

3. props


[*]功能:让组件接收外部传过来的数据
[*]传递数据:<Demo name="xxx"/>
3.1 接收数据


[*]第一种方式(只接收):props:['name','age','sex']
[*]第二种方式(限制类型):props:{name:String}
//接收的同时对数据进行类型限制
props:{
        name:String,
        age:Number,
        sex:String
}

[*]第三种方式(限制类型、限制必要性、指定默认值):
props:{
   name:{
           type:String, //类型
           required:true, //必要性
           default:'老王' //默认值
}
}
3.2 props 使用实例

<template>
        <div>
                <h1>{{msg}}</h1>
                <h2>学生姓名:{{name}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <h2>学生年龄:{{myAge+1}}</h2>
                <button @click="updateAge">尝试修改收到的年龄</button>
        </div>
</template>

<script>
        export default {
                name:'Student',
                data() {
                        console.log(this)
                        return {
                                msg:'我是一个尚硅谷的学生',
                                myAge:this.age// 备注
                        }
                },
                methods: {
                        updateAge(){
                                this.myAge++// 备注
                        }
                },
                //接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
                props:{
                        name:{
                                type:String, //name的类型是字符串
                                required:true, //name是必要的
                        },
                        age:{
                                type:Number,
                                default:99 //默认值
                        },
                        sex:{
                                type:String,
                                required:true
                        }
                }
        }
</script>
   备注:props是只读的,Vue底层会监测你对props的修改,如果举行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。例如:我们要修改年龄,而该年龄是由props 传过来的age,这个时候不能直接对age举行修改,于是我们先在data里面界说一个myAge变量用来接收props传过来的age,再去对myAge举行修改即可。相当于复制props的内容到data中一份,然后去修改data中的数据
4. mixin混入



[*]功能:可以把多个组件共用的配置提取成一个混入对象
[*]在mixin混入里面,如果混入配置与自身组件有同样的属性,那么以自身的属性为主。如果有雷同的方法名,且方法是生命周期函数,那么组件自身与mixin的该方法都会调用。
4.1 使用方式


[*]第一步界说混合:
{
data(){....},
methods:{....}
....
}

[*]第二步使用混入:
全局混入:Vue.mixin(xxx)
局部混入:mixins:['xxx']
4.2 mix混入 案例



[*]暴露mixin混入以及配置:mixin.js
export const hunhe = {
        methods: {
                showName(){
                        alert(this.name)
                }
        },
        mounted() {
                console.log('你好啊!')
        },
}
export const hunhe2 = {
        data() {
                return {
                        x:100,
                        y:200
                }
        },
}


[*]局部混入:School.vue
<template>
        <div>
                <h2 @click="showName">学校名称:{{name}}</h2>
                <h2>学校地址:{{address}}</h2>
        </div>
</template>

<script>
        //局部引入一个hunhe
        import {hunhe,hunhe2} from '../mixin'

        export default {
                name:'School',
                data() {
                        return {
                                name:'尚硅谷',
                                address:'北京',
                                x:666
                        }
                },
                // 局部使用混合
                mixins:,
        }
</script>


[*]全局混入:main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
// 引入mixin
import {hunhe,hunhe2} from './mixin'
//关闭Vue的生产提示
Vue.config.productionTip = false

// 全局使用混入
Vue.mixin(hunhe)
Vue.mixin(hunhe2)


//创建vm
new Vue({
        el:'#app',
        render: h => h(App)
})
5. 插件



[*]Vue 插件是一个包罗 install 方法的对象
[*]通过 install 方法给 Vue 或 Vue 实例添加方法, 界说全局指令等
5.1 插件的界说


[*]功能:用于增强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()
5.2 插件的案例



[*]界说插件:plugins.js
export default {
        install(Vue,x,y,z){
                console.log(x,y,z)
                //全局过滤器
                Vue.filter('mySlice',function(value){
                        return value.slice(0,4)
                })

                //定义全局指令
                Vue.directive('fbind',{
                        //指令与元素成功绑定时(一上来)
                        bind(element,binding){
                                element.value = binding.value
                        },
                        //指令所在元素被插入页面时
                        inserted(element,binding){
                                element.focus()
                        },
                        //指令所在的模板被重新解析时
                        update(element,binding){
                                element.value = binding.value
                        }
                })

                //定义混入
                Vue.mixin({
                        data() {
                                return {
                                        x:100,
                                        y:200
                                }
                        },
                })

                //给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的生产提示
Vue.config.productionTip = false

//应用(使用)插件
Vue.use(plugins,1,2,3)
//创建vm
new Vue({
        el:'#app',
        render: h => h(App)
})


[*]在组件中使用插件1: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>


[*]在组件中使用插件2:Student.vue
<template>
        <div>
                <h2>学生姓名:{{name}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <input type="text" v-fbind:value="name">
        </div>
</template>

<script>
        export default {
                name:'Student',
                data() {
                        return {
                                name:'张三',
                                sex:'男'
                        }
                },
        }
</script>
6. scoped样式


[*]作用:让样式在局部生效,防止冲突。加了scoped,只有当前组件内template的样式才会生效
[*]写法:<style scoped>
[*]还可以在 style 标签里面配置 css 语法,如果不配置该属性,则默认语法为css:
<style lang="less">,此时可能要安装 less-loader
7. Todo-list 案例



[*]效果:
https://i-blog.csdnimg.cn/direct/5d4194ebf52b494f94e16546c420d41e.png
7.1 实现静态组件

将组件按照功能拆分而且举行语义化定名


[*]MyFooter.vue
[*]MyHeader.vue
[*]MyList.vue
[*]MyItem.vue
此中MyItem.vue 是 MyList.vue 的子组件,以是 App.vue 里面只需要引入前三个组件即可,MyItem.vue 组件在 MyList.vue 里面引入即可。

[*]如果是老体系里面把jQuery之类的用组件化去重新编写,起首找到index.html中对应的结构,然后将其内容复制到对应组件template里面当中去,然后原位置用组件标签代替,还原原来老体系的结构布局。
7.2 展示动态数据


[*]数据的类型、名称是什么?
一堆数据用数组,每一个数据里面的属性太多了就用对象
[*]数据保存在哪个组件?
哪个组件使用,就保存在哪个组件里面,以是保存在 MyList.vue 里面,然后通过props引用到 MyItem.vue 里面而且展示到页面当中。


[*]生成唯一的id,而且精简:引用 nanoid 库,而且调用 nanoid() 函数,会直接生成一个唯一的id(字符串)
7.3 交互——从绑定事件监听开始

7.3.1 初级实现同级组件间的数据交流



[*]实现添加功能:在输入框(Header)输入文字并在下面(List)生成待办事项
https://i-blog.csdnimg.cn/direct/69c7369b519b488cb5d3157bca44eee1.png
父组件与子组件通讯:

[*]App 里面存放数据data,然后再给 List绑定:<List :data="data"/>
[*]List 通过props引用该数据data:props["data"]
子组件与父组件通讯:
3. Header 传数据给 App,起首 App 界说一个函数,然后绑定给 Header:<Header :addTode="addTodo"/>,Header 通过 props 引用该函数:props:['addTodo']
4. 通过 Header 自己的一个函数去调用该函数,末了把 Header 的数据通过参数的情势给该函数带走:this.addTodo(数据),然后实现 Header 给 App 传数据
兄弟组件间通讯:
5. 终极 List 从 App 这里拿到数据,实现了同级组件间的数据交流。
7.3.2 勾选功能



[*]给每一项item勾选时并改变其属性done的值为true
[*]以是需要父孙组件通讯,用逐层父子组件绑定来实现
父组件传数据给孙组件:
初级方法:逐层传递

[*]App 界说方法:checkTodo,而且绑定给 List:<List :checkTodo="checkTodo"/>
[*]List 通过 props 引用 checkTodo:props:['checkTodo']
[*]List 将引用的 checkTodo 绑定给 Item:<Item :checkTodo="checkTodo"/>
[*]Item 调用方法:this.checkTodo(id),参数为 todo 的 id
//勾选or取消勾选一个todo
                        checkTodo(id){
                        // 遍历每个item,如果id为所勾选的id,那么将其done值取反
                                this.todos.forEach((todo)=>{
                                        if(todo.id === id) todo.done = !todo.done
                                })
                        }
7.3.3 删除方法:



[*]移动鼠标到对应item上面,显示删除按钮,确定删除后就删除该item

[*]拿到该item的id
[*]然后把id传给app(item所在组件;父组件与孙组件数据交流,方法同上)
[*]app删除后重新渲染dom到页面上
//删除一个todo
                        deleteTodo(id){
                        // 使用过滤器将选中id的item过滤掉,留下没有选中id的item
                                this.todos = this.todos.filter( todo => todo.id !== id )
                        }
7.3.4 底部统计功能



[*]勾选了几个item,显示已完成几个item,同时展示全部有多少个item(使用计算属性实现)
computed: {
        //总数
        total(){
                // 直接返回todos的长度即item的个数
                return this.todos.length
        },
        //已完成数
        doneTotal(){
                //此处使用reduce方法做条件统计
                /* const x = this.todos.reduce((pre,current)=>{
                        console.log('@',pre,current)
                        return pre + (current.done ? 1 : 0)
                },0) */
                //简写
                // 返回属性done值为1即done值为true即已勾选的item
                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
}
7.3.5 全选or取消全选功能



[*]当所有item被勾选时,全选主动勾选
[*]当勾选全选时,勾选所有item
[*]当取消全选时,取消勾选所有item
//全选or取消全选
// methods
checkAllTodo(done){
        this.todos.forEach((todo)=>{
        // 遍历所有todo,全部设置为done
                todo.done = done
        })
}

// computed
isAll:{
        //全选框是否勾选
        get(){
                // 判断勾选数等于全部数,并且全部数大于0(防止出现全部0,勾选0)
                return this.doneTotal === this.total && this.total > 0
        },
        //isAll被修改时set被调用
        // 缺少setter,自己定义,调用setter时则调用全选方法
        set(value){
                this.checkAllTodo(value)
        }
}
7.3.6 清除已经完成的任务



[*]删除已被勾选的item
// 1. App定义,再绑定给Footer
//清除所有已经完成的todo
clearAllTodo(){
        this.todos = this.todos.filter((todo)=>{// 过滤不改变原数据,所以用todos重新被赋值
        // 遍历所有todo,过滤返回所有没有勾选的item
                return !todo.done
        })
}

// 2. Footer 用props接收
props:['todos','checkAllTodo','clearAllTodo']

// 3. 接收后在methods里面配置
methods: {
        /* checkAll(e){
                this.checkAllTodo(e.target.checked)
        } */
        //清空所有已完成
        clearAll(){
                this.clearAllTodo()
        }
}

// 4. 最后以点击事件绑定给按钮
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
7.4 TodoList 案例总结:


[*] 组件化编码流程:

[*]拆分静态组件:组件要按照功能点拆分,定名不要与html元素冲突。
[*]实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
[*]一个组件在用:放在组件自身即可。
[*]一些组件在用:放在他们共同的父组件上(<span style="color:red">状态提升</span>)。

[*]实现交互:从绑定事件开始。

[*] props适用于:

[*]父组件 ==> 子组件 通讯
[*]子组件 ==> 父组件 通讯(要求父先给子一个函数)

[*] 使用 v-model 时要切记:v-model 绑定的值不能是 props 传过来的值,由于 props 是不可以修改的!
[*] props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。
7.5 TodoList 案例实现

项目结构:
- components
        - MyHeader.vue
        - MyList.vue
        - MyItem.vue
        - MyFooter.vue
- App.vue
- main.js


[*]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',
                //接收从App传递过来的addTodo
                props:['addTodo'],
                data() {
                        return {
                                //收集用户输入的title
                                title:''
                        }
                },
                methods: {
                        add(){
                                //校验数据
                                if(!this.title.trim()) return alert('输入不能为空')
                                //将用户的输入包装成一个todo对象
                                const todoObj = {id:nanoid(),title:this.title,done:false}
                                //通知App组件去添加一个todo对象
                                this.addTodo(todoObj)
                                //清空输入
                                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="todoObj in todos"
                        :key="todoObj.id"
                        :todo="todoObj"
                        :checkTodo="checkTodo"
                        :deleteTodo="deleteTodo"
                />
        </ul>
</template>

<script>
        import MyItem from './MyItem'

        export default {
                name:'MyList',
                components:{MyItem},
                //声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件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="handleCheck(todo.id)"/>
                        <!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
                        <!-- <input type="checkbox" v-model="todo.done"/> -->
                        <span>{{todo.title}}</span>
                </label>
                <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        </li>
</template>

<script>
        export default {
                name:'MyItem',
                //声明接收todo、checkTodo、deleteTodo
                props:['todo','checkTodo','deleteTodo'],
                methods: {
                        //勾选or取消勾选
                        handleCheck(id){
                                //通知App组件将对应的todo对象的done值取反
                                this.checkTodo(id)
                        },
                        //删除
                        handleDelete(id){
                                if(confirm('确定删除吗?')){
                                        //通知App组件将对应的todo对象删除
                                        this.deleteTodo(id)
                                }
                        }
                },
        }
</script>

<style scoped>
        /*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" :checked="isAll" @change="checkAll"/> -->
                        <input type="checkbox" v-model="isAll"/>
                </label>
                <span>
                        <span>已完成{{doneTotal}}</span> / 全部{{total}}
                </span>
                <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
        </div>
</template>

<script>
        export default {
                name:'MyFooter',
                props:['todos','checkAllTodo','clearAllTodo'],
                computed: {
                        //总数
                        total(){
                                return this.todos.length
                        },
                        //已完成数
                        doneTotal(){
                                //此处使用reduce方法做条件统计
                                /* const x = this.todos.reduce((pre,current)=>{
                                        console.log('@',pre,current)
                                        return pre + (current.done ? 1 : 0)
                                },0) */
                                //简写
                                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
                        },
                        //控制全选框
                        isAll:{
                                //全选框是否勾选
                                get(){
                                        return this.doneTotal === this.total && this.total > 0
                                },
                                //isAll被修改时set被调用
                                set(value){
                                        this.checkAllTodo(value)
                                }
                        }
                },
                methods: {
                        /* checkAll(e){
                                this.checkAllTodo(e.target.checked)
                        } */
                        //清空所有已完成
                        clearAll(){
                                this.clearAllTodo()
                        }
                },
        }
</script>

<style scoped>
        /*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" :clearAllTodo="clearAllTodo"/>
                        </div>
                </div>
        </div>
</template>

<script>
        import MyHeader from './components/MyHeader'
        import MyList from './components/MyList'
        import MyFooter from './components/MyFooter.vue'

        export default {
                name:'App',
                components:{MyHeader,MyList,MyFooter},
                data() {
                        return {
                                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                                todos:[
                                        {id:'001',title:'抽烟',done:true},
                                        {id:'002',title:'喝酒',done:false},
                                        {id:'003',title:'开车',done:true}
                                ]
                        }
                },
                methods: {
                        //添加一个todo
                        addTodo(todoObj){
                                this.todos.unshift(todoObj)
                        },
                        //勾选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
                        clearAllTodo(){
                                this.todos = this.todos.filter((todo)=>{
                                        return !todo.done
                                })
                        }
                }
        }
</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;
                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;
        }
</style>



[*]main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App)
})
8. 浏览器本地存储

8.1 webStorage


[*]存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
[*]浏览器端通过 Window.sessionStorage和 Window.localStorage属性来实现本地存储机制。
8.2 相干API:


[*] xxxxxStorage.setItem('key', 'value');
该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
[*] xxxxxStorage.getItem('person');
该方法接受一个键名作为参数,返回键名对应的值。
[*] xxxxxStorage.removeItem('key');
该方法接受一个键名作为参数,并把该键名从存储中删除。
[*] xxxxxStorage.clear()
该方法会清空存储中的所有数据。
8.3 备注


[*]SessionStorage存储的内容会随着浏览器窗口关闭而消失。
[*]LocalStorage存储的内容,需要手动清除才会消失。
[*]xxxxxStorage.getItem(xxx)如果xxx对应的value获取不到,那么getItem的返回值是null。
[*]JSON.parse(null)的效果依然是null。
8.4 浏览器本地存储 案例

8.4.1 localStorage.html



[*]数据保存在本地浏览器,关闭了浏览器数据仍旧存在
<!DOCTYPE html>
<html>
        <head>
                <meta charset="UTF-8" />
                <title>localStorage</title>
        </head>
        <body>
                <h2>localStorage</h2>
                <button onclick="saveData()">点我保存一个数据</button>
                <button onclick="readData()">点我读取一个数据</button>
                <button onclick="deleteData()">点我删除一个数据</button>
                <button onclick="deleteAllData()">点我清空一个数据</button>

                <script type="text/javascript" >
                        let p = {name:'张三',age:18}

                        function saveData(){
                        // 保存一个键值对
                                localStorage.setItem('msg','hello!!!')
                                localStorage.setItem('msg2',666)
                                // 将对象进行json字符串化以便于将该数据添加到浏览器存储中
                                localStorage.setItem('person',JSON.stringify(p))
                        }
                        // 根据键名读取数据
                        function readData(){
                                console.log(localStorage.getItem('msg'))
                                console.log(localStorage.getItem('msg2'))
                               
                                // 这是个对象,所以先接收,再进行json解析,方便人查看
                                const result = localStorage.getItem('person')
                                console.log(JSON.parse(result))

                                // console.log(localStorage.getItem('msg3'))
                        }
                        // 根据键名删除数据
                        function deleteData(){
                                localStorage.removeItem('msg2')
                        }
                        // 删除所有数据
                        function deleteAllData(){
                                localStorage.clear()
                        }
                </script>
        </body>
</html>
8.4.2 sessionStorage.html



[*]关闭浏览器数据就会消失
<!DOCTYPE html>
<html>
        <head>
                <meta charset="UTF-8" />
                <title>sessionStorage</title>
        </head>
        <body>
                <h2>sessionStorage</h2>
                <button onclick="saveData()">点我保存一个数据</button>
                <button onclick="readData()">点我读取一个数据</button>
                <button onclick="deleteData()">点我删除一个数据</button>
                <button onclick="deleteAllData()">点我清空一个数据</button>

                <script type="text/javascript" >
                        let p = {name:'张三',age:18}

                        function saveData(){
                        // 保存一个键值对
                                sessionStorage.setItem('msg','hello!!!')
                                sessionStorage.setItem('msg2',666)
                                // 将对象进行json字符串化以便于将该数据添加到浏览器存储中
                                sessionStorage.setItem('person',JSON.stringify(p))
                        }
                        function readData(){
                        // 根据键名读取数据
                                console.log(sessionStorage.getItem('msg'))
                                console.log(sessionStorage.getItem('msg2'))

                                // 这是个对象,所以先接收,再进行json解析,方便人查看
                                const result = sessionStorage.getItem('person')
                                console.log(JSON.parse(result))

                                // console.log(sessionStorage.getItem('msg3'))
                        }
                        function deleteData(){
                        // 根据键名删除数据
                                sessionStorage.removeItem('msg2')
                        }
                        function deleteAllData(){
                        // 删除所有数据
                                sessionStorage.clear()
                        }
                </script>
        </body>
</html>
8.5 TodoList 本地存储案例



[*]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" :clearAllTodo="clearAllTodo"/>
                        </div>
                </div>
        </div>
</template>

<script>
        import MyHeader from './components/MyHeader'
        import MyList from './components/MyList'
        import MyFooter from './components/MyFooter.vue'

        export default {
                name:'App',
                components:{MyHeader,MyList,MyFooter},
                data() {
                        return {
                                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                                // 获取数据
                                // 从localStorage中获取数据然后要JSON.parse(),若没有数据,则返回空数组,因为空数组调用.length()方法会报错
                                todos:JSON.parse(localStorage.getItem('todos')) || []
                        }
                },
                methods: {
                        //添加一个todo
                        addTodo(todoObj){
                                this.todos.unshift(todoObj)
                        },
                        //勾选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
                        clearAllTodo(){
                                this.todos = this.todos.filter((todo)=>{
                                        return !todo.done
                                })
                        }
                },
                watch: {
                        todos:{
                                deep:true,
                                handler(value){
                                        // 监视器 当todos发生变化时,把todos存到localStorage中
                                        localStorage.setItem('todos',JSON.stringify(value))
                                }
                        }
                },
        }
</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;
                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;
        }
</style>



[*]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',
                //接收从App传递过来的addTodo
                props:['addTodo'],
                data() {
                        return {
                                //收集用户输入的title
                                title:''
                        }
                },
                methods: {
                        add(){
                                //校验数据
                                if(!this.title.trim()) return alert('输入不能为空')
                                //将用户的输入包装成一个todo对象
                                const todoObj = {id:nanoid(),title:this.title,done:false}
                                //通知App组件去添加一个todo对象
                                this.addTodo(todoObj)
                                //清空输入
                                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="todoObj in todos"
                        :key="todoObj.id"
                        :todo="todoObj"
                        :checkTodo="checkTodo"
                        :deleteTodo="deleteTodo"
                />
        </ul>
</template>

<script>
        import MyItem from './MyItem'

        export default {
                name:'MyList',
                components:{MyItem},
                //声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件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="handleCheck(todo.id)"/>
                        <!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
                        <!-- <input type="checkbox" v-model="todo.done"/> -->
                        <span>{{todo.title}}</span>
                </label>
                <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        </li>
</template>

<script>
        export default {
                name:'MyItem',
                //声明接收todo、checkTodo、deleteTodo
                props:['todo','checkTodo','deleteTodo'],
                methods: {
                        //勾选or取消勾选
                        handleCheck(id){
                                //通知App组件将对应的todo对象的done值取反
                                this.checkTodo(id)
                        },
                        //删除
                        handleDelete(id){
                                if(confirm('确定删除吗?')){
                                        //通知App组件将对应的todo对象删除
                                        this.deleteTodo(id)
                                }
                        }
                },
        }
</script>

<style scoped>
        /*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" :checked="isAll" @change="checkAll"/> -->
                        <input type="checkbox" v-model="isAll"/>
                </label>
                <span>
                        <span>已完成{{doneTotal}}</span> / 全部{{total}}
                </span>
                <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
        </div>
</template>

<script>
        export default {
                name:'MyFooter',
                props:['todos','checkAllTodo','clearAllTodo'],
                computed: {
                        //总数
                        total(){
                                return this.todos.length
                        },
                        //已完成数
                        doneTotal(){
                                //此处使用reduce方法做条件统计
                                /* const x = this.todos.reduce((pre,current)=>{
                                        console.log('@',pre,current)
                                        return pre + (current.done ? 1 : 0)
                                },0) */
                                //简写
                                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
                        },
                        //控制全选框
                        isAll:{
                                //全选框是否勾选
                                get(){
                                        return this.doneTotal === this.total && this.total > 0
                                },
                                //isAll被修改时set被调用
                                set(value){
                                        this.checkAllTodo(value)
                                }
                        }
                },
                methods: {
                        /* checkAll(e){
                                this.checkAllTodo(e.target.checked)
                        } */
                        //清空所有已完成
                        clearAll(){
                                this.clearAllTodo()
                        }
                },
        }
</script>

<style scoped>
        /*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>


[*]main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App)
})
9. 组件的自界说事件


[*]一种组件间通讯的方式,适用于:子组件 ===> 父组件
[*]使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自界说事件(事件的回调在A中)。
9.1 使用自界说事件:


[*] 第一种方式,在父组件中:<Demo @atguigu="test"/>或 <Demo v-on:atguigu="test"/>,用v-on的方式绑定,前者是自界说事件名,后者是回调名
[*] 第二种方式,在父组件中,以标记的情势来绑定,函数定名好,然后通过mounted函数去执行回调函数,即atguigu身上的test方法
<Demo ref="demo" @test="test1"/>
......
methods: {
        test1(name) {
                console.log(this.name)
        }
}
mounted(){
   this.$refs.demo.$on('atguigu',this.test1)
}

[*] 若想让自界说事件只能触发一次,可以使用once修饰符,或$once方法。
[*] 触发自界说事件:this.$emit('atguigu',数据)
[*] 解绑自界说事件this.$off('atguigu')
[*] 组件上也可以绑定原生DOM事件,需要使用native修饰符。
[*] 留意:通过this.$refs.xxx.$on('atguigu',回调)绑定自界说事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
9.2 组件自界说事件案例

9.2.1 App.vue

<template>
        <div class="app">
                <h1>{{msg}},学生姓名是:{{studentName}}</h1>

                <!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
                <School :getSchoolName="getSchoolName"/>

                <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
                <!-- <Student @atguigu="getStudentName" @demo="m1"/> -->

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

<script>
        import Student from './components/Student'
        import School from './components/School'

        export default {
                name:'App',
                components:{School,Student},
                data() {
                        return {
                                msg:'你好啊!',
                                studentName:''
                        }
                },
                methods: {
                        getSchoolName(name){
                                console.log('App收到了学校名:',name)
                        },
                        getStudentName(name,...params){
                                console.log('App收到了学生名:',name,params)
                                this.studentName = name
                        },
                        m1(){
                                console.log('demo事件被触发了!')
                        },
                        show(){
                                alert(123)
                        }
                },
                mounted() {
                        this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
                        // this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
                },
        }
</script>

<style scoped>
        .app{
                background-color: gray;
                padding: 5px;
        }
</style>

9.2.2 School.vue

<template>
        <div class="school">
                <h2>学校名称:{{name}}</h2>
                <h2>学校地址:{{address}}</h2>
                <button @click="sendSchoolName">把学校名给App</button>
        </div>
</template>

<script>
        export default {
                name:'School',
                props:['getSchoolName'],
                data() {
                        return {
                                name:'尚硅谷',
                                address:'北京',
                        }
                },
                methods: {
                        sendSchoolName(){
                                this.getSchoolName(this.name)
                        }
                },
        }
</script>

<style scoped>
        .school{
                background-color: skyblue;
                padding: 5px;
        }
</style>
9.2.3 Student.vue

<template>
        <div class="student">
                <h2>学生姓名:{{name}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <h2>当前求和为:{{number}}</h2>
                <button @click="add">点我number++</button>
                <button @click="sendStudentlName">把学生名给App</button>
                <button @click="unbind">解绑atguigu事件</button>
                <button @click="death">销毁当前Student组件的实例(vc)</button>
        </div>
</template>

<script>
        export default {
                name:'Student',
                data() {
                        return {
                                name:'张三',
                                sex:'男',
                                number:0
                        }
                },
                methods: {
                        add(){
                                console.log('add回调被调用了')
                                this.number++
                        },
                        sendStudentlName(){
                                //触发Student组件实例身上的atguigu事件
                                this.$emit('atguigu',this.name,666,888,900)
                                // this.$emit('demo')
                                // this.$emit('click')
                        },
                        unbind(){
                                this.$off('atguigu') //解绑一个自定义事件
                                // this.$off(['atguigu','demo']) //解绑多个自定义事件
                                // this.$off() //解绑所有的自定义事件
                        },
                        death(){
                                this.$destroy() //销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效。
                        }
                },
        }
</script>

<style lang="less" scoped>
        .student{
                background-color: pink;
                padding: 5px;
                margin-top: 30px;
        }
</style>

9.2.4 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
        /* mounted() {
                setTimeout(()=>{
                        this.$destroy()
                },3000)
        }, */
})
9.3 TodoList 自界说事件

9.3.1 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" @clearAllTodo="clearAllTodo"/>
                        </div>
                </div>
        </div>
</template>

<script>
        import MyHeader from './components/MyHeader'
        import MyList from './components/MyList'
        import MyFooter from './components/MyFooter.vue'

        export default {
                name:'App',
                components:{MyHeader,MyList,MyFooter},
                data() {
                        return {
                                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(这种操作叫做状态提升)
                                todos:JSON.parse(localStorage.getItem('todos')) || []
                        }
                },
                methods: {
                        //添加一个todo
                        addTodo(todoObj){
                                this.todos.unshift(todoObj)
                        },
                        //勾选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
                        clearAllTodo(){
                                this.todos = this.todos.filter((todo)=>{
                                        return !todo.done
                                })
                        }
                },
                watch: {
                        todos:{
                                //开启深度监听,当todos中的对象发生变化时,会执行handler方法
                                //handler方法的参数是todos的最新值,也就是todos的最新值的副本
                                // 如果不开深度监听,那么todos中的对象属性中的值发生变化时,不会执行handler方法
                                deep:true,
                                handler(value){
                                        localStorage.setItem('todos',JSON.stringify(value))
                                }
                        }
                },
        }
</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;
                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;
        }
</style>

9.3.2 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',
                data() {
                        return {
                                //收集用户输入的title
                                title:''
                        }
                },
                methods: {
                        add(){
                                //校验数据
                                if(!this.title.trim()) return alert('输入不能为空')
                                //将用户的输入包装成一个todo对象
                                const todoObj = {id:nanoid(),title:this.title,done:false}
                                //通知App组件去添加一个todo对象
                                this.$emit('addTodo',todoObj,1,2,3)
                                //清空输入
                                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>
9.3.3 MyList.vue

<template>
        <ul class="todo-main">
                <MyItem
                        v-for="todoObj in todos"
                        :key="todoObj.id"
                        :todo="todoObj"
                        :checkTodo="checkTodo"
                        :deleteTodo="deleteTodo"
                />
        </ul>
</template>

<script>
        import MyItem from './MyItem'

        export default {
                name:'MyList',
                components:{MyItem},
                //声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件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>
9.3.4 MyItem.vue

<template>
        <li>
                <label>
                        <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
                        <!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
                        <!-- <input type="checkbox" v-model="todo.done"/> -->
                        <span>{{todo.title}}</span>
                </label>
                <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        </li>
</template>

<script>
        export default {
                name:'MyItem',
                //声明接收todo、checkTodo、deleteTodo
                props:['todo','checkTodo','deleteTodo'],
                methods: {
                        //勾选or取消勾选
                        handleCheck(id){
                                //通知App组件将对应的todo对象的done值取反
                                this.checkTodo(id)
                        },
                        //删除
                        handleDelete(id){
                                if(confirm('确定删除吗?')){
                                        //通知App组件将对应的todo对象删除
                                        this.deleteTodo(id)
                                }
                        }
                },
        }
</script>

<style scoped>
        /*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>
9.3.5 MyFooter.vue

<template>
        <div class="todo-footer" v-show="total">
                <label>
                        <!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
                        <input type="checkbox" v-model="isAll"/>
                </label>
                <span>
                        <span>已完成{{doneTotal}}</span> / 全部{{total}}
                </span>
                <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
        </div>
</template>

<script>
        export default {
                name:'MyFooter',
                props:['todos'],
                computed: {
                        //总数
                        total(){
                                return this.todos.length
                        },
                        //已完成数
                        doneTotal(){
                                //此处使用reduce方法做条件统计
                                /* const x = this.todos.reduce((pre,current)=>{
                                        console.log('@',pre,current)
                                        return pre + (current.done ? 1 : 0)
                                },0) */
                                //简写
                                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
                        },
                        //控制全选框
                        isAll:{
                                //全选框是否勾选
                                get(){
                                        return this.doneTotal === this.total && this.total > 0
                                },
                                //isAll被修改时set被调用
                                set(value){
                                        // this.checkAllTodo(value)
                                        //使用emit触发父组件的事件
                                        this.$emit('checkAllTodo',value)
                                }
                        }
                },
                methods: {
                        /* checkAll(e){
                                this.checkAllTodo(e.target.checked)
                        } */
                        //清空所有已完成
                        clearAll(){
                                // this.clearAllTodo()
                                //使用emit触发父组件的事件
                                this.$emit('clearAllTodo')
                        }
                },
        }
</script>

<style scoped>
        /*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>
9.3.6 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App)
})
10. 全局事件总线(GlobalEventBus)

https://i-blog.csdnimg.cn/direct/c52a7c4401c5403cbca755cb9c626b34.png
https://i-blog.csdnimg.cn/direct/d68ad9a03f93463bb487fcc3cac2c709.png
10.1 全局事件总线使用


[*] 一种组件间通讯的方式,适用于任意组件间通讯
[*] 安装全局事件总线:
new Vue({
           ......
           beforeCreate() {
                   Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
           },
       ......
   })

[*] 使用事件总线:

[*]接收数据:A组件想接收数据,则在A组件中给$bus绑定自界说事件,事件的回调留在A组件自身。
methods(){
    demo(data){......}
}
......
mounted() {
    this.$bus.$on('xxx',this.demo)// xxx 是要绑定的自定义事件名
}
   
[*]提供数据:this.$bus.$emit('xxxx',数据) // 触发事件而且把数据带过去

[*] 最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
10.2 Vue 原型对象上包罗事件处理的方法


[*]$on(eventName, listener): 绑定自界说事件监听
[*]$emit(eventName, data): 分发自界说事件
[*]$off(eventName): 解绑自界说事件监听
[*]$once(eventName, listener): 绑定事件监听, 但只能处理一次
[*]所有组件实例对象的原型对象的原型对象就是 Vue 的原型对象


[*]所有组件对象都能看到 Vue 原型对象上的属性和方法
[*]Vue.prototype.$bus = new Vue(), 所有的组件对象都能看到$bus 这个属性对象

[*]全局事件总线

[*]包罗事件处理相干方法的对象(只有一个)
[*]所有的组件都可以得到
10.2.1 指定事件总线对象

new Vue({
        beforeCreate () { // 尽量早的执行挂载全局事件总线对象的操作
                Vue.prototype.$bus = this
        },
}).$mount('#root')
10.2.2 绑定事件

this.$bus.$on('eventName', 自界说事件要回调的函数)
10.2.3 分发事件

this.$bus.$emit('eventName', this.要通讯的数据)
10.2.4 解绑事件

this.$bus.$off('eventName')
10.3 全局事件总线 案例

10.3.1 App.vue

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

<script>
        import Student from './components/Student'
        import School from './components/School'

        export default {
                name:'App',
                components:{School,Student},
                data() {
                        return {
                                msg:'你好啊!',
                        }
                }
        }
</script>

<style scoped>
        .app{
                background-color: gray;
                padding: 5px;
        }
</style>

10.3.2 School.vue

<template>
        <div class="school">
                <h2>学校名称:{{name}}</h2>
                <h2>学校地址:{{address}}</h2>
        </div>
</template>

<script>
        export default {
                name:'School',
                data() {
                        return {
                                name:'尚硅谷',
                                address:'北京',
                        }
                },
                mounted() {
                        // console.log('School',this)
                        this.$bus.$on('hello',(data)=>{
                                console.log('我是School组件,收到了数据',data)
                        })
                },
                beforeDestroy() {
                        this.$bus.$off('hello')
                },
        }
</script>

<style scoped>
        .school{
                background-color: skyblue;
                padding: 5px;
        }
</style>
10.3.3 Student.vue

<template>
        <div class="student">
                <h2>学生姓名:{{name}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <button @click="sendStudentName">把学生名给School组件</button>
        </div>
</template>

<script>
        export default {
                name:'Student',
                data() {
                        return {
                                name:'张三',
                                sex:'男',
                        }
                },
                mounted() {
                        // console.log('Student',this.x)
                },
                methods: {
                        sendStudentName(){
                                this.$bus.$emit('hello',this.name)
                        }
                },
        }
</script>

<style lang="less" scoped>
        .student{
                background-color: pink;
                padding: 5px;
                margin-top: 30px;
        }
</style>

10.3.4 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
        beforeCreate() {
                Vue.prototype.$bus = this //安装全局事件总线
        },
})
10.4 TodoList 事件总线案例



[*]组件间通讯用事件总线改写
10.4.1 App.vue

<template>
        <div id="root">
                <div class="todo-container">
                        <div class="todo-wrap">
                                <MyHeader @addTodo="addTodo"/>
                                <MyList :todos="todos"/>
                                <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
                        </div>
                </div>
        </div>
</template>

<script>
        import MyHeader from './components/MyHeader'
        import MyList from './components/MyList'
        import MyFooter from './components/MyFooter.vue'

        export default {
                name:'App',
                components:{MyHeader,MyList,MyFooter},
                data() {
                        return {
                                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                                todos:JSON.parse(localStorage.getItem('todos')) || []
                        }
                },
                methods: {
                        //添加一个todo
                        addTodo(todoObj){
                                this.todos.unshift(todoObj)
                        },
                        //勾选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
                        clearAllTodo(){
                                this.todos = this.todos.filter((todo)=>{
                                        return !todo.done
                                })
                        }
                },
                watch: {
                        todos:{
                                deep:true,
                                handler(value){
                                        localStorage.setItem('todos',JSON.stringify(value))
                                }
                        }
                },
                mounted() {
                        this.$bus.$on('checkTodo',this.checkTodo)
                        this.$bus.$on('deleteTodo',this.deleteTodo)
                },
                beforeDestroy() {
                        this.$bus.$off('checkTodo')
                        this.$bus.$off('deleteTodo')
                },
        }
</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;
                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;
        }
</style>

10.4.2 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',
                data() {
                        return {
                                //收集用户输入的title
                                title:''
                        }
                },
                methods: {
                        add(){
                                //校验数据
                                if(!this.title.trim()) return alert('输入不能为空')
                                //将用户的输入包装成一个todo对象
                                const todoObj = {id:nanoid(),title:this.title,done:false}
                                //通知App组件去添加一个todo对象
                                this.$emit('addTodo',todoObj,1,2,3)
                                //清空输入
                                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>
10.4.3 MyList.vue

<template>
        <ul class="todo-main">
                <MyItem
                        v-for="todoObj in todos"
                        :key="todoObj.id"
                        :todo="todoObj"
                />
        </ul>
</template>

<script>
        import MyItem from './MyItem'

        export default {
                name:'MyList',
                components:{MyItem},
                //声明接收App传递过来的数据
                props:['todos']
        }
</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>
10.4.4 MyItem.vue

<template>
        <li>
                <label>
                        <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
                        <!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
                        <!-- <input type="checkbox" v-model="todo.done"/> -->
                        <span>{{todo.title}}</span>
                </label>
                <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        </li>
</template>

<script>
        export default {
                name:'MyItem',
                //声明接收todo
                props:['todo'],
                methods: {
                        //勾选or取消勾选
                        handleCheck(id){
                                //通知App组件将对应的todo对象的done值取反
                                // this.checkTodo(id)
                                this.$bus.$emit('checkTodo',id)
                        },
                        //删除
                        handleDelete(id){
                                if(confirm('确定删除吗?')){
                                        //通知App组件将对应的todo对象删除
                                        // this.deleteTodo(id)
                                        this.$bus.$emit('deleteTodo',id)
                                }
                        }
                },
        }
</script>

<style scoped>
        /*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>
10.4.5 MyFooter.vue

<template>
        <div class="todo-footer" v-show="total">
                <label>
                        <!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
                        <input type="checkbox" v-model="isAll"/>
                </label>
                <span>
                        <span>已完成{{doneTotal}}</span> / 全部{{total}}
                </span>
                <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
        </div>
</template>

<script>
        export default {
                name:'MyFooter',
                props:['todos'],
                computed: {
                        //总数
                        total(){
                                return this.todos.length
                        },
                        //已完成数
                        doneTotal(){
                                //此处使用reduce方法做条件统计
                                /* const x = this.todos.reduce((pre,current)=>{
                                        console.log('@',pre,current)
                                        return pre + (current.done ? 1 : 0)
                                },0) */
                                //简写
                                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
                        },
                        //控制全选框
                        isAll:{
                                //全选框是否勾选
                                get(){
                                        return this.doneTotal === this.total && this.total > 0
                                },
                                //isAll被修改时set被调用
                                set(value){
                                        // this.checkAllTodo(value)
                                        this.$emit('checkAllTodo',value)
                                }
                        }
                },
                methods: {
                        /* checkAll(e){
                                this.checkAllTodo(e.target.checked)
                        } */
                        //清空所有已完成
                        clearAll(){
                                // this.clearAllTodo()
                                this.$emit('clearAllTodo')
                        }
                },
        }
</script>

<style scoped>
        /*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>
10.4.6 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
        beforeCreate() {
                Vue.prototype.$bus = this//安装全局事件总线,$bus就是当前应用的vm
        },
})
11. 消息订阅与发布



[*]一种组件间通讯的方式,适用于任意组件间通讯。
[*]这种方式的头脑与全局事件总线很相似
[*]它包罗以下操作:
[*]订阅消息 --对应绑定事件监听
[*]发布消息 --分发事件
[*]取消消息订阅 --解绑事件监听

[*]需要引入一个消息订阅与发布的第三方实现库: PubSubJS
11.1 使用 PubSubJS



[*]在线文档: https://github.com/mroderick/PubSubJS
[*]相干语法
[*]import PubSub from 'pubsub-js'// 引入
[*]PubSub.subscribe(‘msgName’, functon(msgName, data){ })// 订阅消息,绑定函数
[*]PubSub.publish(‘msgName’, data): 发布消息, 触发订阅的回调函数调用
[*]PubSub.unsubscribe(token): 取消消息的订阅

11.2 PubSub 使用步骤


[*]安装pubsub:npm i pubsub-js
[*]引入: import pubsub from 'pubsub-js'
[*]接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
methods(){
        demo(data){......}
},
......
mounted() {
        this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
}

[*]提供数据:pubsub.publish('xxx',数据)
[*]最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)去取消订阅。


[*]留意:
   如果调用第三方库来配合vue使用的时候,会出现this指向不到实例也就是VC,这个时候我们就要将回调函数把它从平凡函数写成箭头函数,由于箭头函数是没有自己的this的,它会往上面找,往上一层找。然后上一层刚好就找到了VC。要么就是说在methods方法里面再配置一个方法,再从这个参数里面去调用那个方法也会实现同样的效果。(由于this.demo指向明确,this就是指的是vc)
11.3 消息订阅与发布 案例

11.3.1 App.vue

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

<script>
        import Student from './components/Student'
        import School from './components/School'

        export default {
                name:'App',
                components:{School,Student},
                data() {
                        return {
                                msg:'你好啊!',
                        }
                }
        }
</script>

<style scoped>
        .app{
                background-color: gray;
                padding: 5px;
        }
</style>

11.3.2 School.vue

<template>
        <div class="school">
                <h2>学校名称:{{name}}</h2>
                <h2>学校地址:{{address}}</h2>
        </div>
</template>

<script>
        import pubsub from 'pubsub-js'
        export default {
                name:'School',
                data() {
                        return {
                                name:'尚硅谷',
                                address:'北京',
                        }
                },
                mounted() {
                        // console.log('School',this)
                        /* this.$bus.$on('hello',(data)=>{
                                console.log('我是School组件,收到了数据',data)
                        }) */
                        this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
                                console.log(this)
                                // console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data)
                        })
                },
                beforeDestroy() {
                        // this.$bus.$off('hello')
                        pubsub.unsubscribe(this.pubId)
                },
        }
</script>

<style scoped>
        .school{
                background-color: skyblue;
                padding: 5px;
        }
</style>
11.3.3 Student.vue

<template>
        <div class="student">
                <h2>学生姓名:{{name}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <button @click="sendStudentName">把学生名给School组件</button>
        </div>
</template>

<script>
        import pubsub from 'pubsub-js'
        export default {
                name:'Student',
                data() {
                        return {
                                name:'张三',
                                sex:'男',
                        }
                },
                mounted() {
                        // console.log('Student',this.x)
                },
                methods: {
                        sendStudentName(){
                                // this.$bus.$emit('hello',this.name)
                                pubsub.publish('hello',666)
                        }
                },
        }
</script>

<style lang="less" scoped>
        .student{
                background-color: pink;
                padding: 5px;
                margin-top: 30px;
        }
</style>

11.3.4 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
})
11.4 TodoList PubSub 应用

11.4.1 App.vue

<template>
        <div id="root">
                <div class="todo-container">
                        <div class="todo-wrap">
                                <MyHeader @addTodo="addTodo"/>
                                <MyList :todos="todos"/>
                                <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
                        </div>
                </div>
        </div>
</template>

<script>
        import pubsub from 'pubsub-js'
        import MyHeader from './components/MyHeader'
        import MyList from './components/MyList'
        import MyFooter from './components/MyFooter'

        export default {
                name:'App',
                components:{MyHeader,MyList,MyFooter},
                data() {
                        return {
                                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                                todos:JSON.parse(localStorage.getItem('todos')) || []
                        }
                },
                methods: {
                        //添加一个todo
                        addTodo(todoObj){
                                this.todos.unshift(todoObj)
                        },
                        //勾选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
                        clearAllTodo(){
                                this.todos = this.todos.filter((todo)=>{
                                        return !todo.done
                                })
                        }
                },
                watch: {
                        todos:{
                                deep:true,
                                handler(value){
                                        localStorage.setItem('todos',JSON.stringify(value))
                                }
                        }
                },
                mounted() {
                        this.$bus.$on('checkTodo',this.checkTodo)
                        this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
                },
                beforeDestroy() {
                        this.$bus.$off('checkTodo')
                        pubsub.unsubscribe(this.pubId)
                },
        }
</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;
                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;
        }
</style>

11.4.2 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',
                data() {
                        return {
                                //收集用户输入的title
                                title:''
                        }
                },
                methods: {
                        add(){
                                //校验数据
                                if(!this.title.trim()) return alert('输入不能为空')
                                //将用户的输入包装成一个todo对象
                                const todoObj = {id:nanoid(),title:this.title,done:false}
                                //通知App组件去添加一个todo对象
                                this.$emit('addTodo',todoObj,1,2,3)
                                //清空输入
                                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>
11.4.3 MyList.vue

<template>
        <ul class="todo-main">
                <MyItem
                        v-for="todoObj in todos"
                        :key="todoObj.id"
                        :todo="todoObj"
                />
        </ul>
</template>

<script>
        import MyItem from './MyItem'

        export default {
                name:'MyList',
                components:{MyItem},
                //声明接收App传递过来的数据
                props:['todos']
        }
</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>
11.4.4 MyItem.vue


<template>
        <li>
                <label>
                        <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
                        <!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
                        <!-- <input type="checkbox" v-model="todo.done"/> -->
                        <span>{{todo.title}}</span>
                </label>
                <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        </li>
</template>

<script>
        import pubsub from 'pubsub-js'
        export default {
                name:'MyItem',
                //声明接收todo
                props:['todo'],
                methods: {
                        //勾选or取消勾选
                        handleCheck(id){
                                //通知App组件将对应的todo对象的done值取反
                                // this.checkTodo(id)
                                this.$bus.$emit('checkTodo',id)
                        },
                        //删除
                        handleDelete(id){
                                if(confirm('确定删除吗?')){
                                        //通知App组件将对应的todo对象删除
                                        // this.deleteTodo(id)
                                        // this.$bus.$emit('deleteTodo',id)
                                        pubsub.publish('deleteTodo',id)
                                }
                        }
                },
        }
</script>

<style scoped>
        /*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>
11.4.5 MyFooter.vue

<template>
        <div class="todo-footer" v-show="total">
                <label>
                        <!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
                        <input type="checkbox" v-model="isAll"/>
                </label>
                <span>
                        <span>已完成{{doneTotal}}</span> / 全部{{total}}
                </span>
                <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
        </div>
</template>

<script>
        export default {
                name:'MyFooter',
                props:['todos'],
                computed: {
                        //总数
                        total(){
                                return this.todos.length
                        },
                        //已完成数
                        doneTotal(){
                                //此处使用reduce方法做条件统计
                                /* const x = this.todos.reduce((pre,current)=>{
                                        console.log('@',pre,current)
                                        return pre + (current.done ? 1 : 0)
                                },0) */
                                //简写
                                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
                        },
                        //控制全选框
                        isAll:{
                                //全选框是否勾选
                                get(){
                                        return this.doneTotal === this.total && this.total > 0
                                },
                                //isAll被修改时set被调用
                                set(value){
                                        // this.checkAllTodo(value)
                                        this.$emit('checkAllTodo',value)
                                }
                        }
                },
                methods: {
                        /* checkAll(e){
                                this.checkAllTodo(e.target.checked)
                        } */
                        //清空所有已完成
                        clearAll(){
                                // this.clearAllTodo()
                                this.$emit('clearAllTodo')
                        }
                },
        }
</script>

<style scoped>
        /*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>
11.4.6 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
        beforeCreate() {
                Vue.prototype.$bus = this
        },
})
12. nextTick


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


[*]案例
//编辑
handleEdit(todo){
        if(todo.hasOwnProperty('isEdit')){
                todo.isEdit = true
        }else{
                // console.log('@')
                this.$set(todo,'isEdit',true)
        }
        // 在下次DOM更新循环结束之后执行延迟回调
        this.$nextTick(function(){
                this.$refs.inputTitle.focus()
        })
}
13. Vue 封装的过度与动画



[*]作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。
[*]效果:
https://i-blog.csdnimg.cn/direct/4d4d082989bf49ed827def1a074175ea.png
13.1 过渡效果的写法:

https://i-blog.csdnimg.cn/direct/d5255048fbd04e65af5ef30d0f11899d.png

[*] 准备好样式:

[*]元素进入的样式:
[*]v-enter:进入的起点
[*]v-enter-active:进入过程中
[*]v-enter-to:进入的终点

[*]元素离开的样式:
[*]v-leave:离开的起点
[*]v-leave-active:离开过程中
[*]v-leave-to:离开的终点


[*] 使用<transition>包裹要过度的元素,并配置name属性:
<transition name="hello">
        <h1 v-show="isShow">你好啊!</h1>
</transition>
13.1.1 多个元素需要过度



[*]需要使用:<transition-group>,且每个元素都要指定key值。
13.2 动画效果的写法:


[*]两个时间点

[*]v-enter-active动画进入过程中
[*]v-leave-active动画离开过程中

[*]一个固定写法:@keyframes 名字
13.3 集成第三方动画



[*]也可以引入第三方动画库,方便使用且动画更加炫酷。例如:animate.css
13.4 vue 动画的明确



[*]操作 css 的 trasition 或 animation
[*]vue 会给目的元素添加/移除特定的 class
13.4.1 过渡的相干类名:


[*]xxx-enter-active: 指定显示的 transition
[*]xxx-leave-active: 指定隐藏的 transition
[*]xxx-enter/xxx-leave-to: 指定隐藏时的样式
13.4.2 基本过渡动画的编码



[*]在目的元素外包裹
[*]界说 class 样式
[*]指定过渡样式: transition
[*]指定隐藏时的样式: opacity/别的

13.5 过渡与动画 案例

13.5.1 App.vue

<template>
        <div>
                <Test/>
                <Test2/>
                <Test3/>
        </div>
</template>

<script>
        import Test from './components/Test'
        import Test2 from './components/Test2'
        import Test3 from './components/Test3'

        export default {
                name:'App',
                components:{Test,Test2,Test3},
        }
</script>

13.5.2 动画效果的写法:Test.vue



[*]两个时间点

[*]v-enter-active动画进入过程中
[*]v-leave-active动画离开过程中


[*]一个固定写法:@keyframes 名字
<template>
        <div>
                <button @click="isShow = !isShow">显示/隐藏</button>
                <transition name="hello" appear>
                        <h1 v-show="isShow">你好啊!</h1>
                </transition>
        </div>
</template>

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

<style scoped>
        h1{
                background-color: orange;
        }

        // 动画进入的过程中
        .hello-enter-active{
                animation: atguigu 0.5s linear;
        }
        // 动画离开的过程中
        .hello-leave-active{
                animation: atguigu 0.5s linear reverse;
        }

        // 动画效果
        @keyframes atguigu {
                from{
                        transform: translateX(-100%);
                }
                to{
                        transform: translateX(0px);
                }
        }
</style>
13.5.3 过渡效果的写法和多个元素过度:Test2.vue



[*]如果多组动画,需要使用<transition-group>且需要给 item 指定 key 值
[*]六个时间点都要写
<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>
13.5.4 引用第三方动画库:Test3.vue



[*]引用第三方动画库:animate.css
<template>
        <div>
                <button @click="isShow = !isShow">显示/隐藏</button>
                <!-- 参照第三方动画库文档配置属性 -->
                <transition-group
                        appear
                        name="animate__animated animate__bounce"
                        enter-active-class="animate__swing"
                        leave-active-class="animate__backOutUp"
                >
                        <h1 v-show="!isShow" key="1">你好啊!</h1>
                        <h1 v-show="isShow" key="2">尚硅谷!</h1>
                </transition-group>
        </div>
</template>

<script>
       
        import 'animate.css' // 引入第三方动画库
        export default {
                name:'Test',
                data() {
                        return {
                                isShow:true
                        }
                },
        }
</script>

<style scoped>
        h1{
                background-color: orange;
        }
</style>
13.5.5 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
        beforeCreate() {
                Vue.prototype.$bus = this
        },
})
13.6 TodoList 动画 案例

13.6.1 App.vue

<template>
        <div id="root">
                <div class="todo-container">
                        <div class="todo-wrap">
                                <MyHeader @addTodo="addTodo"/>
                                <MyList :todos="todos"/>
                                <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
                        </div>
                </div>
        </div>
</template>

<script>
        import pubsub from 'pubsub-js'
        import MyHeader from './components/MyHeader'
        import MyList from './components/MyList'
        import MyFooter from './components/MyFooter'

        export default {
                name:'App',
                components:{MyHeader,MyList,MyFooter},
                data() {
                        return {
                                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                                todos:JSON.parse(localStorage.getItem('todos')) || []
                        }
                },
                methods: {
                        //添加一个todo
                        addTodo(todoObj){
                                this.todos.unshift(todoObj)
                        },
                        //勾选or取消勾选一个todo
                        checkTodo(id){
                                this.todos.forEach((todo)=>{
                                        if(todo.id === id) todo.done = !todo.done
                                })
                        },
                        //更新一个todo
                        updateTodo(id,title){
                                this.todos.forEach((todo)=>{
                                        if(todo.id === id) todo.title = title
                                })
                        },
                        //删除一个todo
                        deleteTodo(_,id){
                                this.todos = this.todos.filter( todo => todo.id !== id )
                        },
                        //全选or取消全选
                        checkAllTodo(done){
                                this.todos.forEach((todo)=>{
                                        todo.done = done
                                })
                        },
                        //清除所有已经完成的todo
                        clearAllTodo(){
                                this.todos = this.todos.filter((todo)=>{
                                        return !todo.done
                                })
                        }
                },
                watch: {
                        todos:{
                                deep:true,
                                handler(value){
                                        localStorage.setItem('todos',JSON.stringify(value))
                                }
                        }
                },
                mounted() {
                        this.$bus.$on('checkTodo',this.checkTodo)
                        this.$bus.$on('updateTodo',this.updateTodo)
                        this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
                },
                beforeDestroy() {
                        this.$bus.$off('checkTodo')
                        this.$bus.$off('updateTodo')
                        pubsub.unsubscribe(this.pubId)
                },
        }
</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;
                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-edit {
                color: #fff;
                background-color: skyblue;
                border: 1px solid rgb(103, 159, 180);
                margin-right: 5px;
        }
        .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>

13.6.2 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',
                data() {
                        return {
                                //收集用户输入的title
                                title:''
                        }
                },
                methods: {
                        add(){
                                //校验数据
                                if(!this.title.trim()) return alert('输入不能为空')
                                //将用户的输入包装成一个todo对象
                                const todoObj = {id:nanoid(),title:this.title,done:false}
                                //通知App组件去添加一个todo对象
                                this.$emit('addTodo',todoObj,1,2,3)
                                //清空输入
                                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>
13.6.3 MyList.vue

<template>
        <ul class="todo-main">
                <transition-group name="todo" appear>
                        <MyItem
                                v-for="todoObj in todos"
                                :key="todoObj.id"
                                :todo="todoObj"
                        />
                </transition-group>
        </ul>
</template>

<script>
        import MyItem from './MyItem'

        export default {
                name:'MyList',
                components:{MyItem},
                //声明接收App传递过来的数据
                props:['todos']
        }
</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;
        }

        .todo-enter-active{
                animation: atguigu 0.5s linear;
        }

        .todo-leave-active{
                animation: atguigu 0.5s linear reverse;
        }

        @keyframes atguigu {
                from{
                        transform: translateX(100%);
                }
                to{
                        transform: translateX(0px);
                }
        }
</style>
13.6.4 MyItem.vue

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

<script>
        import pubsub from 'pubsub-js'
        export default {
                name:'MyItem',
                //声明接收todo
                props:['todo'],
                methods: {
                        //勾选or取消勾选
                        handleCheck(id){
                                //通知App组件将对应的todo对象的done值取反
                                // this.checkTodo(id)
                                this.$bus.$emit('checkTodo',id)
                        },
                        //删除
                        handleDelete(id){
                                if(confirm('确定删除吗?')){
                                        //通知App组件将对应的todo对象删除
                                        // this.deleteTodo(id)
                                        // this.$bus.$emit('deleteTodo',id)
                                        pubsub.publish('deleteTodo',id)
                                }
                        },
                        //编辑
                        handleEdit(todo){
                                if(todo.hasOwnProperty('isEdit')){
                                        todo.isEdit = true
                                }else{
                                        // console.log('@')
                                        this.$set(todo,'isEdit',true)
                                }
                                this.$nextTick(function(){
                                        this.$refs.inputTitle.focus()
                                })
                        },
                        //失去焦点回调(真正执行修改逻辑)
                        handleBlur(todo,e){
                                todo.isEdit = false
                                if(!e.target.value.trim()) return alert('输入不能为空!')
                                this.$bus.$emit('updateTodo',todo.id,e.target.value)
                        }
                },
        }
</script>

<style scoped>
        /*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>
13.6.5 MyFooter.vue

<template>
        <div class="todo-footer" v-show="total">
                <label>
                        <!-- <input type="checkbox" :checked="isAll" @change="checkAll"/> -->
                        <input type="checkbox" v-model="isAll"/>
                </label>
                <span>
                        <span>已完成{{doneTotal}}</span> / 全部{{total}}
                </span>
                <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
        </div>
</template>

<script>
        export default {
                name:'MyFooter',
                props:['todos'],
                computed: {
                        //总数
                        total(){
                                return this.todos.length
                        },
                        //已完成数
                        doneTotal(){
                                //此处使用reduce方法做条件统计
                                /* const x = this.todos.reduce((pre,current)=>{
                                        console.log('@',pre,current)
                                        return pre + (current.done ? 1 : 0)
                                },0) */
                                //简写
                                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0) ,0)
                        },
                        //控制全选框
                        isAll:{
                                //全选框是否勾选
                                get(){
                                        return this.doneTotal === this.total && this.total > 0
                                },
                                //isAll被修改时set被调用
                                set(value){
                                        // this.checkAllTodo(value)
                                        this.$emit('checkAllTodo',value)
                                }
                        }
                },
                methods: {
                        /* checkAll(e){
                                this.checkAllTodo(e.target.checked)
                        } */
                        //清空所有已完成
                        clearAll(){
                                // this.clearAllTodo()
                                this.$emit('clearAllTodo')
                        }
                },
        }
</script>

<style scoped>
        /*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>
13.6.6 main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
        el:'#app',
        render: h => h(App),
        beforeCreate() {
                Vue.prototype.$bus = this
        },
})

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 第 3 章:使用 Vue 脚手架