vue中展开收起的实现方式

vue中展开收起的实现方式_第1张图片
WechatIMG729.jpeg

这个页面的API会返回N多期课程,每期课程下面会有N多次课,每次课里面有N多节视频,默认第一期的第一课是展开的,其他期就只展开每次课,但不显示每节视频,当我们点击每次课右边的向下箭头就展开这次课的所有视频,再点击则收起。
因为返回的数据中每次课下面没有用来表示展开和收起的字段,因此我就想到在拿到数据后,对数据做一些修改,在每次课的同级数据里面加一个_expanded的字段,第一期的第一课中的_expanded值为true,其他的为false,通过控制_expanded的true或false来展开和收起每次课下面的视频。
a._expanded = !a._expanded 就是我们在点击向下箭头时要执行的代码。就像下面这样:


vue中展开收起的实现方式_第2张图片
添加一个自定义属性的思路.jpeg

起初在没有拿到后台数据的时候,我先假设数据结构是这个样子的:

      courseList: [
          {
            course_id: 1,
            course_name: '黑马营十七期',
            content: [
              {
                class_id: 1,
                class_name: '1课',
                class_content: [
                  '黑马营十七期1课DAY1 第一节视频',
                  '黑马营十七期1课DAY1 第二节视频',
                  '黑马营十七期1课DAY1 第三节视频',
                  '黑马营十七期1课DAY1 第四节视频'
                ]
              },
              {
                class_id: 2,
                class_name: '2课',
                class_content: [
                  '黑马营十七期1课DAY1 第一节视频',
                  '黑马营十七期1课DAY1 第二节视频',
                  '黑马营十七期1课DAY1 第三节视频',
                  '黑马营十七期1课DAY1 第四节视频',
                  '黑马营十七期1课DAY1 第五节视频'
                ]
              }
            ]
          },
          {
            course_id: 2,
            course_name: '李竹实验室一期',
            content: [
              {
                class_id: 1,
                class_name: '1课',
                class_content: [
                  '李竹实验室一期1课DAY1 第一节视频',
                  '李竹实验室一期2课DAY1 第二节视频',
                  '李竹实验室一期3课DAY1 第三节视频',
                  '李竹实验室一期4课DAY1 第四节视频'
                ]
              },
              {
                class_id: 2,
                class_name: '2课',
                class_content: [
                  '李竹实验室一期1课DAY1 第一节视频',
                  '李竹实验室一期2课DAY1 第二节视频',
                  '李竹实验室一期3课DAY1 第三节视频',
                  '李竹实验室一期4课DAY1 第四节视频',
                  '李竹实验室一期5课DAY1 第五节视频'
                ]
              }
            ]
          }
        ]

结构很清晰明了,我们的代码也比较简单,先做代码的初始化,给每次课内部添加一个_expanded的字段:

    methods: {
      expandClazzToggle(clazz) {
        clazz._expanded = !clazz._expanded;
      },
      normalizedCourseList(){
        this.courseList.forEach((course, courseIndex, array) => {
          course.content.forEach((clazz, clazzIndex, arr) => {
          if (courseIndex === 0 && clazzIndex === 0) {
              Vue.set(clazz, '_expanded', true)
            } else {
              Vue.set(clazz, '_expanded', false)
            }
          })
        })
      }
    },

    created(){
      this.normalizedCourseList()
    }

normalizedCourseList()方法就是初始化数据用的,

            if (courseIndex === 0 && clazzIndex === 0) {
              Vue.set(clazz, '_expanded', true)
            } else {
              Vue.set(clazz, '_expanded', false)
            }

上面这一段的作用就是给第一期的第一课的_expanded设为true,其他的设置为false。

这里解释一下上面为什么要用Vue.set

