《web前端全栈成长计划》Vue第四章学习笔记(上)

举报
张辉 发表于 2020/09/10 16:48:09 2020/09/10
【摘要】 web前端全站成长计划第三阶段vue课程第四章的学习笔记(上)

本文摘自 论坛 https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=71831&authorid=70062&page=1 笔者对vue学习的笔记。由于vue笔者也是刚刚接触,基本上都是以观看视频和具体动手实践的方式进行。为防止迷路,特收集整理成本博文。

文中的图片、代码很多来自于教程本身,但都来自于本人实践后截图和拷贝的结果,特此说明。


Vue 第四章 Vuex

66 vuex:counter应用的vuex实战1

66.1 安装vuex

image.png

66.2 实战

创建vuex的核心管理对象模块vuex

编写store.js ,main.js及App.vue

store.js

/* vuex的核心管理对象模块 */
import Vue from 'vue'
import Vuex from 'vuex'

// 声明使用
Vue.use(Vuex)


// 状态
const state = {
  count: 0  // 从App的data移过来,初始化状态
}
// 包含多个更新state函数的对象
const mutations = {
  // 本质上只有+1和-1两个操作
  // 增加的mutation
  INCREMENT(state){
    state.count ++
  },
  // 减少的mutation
  DECREMENT(state){
    state.count --
  }
}
// 包含多个对应事件回调函数的对象
const actions = {
  // 一个函数就是一个action
  // 增加
  increment({commit}){
    // 提交一个mutation commit最终导致一个mutation调用
    commit('INCREMENT')
  },

  decrement({commit}){
    commit('DECREMENT')
  },

  // 带条件的action
  incrementIfOdd({commit,state}){
    // 提交一个mutation commit最终导致一个mutation调用
    if(state.count %2 === 1){
      commit('INCREMENT')
    }
  },
  // 异步action
  incrementAsync({commit}){
    // 在action中可以直接执行异步代码
    setTimeout(() => {
      commit('INCREMENT')
    },1000)
  }
}
// 包含多个getter计算属性函数的对象
const getters = {
  evenOrOdd (state) {
    return state.count % 2 === 0 ? '偶数': '奇数'
  }
}

// 以下都是固定的
export default new Vuex.Store({
  state, // 状态
  mutations, // 包含多个更新state函数的对象
  actions, // 包含多个对应事件回调函数的对象
  getters // 包含多个getter计算属性函数的对象
})

main.js. 引入store对象

/*
入口JS
 */
import Vue from 'vue'
import App from './App.vue'
import store from './store'

// 创建vm
/* eslint-disable no-new */
new Vue({
  el: '#app',
  components: {App}, // 映射组件标签
  template: '<App/>', // 指定需要渲染到页面的模板
  store  // 会产生 $store对象,有state,getter属性。
})

App.vue

<template>
  <div>
    click {{ $store.state.count }} times, count {{ evenOrOdd }}     <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementIfOdd">increment if odd</button>
    <button @click="incrementAsync">increment async</button>
  </div>
</template>

<script>

  export default {
    // data () {
    //   return {
    //   }
    // },
    computed: {
      evenOrOdd () {
        // return this.count % 2 === 0 ? '偶数': '奇数'
        return this.$store.getters.evenOrOdd
        // 不需要调用,只需要读取属性值,即可自动调用getter
        // this代表组件对象
      }
    },
    methods: {
      increment () {
        // 需通知vuex去增加
        this.$store.dispatch('increment')
        // 触发store中对应的action调用。
      },
      decrement () {
        this.$store.dispatch('decrement')
      },
      incrementIfOdd () {
         this.$store.dispatch('incrementIfOdd')
      },
      incrementAsync () {
        this.$store.dispatch('incrementAsync')
      }
    }
  }
</script>

<style>

</style>

从上面可以看出,App.cue变得异常简单。很多逻辑都迁移到了store.js

今后,如果store.js比较多,可能也会将

state, // 状态
  mutations, // 包含多个更新state函数的对象
  actions, // 包含多个对应事件回调函数的对象
  getters

这四个定义放到多个文件中。分别去定义。

66.3 演示效果:

image.png

67 vuex:counter应用的vuex实战2

使用mapState, mapGetters, mapActions简化映射:

App.vue:

<template>
  <div>
    click {{ $store.state.count }} times, count {{ evenOrOdd }}     <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementIfOdd">increment if odd</button>
    <button @click="incrementAsync">increment async</button>
  </div>
</template>

<script>
  import { mapState, mapGetters, mapActions} from 'vuex'

  export default {
     computed: {
      ...mapState(['count']), //相当于上面的内容  mapState返回值也是对象

      ...mapGetters(['evenOrOdd']), // mapGetters 返回值是对象
    },
    methods: {
       // 回调函数名要跟action一致
      ...mapActions(['increment','decrement','incrementIfOdd','incrementAsync'])
    }
  }
</script>

<style>

</style>

当回调函数跟action的函数名不一致时,可以用以下方法做对照:

比如:

store.js改为

