vue2(脚手架、组件)

2.1 脚手架

使用前置:

第一步(没有安装过的执行):全局安装 @vue/cli

npm install -g @vue/cli

第二步:切换到要创建项目的目录,然后使用命令创建项目

vue create xxxxx

第三步:启动项目

npm run serve

脚手架文件结构

复制代码
├── 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:包版本控制文件
复制代码

脚手架demo

components:

就直接把单文件组件的 School.vue 和 Student.vue 两个文件直接拿来用,不需要修改。

App.vue:

引入这两个组件,注册一下这两个组件,再使用。

复制代码
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <Student></Student>
    <School></School>
  </div>
</template>

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

export default {
  name: 'App',
  components: {
    School,
    Student
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
复制代码

main.js:

入口文件

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

接下来就要详细讲解 main.js 中的 render 函数

render函数

插入一个小知识:

使用 import 导入第三方库的时候不需要 加 './'

导入我们自己写的:

import App from './App.vue'

导入第三方的

import Vue from 'vue'

不需要在 from 'vue' 加 './' 的原因是第三方库 node_modules 人家帮我们配置好了。

我们通过 import 导入第三方库,在第三方库的 package.json 文件中确定了我们引入的是哪个文件

通过 module 确定了我们要引入的文件。

回到 render 函数

之前的写法是这样:

import App from './App.vue'

new Vue({
    el:'#root',
    template:`<App></App>`,
    components:{App},
})

如果这样子写,运行的话会引发如下的报错

报错的意思是,是在使用运行版本的 vue ,没有模板解析器。

从上面的小知识可以知道,我们引入的 vue 不是完整版的,是残缺的(为了减小vue的大小)。所以残缺的vue.js 只有通过 render 函数才能把项目给跑起来。

来解析一下render

复制代码
// render最原始写的方式
// render是个函数,还能接收到参数a
// 这个 createElement 很关键,是个回调函数
new Vue({
  render(createElement) {
      console.log(typeof createElement);
      // 这个 createElement 回调函数能创建元素
      // 因为残缺的vue 不能解析 template,所以render就来帮忙解决这个问题
      // createElement 能创建具体的元素
      return createElement('h1', 'hello')
  }
}).$mount('#app')
复制代码

因为 render 函数内并没有用到 this,所以可以简写成箭头函数。

new Vue({
  // render: h => h(App),
  render: (createElement) => {
    return createElement(App)
  }
}).$mount('#app')

再简写:

new Vue({
  // render: h => h(App),
  render: createElement => createElement(App)
}).$mount('#app')

最后把 createElement 换成 h 就完事了。

算啦算啦,把简写都整理一遍吧,js里的简写确实多哇。

对象内写方法最原始的:

let obj = {
    name: 'aaa',
    work: function (salary) {
        return '工资' + salary;
    }
}

ES6 简化版:

let obj = {
    name: 'aaa',
    work(salary) {
        return '工资' + salary;
    }
}

箭头函数简化版:

let obj = {
    name: 'aaa',
    work: (salary) => {
        return '工资' + salary;
    }
}

箭头函数再简化(最终版):

// 只有一个参数就可以把圆括号去了,函数体内部只有一个 return 就可以把大括号去掉,return去掉
let obj = {
    name: 'aaa',
    work: salary => '工资' + salary;
}

这样就可以理解 render 函数的简写方式了。

来个不同版本 vue 的区别

  • vue.js与vue.runtime.xxx.js的区别:

    • vue.js是完整版的Vue,包含:核心功能+模板解析器。

    • vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。

  • 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容。

脚手架中的index.html

复制代码
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <!-- 针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的渲染级别渲染页面 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- 开启移动端的理想视口 -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!-- 配置页签图标 -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!-- 引入第三方样式 -->
    <link rel="stylesheet" href="<%= BASE_URL %>css/bootstrap.css">
    <!-- 配置网页标题 -->
    <title>硅谷系统</title>
  </head>
  <body>
        <!-- 当浏览器不支持js时noscript中的元素就会被渲染 -->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
        <!-- 容器 -->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
复制代码

2.2 vue 零碎的一些知识

ref属性

  • 被用来给元素或子组件注册引用信息(id的替代者)

  • 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)

  • 使用方式:

    • 打标识:<h1 ref="xxx">.....</h1><School ref="xxx"></School>

    • 获取:this.$refs.xxx

具体案例

复制代码
<template>
    <div>
        <h1 v-text="msg" ref="title"></h1>
        <button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
        <School ref="sch"/>
    </div>
</template>

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

    export default {
        name:'App',
        components:{School},
        data() {
            return {
                msg:'欢迎学习Vue!'
            }
        },
        methods: {
            showDOM(){
                console.log(this.$refs.title) //真实DOM元素
                console.log(this.$refs.btn) //真实DOM元素
                console.log(this.$refs.sch) //School组件的实例对象(vc)
            }
        },
    }
</script>
复制代码

props配置项

  1. 功能:让组件接收外部传过来的数据

  2. 传递数据:<Demo name="xxx"/>

  3. 接收数据:

    1. 第一种方式(只接收):props:['name']

    2. 第二种方式(限制类型):props:{name:String}

    3. 第三种方式(限制类型、限制必要性、指定默认值):

props:{
    name:{
        type:String, //类型
        required:true, //必要性
        default:'老王' //默认值
    }
}
  1. 备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。

示例代码:

父组件给子组件传数据

App.vue

复制代码
<template>
  <div>
    <!-- 多个vc互不影响 -->
    <!-- v-bind的作用重要 获取后面表达式值的赋给props的属性,不然student组件接受的是字符串 -->
    <student name="张三" sex="男" :age="18" />
  </div>
</template>

<script>
import Student from "./components/Student";

export default {
  name: "App",
  components: {
    Student,
  },
};
</script>
复制代码

Student.vue

复制代码
<template>
  <div>
    <!-- 注意props传递过来的值是不能改的(尽量避免去改,控制台会有警告) -->
    <h1>{{ msg }}</h1>
    <h1>学生姓名:{{ name }}</h1>
    <h1>学生性别: {{ sex }}</h1>
    <h1>学生年龄: {{ myAge }}</h1>
    <button @click="updateAge">尝试修改年龄</button>
  </div>
</template>

<script>
export default {
  name: "Student",
  data() {
    console.log(this);
    return {
      //如果props和data存在同名的属性,会报错,但已props传递的属性值为主,props优先
      //注意props属性名不能是vue底层已征用的属性名(比如key, ref等等)
      msg: "我一个学生",
      myAge: this.age, //把props传递过来的值当成vc的状态,这样改age是不会出问题的,因为你没有直接去修改props
    };
  },
  methods: {
    updateAge() {
      this.myAge++;
    },
  },
  props: ["name", "age", "sex"], // 简单申明接受

  //限制props中属性的类型 类型错误了会提示错误信息
  // props: {
  //   name: String,
  //   sex: String,
  //   age: Number
  // }

  // 接受的同时对数据:进行类型限制+默认值的指定+必要性的限制
  // props: {
  //   name: {
  //     type: String, //  name的类型是字符串
  //     required: true, // name是必要的
  //   },
  //   age: {
  //     type: Number,
  //     default: 99, //默认值
  //   },
  //   sex: {
  //     type: String,
  //     required: true,
  //   },
  // },
};
</script>
<style scoped>
.demo {
  background: aqua;
}
</style>
复制代码

mixin(混入)

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

  1. 功能:可以把多个组件共用的配置提取成一个混入对象

  2. 使用方式:

    第一步定义混合:

{
    data(){....},
    methods:{....}
    ....
}
  1. 第二步使用混入:

    全局混入:Vue.mixin(xxx) 局部混入:mixins:['xxx']

mixin.js

复制代码
export const mixin = {
    methods: {
        showName() {
            alert(this.name)
        }
    },
    //挂载完毕就执行
    mounted() {
        console.log('你好啊')
    }
}

export const shareData = {
    data() {
        return {
            x: 100,
            y: 200
        }
    }
}
复制代码

Student.vue

复制代码
<template>
  <div>
    <h2 @click="showName">姓名:{{ name }}</h2>
    <h2>性别: {{ sex }}</h2>
  </div>
</template>

<script>
import { mixin, shareData } from "../mixin";
export default {
  name: "Student",
  data() {
    console.log(this);
    return {
      name: "张三",
      sex: "",
      x: 666, //如果混合中配置了与data(或者配置了相同的methods)相同的属性值,则以你的配置的属性为主(而不以mixin为主)
    };
  },
  mounted() {
    console.log("你好啊啊!!!!"); //但对于生命周期钩子是都会保存的(混合的钩子比你配置的钩子先跑)
  },
  mixins: [mixin, shareData],
};
</script>
复制代码

School.vue

复制代码
<template>
  <div>
    <h2 @click="showName">学校名称:{{ name }}</h2>
    <h2>学校地址: {{ address }}</h2>
  </div>
</template>

<script>
//引入混合
import { mixin, shareData } from "../mixin";
export default {
  name: "School",
  data() {
    console.log(this);
    return {
      name: "wust",
      address: "武汉科技大学",
    };
  },
  mixins: [mixin, shareData],
};
</script>
复制代码

全局混入不建议使用

插件

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制。

通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成:

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new 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
}
复制代码