因为Vue 不允许在已经创建的实例上动态添加新的根级响应式属性,我们这里的_expanded一开始数据中是没有的,我们是通过后面的初始化方法来添加的,所以需要使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。
处理完之后的数据就像这样:

        courseList: [
          {
            course_id: 1,
            course_name: '黑马营十七期',
            content: [
              {
                class_id: 1,
                class_name: '1课',
                class_content: [
                  '黑马营十七期1课DAY1 第一节视频',
                  '黑马营十七期1课DAY1 第二节视频',
                  '黑马营十七期1课DAY1 第三节视频',
                  '黑马营十七期1课DAY1 第四节视频'
                ],
                _expanded: true
              },
              {
                class_id: 2,
                class_name: '2课',
                class_content: [
                  '黑马营十七期1课DAY1 第一节视频',
                  '黑马营十七期1课DAY1 第二节视频',
                  '黑马营十七期1课DAY1 第三节视频',
                  '黑马营十七期1课DAY1 第四节视频',
                  '黑马营十七期1课DAY1 第五节视频'
                ],
                _expanded: false
              }
            ]
          },
          {
            course_id: 2,
            course_name: '李竹实验室一期',
            content: [
              {
                class_id: 1,
                class_name: '1课',
                class_content: [
                  '李竹实验室一期1课DAY1 第一节视频',
                  '李竹实验室一期2课DAY1 第二节视频',
                  '李竹实验室一期3课DAY1 第三节视频',
                  '李竹实验室一期4课DAY1 第四节视频'
                ],
                _expanded: false
              },
              {
                class_id: 2,
                class_name: '2课',
                class_content: [
                  '李竹实验室一期1课DAY1 第一节视频',
                  '李竹实验室一期2课DAY1 第二节视频',
                  '李竹实验室一期3课DAY1 第三节视频',
                  '李竹实验室一期4课DAY1 第四节视频',
                  '李竹实验室一期5课DAY1 第五节视频'
                ],
                _expanded: false
              }
            ]
          }
        ]

template里面的写法如下:


在h5上面添加一个点击事件,将每次课的全部内容都传到方法内部,然后在方法内部修改_expanded的值。请看上面的expandClazzToggle(clazz)方法。

            
{{clazz.class_name}}

如果数据就像上面这样,那我们这个功能就算是实现了,但现实并不会尽如人意,如果真正返回的数据像下面这样,就需要多花一点功夫了。

{
    "code": "0",
    "msg": "success",
    "tip": "success",
    "data": {
        "list": {
            "189": {
                "term_name": "李祝捷实验室一期",
                "chapter_list": {
                    "2": {
                        "chapter_name": "第1课",
                        "file_list": [
                            {
                                "file_name": "测试文件",
                                "url": "http://tup.iheima.com/hmy/course/nZ2Yzsy3rm.jpeg"
                            }
                        ]
                    }
                }
            },
            "581": {
                "term_name": "黑马营十七期",
                "chapter_list": {
                    "404": {
                        "chapter_name": "第1课",
                        "file_list": [
                            {
                                "file_name": "课件1",
                                "url": "http://tup.iheima.com/hmy/course/yXjCiEcmcb.pdf"
                            }
                        ]
                    }
                }
            }
        }
    }
}

除了视频数据部分是数组,其他全是对象,层级关系也有所不同。下面是我写出的两个方法:
第一种,用原生JS实现,先用Object.keys()取出每期课程的key值,存放在数组中,然后根据这个key值取出每期的数据,再用Object.keys()取出每次课的key值,存放在数组中,通过用每次课的key取出每次课的内容。在循环数组的时候都是从0开始,所以在courseIndex 和 clazzIndex 都为0的时候就将_expanded设置为true,其余设置为false。

    created() {
      let jkroot = process.env.YUNTI_API_ROOT
      this.$http.get(jkroot + '/course/term-attach').then((response) => {
        let data = response.body
        if (data.code === '0') {
          this.courseDataObj = data.data.list;

          let courseKeys = Object.keys(this.courseDataObj); //courseDataObj的所有可枚举属性,返回所有字段的数组
          for (let courseIndex = 0; courseIndex < courseKeys.length; courseIndex++) {
            let clazzListObj = this.courseDataObj[courseKeys[courseIndex]].chapter_list;
            let clazzKeys = Object.keys(clazzListObj); // chapter_list的所有可枚举属性,返回所有字段的数组
            for (let clazzIndex = 0; clazzIndex < clazzKeys.length; clazzIndex++) {
              let clazz = clazzListObj[clazzKeys[clazzIndex]];
              clazz._expanded = courseIndex === 0 && clazzIndex === 0;
            }
          }
          // 初始化数据之后再重新赋值一次
          this.courseDataObj = JSON.parse(JSON.stringify(this.courseDataObj))

        } else if(data.code === '60088') {
          this.$router.push({path: '/login', replace: true})
        }
      }, (response) => {
      })
    },
    methods: {
      expandClazzToggle(clazz) {
        clazz._expanded = !clazz._expanded
      }
    }

这里解释为什么要再重新赋值一次

