目录
03: 通用组件-瀑布流:构建瀑布流布局,获取容器宽度与列宽
11: 通用组件:应用 infinite 结合 waterfall 实现长列表瀑布流效果
12: 通用组件:解决首次数据无法铺满全屏时,数据无法继续加载
01: 业务组件:构建基础列表展示
// tailwind 类名
xl:mt-4
// 编译为
@media (min-width: 1280px) {
.xl\:mt-4 {
margin-top: 1rem
}
}
- src/views/main
- - components
- - - list
- - - - index.vue
- - - - item.vue
# main/index.vue 中
<list-vue></list-vue>
02: 通用组件:瀑布流组件构建分析
现在的样式中存在的问题:每个 item 应该横向排列,第二行的 item 顺序连接到当前最短的列中,而这个也就是构建瀑布流的核心逻辑。
想要实现这个核心逻辑,我们的每个 item 肯定就不可以使用正常的布局方式,而必须使用 absolute 绝对布局,通过 top 和 left 来手动控制位置。
明确好了这个核心逻辑之后,接下来如何进行构建呢?
想要搞明白一个复杂组件的构建机制,那么最好的方式是想象一下:当你去使用这个组件时,你希望如何进行使用。
按照你期望的使用方式来反向思考构建过程,这样在主要的思想上就不会出现大的偏差。
那么我们期望,将来这个通用组件可以这样使用:
<m-waterfall
:data="" // 数据源
:nodeKey="" // 唯一标识的 key
:column="" // 渲染的列数
:picturePreReading="" // 是否需要图片预渲染(在不知道图片高度的情况下)
>
<template v-slot="{ item, width }">
// 对应的 item
</template>
</m-waterfall>
综合来看,整个瀑布流组件的构建,我们需要分成几部分:
1. 通过 props 传递关键数据(部分):
1. data:数据源
2. nodeKey:唯一标识
3. column:渲染的列数
4. picturePreReading:是否需要图片预渲染
2. 瀑布流渲染机制:通过 absolute 配合 relative 完成布局,布局逻辑:每个 item 应该横向排列,第二行的 item 顺序连接到当前最短的列中。
3. 通过 作用域插槽 将每个 item 中涉及到的关键数据,传递到 item 视图中。
明确好了以上几点之后,接下来我们就可以进行对应的实现了。
03: 通用组件-瀑布流:构建瀑布流布局,获取容器宽度与列宽
- src/libs
- - waterfall
- - - index.vue
<template>
<div
class="relative"
ref="containerTarget"
:style="{
// 因为当前为 relative 布局,所以需要主动指定高度
height: containerHeight + 'px'
}"
>
<!-- 因为列数不确定,所以需要根据列数计算每列的宽度,-->
<!-- 所以等待列宽计算完成,并且有了数据源之后进行渲染 -->
<template v-if="columnWidth && data.length">
<!-- 通过动态的 style 来去计算对应的列宽、left、top -->
<div
class="m-waterfall-item absolute duration-300"
:style="{
width: columnWidth + 'px',
left: item._style?.left + 'px',
top: item._style?.top + 'px'
}"
v-for="(item, index) in data"
:key="nodeKey ? item[nodeKey] : index"
>
<slot :item="item" :width="columnWidth" :index="index" />
</div>
</template>
<!-- 可以给一个加载中的描述,没有也无所谓 -->
<!-- <div v-else>加载中...</div> -->
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch, nextTick, onUnmounted } from 'vue'
const props = defineProps({
// 数据源
data: {
type: Array,
required: true
},
// 唯一标识的 key
nodeKey: {
type: String
},
// 列数
column: {
default: 2,
type: Number
},
// 列间距
columnSpacing: {
default: 20,
type: Number
},
// 行间距
rowSpacing: {
default: 20,
type: Number
},
// 是否需要进行图片预读取
picturePreReading: {
type: Boolean,
default: true
}
})
// 容器的总高度
const containerHeight = ref(0)
// 记录每列高度的容器。key:所在列 val:列高
const columnHeightObj = ref({})
/**
* 构建记录各列的高度的对象。
*/
const useColumnHeightObj = () => {
columnHeightObj.value = {}
for (let i = 0; i < props.column; i++) {
columnHeightObj.value[i] = 0
}
}
// 容器实例
const containerTarget = ref(null)
// 容器总宽度(不包含 padding、margin、border)
const containerWidth = ref(0)
// 容器左边距,计算 item left 时,需要使用定位
const containerLeft = ref(0)
/**
* 计算容器宽度
*/
const useContainerWidth = () => {
const { paddingLeft, paddingRight } = getComputedStyle(
containerTarget.value,
null
)
// 容器左边距
containerLeft.value = parseFloat(paddingLeft)
// 容器宽度
containerWidth.value =
containerTarget.value.offsetWidth -
parseFloat(paddingLeft) -
parseFloat(paddingRight)
}
// 列宽 = (容器的宽度 - 所有的列间距宽度) / 列数
const columnWidth = ref(0)
// 列间距合计
const columnSpacingTotal = computed(() => {
// 如果是5列,则存在 4 个列间距
return (props.column - 1) * props.columnSpacing
})
/**
* 开始计算
*/
const useColumnWidth = () => {
// 获取容器宽度
useContainerWidth()
// 计算列宽
columnWidth.value =
(containerWidth.value - columnSpacingTotal.value) / props.column
}
onMounted(() => {
// 计算列宽
useColumnWidth()
})
04: 通用组件-瀑布流:区分图片预加载,获取元素关键属性
想要计算每列的 left、right,就需要拿到每个 item 的高度,因为只有有了每个 item 的高度,才可以判断下一列的第一个 item 的位置。
同时我们根据 picturePreReading 又可以分为两种情况:
1. 需要图片预加载时:图片高度未知
2. 不需要图片预加载时:图片高度已知
根据以上分析可得出以下代码:
// item 高度集合
let itemHeights = []
/**
* 监听图片加载完成
*/
const waitImgComplete = () => {
itemHeights = []
// 拿到所有元素
let itemElements = [...document.getElementsByClassName('m-waterfall-item')]
// 获取所有元素的 img 标签
const imgElements = getImgElements(itemElements)
// 获取所有 img 标签的图片
const allImgs = getAllImg(imgElements)
onCompleteImgs(allImgs).then(() => {
// 图片加载完成,获取高度
itemElements.forEach((el) => {
itemHeights.push(el.offsetHeight)
})
// 渲染位置
useItemLocation()
})
}
/**
* 图片不需要预加载时,计算 item 高度
*/
const useItemHeight = () => {
itemHeights = []
// 拿到所有元素
let itemElements = [...document.getElementsByClassName('m-waterfall-item')]
// 计算 item 高度
itemElements.forEach((el) => {
// 依据传入数据计算出的 img 高度
itemHeights.push(el.offsetHeight)
})
// 渲染位置
useItemLocation()
}
/**
* 渲染位置
*/
const useItemLocation = () => {
console.log(itemHeights)
}
/**
* 触发计算
*/
watch(
() => props.data,
(newVal) => {
nextTick(() => {
if (props.picturePreReading) {
waitImgComplete()
} else {
useItemHeight()
}
})
},
{
immediate: true,
deep: true
}
)
/**
* 从 itemElement 中抽离出所有的 imgElements
*/
export const getImgElements = (itemElements) => {
const imgElements = []
itemElements.forEach((el) => {
imgElements.push(...el.getElementsByTagName('img'))
})
return imgElements
}
/**
* 生成所有的图片链接数组
*/
export const getAllImg = (imgElements) => {
return imgElements.map((imgElement) => {
return imgElement.src
})
}
/**
* 监听图片数组加载完成(通过 promise 完成)
*/
export const onComplateImgs = (imgs) => {
// promise 集合
const promiseAll = []
// 循环构建 promiseAll
imgs.forEach((img, index) => {
promiseAll[index] = new Promise((resolve, reject) => {
const imageObj = new Image()
imageObj.src = img
imageObj.onload = () => {
resolve({
img,
index
})
}
})
})
return Promise.all(promiseAll)
}
05: 通用组件-瀑布流:触发计算,定位 item 位置
/**
* 为每个 item 生成位置属性
*/
const useItemLocation = () => {
// 遍历数据源
props.data.forEach((item, index) => {
// 避免重复计算
if (item._style) {
return
}
// 生成 _style 属性
item._style = {}
// left
item._style.left = getItemLeft()
// top
item._style.top = getItemTop()
// 指定列高度自增
increasingHeight(index)
})
// 指定容器高度
containerHeight.value = getMaxHeight(columnHeightObj.value)
}
/**
* 返回下一个 item 的 left
*/
const getItemLeft = () => {
// 最小高度所在的列 * (列宽 + 间距)
const column = getMinHeightColumn(columnHeightObj.value)
return (
column * (columnWidth.value + props.columnSpacing) + containerLeft.value
)
}
/**
* 返回下一个 item 的 top
*/
const getItemTop = () => {
// 列高对象中的最小的高度
return getMinHeight(columnHeightObj.value)
}
/**
* 指定列高度自增
*/
const increasingHeight = (index) => {
// 最小高度所在的列
const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
// 该列高度自增
columnHeightObj.value[minHeightColumn] +=
itemHeights[index] + props.rowSpacing
}
/**
* 在组件销毁时,清除所有的 _style
*/
onUnmounted(() => {
props.data.forEach((item) => {
delete item._style
})
})
// 触发计算
watch(
() => props.data,
(newVal) => {
// 重置数据源
const resetColumnHeight = newVal.every((item) => !item._style)
if (resetColumnHeight) {
// 构建高度记录容器
useColumnHeightObj()
}
nextTick(() => {
if (props.picturePreReading) {
waitImgComplate()
} else {
useItemHeight()
}
})
},
{
immediate: true,
deep: true
}
)
/**
* 返回列高对象中的最小高度所在的列
*/
export const getMinHeightColumn = (columnHeightObj) => {
const minHeight = getMinHeight(columnHeightObj)
return Object.keys(columnHeightObj).find((key) => {
return columnHeightObj[key] === minHeight
})
}
/**
* 返回列高对象中的最小的高度
*/
export const getMinHeight = (columnHeightObj) => {
const columnHeightArr = Object.values(columnHeightObj)
return Math.min(...columnHeightArr)
}
/**
* 返回列高对象中的最大的高度
*/
export const getMaxHeight = (columnHeightObj) => {
const columnHeightArr = Object.values(columnHeightObj)
return Math.max(...columnHeightArr)
}
06: 通用组件-瀑布流:适配移动端,动态列
目前,我们的瀑布流组件可以适配 pc 端,但是移动端还存在一些问题。下一步,需要让我们的瀑布流组件可以适配移动端场景。
想要适配移动端,关键在于三点:
列数的变化
列数的变化想要处理比较简单,我们只需要利用 isMobileTerminal 方法进行判断即可。
在 src/views/main/components/list/index.vue 中:
:column="isMobileTerminal ? 2 : 5"
<m-waterfall
:data="pexelsList"
:column="isMobileTerminal ? 2 : 5"
:picturePreReading="false"
class="w-full px-1"
>
<template v-slot="{ item, width }">
<itemVue :data="item" :width="width" @click="onToPins" />
</template>
</m-waterfall>
列宽和定位
比较麻烦的地方在于 列宽和定位 如何进行重新计算,即:重新计算的时机是什么?
我们期望当 列数发生变化时,重新进行计算:
/**
* 监听列数变化,重新构建瀑布流
*/
const reset = () => {
// 延迟 100 毫秒,否则会导致宽度计算不正确
setTimeout(() => {
// 重新计算列宽
useColumnWidth()
// 重置所有的定位数据,因为 data 中进行了深度监听,所以该操作会触发 data 的 watch
props.data.forEach((item) => {
item._style = null
})
}, 100)
}
/**
* 监听列数变化
*/
watch(
() => props.column,
() => {
if (props.picturePreReading) {
// 在 picturePreReading 为 true 的前提下,需要首先为列宽设置空,
// 列宽设置空之后,会取消瀑布流渲染
columnWidth.value = 0
// 等待页面渲染之后,重新执行计算。否则在 item 没有指定过高度的前提下,
// 计算出的 item 高度会不正确
nextTick(reset)
} else {
reset()
}
}
)
07: 通用组件-瀑布流:无需图片预加载时,优化功能处理
// src/views/main/components/list/index.vue
<template>
<itemVue :width="width" ></itemVue>
</template>
<script setup>
部分逻辑在上一节中
</script>
// src/views/main/components/list/item.vue
<script setup>
const props = defineProps({
width: {
type: Number,
default: 0
}
})
</script>
<template>
<img :style="{
height: (width / data.photoWidth) * data.photoHeight + 'px'
}" />
</template>
08: 通用组件-瀑布流:总结
瀑布流是一个比较复杂的通用组件,因为我们要尽量做到 普适,所以就需要考虑到各种场景下的处理方案,尽量可以满足日常开发的场景。这就在原本复杂的前提下,让这个功能变得更加复杂了。
下面我们就再来梳理一下整个瀑布流的构建过程:
1. 瀑布流的核心就是:通过 relative 和 absolute 定位的方式,来控制每个 item 的位置。
2. 影响瀑布流高度的主要元素,通常都是 img 标签。
3. 有些服务端会返回关键 img 的高度,有些不会。所以我们就需要分别处理:
1. 当服务端 不返回 高度时:我们需要等待 img 加载完成之后,再来计算高度。然后通过得到的高度计算定位。否则会出现高度计算不准确,导致定位计算不准确的问题。
2. 当服务端 返回 高度时:开发者必须利用此高度为 item 进行高度设定。一旦 item 具备指定高度,我们就不需要等待 img 加载的过程。这样效率更高,并且可以使业务的逻辑变得更加简单。
4. 当进行响应式切换时,同样需要区分对应的场景:
1. 当服务器 不返回 高度时:我们需要 重新执行整个渲染流程,虽然会耗费一些性能,但是这样可以最大可能的避免出现逻辑错误。让组件拥有更强的普适性。
2. 当服务端 返回 高度时:我们同样需要重新计算 列宽 和 定位。但是因为 item 具备明确的高度,所以我们可以直接拿到具体的高度,而无需重复整个渲染流程。从而可以实现更多的交互逻辑。比如:位移动画、将来的图片懒加载占位……
09: 通用组件:长列表 infinite 构建分析
处理好瀑布流之后,接下来我们就需要处理对应的长列表功能。
我们知道对于首页中的瀑布流而言,是需要进行长列表展示的,也就是说它是一个分页的数据。
对于这种分页功能而言,又应该如何进行实现呢?
想要搞明白这个问题,同样需要分成两个方面去看:
1. 长列表的实现原理是什么?
2. 使用长列表时,希望如何进行使用?
长列表的实现原理
所谓长列表分页加载,其实指的就是:当滚动到列表底部时,加载数据。
想要实现长列表组件,围绕着的依然是这句话。
想要实现这个功能,我们需要做的核心一点就是能够 监听到列表滚动到底部。
想要监听到列表滚动到底部的话,可以利用 intersectionObserver,该接口可以判断:目标元素与其祖先元素或顶级文档视口(viewport)的交叉状态(是否可见)。
我们可以利用这个特性,把一个元素 置于列表底部,当这个元素可见时,则表示 列表滚动到了底部。
原生的 intersectionObserver 使用起来比较复杂,好在 vueuse 提供了 useIntersectionObserver 方法。
我们使用长列表时,希望如何进行使用
这个的判断和瀑布流的判断逻辑一样,通过这样的逻辑,可以让我们知道这个组件的 prop 应该如何构建。
我们期望使用它时是这样的:
<m-infinite-list
v-model="" // 当前是否处于加载状态
:isFinished="" // 数据是否全部加载完成
@onLoad="" // 加载下一页数据的触发事件
>
列表
</m-infinite-list>
10: 通用组件:构建长列表 infinite 组件
- src/libs
- - infinite
- - - index.vue
<template>
<div>
<!-- 内容 -->
<slot />
<div ref="laodingTarget" class="h-6 py-4">
<!-- 加载更多 -->
<m-svg-icon
v-show="loading"
class="w-4 h-4 mx-auto animate-spin"
name="infinite-load"
></m-svg-icon>
<!-- 没有更多数据了 -->
<p v-if="isFinished" class="text-center text-base text-zinc-400">
已经没有更多数据了!
</p>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useIntersectionObserver, useVModel } from '@vueuse/core'
const props = defineProps({
// 是否处于加载状态
modelValue: {
type: Boolean,
required: true
},
// 数据是否全部加载完成
isFinished: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['onLoad', 'update:modelValue'])
// 处理 loading 状态
const loading = useVModel(props)
// 滚动的元素
const laodingTarget = ref(null)
// 记录当前是否在底部(是否交叉)
const targetIsIntersecting = ref(false)
useIntersectionObserver(
laodingTarget,
([{ isIntersecting }], observerElement) => {
// 获取当前交叉状态
targetIsIntersecting.value = isIntersecting
// 触发 load
emitLoad()
}
)
/**
* 触发 load
*/
const emitLoad = () => {
// 当 加载更多的视图可见、loading为false、数据尚未全部加载完 时,加载更多数据。
if (targetIsIntersecting.value && !loading.value && !props.isFinished) {
// 修改加载数据标记
loading.value = true
// 触发加载更多行为
emits('onLoad')
}
}
/**
* 监听 loading 的变化,解决数据加载完成后,首屏未铺满的问题
*/
watch(loading, (val) => {
// 触发 load,延迟处理,等待 渲染和 useIntersectionObserver 的再次触发
setTimeout(() => {
emitLoad()
}, 100)
})
</script>
<style lang="scss" scoped></style>
11: 通用组件:应用 infinite 结合 waterfall 实现长列表瀑布流效果
// src/views/main/components/list/index.vue
<template>
<!-- 列表处理 -->
<m-infinite-list
v-model="isLoading"
:isFinished="isFinished"
@onLoad="getPexelsData"
>
<m-waterfall
:data="pexelsList"
:column="isMobileTerminal ? 2 : 5"
:picturePreReading="false"
class="w-full px-1"
>
<template v-slot="{ item, width }">
<itemVue :data="item" :width="width" @click="onToPins" />
</template>
</m-waterfall>
</m-infinite-list>
</template>
<script setup>
const store = useStore()
/**
* 构建数据请求
*/
let query = {
page: 1,
size: 20,
categoryId: '',
searchText: ''
}
// 数据是否在加载中
const isLoading = ref(false)
// 数据是否全部加载完成
const isFinished = ref(false)
// 数据源
const pexelsList = ref([])
/**
* 加载数据的方法
*/
const getPexelsData = async () => {
// 数据全部加载完成则 return
if (isFinished.value) {
return
}
// 完成第一次请求之后,后续请求让 page 自增
if (pexelsList.value.length) {
query.page += 1
}
// 触发接口请求
const res = await getPexelsList(query)
// 初始请求清空数据源
if (query.page === 1) {
pexelsList.value = res.list
} else {
pexelsList.value.push(...res.list)
}
// 判断数据是否全部加载完成
if (pexelsList.value.length === res.total) {
isFinished.value = true
}
// 修改 loading 标记
isLoading.value = false
}
</script>
12: 通用组件:解决首次数据无法铺满全屏时,数据无法继续加载
问题:
isIntersecting 为 true、!loading.vlaue 为 false、!props.isFinished 为 true。这样的条件 只会触发一次 emits('onLoad')。
解决:
明确好了原因之后,再解决起来就比较简单了。只需要让 加载数据 的方法,在 loading.value 发生变化之后,重新进行一次判断,就可以了。使用 watch。
问题:
src/views/components/list/index.vue 中,获取完数据,我们把 loading.value 设为 false。在 src/libs/infinite/index.vue 中,监听到 loading 的改变,再次触发加载更多行为。数据渲染需要时间,此时数据未渲染完成,targetIsIntersecting 依旧为 true,因此会出现 连续两次 触发加载更多行为 的问题。
性能 和 简单逻辑 的衡量。 普适的东西就是权衡的艺术。
解决:
使用 setTimeout。
// src/libs/infinite/index.vue
// 滚动的元素
const laodingTarget = ref(null)
// 记录当前是否在底部(是否交叉)
const targetIsIntersecting = ref(false)
useIntersectionObserver(
laodingTarget,
([{ isIntersecting }], observerElement) => {
// 获取当前交叉状态
targetIsIntersecting.value = isIntersecting
// 触发 load
emitLoad()
}
)
/**
* 触发 load
*/
const emitLoad = () => {
// 当 加载更多的视图可见、loading为false、数据尚未全部加载完 时,加载更多数据。
if (targetIsIntersecting.value && !loading.value && !props.isFinished) {
// 修改加载数据标记
loading.value = true
// 触发加载更多行为
emits('onLoad')
}
}
/**
* 监听 loading 的变化,解决数据加载完成后,首屏未铺满的问题
*/
watch(loading, (val) => {
// 触发 load。延迟处理。等待 渲染 和 useIntersectionObserver 的再次触发。
setTimeout(() => {
emitLoad()
}, 100)
})
13: 图片懒加载构建原因+实现原理
屏幕未显示的已加载的图片,代表着多余的请求。多余的这些请求就会很浪费。如果我们不想有这种浪费,就可以利用 图片懒加载 功能进行实现。
图片懒加载实现原理
图片懒加载如何进行实现呢?想要搞明白这个,我们就需要先明白图片懒加载的原理是什么。
所谓图片懒加载指的是:当图片不可见时,不见在图片。当图片可见时,才去加载图片。
大家看见这个“不可见 && 可见”是不是觉得很眼熟。这不就是实现 长列表 时用过的套路吗?
据此,咱们的实现方案就呼之欲出了。
我们可以 监听所有图片是否被可见,如果图片处于不可见状态,那么就不加载图片。如果图片处于可见状态,那么就开始加载图片。
而这个功能的实现关键就是 IntersectionObserver 。
14: 通用指令:实现图片懒加载
- src
- - directives
- - - modules
- - - - lazy.js
- - - index.js
// src/directives/modules/lazy.js
import { useIntersectionObserver } from '@vueuse/core'
export default {
// 图片懒加载:在用户无法看到图片时,不加载图片,在用户可以看到图片后加载图片。
// 如何判断用户是否看到了图片:useIntersectionObserver。
// 如何做到不加载图片(网络):img 标签渲染图片,指的是 img 的 src 属性,
// src 属性是网络地址时,则会从网络中获取该图片资源。
// 那么如果 img 标签不是网络地址呢?把该网络地址默认替换为非网络地址,
// 然后当用户可见时,再替换成网络地址。
mounted(el) {
// 1. 拿到当前 img 标签的 src
const imgSrc = el.src
// 2. 把 img 标签的 src 替换为本地地址
el.src = ''
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
if (isIntersecting) {
// 3. 当图片可见时,加载图片
el.src = imgSrc
// 4. 停止监听
stop()
}
})
}
}
// src/directives/index.js
import lazy from './modules/lazy'
/**
* 全局注册指令
*/
export default {
install(app) {
app.directive('lazy', lazy)
}
}
// src/main.js
import mDirectives from './directives'
app.use(mDirectives)
15. 深入 vite:指令的自动注册
此时,在 src/directives/index.js 中,我们面临一个和注册组件时同样的问题。那就是:如果指令过多,一个一个地注册未免过于麻烦。最好有一种方式完成 指令的自动注册。
想要完成这个功能,我们依然要使用 Glob导入(import.meta.globEager)和 Object.entries 功能。
/**
* 全局指令注册
*/
export default {
async install(app) {
// https://cn.vitejs.dev/guide/features.html#glob-import
// import.meta.glob 函数中 { eager: true } 作为第二个参数 为同步导入
const directives = import.meta.glob('./modules/*.js', { eager: true })
for (const [key, value] of Object.entries(directives)) {
// 拼接组件注册的 name
const arr = key.split('/')
const directiveName = arr[arr.length - 1].replace('.js', '')
// 完成注册
app.directive(directiveName, value.default)
}
}
}
16: 指定彩色占位图
- src/utils
- - color.js
// src/utils/color.js
/**
* 生成随机色值
*/
export const randomRGB = () => {
const r = Math.floor(Math.random() * 255)
const g = Math.floor(Math.random() * 255)
const b = Math.floor(Math.random() * 255)
return `rgb(${r}, ${g}, ${b})`
}
// 使用
<div :style="{ backgroundColor: randomRGB() }" ></div>
17: 总结
本篇文章借用首页 list 模块业务,实现了:
1. 瀑布流
2. 长列表
3. 懒加载
4. 指令自动注册
5. ……
等通用组件或指令。
这些内容都是咱们日常开发中非常常见的功能。现在我们把它们抽离出来之后,日后的开发中就可以直接拿过来使用了。
现在对于首页而言,我们所欠缺的内容还是比较多的,比如:
1. search 模块到首页的联动
2. item 上的点击处理
这些内容又会给大家带来什么新奇的体验呢?我们后面继续更新。