这个页面的API会返回N多期课程,每期课程下面会有N多次课,每次课里面有N多节视频,默认第一期的第一课是展开的,其他期就只展开每次课,但不显示每节视频,当我们点击每次课右边的向下箭头就展开这次课的所有视频,再点击则收起。
因为返回的数据中每次课下面没有用来表示展开和收起的字段,因此我就想到在拿到数据后,对数据做一些修改,在每次课的同级数据里面加一个_expanded的字段,第一期的第一课中的_expanded值为true,其他的为false,通过控制_expanded的true或false来展开和收起每次课下面的视频。
a._expanded = !a._expanded 就是我们在点击向下箭头时要执行的代码。就像下面这样:
起初在没有拿到后台数据的时候,我先假设数据结构是这个样子的:
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里面的写法如下:
-
{{course.course_name}}
-
{{clazz.class_name}}
-
{{classContent}}
在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所实现的功能,善于利用工具能有效的工作。
五,以上内容有错落之处,还望各位大牛多多提点。