在Vue初始化实例的时,我们在Vue实例的Data选项中添加一个属性时,Vue会遍历这个属性,将其转化为getter/setter,Vue会追踪它的依赖,在这个属性被访问和修改时通知变化,并实时渲染,及时响应。其主要原因是每个实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
Vue本身没有给实例中data(){}方法以外的数据绑定getter和setter方法,所以直接修改从外部接口获取的值,不会触发watcher,也就不会重绘DOM,但是给实例中data(){}方法以内的数据赋值就可以触发,我这个页面是先将获取到值赋给了data(){}方法中的courseDataObj,然后再初始化数据,所以在初始化完数据后能打印出想要的结果,也能渲染到view上面,只是当点击expandClazzToggle(clazz)这个方法修改的是引用类型的值,直接赋值原来的对象,也不会触发watcher,所以才不会重绘DOM,因此,数据修改了,但是页面上显示的还是原来的值。不过呢,我们在初始化之后再重新赋值到data(){}方法中的courseDataObj,那么expandClazzToggle(clazz)方法就不再是修改引用类型的值了,因为实例中data(){}方法内已经接管了初始化后的数据,所以expandClazzToggle(clazz)方法才会起作用。
不过,此处我们还有更好的解决办法,拿到接口返回的数据先初始化,完了之后再赋值到data(){}方法中的courseDataObj,最终目的就是要让实例去接管我们最终需要渲染的数据。
下面我们来看看改进之后的方法:

    created() {
      let jkroot = process.env.YUNTI_API_ROOT
      this.$http.get(jkroot + '/course/term-attach').then((response) => {
        let data = response.body
        // console.log(data)
        if (data.code === '0') {
          // 先初始化数据,再赋值给data()中的courseDataObj
          let courseData = data.data.list
          let courseKeys = Object.keys(courseData);
          for (let courseIndex = 0; courseIndex < courseKeys.length; courseIndex++) {
            let clazzListObj = courseData[courseKeys[courseIndex]].chapter_list;
            let clazzKeys = Object.keys(clazzListObj);
            for (let clazzIndex = 0; clazzIndex < clazzKeys.length; clazzIndex++) {
              let clazz = clazzListObj[clazzKeys[clazzIndex]];
              clazz._expanded = courseIndex === 0 && clazzIndex === 0;
            }
          }
          this.courseDataObj = courseData
        } else if(data.code === '60088') {
          this.$router.push({path: '/login', replace: true})
        }
      }, (response) => {
      })
    },

先初始化数据,再赋值给data()中的courseDataObj,可以避免不必要的操作,提高性能。
下面是我用lodash中的.each方法来实现上面同样的效果,代码会简洁一点:

   created() {
      let jkroot = process.env.YUNTI_API_ROOT
      this.$http.get(jkroot + '/course/term-attach').then((response) => {
        let data = response.body
        if (data.code === '0') {
          let courseData = data.data.list;
          let courseIndex = 0; //lodash的_.each既可以遍历Array,也可以遍历Object,但是遍历Object时,没有索引值,所以人工加上
          lodash.each (courseData, (course, courseKey, list) => {
            let clazzIndex = 0;
            lodash.each (course.chapter_list, (clazz, clazzKey, clazzList) => {
              if  (courseIndex === 0 && clazzIndex ===0)  {
                clazz._expanded = true;
                console.log(`${courseIndex}-${clazzIndex}`)
              } else {
                clazz._expanded = false;
              }
              clazzIndex++;
            });
            courseIndex++;
          });
          this.courseDataObj = courseData;
        } else if (data.code === '60088') {
          this.$router.push({path: '/login', replace: true})
        }
      }, (response) => {
      })
    }
    }

在项目中引入lodash的方式:

先用npm安装
$ npm i --save lodash
然后在项目中通过import去引用
import lodash from 'lodash/core'
然后在vue实例中就可以直接使用了。

最后总结

虽然是一个小小的需求,但在实现的过程中也学到了很多的东西:
一,当接到需求的时候先思考最快最好的解决办法。
二,当我们给data()中动态添加新的根级响应式属性时,需要用到Vue.set()来赋值,否则,Vue实例接管不到这个动态添加的新属性。
三,通过接口获取数据,并做初始化的时候,可以先初始化数据再赋值,这样能确保赋值给data()中的数据是最终的数据,而且Vue实例也能处理内部的数据,并实时响应出来。
四,学会使用JS库能更快更有效的实现原生js所实现的功能,善于利用工具能有效的工作。
五,以上内容有错落之处,还望各位大牛多多提点。

你可能感兴趣的:(vue中展开收起的实现方式)