⚠️ 本文为掘金社区首发签约文章,未获受权禁止转载css
百万PV商城系列
主要收录我在商城项目中的实践心得
,我会从视觉体验
、性能优化
、架构设计
等三个维度出发,一步一步为你们讲解商城项目中前端出现的问题
,问题解决的思路与方案
,最后再进行代码实践
。让你在工做中碰到相似问题时,可以更加驾轻就熟。前端
本篇是商城实践系列的第一篇文章,主要内容是对商城项目中常见场景的图片资源加载进行优化,提高视觉体验与页面性能。node
在平常生活中,Yoyo们
确定都用过一些电商公司的App
、Web
、小程序
等应用。在浏览的过程当中,会有很是多琳琅满目的图片,好比常见的商品卡、商品详情、轮播图、 广告图等组件都须要使用一些后台管理配置上传的图片资源。react
除此以外,这些组件所在的页面也大都是流量承担很是大的页面,若是用户的体验感官比较差,必然会影响用户的留存转化。那么,本篇文章咱们就来学习一下图片资源优化的一些方案技巧,我会分别从普通图片、高保真图片、长屏渲染图这三个场景,依次给你们讲讲工做中的实战操做。若是碰到相应的优化场景,你就可以从容以对了。git
对于普通图片优化,我以懒加载(lazyLoad)做为主要的实施手段。懒加载说白了就是优先加载可视窗口内的图片资源,而可视窗口外的内容只有滚动后进入可视窗口内才会进行加载。shell
那么,我为何会用懒加载,以及懒加载的实现方案究竟是什么呢?下面,我经过一个简单的React
的图片懒加载方案的实现,来一步一步说明。小程序
通常来讲,刚进入网页页面就会有大批量的图片资源加载,这会间接影响页面的加载,增长白屏加载时间,影响用户体验。所以,咱们的诉求就是不在可视化窗口
内的图片不尽兴加载,尽量减小本地带宽
的浪费和请求资源
的数量。后端
那么,为何我会推荐懒加载作为普通图片资源的主要实施手段呢?由于它有两大优势。浏览器
那么,懒加载怎么实现呢?实现的方式有两种。缓存
scroll
事件来监听视窗滚动区域实现。该方法兼容性好,绝大多数浏览器和WebView
都兼容支持。IntersectionObserver API
观察DOM
是否出如今视窗内,该方法优势在于调用简单,只是部分移动端兼容没有上一种方式好。两种形式都是在观察当前DOM
是否出如今了可视窗口内,若是出现的话就将data-src
中的图片地址赋值给src
,而后开始加载当前的图片。
那么,下面咱们就开始着手实现一个基于scroll
事件的懒加载示例吧。
咱们先画一个基本的页面布局出来,主要是将视窗和图片加载出来。
const ImageLazy = () => {
const [list, setList] = useState([
1,2,3,4,5,6,7,8
])
const ref = useRef<HTMLDivElement | null>(null)
return (
<div className="scroll-view" ref={ ref }> {list.map((id) => { return ( <div key={id} className="scroll-item"> <img style={{ width: '100%', height: '100%' }} data-src={ `${ prefix }split-${id}.jpg` } /> </div> ); })} </div>
)
}
复制代码
.scroll-item {
height: 200px;
}
.scroll-view {
height: 400px;
overflow: auto;
}
复制代码
能够看效果图,在页面上只显示了两张图片,但其实全部的图片都已经加载完了。
为scroll-view
绑定了ref
以后,同时须要在useEffect
中对scroll
事件进行绑定和注销。
以下,我先获取当前组件全部的img
元素(真实操做最好使用指定className),为ref.current
进行addEventListener
添加事件监听操做,而后在回调中执行对应的方法。
同时,在return
的时候,也须要将其事件移除,避免形成一些意外状况。
useEffect(() => {
const imgs = document.getElementsByTagName('img');
console.log(ref.current, 'current')
ref.current?.addEventListener('scroll', () => {
console.log('listens run')
})
return (
ref.current?.removeEventListener('scroll', () => {
console.log('listens end')
})
)
}, [])
复制代码
以下图,在我滚动的时候,同时执行了ScrollCallback
,控制台打印了不少执行结果,意味着咱们的事件已经添加成功了。
经过下面的分析图,咱们能够看到clientHeight
是咱们视窗的高度,而在ScrollView
当中每次滚动都会触发scroll
方法回调,能够拿到当前页面视窗的滚动距离scrollTop
。
那么,咱们又如何判断元素是否出如今页面上呢?
经过元素的offsetTop
属性,能够知道当前元素距离顶部的偏移距离。那么,当咱们拿到窗口的高度clientHeight
,滚动的距离scrollTop
,以及元素距离顶部的距离offsetTop
,就能够推断出下面一套条件公式,经过视窗高度(dom.clientHeight) + 滚动距离(dom.scrollTop) > 元素距离顶部距离(image.offsetTop)
来判断当前元素是否出如今页面可视范围内了。
将其转换为函数方法实现结果以下:
function scrollViewEvent (images: HTMLCollectionOf<HTMLImageElement>) {
// 可视化区域高度
const clientHeight = ref.current?.clientHeight || 0
// 滚动的距离
const scrollTop = ref.current?.scrollTop || 0
// 遍历imgs元素
for (let image of images) {
if (!image.dataset.src) continue
// 判断src是否已经加载
if (image.src) continue
//图片距离顶部距离
let top = image.offsetTop
// 公式
if (clientHeight + scrollTop > top) {
// 设置图片源地址,完成目标图片加载
image.src = image.dataset.src || ''
image.removeAttribute('data-src')
}
}
}
复制代码
在这里,我也经过ahook
中的useThrottleFn
作一点节流的小优化来避免频繁的进行函数回调。在500ms
内,事件只会执行一次,避免额外执行带来的性能消耗。
import { useThrottleFn } from 'ahooks'
// 截流函数hook
const { run } = useThrottleFn(scrollViewEvent, {
wait: 500
})
复制代码
同时,咱们将其放入到scroll
事件回调中执行。不过,在组件一开始实际上是触发不了scroll
事件,所以,须要咱们手动来初始化当前第一次页面中的图片数据。
useEffect(() => {
const imgs = document.getElementsByTagName('img');
console.log(ref.current, 'current')
ref.current?.addEventListener('scroll', () => {
run(imgs)
})
run(imgs)
return () => {
ref.current?.removeEventListener('scroll', () => {
console.log('listens end')
})
}
}, [])
复制代码
到此,咱们的一个图片懒加载
基本就实现完成了,咱们来看看效果吧。
对于懒加载来讲,每一个
item
最好设置一个高度,防止在一开始没有图片时,组件由于没有高度而致使页面元素暴露下视窗内致使懒加载失效。
对于高保真这类图片而言,不少都是由相关运营人员
配置的活动图,通常在大促期间会有不少微页面
,或者是图片连接
,都是经过图片 + 热区
的形式发布给用户浏览的。
所以,绝大多数运营诉求都是尽量清晰展现对应的图片。那么懒加载显然并不能很好的解决问题,所以我在原先懒加载的基础上,新增了一些加载状态给用户视觉上的体验感官,目前市面上产品主要使用骨架屏
或者是渐进式加载
等方案来让图片显示过渡更加的平滑,避免加载失败或者加载图片卡顿的尴尬。
经过下面的这张图,依旧先来梳理下实现逻辑。在图片组件中,分别有两张图进行轮流替换,当高清资源图加载完毕后,须要将骨架图
或者缩略图
隐藏,显示已经加载好的高清图。
分析结束后, 能够跟我来实现一个简单的图片组件,经过img
中的onLoad
事件来判断须要显示的图片是否已经加载完了。经过对应的状态(status)
来控制略缩图的显示和隐藏。下面我就以渐进式加载来做为案例,参考下图,咱们来实现一个简单的状态切换组件。
import React from "react";
import "./index.css";
interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
thumb: string;
}
type ImageStatus = 'pending' | 'success' | 'error'
const Image: React.FC<ImageProps> = (props) => {
const [status, setImageStatus] = React.useState<ImageStatus>('pending');
/** * 修改图片状态 * @param status 修改状态 */
const onChangeImageStatus = (status: ImageStatus) => {
/** TODO setTime模拟请求时间 */
setTimeout(() => setImageStatus(status), 2000)
}
return (
<> <img className="image image__thumb" alt={props.alt} src={props.thumb} style={{ visibility: status === 'success' ? "hidden" : "visible" }} /> <img onLoad={() => onChangeImageStatus('success')} onError={() =>onChangeImageStatus('error')} className="image image__source" alt={props.alt} src={props.src} /> </>
);
};
export default Image;
复制代码
经过filter: blur(25px);
属性,对略缩图添加了一部分模糊效果,这样就能够避免一些马赛克图的尴尬,来达到部分毛玻璃,或者说是高斯模糊的一些小特效。
经过
onLoad
加载完毕的事件,咱们作了一个简单的渐进式图片加载,那么相应的相似于骨架屏等其它的加载态也能够经过同样的状态判断来进行实现的。只是将缩略图换成了其它组件,仅此而已。
在商品详情页面,运营会配置一些商品的详细描述图文,不只对图片的质量会比较高,同时图片也会很是长。那么很显然,咱们并不可能说直接拿到图片就显示在页面上,若是用户的网速比较慢的状况下,页面上就会直接出现一个很长的白条,或者一张加载失败的错误图。这些很明显不是咱们想要的结果。
那么,该怎么办呢?
咱们先看下淘宝等电商平台的一个商品详情,当你点开看大图时,会发现只显示了图片的一部分,会分红不少张张大小一致的图片给咱们。
依照这个思路,咱们也作了相对应的切图优化,将一张长图分红多个等比例大小的多张图块,来进行一个分批渲染调优,减小单次渲染长图的压力。
那么,下面我会从模拟后端长图切短图在将其切割好的图片依次显示在页面中进行展现。话很少说,咱们直接进入正题。
首先,结合下面的分析图,咱们的切图原理其实很是简单,将一张长图分红长宽相等的小图,若是最后一张不知足切割块高度
的话直接将剩余高度给单切成图片。
那么,下面我就写一个简单的node代码来带你们实战一下切图的过程。
以下图,我会模拟一张运营上传的一张长图,而后切割成若干份右边高200
的短图(大小按照需求评估)。下面,咱们就来看下实现效果的教程和代码吧。
首先,安装sharp
用于图片处理,image-size
用于图片大小信息的获取。
# shell
yarn add sharp image-size
复制代码
引入对应的包后,经过image-size
获取咱们须要切割的原长图的信息,好比width
和height
。
const sharp = require('sharp')
const sizeOf = require('image-size');
const currentImageInfo = sizeOf('./input.jpg');
复制代码
当拿到图片的高度和宽度的时候,那么意味着我能够经过一个while
循环将切割的等份高度给计算好。
/** @name 每块大小 200px height */
const SPLIT_HEIGHT = 200
/** @name 长图高度 */
let clientHeight = currentImageInfo.height
/** @name 切割小图高度 */
const heights = []
while (clientHeight > 0) {
/** @if 切图高度充足时 */
if (clientHeight >= SPLIT_HEIGHT) {
heights.push(SPLIT_HEIGHT)
clientHeight -= SPLIT_HEIGHT
} else {
/** @else 切割高度不够时,直接切成一张,高度清0 */
heights.push(clientHeight)
clientHeight = 0
}
}
复制代码
那么,当我知道了切割的图片大小后,就能够对切割好的heights
遍历,经过裁剪偏移
切割成为真实的图片,并生成新的文件并保存起来。
下面代码中,我建立一个marginTop
偏移量,每切割一次,就会将其height
累加向下偏移,直到切割图片到最后一页结束为止。此时mariginTop
为图片的高度。
/** @name 偏移量 */
let marginTop = 0
heights.forEach((h, index) => {
sharp('./input.jpg')
.extract({ left: 0, top: marginTop, width: currentImageInfo.width, height: h })
.toFile(`./img/split_${index + 1}_block.jpg`).then(info => {
console.log(`split_${index + 1}_block.jpg切割成功`)
}).catch(err => {
console.log(JSON.stringify(err), 'error')
})
marginTop += h
})
复制代码
以下图,img文件夹
下多了一些零碎的图片
,而后检查一下图片是否拼接完整
,若是没有问题的话,那么咱们就完成了一个简单的长图切块的需求,下一步就是放到前端进行渲染
了。
前端拿到对应的切片后,直接拼凑在前端页面上展现,处理掉中间的缝隙或者和毛边后,和长图渲染毫无差异。我渲染时依旧是采用懒加载的形式作了简单的加载优化,总体效果以下:
看完了加载效果后,那么来看看加载的响应时间吧。
因为我切好的图片并无作文件上的优化
,所以单张图存在体积过大
,可是丝绝不影响首屏的加载,下面能够看一张在Flow3G
下的加载时间对比。
对于单张长图意味着用户可能会看到4s左右的白屏
或者是骨架屏
,而切片后加载能够优先的将部份内容展示给用户。
本方案在最终实现时,可能会有部分瑕疵,具体切图方案和设备型号有关系。若是碰到问题,能够参考个人一些解决方案:
vertical-center
设置垂直中心值来解决基线对齐问题。img
设置成一个真实block
元素解决。flex-direction
设置为column
为子元素作垂直排列解决问题。background
图片的方式解决问题。但这样作的话若是想使用懒加载,就须要更改部分css
样式偏移来达到可视窗口显示。本篇文章中,我讲了三种不一样图片的优化策略。市面上已经有不少开源库
可以较为方便的实现懒加载
和渐进加载
的方式。同时,对于骨架屏,不少组件库都有对应的组件,封装起来成本也较小。
所以,若是在项目中确实涉及不少图片资源,那么文章中提到的优化方案是我比较推荐的。
对于一个商城项目来讲,它的挑战性不是在于功能的实现逻辑上,而在于部分视觉感觉与体验的优化上。若是以为文章对你有帮助,能够点个👍,给我加个油。若是对前端电商项目想了解更多的Yoyo们能够关注本专栏。
本文首发于:掘金技术社区
类型:签约文章
做者:wangly19
收藏于专栏:# 百万PV商城实践系列 公众号: ItCodes 程序人生