具体案例:

plugin.js

复制代码
export default {
    install(Vue) {
        //vue帮你调用install方法
        // console.log('install');
        // console.log(Vue); //vm的构造函数Vue


        //注意配置一定要new vue实例之前确定
        Vue.filter('mySlice', function (val) {
            return val.slice(0, 4);
        });

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

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

        //给vue原型上添加一个方法 vc/vm都可以使用
        Vue.prototype.hello = function () {
            alert('hello')
        }
    }
}
复制代码

main.js

// 引入插件
import plugin from './plugin'

// 使用插件
Vue.use(plugin)

然后就可以在别的组件使用插件里的功能了。

School.vue

复制代码
<template>
  <div>
    <h2>学校名称:{{ name | mySlice }}</h2>
    <h2>学校地址: {{ address }}</h2>
    <button @click="test">测试一下hello方法</button>
  </div>
</template>

<script>
export default {
  name: "School",
  data() {
    console.log(this);
    return {
      name: "wut@重庆",
      address: "武汉科技大学",
    };
  },
  methods: {
    test() {
      this.hello();
    },
  },
};
</script>
复制代码

Student.vue

复制代码
<template>
  <div>
    <h2>姓名:{{ name }}</h2>
    <h2>性别: {{ sex }}</h2>
    <input v-fbind:value="name" />
  </div>
</template>

<script>
export default {
  name: "Student",
  data() {
    console.log(this);
    return {
      name: "张三",
      sex: "",
    };
  },
};
</script>
复制代码

scoped样式

  1. 作用:让样式在局部生效,防止冲突。

  2. 写法:<style scoped>

具体案例:

<style lang="less" scoped>
    .demo{
        background-color: pink;
        .atguigu{
            font-size: 40px;
        }
    }
</style>

Schoool.vue

复制代码
<template>
   <div class="demo">
     <h2>学校名称:{{  name }}</h2>
     <h2>学校地址: {{ address }}</h2>
   </div>
</template>

<script>
export default {
  name: "School",
  data(){
    console.log(this);
    return {
       name: 'wust university',
       address: '武汉科技大学'
    }
  },
}
</script>

<style scoped>
   /*scoped代表局部的*/
  .demo{
    background: skyblue;
  }
</style>
复制代码

总结TodoList案例

  1. 组件化编码流程:

    (1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

    (2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

    1).一个组件在用:放在组件自身即可。

    2). 一些组件在用:放在他们共同的父组件上(状态提升)。

    (3).实现交互:从绑定事件开始。

  2. props适用于:

    (1).父组件 ==> 子组件 通信

    (2).子组件 ==> 父组件 通信(要求父先给子一个函数)

  3. 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

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

App.vue

复制代码
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader :addTodo="addTodo" />
        <List :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo" />
        <MyFooter
          :todos="todos"
          :checkAllTodo="checkAllTodo"
          :clearAllDoneTodo="clearAllDoneTodo"
        />
      </div>
    </div>
  </div>