const getters = {
  evenOrOdd2 (state) {
    return state.count % 2 === 0 ? '偶数': '奇数'
  }
}

App.vue的mapGetters改为:

...mapGetters({evenOrOdd: 'evenOrOdd2'}) //后面是getters的名字,前面是app中回调函数的名字

执行效果如下:

image.png

跟前面的一致。

68 vuex结构图

image.png

backend api:后台api

在action可以发送后台请求,跟后台交互

devtools: chrome开发工具

监视者mutation的调用。

我们将具体的vuex结构图完善一下:

image.png


69 vuex版本的todolist——创建结构

70 vuex版本的todolist——header组件

71 vuex版本的todolist——list和item组件

72 vuex版本的todolist——footer组件

实战配置:

vuex用来管理多组件共享的状态。

分析:

todos状态数据应该放入到vuex的store中。

真实项目:拆分成多个模块

App.vue

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <!-- 以前传函数,现在不要传了,都从store读就可以了-->
<!--      <TodoHeader :addTodo="addTodo"/>-->
      <TodoHeader />
<!--      <TodoList :todos="todos" :deleteTodo="deleteTodo"/>-->
      <TodoList />
<!--      <TodoFooter :todos="todos" :deleteCompleteTodos="deleteCompleteTodos" :selectAllTodos="selectAllTodos"/>-->
      <TodoFooter />
      <!-- 另一种写法: todo-footer -->
    </div>
  </div>
</template>

<script>
  // 引入
  import TodoHeader from './components/TodoHeader'
  import TodoList from './components/TodoList'
  import TodoFooter from './components/TodoFooter'

  export default {

    data() {
      return {
        // 换成从localStorage读取到todos

        // todos: [ // 交给list显示
        //   {title: '前端开发工作', complete: false},
        //   {title: '后端开发工作', complete: true},
        //   {title: '锻炼身体', complete: false}
        // ]
        // 存的是文本,字符串转为数组
        // 如果没有数据,则存储 []
        todos: JSON.parse(window.localStorage.getItem('todos_key') || '[]')
        // 深度监视
        // 一般的监视不能监视属性。
      }
    },

    // methods: {
    //   addTodo(todo) {
    //     this.todos.unshift(todo)
    //   },
    //   deleteTodo(index) {
    //     this.todos.splice(index,1)
    //   },
    //
    //   //全选或者全不选,取自于左下角的check
    //   selectAllTodos(check) {
    //     this.todos.forEach(todo => todo.complete = check)
    //   },
    //   // 找到所有的成功的,删掉
    //   deleteCompleteTodos() {
    //     this.todos = this.todos.filter(todo => !todo.complete)
    //   }
    //
    // },

    // watch: {
    //   todos: {
    //     deep: true, // 深度监视
    //     handler: function( value){
    //       //将todos最新值的JSON数据,保存到localStorage
    //       window.localStorage.setItem('todos_key',JSON.stringify(value))
    //     }
    //   }
    // },
    // 加入标签
    components: {
      TodoHeader,
      TodoList,
      TodoFooter
    }
  }
</script>

<style>
/*app*/
.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

</style>

main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
/* 入口js */
/* 创建vue实例 */

import Vue from 'vue' /* 注意大小写 */
import App from './App'
import store from './store'

import './base.css'
// Vue.config.productionTip = false

/* eslint-disable no-new */
/* 对应index.html的app */
/* 引入组件 将组件的映射名变为标签 组件一般是一个局部功能界面,包含了html,css,js,img等资源 */
/* 模板插入到el匹配的页面上的div去 参见 生命周期 当时判断了 是否有template选项,如果有,则编译template,挂载到页面上去 */
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>',
  store
})

store/state.js

/* 状态对象 */
export default {
  todos: []
}

store/index.js

/* vuex 最核心的管理对象 */
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
})

store/actions.js

// 接收xxx.vue的组件通知,触发mutation。js调用间接更新状态的方法的对象
import { ADD_TODO,DELETE_TODO,SELECT_ALL_TODOS,CLEAR_ALL_COMPLETED } from './mutation-types'

export default {

  addTodo ({commit},todo) {
    // 提交一个mutation commit最终导致一个mutation调用
    // 无论是什么类型,都用对象包起来
    commit(ADD_TODO, {todo})
  },
  deleteTodo ({commit},index) {
    // 无论是什么类型,都用对象包起来
    commit(DELETE_TODO, {index})
  },

  selectAllTodos ({commit},check) {
    commit(SELECT_ALL_TODOS, {check})
  },

  clearAllCompleted ({commit}) {
    commit(CLEAR_ALL_COMPLETED)
  }
}

store/getters.js

// 包含所有state的所有计算属性

export default {

  //总数
  totalCount(state) {
    return state.todos.length
  },

  //完成的数量
  completeCount(state) {
    return state.todos.reduce((preTotal, todo) => {
      return preTotal + (todo.complete ? 1 : 0)
    },0)
  },

  //判断是否全部选中
  isAllSelected (state, getters) {
    // return getters.totalCount === getters.completeCount  && getters.totalCount > 0
    return getters.totalCount === getters.completeCount  && state.todos.length > 0
  }

}

