为自己的项目媒体资源添加固定高度
未媒体资源添加固定高度,不仅有利于确定懒加载后的切确位置,还可以做骨架屏、loading动画等等,但是因为历史数据中很多没有加高度的媒体资源,所以一直嫌麻烦没有做。
直到这个季度有一个自上而下(不可抗力)的push。一个需求需要在懒加载的情况下跳转到底部的一个坐标。
一开始我们拟定的方案是中途查询,边查询边修改目标高度,但是这样做无法避免有些很大的图片加载慢的情况,跳转到正确位置后整整1~2秒才加载出来,这时候我们已经跳转到目标地在那里正常浏览了,页面突然被顶下去的效果是我们无法接受的。(尤其是在移动端真机测试上表现得更加明显)。
于是不得已之下必须还是得选择固定高度的方案,翻找了一下网上的建议,最后找到了一个比较快捷的方式如下,只需要设定好每个媒体资源的宽高比例,以及宽度,即可自动设定好高度。
根据padding-bottom
配置比例。
图片和视频资源都需要配置固定高度,以前一直以为视频是有高度的,但是在精确测试后发现视频未加载时候的高度和它加载后的高度不匹配。
把懒加载组件和中途查询函数的代码列如下方
图片固定高度组件
需注意:因为固定高度的span包裹在lazyload里面,所以它仍然会用户下滑到某个距离才显示出内部内容 以及 高度,但是span \ padding-bottom的加载速度极快,比媒体资源快很多,所以可以达到下滑时看到固定高度的效果。
但是要达到我上面的需求:锚点定位跳转,可能还是会有定位不准的情况,因此我采用两种方式相结合,也就是中途查询+固定高度。
import React, { useEffect } from 'react';
import LazyLoad from 'react-lazyload';
import ossImgCompress from '@/utils/ossImgCompress';
import styles from './index.module.less';/*** @param {string} className* @param {Array} ratio [长,宽]图片比例,设置比例后必须设定懒加载组件className或者outerStyle中的宽度。* @param {Object} outerStyle* @returns*/
const LazyLoadImg = ({ children, ...props }) => {let {src,offset = 400,ratio,className,coverClassName,outerStyle = {},style = { width: '100%' },onClick,webp,type = 'img',key,...restProps} = props;src = webp ? ossImgCompress(src, { webp }) : src;const hasRatio = Array.isArray(ratio) && type !== 'cover';return (<LazyLoad offset={offset} className={[styles.lazyimg, className].join(' ')} style={outerStyle}>{hasRatio && <span className={styles.ratioSpan} style={{ paddingBottom: `${(ratio[1] / ratio[0]) * 100}%` }} />}{type === 'img' && <img {...restProps} src={src} style={style} onClick={onClick} alt='' />}{type === 'cover' && (<div{...restProps}className={coverClassName ?? styles.cover}style={{ backgroundImage: `url(${src})`, ...style }}onClick={onClick}>{children}</div>)}</LazyLoad>);
};
export default LazyLoadImg;
// css.cover {width: 100%;height: 100%;background-size: cover;background-position: center;background-repeat: no-repeat;
}.lazyimg {position: relative;font-size: 0;line-height: 0;overflow: hidden;.ratioSpan {position: unset !important;display: inline-block;box-sizing: border-box;width: initial;height: initial;background: none;opacity: 0;border: 0px !important;margin: 0px !important;& + img {border-radius: inherit;position: absolute;inset: 0px;box-sizing: border-box;padding: 0px;border: none;margin: auto;display: block;width: 0px;height: 0px;min-width: 100%;max-width: 100%;min-height: 100%;max-height: 100%;}}
}
视频固定高度组件
视频的懒加载函数既是将data-src的内容放到src里,这个函数在其他文章《如何优化一个很多视频的网页》中给出了。
video的固定高度放在了lazyload组件上,所以高度会一直存在,基本不存在图片固定高度组件中 滑动一定距离才加载出内部元素 & 高度的情况。
import React from 'react';
import LazyLoad from 'react-lazyload';
import styles from './index.module.less';/*** 存在两种懒加载方案、可以自动设置高度的video组件* @param {Object} style video元素的style* @param {Object} outerStyle video元素父级组件的style* @param src 采用LazyLoad 懒加载组件加载* @param poster 采用LazyLoad 懒加载组件加载* @param data_src 不采用懒加载组件(否则获取不到video元素),配合VideoLazyLoad使用* @param data_poster 不采用懒加载组件,配合VideoLazyLoad使用* @param offset* @param {Array} ratio 视频宽高比例,设置后需要设定父级className的宽度* @param className* @param autoPlay* @param onClick* @param {object} videoProps* @returns*/
const LazyLoadVideo = ({ children, ...props }) => {let {src,poster,data_src,data_poster,offset = 400,ratio,className,autoPlay = true,onClick,videoProps,outerStyle = {},style = { width: '100%' },key,} = props;return data_src ? (<div key={key} className={[styles.lazyVideo1, className].join(' ')} style={outerStyle}><spanclassName={styles.ratioSpan}style={{ paddingBottom: Array.isArray(ratio) && `${(ratio[1] / ratio[0]) * 100}%` }}/><videodata-src={data_src}data-poster={data_poster || poster}poster={poster}src={src}style={style}disablePictureInPictureautoPlay={autoPlay}loopmutedplaysInlinecontrols={false}onClick={onClick}{...videoProps}/></div>) : (<LazyLoadkey={key}offset={offset}className={[styles.lazyVideo, className].join(' ')}style={{ ...outerStyle, paddingBottom: Array.isArray(ratio) ? `${(ratio[1] / ratio[0]) * 100}%` : 'auto' }}><videosrc={src}style={style}poster={poster}disablePictureInPictureautoPlay={autoPlay}loopmutedplaysInlinecontrols={false}onClick={onClick}{...videoProps}/></LazyLoad>);
};
export default LazyLoadVideo;
// css
.lazyVideo {position: relative;width: 100%;height: 0;overflow: hidden;display: inline-block;box-sizing: border-box;background: none;border: 0px;margin: 0px;font-size: 0;line-height: 0;video {object-fit: fill;position: absolute;inset: 0px;box-sizing: border-box;padding: 0px;border: none;margin: 0 auto;display: block;width: 100%;height: 100%;font-size: 0;line-height: 0;border-radius: inherit;}
}.lazyVideo1 {position: relative;font-size: 0;line-height: 0;.ratioSpan {width: 100%;inset: 0px;height: 0;overflow: hidden;display: inline-block;box-sizing: border-box;background: none;border: 0px;margin: 0px;font-size: 0;line-height: 0;}video {object-fit: fill;position: absolute;inset: 0px;box-sizing: border-box;padding: 0px;border: none;margin: 0 auto;display: block;width: 100%;height: 100%;border-radius: inherit;}
}
中途查询修改目标高度的函数
其实JQUERY似乎是有一个$().animation()函数已经直接实现了这个功能,但是因为我的项目没有用到,所以就只能自己实现一下了。
const targetRef = useRef(null);
const menuHeader = useRef(null);
const timer = useRef(null);
const max_call = 10;
let max_call_count = 0;
const [res, setRes] = useState([]);<button onClick={handleScrollToTarget}>跳转</button>const scrollToElement = async (lastScrollTop, lastWindowScrollY, lastElementTop) => {// 安卓手机端兼容性const windowScrollY = document.documentElement.scrollTop || document.body.scrollTop;const nowElementTop = targetRef.current.offsetTop;clearTimeout(timer.current);timer.current = null;const margin = isMobile ? (menuHeader.current ? menuHeader.current.clientHeight : 80) : 50;const target = nowElementTop - margin;const step1 = nowElementTop / 3;const step2 = nowElementTop * 0.75;let scrollTop = 0; // 记录本次指定的高度,避免反复赋值造成卡顿// 分阶段赋予高度,避免过快跳转导致中间有高度变化查询不到if (windowScrollY < step1 - 300) {scrollTop = step1;}if (windowScrollY >= step1 - 300 && windowScrollY < step2 - 300) {scrollTop = step2;}if (windowScrollY >= step2 - 300) {scrollTop = target;}if (isReachBottom(10)) {max_call_count = 0;return window.scrollTo(0, targetRef.current.offsetTop);}if (targetRef.current.getBoundingClientRect().top.toFixed(0) > margin) {// 判断当前高度 避免反复赋值造成卡顿if (windowScrollY >= lastScrollTop - 300 || nowElementTop.toFixed(0) !== lastElementTop.toFixed(0)) {window.scrollTo({top: scrollTop,left: 0,behavior: 'instant',});}// 每100ms更新一次if (max_call_count < max_call) {if (lastWindowScrollY === windowScrollY) max_call_count += 1;timer.current = setTimeout(() => {scrollToElement(scrollTop, windowScrollY, nowElementTop);}, 90);}}};// 避免懒加载一直加载导致屏幕滚动失败,将路程分为4段,每隔一段查询一次目标高度,直到滚动到目标位置const handleScrollToTarget = async () => {try {// 查询目标let firstScrollTop = 0;if (!targetRef.current) {const t = await querySelector('#target').then((ele) => {targetRef.current = ele;return ele;});if (!t) return;firstScrollTop = t.offsetTop;} else {firstScrollTop = targetRef.current.offsetTop;}// 查询documentElement & 错误捕获const documentElement =document.documentElement ||(await new Promise((resolve, reject) => {const interval = setInterval(() => {if (max_call_count < max_call) {if (!!document.documentElement) {max_call_count = 0;clearInterval(interval);resolve(document.documentElement);} else {max_call_count++;}} else {clearInterval(interval);max_call_count = 0;resolve(null);}}, 100);}));if (documentElement || document.documentElement) {window.scrollTo(0, firstScrollTop);scrollToElement(firstScrollTop / 2, 0, firstScrollTop);} else {throw Error('无法获取document.documentElement');}} catch (e) {console.error('滚动函数失败——', e);// 兜底函数,直接滚动}};/*** 页面触底判断* @param margin 触底函数判断范围*/
function isReachBottom(margin = 36) {// 窗口高度var windowHeight = document.documentElement.clientHeight || document.body.clientHeight;// 滚动高度var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;// 页面高度var documentHeight = document.documentElement.scrollHeight || document.body.scrollHeight;if (windowHeight + scrollTop + margin > documentHeight) {return true;} else {return false;}
}