</template>

<script>
import MyHeader from "@/components/MyHeader.vue";
import List from "@/components/List.vue";
import MyFooter from "@/components/MyFooter.vue";

export default {
  name: "App",
  components: {
    MyHeader,
    List,
    MyFooter,
  },
  data() {
    return {
      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) => {
        return todo.id !== id;
      });
    },
    // 全选or取消全选
    checkAllTodo(done) {
      this.todos.forEach((todo) => (todo.done = done));
    },
    // 清除所有已经完成的todozz
    clearAllDoneTodo() {
      this.todos = this.todos.filter((todo) => {
        return !todo.done;
      });
    },
  },
};
</script>
复制代码

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: "",
    };
  },
  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 = "";
    },
  },
  props: ["addTodo"],
};
</script>
复制代码

List.vue

复制代码
<template>
  <ul class="todo-main">
    <Item
      v-for="todoObj in todos"
      :key="todoObj.id"
      :todo="todoObj"
      :checkTodo="checkTodo"
      :deleteTodo="deleteTodo"
    />
  </ul>
</template>

<script>
import Item from "./Item";

export default {
  name: "List",
  components: {
    Item,
  },

  props: ["todos", "checkTodo", "deleteTodo"], //在vc身上,直接用
};
</script>
复制代码

Item.vue

复制代码
<template>
  <li>
    <label>
      <!--      这里勾选和取消勾选可以使用change和click作为事件处理-->
      <input
        type="checkbox"
        :checked="todo.done"
        @change="handleCheck(todo.id)"
      />
      <!--v-model数据的双向绑定,checkbox使用v-model来双向绑定其是否被勾选,也可以实现效果但不推荐(因为其实修改了props中的数据)-->
      <!--这里修改了从List修改过来的props,这里的不允许改是浅层次,就是如果props是一个对象则这个修改这个对象的某一个属性vue是放行的-->
      <!-- <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: "Item",
  methods: {
    // 勾选or取消勾选
    handleCheck(id) {
      // 通过App组件将对应的todo对象的done值去反
      this.checkTodo(id);
    },
    // 删除
    handleDelete(id) {
      if (confirm("确定删除吗?")) {
        this.deleteTodo(id);
      }
    },
  },
  // 声明接受todo对象
  props: ["todo", "checkTodo", "deleteTodo"],
};
</script>
复制代码

MyFooter.vue

复制代码
<template>
  <!--隐式类型转换-->
  <div class="todo-footer">
    <label>
      <!--这里也可用v-model来替代,此时不需要计算属性了-->
      <!-- <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", "clearAllDoneTodo"],
  computed: {
    total() {
      return this.todos.length;
    },
    doneTotal() {
      // const x = this.todos.reduce((pre, current) => {
      //   console.log("@", pre, current);
      //   return pre + (current.done ? 1 : 0);
      // }, 0);
      // return x;
      // 简写
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
    },
    isAll: {
      get() {
        return this.doneTotal === this.total && this.total > 0;
      },
      set(value) {
        this.checkAllTodo(value);
      },
    },
  },
  methods: {
    //   checkAll(e) {
    //     this.checkAllTodo(e.target.checked);
    //   },
    clearAll() {
      this.clearAllDoneTodo();
    },
  },
};
</script>
复制代码

2.3 浏览器本地存储

Cookie

Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用 于控制 Cookie 有效期、安全性、使用范围的可选属性组成。不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。

Cookie的特性:

  • Cookie一旦创建成功,名称就无法修改

  • Cookie是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的,这也是由Cookie的隐私安全性决定的,这样就能够阻止非法获取其他网站的Cookie

  • 每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb

  • 有安全问题,如果Cookie被拦截了,那就可获得session的所有信息,即使加密也于事无补,无需知道cookie的意义,只要转发cookie就能达到目的

  • Cookie在请求一个新的页面的时候都会被发送过去

Cookie 在身份认证中的作用

客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动 将 Cookie 保存在浏览器中。

随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给 服务器,服务器即可验明客户端的身份。

 

Cookie 不具有安全性

由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全 性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。

注意:千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。

Session

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了session是一种特殊的cookie。cookie是保存在客户端的,而session是保存在服务端。

为什么要用session 由于cookie 是存在用户端,而且它本身存储的尺寸大小也有限,最关键是用户可以是可见的,并可以随意的修改,很不安全。那如何又要安全,又可以方便的全局读取信息呢?于是,这个时候,一种新的存储会话机制:session 诞生了

session原理 当客户端第一次请求服务器的时候,服务器生成一份session保存在服务端,将该数据(session)的id以cookie的形式传递给客户端;以后的每次请求,浏览器都会自动的携带cookie来访问服务器(session数据id)。

图示:

session我觉得可以简单理解为一个表,根据cookie传来的值查询表中的内容

session 标准工作流程

我在 node.js 中详细演示了一遍 session 的使用,具体看了另一篇博客:https://blog.csdn.net/hangao233/article/details/123089029

LocalStorage

LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了。

LocalStorage的优点:

  • 在大小方面,LocalStorage的大小一般为5MB,可以储存更多的信息

  • LocalStorage是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在

  • 仅储存在本地,不像Cookie那样每次HTTP请求都会被携带

LocalStorage的缺点:

  • 存在浏览器兼容问题,IE8以下版本的浏览器不支持

  • 如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage

  • LocalStorage受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问

LocalStorage的常用API:

复制代码
// 保存数据到 localStorage
localStorage.setItem('key', 'value');

// 从 localStorage 获取数据
let data = localStorage.getItem('key');

// 从 localStorage 删除保存的数据
localStorage.removeItem('key');

// 从 localStorage 删除所有保存的数据
localStorage.clear();

// 获取某个索引的Key
localStorage.key(index)
复制代码

LocalStorage的使用场景:

  • 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的LocalStorage中,当需要换肤的时候,直接操作LocalStorage即可

  • 在网站中的用户浏览信息也会存储在LocalStorage中,还有网站的一些不常变动的个人信息等也可以存储在本地的LocalStorage中

SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。

SessionStorage与LocalStorage对比:

  • SessionStorage和LocalStorage都在本地进行数据存储

  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享

  • LocalStorage和SessionStorage都不能被爬虫爬取

SessionStorage的常用API:

复制代码
// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');

// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');

// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');

// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();

// 获取某个索引的Key
sessionStorage.key(index)
复制代码

SessionStorage的使用场景

由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了。

具体案例:

localStorage

复制代码
<!DOCTYPE html>
<html lang="en">
<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="removeAllData()">清空所有数据</button>
</body>
<script type="text/javascript">
    function saveData(){
        localStorage.setItem('msg1', 'panyue,hello');
        //对象转换为字符串
        localStorage.setItem('msg2',JSON.stringify({
            name: 'panyue',
            age: '21',
            school: 'wust(WuHan)'
        }))
    }

    function readData(){
        console.log(localStorage.getItem('msg1'))
        console.log(JSON.parse(localStorage.getItem('msg2')));
        console.log(localStorage.getItem('text')); //读不出来就是null Json.parse(null)仍然是null注意
    }

    function deleteData(){
        localStorage.removeItem('msg2');
    }

    function removeAllData(){
       localStorage.clear();
    }
</script>
</html>
复制代码

sessionStorage

复制代码
<!DOCTYPE html>
<html lang="en">
<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="removeAllData()">清空所有数据</button>
</body>
<script type="text/javascript">
    function saveData(){
        sessionStorage.setItem('msg1', 'panyue,hello');
        //对象转换为字符串
        sessionStorage.setItem('msg2',JSON.stringify({
            name: 'panyue',
            age: '21',
            school: 'wust(WuHan)'
        }))
    }

    function readData(){
        console.log(sessionStorage.getItem('msg1'))
        console.log(JSON.parse(sessionStorage.getItem('msg2')));
        console.log(sessionStorage.getItem('text')); //读不出来就是null Json.parse(null)仍然是null注意
    }

    function deleteData(){
        sessionStorage.removeItem('msg2');
    }

    function removeAllData(){
        sessionStorage.clear();
    }
</script>
</html>
复制代码

TodoList本地存储案例

复制代码
data() {
    return {
      todos: JSON.parse(localStorage.getItem("todos")) || [],
    };
  },

// 监视todos
 watch: {
    // 浅监视,只监视了数组的变化,没有监视数组内部的变化
    // todos(value) {
    //   localStorage.setItem("todos", JSON.stringify(value));
    // },
    // 深度监视
    todos: {
      deep: true,
      handler(value) {
        localStorage.setItem("todos", JSON.stringify(value));
      },
    },
  },
 
复制代码

 

2.4 组件自定义事件

组件自定义事件是一种组件间通信的方式,适用于:子组件 ===> 父组件

使用场景

A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。

绑定自定义事件:

第一种方式,在父组件中:<Demo @atguigu="test"/><Demo v-on:atguigu="test"/>

具体代码

App.vue

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

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

    export default {
        name:'App',
        components:{Student},
        data() {
            return {
                msg:'你好啊!',
                studentName:''
            }
        },
        methods: {
            getStudentName(name,...params){
                console.log('App收到了学生名:',name,params)
                this.studentName = name
            }
        }
    }
</script>

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

Student.vue

复制代码
<template>
    <div class="student">
        <button @click="sendStudentlName">把学生名给App</button>
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'张三',
            }
        },
        methods: {
            sendStudentlName(){
                //触发Student组件实例身上的atguigu事件
                this.$emit('atguigu',this.name,666,888,900)
            }
        },
    }
</script>

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

第二种方式,在父组件中:

使用 this.$refs.xxx.$on() 这样写起来更灵活,比如可以加定时器啥的。

具体代码

App.vue

复制代码
<template>
    <div class="app">
        <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
        <Student ref="student"/>
    </div>