stores/mutation-types.js

/*
  所有mutation 的名称常量
 */

export const ADD_TODO = 'add_todo'  // 添加todo
export const DELETE_TODO = 'delete_todo'  // 删除todo
export const SELECT_ALL_TODOS = 'select_all_todos' // 选择所有
export const CLEAR_ALL_COMPLETED = 'clear_all_completed' // 清除已完成的todo

stores/mutation.js

/*
  包含多个由action触发,直接更新状态方法的对象
 */

import {ADD_TODO,DELETE_TODO,SELECT_ALL_TODOS,CLEAR_ALL_COMPLETED} from './mutation-types'

export default {
  // 这里变成中括号比较难以理解
  [ADD_TODO] (state, {todo}) {
    state.todos.unshift(todo)
  },

  [DELETE_TODO] (state, {index}) {
    state.todos.splice(index,1)
  },

  [SELECT_ALL_TODOS] (state, {check}) {
    state.todos.forEach(todo => todo.complete = check)
  },

  [CLEAR_ALL_COMPLETED] (state) {
    state.todos = state.todos.filter( todo => !todo.complete)
  }
}
// 从组件开始,到action,到mutation,再更新状态

components/TodoHeader.vue

<template>
  <div class="todo-header">
    <!-- 第一步 给目标元素绑定监听 -->
    <input type="text" placeholder="请输入你的任务名称,按回车键确认"
           v-model="title" @keyup.enter="addItem"/>
  </div>
</template>

<script>
  export default {
    // props: {
    //   // 声明接收
    //   addTodo: Function,
    // },
    data() {
      return {
        title: ''  //这个只是当前组件内部使用,仍然放在这里即可,不需要被vuex管理
      }
    },

    methods: {
      addItem() {
        // 1检查输入的合法性
        const title = this.title.trim()
        if(!title){
          alert('不能为空')
          return
        }

        // 2.根据输入生成一个对象
        const todo = {
          title,
          complete: false
        }

        // 3.将todo对象添加到todos
        // this.addTodo(todo)
        // 这时需要通知vuex
        this.$store.dispatch('addTodo',todo)

        // 4.清除输入
        this.title = ''
      }
    }
  }
</script>

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

components/TodoList.vue

<template>

    <ul class="todo-main">
      <TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo" :index="index" />
    </ul>

</template>

<script>
  import { mapState} from 'vuex'
  import TodoItem from './TodoItem'
  export default {
    //声明
    // props: {
    //   todos: Array,
    //   deleteTodo: Function
    // },
    computed: {
      ...mapState(['todos'])
    },

    components: {
      TodoItem
    }

  }
</script>

<style>
/*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;
}
/*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;
}

</style>

components/TodoItem.vue

<template>
  <!--进入内部元素 会促发mouseout;只有离开才会使用mouseleave
  所以应该选择enter和leave
  -->
  <li @mouseenter="handleEnter(true)" @mouseleave="handleEnter(false)"
    :style="{background: bgColor}">
    <label>
      <input type="checkbox" v-model="todo.complete"/>
      <span>{{ todo.title }}    </label>
    <button class="btn btn-danger" v-show="isShow"  @click="deleteItem">删除</button>
  </li>
</template>

<script>
  export default {
    props: {
      todo: Object,
      index: Number
      // deleteTodo: Function
    },

    data() {
      return {
        bgColor: 'white',  // 背景颜色
        isShow: false   // 按钮默认是否显示
      }
    },

    methods: {
      handleEnter(isEnter) {
        if(isEnter) {
            this.bgColor = 'gray'
            this.isShow = true
        } else {
            this.bgColor = 'white'
            this.isShow = false
        }
      },

      deleteItem() {
        // const {todo, index, deleteTodo} = this
        // if(window.confirm(`确认删除${todo.title}`)){
        //   // deleteTodo(index)
        //   this.$store.dispatch("deleteTodo",index)
        // }
        this.$store.dispatch('deleteTodo', this.index)
      }
    }

  }
</script>

<style>
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

</style>

components/TodoFooter.vue

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox" v-model="isAllComplete"/>
    </label>
    <span>
          <span>已完成{{completeCount}} / 全部{{totalCount}}
            <button class="btn btn-danger" v-show="completeCount" @click="clearAllCompleted">清除已完成任务</button>
  </div>
</template>

<script>
  import { mapGetters, mapActions } from 'vuex'
  export default {

    computed: {
       ...mapGetters(['totalCount','completeCount']),
      // isAllComplete有点特殊
      isAllComplete: {
        get() {
          return this.$store.getters.isAllComplete
        },

        set(value) { // value是checkbox最新值
          this.$store.dispatch('selectAllTodos', value)
        }
      }
    },
    methods: {
      ...mapActions(['clearAllCompleted'])
    }

  }
</script>

<style>
/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}
</style>

实战效果:

新增后:

image.png

删除:

image.png

清除已完成:

image.png

(待续)


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。