</template>

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

    export default {
        name:'App',
        components:{Student},
        data() {
            return {
                studentName:''
            }
        },
        methods: {
            getStudentName(name,...params){
                console.log('App收到了学生名:',name,params)
                this.studentName = name
            },
        },
        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>
复制代码

Student.vue

复制代码
<template>
    <div class="student">
        <button @click="sendStudentlName">把学生名给App</button>
    </div>
</template>

<script>
    export default {
        name:'Student',
        data() {
            return {
                name:'张三',
            }
        },
        methods: {
            sendStudentlName(){
                //触发Student组件实例身上的atguigu事件
                this.$emit('atguigu',this.name,666,888,900)
            }
        },
    }
</script>

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

若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。

触发自定义事件:this.$emit('atguigu',数据)

使用 this.$emit() 就可以子组件向父组件传数据

 

解绑自定义事件this.$off('atguigu')

代码

this.$off('atguigu') //解绑一个自定义事件
// this.$off(['atguigu','demo']) //解绑多个自定义事件
// this.$off() //解绑所有的自定义事件

组件上也可以绑定原生DOM事件,需要使用native修饰符。

代码

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

注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中要么用箭头函数,否则this指向会出问题!

代码示例

App.vue

复制代码
<template>
  <div class="app">
    <h1>{{ msg }},学生姓名是:{{ studentName }}</h1>
    <!--通过绑定一个自定义事件实现了子给父传递数据(自定义事件绑在子组件上) 第一种写法使用@或v-on-->
    <!--once代表改事件只执行一次-->
    <!--      <Student @personalEvent="getStudentName" @demo="demo"/>-->
    <!--第二种写法使用ref绑定事件--->
    <Student ref="student" @click.native="show" />
    <!--通过父组件给子组件传递函数类型的props实现了子给父传递数据-->
    <School :getSchoolName="getSchoolName" />
  </div>
</template>

<script>
import Student from "@/components/Student";
import School from "@/components/School";
export default {
  name: "App",
  components: {
    School,
    Student,
  },
  data() {
    return {
      msg: "helloこんにちは",
      studentName: "",
    };
  },
  methods: {
    getSchoolName(name) {
      console.log(`app收到了学校名,${name}`);
    },
    getStudentName(name, ...params) {
      console.log("自定义");
      console.log(`app收到了学生名, ${name}`);
      this.studentName = name;
      console.log(`剩余参数,${params}`);
    },
    demo() {
      console.log("demo事件被触发了");
    },
    show() {
      console.log(`123`);
    },
  },
  mounted() {
    //可以通过ref拿到组件实例对象
    // setTimeout(() => {
    //   this.$refs.student.$on('personalEvent', this.getStudentName); //当App组件一挂载完毕经过三秒我就在Student组件上绑定事件
    //   //this.$refs.student.$once('personalEvent', this.getStudentName); //注意此时事件只执行一次就不执行了(once),一次性
    // },3000)
    //注意这里回调要写成剪头函数,不然this会丢失,这个this就是(vc)app,而不是(vc)student
    this.$refs.student.$on("personalEvent", (name) => {
      console.log(this);
      console.log(name);
      this.studentName = name;
    });
  },
};
</script>

<style>
/*
   全局的样式是不需要加scoped
   全局共享
   */
.app {
  background: gray;
  padding: 5px;
}
</style>
复制代码

School.vue

复制代码
<template>
  <div class="demo">
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址: {{ address }}</h2>
    <button @click="sendSchoolName">把学校名传递给app</button>
  </div>
</template>

<script>
export default {
  name: "School",
  data() {
    console.log(this);
    return {
      name: "wust university",
      address: "武汉科技大学",
    };
  },
  methods: {
    sendSchoolName() {
      this.getSchoolName(this.name);
    },
  },
  props: ["getSchoolName"],
};
</script>

<style scoped>
/*scoped代表局部的*/
.demo {
  background: skyblue;
  padding: 5px;
}
</style>
复制代码

Student.vue

复制代码
<template>
  <div class="student">
    <h2>姓名:{{ name }}</h2>
    <h2>性别: {{ sex }}</h2>
    <h2>当前求和为:{{ number }}</h2>
    <button @click="add">点我加一</button>
    <button @click="sendStudentName">把学生名传递给app</button>
    <button @click="unbind">解绑自定义(personalEvent)事件</button>
    <button @click="death">销毁当前student组件的实例对象</button>
  </div>
</template>

<script>
export default {
  name: "Student",
  data() {
    console.log(this);
    return {
      name: "张三",
      sex: "",
      number: 0,
    };
  },
  methods: {
    add() {
      console.log(`add回调被调用了`);
      this.number++;
    },
    sendStudentName() {
      //emit触发绑定在指定vc上的自定义事件 vc实例对象可以使用该方法
      //后面多余参数演示es6的参数收集
      this.$emit("personalEvent", this.name, 666, 777, 888);
      // this.$emit('demo'); //同时触发两个事件
      // this.$emit('click'); 如果在组件身上使用原生事件不加native修饰符则会让vue认为你这是自定义事件
    },
    unbind() {
      //解绑事件
      this.$off("personalEvent"); //这种写法只能解绑一种自定义事件
      //this.$off([ 'personalEvent', 'demo' ]);// 解绑多个事件,参数为包含多个事件名的数组
      // this.$off(); //比较暴力,有几个自定义事件就全给你解绑了
    },
    death() {
      this.$destroy(); //销毁当前组件实例,销毁后所有该实例的自定义事件都不奏效了
    },
  },
};
</script>

<style scoped>
.student {
  background: orange;
  padding: 5px;
  margin-bottom: 10px;
}
</style>
复制代码

TodoList自定义版本

App.vue

<MyHeader @addTodo="addTodo" />
<MyFooter
   :todos="todos"
   @checkAllTodo="checkAllTodo"
   @clearAllDoneTodo="clearAllDoneTodo"
/>

Student.vue

// 组件自定义事件,把数据传给父组件
      this.$emit("addTodo", todoObj);

2.5 全局事件总线

  1. 一种组件间通信的方式,适用于任意组件间通信

  2. 安装全局事件总线:

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

  3.使用事件总线:

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

methods(){
  demo(data){......}
}
......
mounted() {
  this.$bus.$on('xxxx',this.demo)
}

           2.提供数据:this.$bus.$emit('xxxx',数据)

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

示例代码(学生组件将数据传给学校组件)

main.js

复制代码
//引入Vue
import Vue from "vue";
//引入App
import App from './App';

//关闭Vue的生产提示
Vue.config.productionTip = false;

// Vue.prototype.x = { a: 1, b: 2 }

// 创建一个vc(第一种方法)
// const Demo = Vue.extend({});
// const d = new Demo()    // 此时这个d就是组件实例对象==》vc
// Vue.prototype.x = d;

new Vue({
    el: '#app',
    render: h => h(App),
    // 第二种方法
    beforeCreate() {
        //此时这个this就是vm,只不过这个时候还并没有去解析模版
        Vue.prototype.$bus = this; // 安装全局事件总线
    }
});
复制代码

School.vue

复制代码
<template>
  <div class="demo">
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址: {{ address }}</h2>
  </div>
</template>

<script>
export default {
  name: "School",
  data() {
    return {
      name: "wust university",
      address: "武汉科技大学",
    };
  },
  mounted() {
    // console.log("School组件", this);
    this.$bus.$on("hello", (data) => {
      console.log("我是School组件,收到数据", data);
    });
  },
  // 实例销毁前,解绑hello事件(自定义组件中为解绑,因为组件销毁了,实例也会销毁,事件绑定也会解绑)
  beforeDestroy() {
    this.$bus.$off("hello");
  },
};
</script>

<style scoped>
/*scoped代表局部的*/
.demo {
  background: skyblue;
  padding: 5px;
}
</style>
复制代码

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", 666);
    },
  },
};
</script>

<style scoped>
.student {
  background: orange;
  padding: 5px;
  margin-bottom: 10px;
}
</style>
复制代码

TodoList事件总线版本

Item.vue

复制代码
  methods: {
    // 勾选or取消勾选
    handleCheck(id) {
      // 通过App组件将对应的todo对象的done值去反
      this.$bus.$emit("checkTodo", id);
    },
    // 删除
    handleDelete(id) {
      if (confirm("确定删除吗?")) {
        this.$bus.$emit("deleteTodo", id);
      }
    },
  },
复制代码

App.vue

mounted() {
    this.$bus.$on("checkTodo", this.checkTodo);
    this.$bus.$on("deleteTodo", this.deleteTodo);
  },
  beforeDestroy() {
    this.$bus.$off("checkTodo");
    this.$bus.$off("deleteTodo");
  },

2.6 消息订阅与发布

  1. 一种组件间通信的方式,适用于任意组件间通信

  2. 使用步骤:

    1. 安装pubsub:npm i pubsub-js

    2. 引入: import pubsub from 'pubsub-js'

    3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。

      methods:{
        demo(data){......}
      }
      ......
      mounted() {
        this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
      }
       
    4. 提供数据:pubsub.publish('xxx',数据)

    5. 最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)取消订阅。

示例代码

订阅消息

School.vue

复制代码
<template>
  <div class="demo">
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址: {{ address }}</h2>
  </div>
</template>

<script>
import pubsub from "pubsub-js";
export default {
  name: "School",
  data() {
    return {
      name: "wust university",
      address: "武汉科技大学",
    };
  },
  mounted() {
    // console.log("School组件", this);
    // this.$bus.$on("hello", (data) => {
    //   console.log("我是School组件,收到数据", data);
    // });
    this.pubId = pubsub.subscribe("hello", function (msgName, data) {
      console.log(this); // undefined,跟自定义事件中的this一样,需要写成箭头函数,this是vc
      console.log("有人发布了hello消息,hello消息的回调执行了", msgName, data);
    });
  },
  // 实例销毁前,解绑hello事件(自定义组件中为解绑,因为组件销毁了,实例也会销毁,事件绑定也会解绑)
  beforeDestroy() {
    // this.$bus.$off("hello");
    // 取消订阅
    pubsub.unsubscribe(this.pubId);
  },
};
</script>

<style scoped>
/*scoped代表局部的*/
.demo {
  background: skyblue;
  padding: 5px;
}
</style>
复制代码

发布消息

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", 666);
      pubsub.publish("hello", 666);
    },
  },
};
</script>

<style scoped>
.student {
  background: orange;
  padding: 5px;
  margin-bottom: 10px;
}
</style>
复制代码

2.7 nextTick

  1. 语法:this.$nextTick(回调函数)

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

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

具体案例

复制代码
    // 编辑
    handleEdit(todo) {
      // todo是否有isEdit属性
      if (Object.prototype.hasOwnProperty.call(todo, "isEdit")) {
        todo.isEdit = true;
      } else {
        // 给todo添加响应式的属性
        this.$set(todo, "isEdit", true);
      }
      // 更新完数据后,新的dom获取焦点
      this.$nextTick(function () {
        this.$refs.inputTitle.focus();
      });
    },
复制代码

TodoList订阅与发布及nextTick版本

完整版

main.js

复制代码
//引入Vue
import Vue from "vue";
//引入App
import App from './App';

//关闭Vue的生产提示
Vue.config.productionTip = false;


new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
});
复制代码

App.vue

复制代码
<template>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <MyHeader @addTodo="addTodo" />
        <List :todos="todos" />
        <MyFooter
          :todos="todos"
          @checkAllTodo="checkAllTodo"
          @clearAllDoneTodo="clearAllDoneTodo"
        />
      </div>
    </div>
  </div>
</template>

<script>
import pubsub from "pubsub-js";
import MyHeader from "@/components/MyHeader.vue";
import List from "@/components/List.vue";
import MyFooter from "@/components/MyFooter.vue";
import PubSub from "pubsub-js";

export default {
  name: "App",
  components: {
    MyHeader,
    List,
    MyFooter,
  },
  data() {
    return {
      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) => {
        return todo.id !== id;
      });
    },
    // 全选or取消全选
    checkAllTodo(done) {
      this.todos.forEach((todo) => (todo.done = done));
    },
    // 清除所有已经完成的todozz
    clearAllDoneTodo() {
      this.todos = this.todos.filter((todo) => {
        return !todo.done;
      });
    },
  },
  // 监视todos
  watch: {
    // 浅监视,只监视了数组的变化,没有监视数组内部的变化
    // todos(value) {
    //   localStorage.setItem("todos", JSON.stringify(value));
    // },
    // 深度监视
    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.$bus.$on("deleteTodo", this.deleteTodo);
    this.pubId = pubsub.subscribe("deleteTodo", this.deleteTodo);
  },
  beforeDestroy() {
    this.$bus.$off("checkTodo");
    this.$bus.$off("updateTodo", this.updateTodo);
    // this.$bus.$off("deleteTodo");
    pubsub.unsubscribe(this.pubId);
  },
};
</script>

<style lang="css">
/*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-eidt {
  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>
复制代码

Item.vue

复制代码
<template>
  <li>
    <label>
      <!--      这里勾选和取消勾选可以使用change和click作为事件处理-->
      <input
        type="checkbox"
        :checked="todo.done"
        @change="handleCheck(todo.id)"
      />
      <!--v-model数据的双向绑定,checkbox使用v-model来双向绑定其是否被勾选,也可以实现效果但不推荐(因为其实修改了props中的数据)-->
      <!--这里修改了从List修改过来的props,这里的不允许改是浅层次,就是如果props是一个对象则这个修改这个对象的某一个属性vue是放行的-->
      <!-- <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-eidt"
      @click="handleEdit(todo)"
    >
      编辑
    </button>
  </li>
</template>

<script>
import pubsub from "pubsub-js";
export default {
  name: "Item",
  methods: {
    // 勾选or取消勾选
    handleCheck(id) {
      // 通过App组件将对应的todo对象的done值去反
      this.$bus.$emit("checkTodo", id);
    },
    // 删除
    handleDelete(id) {
      if (confirm("确定删除吗?")) {
        // this.$bus.$emit("deleteTodo", id);
        pubsub.publish("deleteTodo", id);
      }
    },
    // 编辑
    handleEdit(todo) {
      // todo是否有isEdit属性
      if (Object.prototype.hasOwnProperty.call(todo, "isEdit")) {
        todo.isEdit = true;
      } else {
        // 给todo添加响应式的属性
        this.$set(todo, "isEdit", true);
      }
      // 更新完数据后,新的dom获取焦点
      this.$nextTick(function () {
        this.$refs.inputTitle.focus();
      });
    },
    // 失去焦点回调(真正执行修改逻辑)
    handleBlur(todo, e) {
      // 已经有了isEdit属性,不需再用添加响应式属性的方法了
      todo.isEdit = false;
      if (!e.target.value.trim()) return alert("输入不能为空");
      this.$bus.$emit("updateTodo", todo.id, e.target.value);
    },
  },
  // 声明接受todo对象
  props: ["todo"],
};
</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: #ddd;
}

li:hover button {
  display: block;
}
</style>
复制代码

List.vue

复制代码
<template>
  <ul class="todo-main">
    <Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" />
  </ul>
</template>

<script>
import Item from "./Item";

export default {
  name: "List",
  components: {
    Item,
  },

  props: ["todos"], //在vc身上,直接用
};
</script>

<style scoped>
/*main*/
.todo-main {
  margin-left: 0;
  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>
复制代码

MyFooter.vue

复制代码
<template>
  <!--隐式类型转换-->
  <div class="todo-footer">
    <label>
      <!--这里也可用v-model来替代,此时不需要计算属性了-->
      <!-- <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() {
      // const x = this.todos.reduce((pre, current) => {
      //   console.log("@", pre, current);
      //   return pre + (current.done ? 1 : 0);
      // }, 0);
      // return x;
      // 简写
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
    },
    isAll: {
      get() {
        return this.doneTotal === this.total && this.total > 0;
      },
      set(value) {
        this.$emit("checkAllTodo", value);
      },
    },
  },
  methods: {
    //   checkAll(e) {
    //     this.checkAllTodo(e.target.checked);
    //   },
    clearAll() {
      this.$emit("clearAllDoneTodo");
    },
  },
};
</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>
复制代码

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: "",
    };
  },
  methods: {
    add() {
      // 校验数据
      if (!this.title.trim()) return alert("输入不能为空");
      //将用户的输入包装成一个todo对象
      const todoObj = { id: nanoid(), title: this.title, done: false };
      // 组件自定义事件,把数据传给父组件
      this.$emit("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>
复制代码
  1. 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。

  2. 图示: 在这里插入图片描述

  1. 写法:

    1. 准备好样式:

      • 元素进入的样式:

        1. v-enter:进入的起点

        2. v-enter-active:进入过程中

        3. v-enter-to:进入的终点

      • 元素离开的样式:

        1. v-leave:离开的起点

        2. v-leave-active:离开过程中

        3. v-leave-to:离开的终点

    2. 使用<transition>包裹要过渡的元素,并配置name属性:

      <transition name="hello">
      <h1 v-show="isShow">你好啊!</h1>
      </transition>
    3. 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key值。

具体案例(单个元素过渡)

复制代码
<template>
    <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition 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;
    }

    .v-enter-active{
        animation: move 0.5s linear;
    }

    .v-leave-active{
        animation: move 0.5s linear reverse;
    }

    @keyframes move {
        from{
            transform: translateX(-100%);
        }
        to{
            transform: translateX(0px);
        }
    }
</style>
复制代码

name 的作用可以让让不同的元素有不同的动画效果

复制代码
<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: move 0.5s linear;
    }

    .hello-leave-active{
        animation: move 0.5s linear reverse;
    }

    @keyframes move {
        from{
            transform: translateX(-100%);
        }
        to{
            transform: translateX(0px);
        }
    }
</style>
复制代码

具体案例(多个元素过渡)

复制代码
<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>
复制代码

使用第三库的具体案例(随便看看,这个不重要)

库的名称:Animate.css

安装:npm i animate.css

引入:import 'animate.css'

复制代码
<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <!--transition 只能包裹一个元素而transition-group可以包含多个元素-->
    <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">Shanghai</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>
复制代码

TodoList动画版本

复制代码
<transition name="todo" appear>
    <li>
      <label>
        <!--      这里勾选和取消勾选可以使用change和click作为事件处理-->
        <input
          type="checkbox"
          :checked="todo.done"
          @change="handleCheck(todo.id)"
        />
        <!--v-model数据的双向绑定,checkbox使用v-model来双向绑定其是否被勾选,也可以实现效果但不推荐(因为其实修改了props中的数据)-->
        <!--这里修改了从List修改过来的props,这里的不允许改是浅层次,就是如果props是一个对象则这个修改这个对象的某一个属性vue是放行的-->
        <!-- <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-eidt"
        @click="handleEdit(todo)"
      >
        编辑
      </button>
    </li>
  </transition>


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

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

@keyframes atguigu {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0px);
  }
}
复制代码

2.9 vue脚手架配置代理

解决跨域问题的方法

方法1:后端的cors方法,后端加字头;

方法2:jsonp,用的很少;

方法3:代理服务器。

代理服务器的方法,一种方法是nigx(反向代理),另一种方法是vue-cli生成代理服务器。

可以用来解决跨域的问题

方法一

在vue.config.js中添加如下配置:

// 开启代理服务器
devServer:{
  proxy:"http://localhost:5000"
}

说明:

  1. 优点:配置简单,请求资源时直接发给前端(8080)即可。

  2. 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。

  3. 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

方法二

编写vue.config.js配置具体代理规则:

复制代码
module.exports = {
    devServer: {
      proxy: {
      '/api1': {// 匹配所有以 '/api1'开头的请求路径
        target: 'http://localhost:5000',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api1': ''}//代理服务器将请求地址转给真实服务器时会将 /api1 去掉
      },
      '/api2': {// 匹配所有以 '/api2'开头的请求路径
        target: 'http://localhost:5001',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api2': ''}
      }
    }
  }
}
/*
   changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
   changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
   changeOrigin默认值为true
*/
复制代码

说明:

  1. 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。

  2. 缺点:配置略微繁琐,请求资源时必须加前缀。

案例

复制代码
<template>
  <div>
    <button @click="getStudents">获取学生信息</button>
    <button @click="getCars">获取汽车信息</button>
  </div>
</template>

<script>
import axios from 'axios';
export default {
  name: "App",
  methods:{
     getStudents(){
       axios.get('/api/students') //其实这里就是请求http://localhost:8080/students只不过把请求转移给了端口5001
       .then(res => console.log(res.data))
       .catch(e => console.log(`msg: ${e.message}, stack: ${e.stack}`));
    },
    getCars(){
      axios.get('/demo/cars') //同理
          .then(res => console.log(res.data))
          .catch(e => console.log(`msg: ${e.message}, stack: ${e.stack}`));
    }
  }
}
</script>
复制代码

2.10 github搜索案例(vue-resource版本)

App.vue

复制代码
<template>
    <div class="container">
      <Search/>
      <List/>
    </div>
</template>

<script>
import Search from "@/components/Search";
import List from "@/components/List";
export default {
  name: "App",
  components:{
    Search,
    List
  }
}
</script>
<style lang="css">
</style>
复制代码

List.vue

复制代码
<template>
  <div class="row">
    <!--展示用户列表-->
    <div v-show="info.users.length" class="card" v-for="user in info.users" :key="user.login">
      <a :href="user.html_url" target="_blank">
        <img :src="user.avatar_url" style='width: 100px'/>
      </a>
      <p class="card-text">{{ user.login }}</p>
    </div>
    <!---欢迎词-->
    <h1 v-show="info.isFirst">Welcome!</h1>
    <!--加载中--->
    <h1 v-show="info.isLoading">Loading...</h1>
    <!---错误信息-->
    <h1 v-show="info.errMsg">Something has been wrong, errorMessage: {{ info.errMsg }}</h1>
  </div>
</template>

<script>
export default {
  name: "List",
  data(){
    return {
      info : {
        isFirst: true, //是否为第一次使用
        users:[],
        isLoading: false, //是否在加载中
        errMsg: '',
      }
    }
  },
  mounted() {
    this.$bus.$on('updateListData', (dataObj) => {
      // console.log(`我是list,接到了数据data:`, users);
      // this.isFirst = isFirst;
      // this.isLoading = isLoading;
      // this.errMsg = errMsg;
      // this.users = users;
      // 两个对象进行比较,有的保留
      this.info = { ...this.info, ...dataObj };
    });
  }

}
</script>

<style scoped>
.album {
  min-height: 50rem; /* Can be removed; just added for demo purposes */
  padding-top: 3rem;
  padding-bottom: 3rem;
  background-color: #f7f7f7;
}

.card {
  float: left;
  width: 33.333%;
  padding: .75rem;
  margin-bottom: 2rem;
  border: 1px solid #efefef;
  text-align: center;
}

.card > img {
  margin-bottom: .75rem;
  border-radius: 100px;
}

.card-text {
  font-size: 85%;
}
</style>
复制代码

Search.vue

 

复制代码
<template>
  <section class="jumbotron">
    <h3 class="jumbotron-heading">Search Github Users</h3>
    <div>
      <input type="text" placeholder="enter the name you search" v-model="keyword"/>&nbsp;
      <button @click="searchUsers">Search</button>
    </div>
  </section>
</template>

<script>
export default {
  name: "Search",
  data() {
    return {
      keyword: '',
    }
  },
  methods:{
    //使用全局事件总线在组件间传递数据
    searchUsers(){
      this.$bus.$emit('updateListData', {
        isFirst: false,
        isLoading: true,
        errMsg: '',
        users: []
      })
      // get请求携带数据
      this.$http.get(`https://api.github.com/search/users?q=${this.keyword}`)
      .then(res => {
        console.log(res.data.items);
        this.$bus.$emit("updateListData", {
          isLoading: false,
          errMsg: '',
          users: res.data.items
        });
      })
      .catch(e => {
        console.log(`请求失败:${e.message}`)
        this.$bus.$emit("updateListData", {
          isLoading: false,
          errMsg: e.message,
          users: []
        });
      });
    }
  }
}
</script>

<style scoped>

</style>
复制代码

2.11 slot插槽

  1. 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件

  2. 分类:默认插槽、具名插槽、作用域插槽

  3. 使用方式:

默认插槽:

复制代码
父组件中:
        <Category>
           <div>html结构1</div>
        </Category>
子组件中:
        <template>
            <div>
               <!-- 定义插槽 -->
               <slot>插槽默认内容...</slot>
            </div>
        </template>
复制代码

具名插槽:

复制代码
父组件中:
        <Category>
            <template slot="center">
              <div>html结构1</div>
            </template>

            <template v-slot:footer>
               <div>html结构2</div>
            </template>
        </Category>
子组件中:
        <template>
            <div>
               <!-- 定义插槽 -->
               <slot name="center">插槽默认内容...</slot>
               <slot name="footer">插槽默认内容...</slot>
            </div>
        </template>
复制代码

作用域插槽:

  1. 理解:数据在组件的自身(子组件),但根据数据生成的结构需要组件的使用者(父组件)来决定。(games数据在Category(子)组件中,但使用数据所遍历出来的结构由App(父)组件决定)

  2. 具体编码:

复制代码
父组件中:
        <Category>
            <template scope="scopeData">
                <!-- 生成的是ul列表 -->
                <ul>
                    <li v-for="g in scopeData.games" :key="g">{{g}}</li>
                </ul>
            </template>
        </Category>

        <Category>
            <template slot-scope="scopeData">
                <!-- 生成的是h4标题 -->
                <h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
            </template>
        </Category>
子组件中:
        <template>
            <div>
            <!-- 通过数据绑定就可以把子组件的数据传到父组件 -->
                <slot :games="games"></slot>
            </div>
        </template>
        
        <script>
            export default {
                name:'Category',
                props:['title'],
                //数据在子组件自身
                data() {
                    return {
                        games:['红色警戒','穿越火线','劲舞团','超级玛丽']
                    }
                },
            }
        </script>
复制代码

 

posted @   coder雪山  阅读(1330)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示