图片一直是页面中重要的视觉部分,尤其以图片作为主要视觉素材的C端页面。如果用户打开页面后,看到成片成片的图片正在缓慢呈现,脑壳难免一痛,轻则吐槽,重则直接退出,影响留存。
所以,我们有必要将页面中的关键图片进行预加载处理。保证开屏后,用户看到的图片是完整无加载的。
完整阅读本文,你会了解到:
- 我对图片预加载概念的理解;
- 浏览器对图片加载的时机,及借此实现的预加载思路;
- 我的 Vue3 hook + directive 预加载策略的实现详解。
一. 什么是图素的预加载
我对预加载的定义是------在"页面主要视觉呈现前",而非"页面出现前"将物料图素加载完成。
这也就意味着,在SPA应用中, 你不用纠结图片必须在当前组件挂载前就做好所有的图片加载。实际上,你完全可以在组件挂载后进行图片的预加载操作,加载期间,可以使用loading遮盖图素区域,当加载完成后,loading消失,展现给用户的就是完整无卡顿的图片。
在我的实践中,我会为每一个页面级别的组件都增加一个整页loading。当页面组件挂载后,开始预加载所有关键图素,当全部加载完成后,关闭整页loading。这一做法尤其适合物料图素多的页面。
本文介绍的 Vue3 hook + directive 解决方案,更是有利于这种整页loading的实现。
二. 实现效果
先来看看,不使用任何图片预加载策略的页面是什么体验。
TIP:已清除浏览器缓存,并关闭控制台的 Disable cache。
从上图可以看出,页面中的 图素多,有大有小;有<img />
标签的、background-image的、js动态创建的。但无论大小和种类,刷新后都有肉眼可见的加载过程,体验非常不好,这也正是我们预加载需要解决的问题。
使用本文的策略,在各种图素加载完成前呈现整页loading,之后图片的展示就会无比丝滑。开发者使用起来也非常方便:
三. 到底有哪些图需要预加载?
预加载的处理范围并不仅仅只是<img />
标签对应的图片,在这里我总结了下面几种需要处理的图素类型:
<img />
标签对应的链接图片;- 容器节点的CSS background-image;
display:none
隐藏且未来可能展示的上述节点;- 通过 javascript 动态挂载的上述节点。
当然,图片也并非一定要做预加载,过长的整页loading也很劝退用户,因此图素是否需要预加载可考虑下面几点:
- 图片很小,可以瞬间加载的,考虑不用;
- 图片不醒目,信息承载不重要,考虑不用;
- 图片首屏看不到,看到时大概率已加载完成,考虑不用; 比如:页面需要滚动很久才能看到的底端图片(此时应该考虑使用滚动懒加载,而非预加载)。
TIP:不要滥用预加载。预加载 + 懒加载 才是最佳的呈现方式。一次性预加载过多图片,会占用网络带宽,抬高整页loading的覆盖时间,劝退用户,阻塞接口。
四. 预加载的思路?
图片预加载技术并不是神奇的魔法,本质是也是借助了浏览器特性和缓存策略 。下面列举了一些浏览器对图片的加载特性,你需要深知这些特性,才能找到各种图片预加载的场景和解决方案:
-
DOM树中只要
<img />
存在,尽管是被display: none
掉,或者他所在的容器被display:none
掉,浏览器都会进行图片加载。 (意味着css中不能第一时间加载的图片可以借助创建<img />
加载) -
new Image()
和<img />
一样,只要image.src
被赋值,无论是否被append到页面上,都会进行图片加载。 (意味着可以不用将<img />
创建到dom上) -
CSS中的任何图片,只有在用到他们的时候才会临时加载。这就意味着,如果div没有被渲染,那么div的background-image也不会被加载。 (这是预加载需要解决的一个问题)
-
更多相同的
src
的<img />
,浏览器都只会请求第一次的img加载链接,不会有多个<img />
就加载多少次相同的链接。 -
如果
<img />
已经出现过一次,后面通过js动态插入了更多img标签,那么要根据disable-cache分情况讨论:- 开启了disable-cache:每次创建
<img />
,都请求一次,无论src是否此前已经请求过。 (这种情况下无解,毕竟用户直接关闭了浏览器缓存,但通常情况下不会这样) - 没开启disable-cache:只有创建的
<img />
的 src 是从未出现过的,才会重新请求,否则不会发送请求。 (network 中没有from-cache,连这条图片的请求记录都没有)(可以用作处理动态创建<img />
的预加载思路)
- 开启了disable-cache:每次创建
五. 我为什么用hook + directives组合式解决方案
结合上面的浏览器特性,我们的脑中很容易蹦出一个常见的解决思路:
- SPA实际上是一个页面,那就在上一个页面级组件中,通过
new Image()
的方式加载下个页面的所有图片。
我认为,这样的方式很不耐用,也经不起考验,因为他们有3个重大缺陷:
- 太麻烦: 需要收集下个页面的需要预加载的图素链接,人肉遍历HTML和CSS以及JS中动态创建的图素。
- 无法保证全部图素预加载完成: 在前一个页面预加载期间切换到下一个页面,就会出现部分图片未预加载生效的问题。
- 作为入口页面时,无法进行预加载: 假设你分享了一个图素丰富的H5活动页,用户直接打开的是活动页面路由,此时只会挂载活动页面组件,不会先挂载承担预加载逻辑的页面,你的预加载逻辑也就得不到执行。
所以,不必想办法在前一个页面中预加载,这样的方法不通用、不可靠。所以不如就在当前页面实现预加载,在图片加载前开启整页 loading,完成后关闭整页loading。
如果搭配使用 Vue3 hook + directive 实现方式,那么图片预加载将变得简单且有效,这样做有3个好处:
- 使用简单;
- 一眼能看出图片哪些图片预加载了,哪些没有;
- Hook和directive本身就具备很强的复用性;
六. 实现预加载hook------useAllImageLoaded()
核心就是去监听图片们是否已经全部加载完成,然后触发hook回调。在我的实践中,hook中的回调通常负责关闭整页loading。
使用方式如下:
typescript
const loading = ref(true) // 整页loading是否开启
const container = ref(null) // container是DOM类型,表示container下的<img />全都需要预加载
useAllImageLoaded(container, () => {
// 当container下的<img />都加载完成后,关闭整页loading
loading.value = false
})
1. useAllImageLoaded() 支持收集和监听
我们先来看useAllImageLoaded需要处理的第一个具体需求:
- 在组件mounted时,和updated时,对container 下的
<img />
进行收集,并监听load事件。
想要实现这个需求并不困难,但你需要注意以下几个细节,我们才能做的更好:
- 做参数转换兼容: 如果用户提供的container不存在,则抛出警告,不做任何处理;如果container存在但类型不是DOM,则用body作为container;
- 防止内存泄露: 当图片已经加载过,立刻删除load事件监听回调;
- 考虑到页面更新: 某些图片在组件更新后复用,他还是已经加载过了,此时不应该重复设置load监听。
- 对用户的逻辑做容错: 用户的逻辑就是hook的回调参数,发生错误应该catch捕获,保证不终止程序运行,不让预加载成为页面的杀手。
typescript
import { onMounted, onUpdated, Ref } from "vue";
type AnyFunc = () => any
function noop () { /* do nothing... */ }
export default function useAllImageLoaded(container: Ref<HTMLElement> | string[] | null, dynamicSrcs?: string[] | AnyFunc, callback?: AnyFunc) {
if (arguments.length < 1) {
// 不带入参数是没有意义的
console.warn('At least 1 parameter are required! ------from useAllImageLoaded Hook.')
return false
}
function collectImageLoaded() {
if ([null, undefined].includes((container as Ref<HTMLElement>).value)) {
console.warn('useAllImageLoaded hook warning: no container, no static images preloaded!')
container = null
}
if (container !== null && !((container as Ref<HTMLElement>).value as any instanceof HTMLElement)) {
console.warn('useAllImageLoaded hook warning: container is not a HTMLElement instance, use body')
container = { value: document.body } as Ref<HTMLElement>
}
let images = [] // 所有<img /> DOM对象集合
// 获取container下的所有<img />标签。
// 注意querySelectorAll获取的是伪数组,这里应该转换为数组
const imgDomsUnderContainer =
Array.prototype.slice.call(
(
(container as Ref<HTMLElement>)?.value && (container as Ref<HTMLElement>)?.value.querySelectorAll('img')
) || []
)
images = imgDomsUnderContainer
const promises = images.map(node => {
// 遍历images中的每一个图片节点
return new Promise((resolve) => {
// 图片loaded之后的回调
function onceLoaded() {
// 图片加载过一次之后,移除load event listener,防止内存泄露
node.removeEventListener('load', onceLoaded)
resolve(true)
}
// 如果 images 已经加载过一次了,不必在增加额外的load
if (node.complete) {
// 考虑到页面更新,某些图片src未发生改变,diff后复用的情况下,load事件不会被触发
// 因此对于已经load又被复用的图片,应该根据node.complete属性来判断图片是否已经加载完成。
resolve(true)
} else {
// 设置对图片的load监听
node.addEventListener('load', loadedCallback)
}
})
})
Promise.all(promises).then(() => {
// 当收集的所有需要预加载的图素都loaded,那么触发参数回调
callback && callback();
}).catch(e => {
// 因为callback是用户提供的,随时可能发生错误。
// 但预加载发生错误不致命,此时应该catch捕获,通过不终止程序的手段抛出错误
console.error('网络异常或其他程序异常', e);
})
}
onMounted(() => {
// 当组件挂载后,子DOM挂载全部完成,开始收集img
collectImageLoaded()
})
onUpdated(() => {
// 组件更新完成后,根据最新的DOM,重新收集img
collectImageLoaded()
})
}
现在,通过hook,我们已经可以实现对container下,所有的<img />
进行收集,并设置监听,在全部<img />
预加载完成后,触发hook回调,用户可以通过hook回调关闭loading蒙层。
接下来,我们可以进一步扩展useAllImageLoaded的功能,支持动态图素预加载。
2. useAllImageLoaded() 对动态图素的预加载
什么是动态图素?
- 通过 javascript 创建的图素就是动态图素。 其中也包括了动态创建的
<img />
、带有background-image
的DOM容器等,还包括在其他页面中出现的图素。
对于动态图素,如果通过Vue数据驱动的形式去创建的就还好,因为有updated
生命周期,useAllImageLoaded()
可以实现自动收集和监听。但通过自己写的js创建的图素,目前没有太好的办法进行自动收集和监听。
为此,我特别为 useAllImageLoaded()
追加了一个动态图片的加载入口。 可以让用户手动将那些动态图素链接代入到useAllImageLoaded()
中,useAllImageLoaded()
就会根据这些动态图素的链接构造出 <img />
对象,并和静态图素一起实现load监听。这样对动态图素的预加载能力也就实现了。
使用方式如下:
typescript
const loading = ref(true)
const container = ref(null)
useAllImageLoaded(container,
// 数组中包含着你想要预加载的动态图素链接
[
'https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg',
"https://img0.baidu.com/it/u=3363994336,562770620&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400"
],
() => {
loading.value = false
}
)
另外,参数container
未必总有意义,用户可能在当前页面没有想要预加载的图素,比如:只想在当前页面为未来某个页面预加载的图素。此场景下,用户可能希望这样使用:
typescript
const loading = ref(true)
// 无需需要回调
useAllImageLoaded(['https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg'])
// 需要回调
useAllImageLoaded(['https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg'], () => {
loading.value = false
})
所以,现在我们可以整理出当前useAllImageLoaded()需要补充的能力:
- 支持函数重载;
- 将动态图素链接通过new Image()的方式构造成<img / >,掺入预加载图素集合中,并设置load监听。
useAllImageLoaded() 完整版代码如下,内含注释详细讲解:
typescript
import { onMounted, onUpdated, Ref } from "vue";
type AnyFunc = () => any
function noop () { /* do nothing... */ }
export default function useAllImageLoaded(container: Ref<HTMLElement> | string[] | null, dynamicSrcs?: string[] | AnyFunc, callback?: AnyFunc) {
if (arguments.length < 1) {
// 没参数是没有意义的
console.warn('At least 1 parameter are required! ------from useAllImageLoaded Hook.')
return false
}
if (arguments.length === 1) {
// 如果带入1个参数,只能是 动态链接集合
// 此时只有参数为需要动态加载的图片集合方面存在意义,如果参数为container或者callback函数都没有意义
if (Array.isArray(container)) {
dynamicSrcs = container
container = null
callback = noop
} else {
// 如果用户代入错误的参数情况,终止预加载,并抛出警告
console.warn('When only 1 parameter is brought in, it is required that an image link array be required! ------from useAllImageLoaded Hook.')
return false
}
}
if (arguments.length === 2) {
// 如果带入2个参数,就是 (动态链接集合 + 回调) 和 (container + 回调)
// 如果是 (container + 动态链接集合)实际上没什么意义,但我们也进行container下的预加载处理
if (typeof dynamicSrcs === 'function') {
if (Array.isArray(container)) {
// (动态链接集合 + 回调)
callback = dynamicSrcs as unknown as AnyFunc
dynamicSrcs = container
container = null
} else {
// (container + 回调)
callback = dynamicSrcs
dynamicSrcs = []
}
} else if (Array.isArray(dynamicSrcs)) {
// (container + 动态链接集合)
callback = noop
}
}
function collectImageLoaded() {
// 如果container是null或undefined,说明当前用户无需做静态图片预加载
if ([null, undefined].includes((container as Ref<HTMLElement>).value)) {
console.warn('useAllImageLoaded hook warning: no container, no static images preloaded!')
container = null
}
if (container !== null && !((container as Ref<HTMLElement>).value as any instanceof HTMLElement)) {
// 如果container存在,但不是DOM类型,说明用户有心预加载,但参数错误,用body替代container
console.warn('useAllImageLoaded hook warning: container is not a HTMLElement instance, use body')
container = { value: document.body } as Ref<HTMLElement>
}
let images = []
const imgDomsUnderContainer =
Array.prototype.slice.call(
(
(container as Ref<HTMLElement>)?.value && (container as Ref<HTMLElement>)?.value.querySelectorAll('img')
) || []
)
const dynamicImages = (dynamicSrcs && (dynamicSrcs as string[]).map(imgSrc => {
// 将动态链接实例化为<img />实例
const img = new Image()
img.src = imgSrc
return img
})) || []
images = [...imgDomsUnderContainer, ...dynamicImages]
const promises = images.map(node => {
return new Promise((resolve) => {
function onceLoaded() {
node.removeEventListener('load', onceLoaded)
resolve(true)
}
if (node.complete) {
// 考虑到页面更新,某些图片src未发生改变,diff后复用的情况下,load事件不会被触发
// 因此对于已经load又被复用的图片,应该根据node.complete属性来进行判断。
resolve(true)
} else {
node.addEventListener('load', onceLoaded)
}
})
})
Promise.all(promises).then(() => {
callback && callback();
}).catch(e => {
console.error('网络异常或其他程序异常', e);
})
}
onMounted(() => {
console.log('useAllImageLoaded mounted')
collectImageLoaded()
})
onUpdated(() => {
collectImageLoaded()
})
}
七. Hook 搭配 v-preImg 实现 background-image 预加载
useAllImageLoaded() Hook 实现了某容器下全部 <img />
预加载,以及动态图素的加载。可一些图素是在CSS的background-image里的,useAllImageLoaded()
还不能做到对背景图的收集和加载监听。
为了解决CSS背景图的预加载问题,我们可以回想前文所提到的浏览器特性:
- DOM树中只要
<img />
存在,尽管是被display: none
掉,或者他所在的容器被display:none
掉,浏览器都会进行图片加载。
这意味着css中不能第一时间加载的图片可以借助创建<img />
的方式加载。这也是v-preImage
需要去协助useAllImageLoaded()完成的工作。
在实现 v-preImage 前,我们先看看将一个url创建为 <img />
节点有什么讲究:
typescript
const body = document.body
// 用map去记录指令append的url-div有哪些
const wrappers = new Map([])
let wrapperId = 0
function createImgSection(url: string) {
// 如果没有url或者相同url已经创建过了,那就不用再创建div img了
if (!url) return false
if (wrappers.has(url)) return true
// 创建div img
const wrapper = document.createElement('div')
const img = new Image()
img.src = url
wrapper.classList.add(`_preload_img_section_wrapper_${wrapperId++}`)
wrapper.style.display = 'none' // 不渲染
wrapper.appendChild(img)
wrappers.set(url, wrapper)
body.appendChild(wrapper)
return true
}
我用一个Map结构去缓存所有已经挂载过的wrapper,在下次根据url创建wrapper前,先去判断当前Url是不是已经挂载过了。如果没有,就会在wrapper下创建一个<img />
节点,然后通过 display:none
的方式挂载到页面上。
除此之外,我还给wrapper增加了一个独特的class + 唯一的id。这些是为了方便调试,在你的需求中,你可以不这么做。
接下来,将v-preImage的实现就简单了,只需要做好3件事情:
- 拿到指令挂载的DOM节点,并拿到 background-image 链接;
- 将连接通过
createImgSection()
创建为隐藏的<img />
; - 在 background-image 所在节点销毁时,也同步删除
<img />
。
实现代码如下:
typescript
const body = document.body
// 用map去记录指令append的url-div有哪些
const wrappers = new Map([])
let wrapperId = 0
export default function createPreLoadImgDirectives(config: {unmount: boolean} = { unmount : true }) {
return {
mounted(el) {
console.log('preload mounted')
if (el.tagName === 'img') {
// 如果当前是img标签,无需操作,因为Img无论display如何,那么都会进行加载
return
}
// 如果不是img的其他容器,拿到css中的背景图
const bgUrl = getComputedStyle(el).backgroundImage
// 将url从css中解构出来
const url = bgUrl.slice(5, bgUrl.length - 2) // 拿到background-image链接
// 由于vue指令的this是undefined,所以指令不同生命周期之间不能传递数据,通过把数据挂载到el上
el._vpreloadUrl = url
// 创建div img标签在body上
createImgSection(url)
},
unmounted(el) {
if (config.unmount) {
// 当指令元素被卸载时,如果开启了unmounted配置
// 从div img集合中找到对应的div
const wrapper = wrappers.get(el._vpreloadUrl)
if (wrapper) {
// 从body上删除掉wrapper
body.removeChild(wrapper as HTMLElement)
// 从div img集合中删除掉
wrappers.delete(el._vpreloadUrl)
}
}
}
}
}
这样,对于在 background-image 中的图素,v-preImg 也可以将其创建为 <img />
。搭配 useAllImageLoaded() Hook,可以将 v-preImg 创建的 <img />
收集起来,并同其他预加载图素loaded后,执行 hook 的 callback,关闭整页 loading。
-
你可能想问,useAllImageLoaded() 一定能保证在 v-preImg 创建完
<img
/
>
后收集么?- 答案:一定的!这和 Vue 的组件渲染原理有关。在 useAllImageLoaded() 中我们是在 mounted 生命周期中进行
<img
/
>
收集的。在 mounted 时,当前组件的下的全部子节点(不包括子组件的子节点)一定是挂载完成的。updated 生命周期也同理。
- 答案:一定的!这和 Vue 的组件渲染原理有关。在 useAllImageLoaded() 中我们是在 mounted 生命周期中进行
-
子组件下的子节点即然不能保证?那该怎么办?
- 答案很简单,useAllImageLoaded() 和 v-preImg 不推荐跨组件层级使用,应该在子组件下使用 useAllImageLoaded() 和 v-preImg。
八. 开始轻松的使用预加载
下面代码为在本文标题二中的demo示例。展示了如何通过本文的 useAllImageLoaded() Hook + v-preImg 来对不同类型的图素进行预加载。
html
<template>
<Loader v-if="loading"></Loader>
<div ref="container" class="test-img-wrapper">
<button @click="toggleShow" v-pre-img>显示or隐藏 含有background-image的div</button> <br />
<button @click="showImg">通过js挂载曾加载过src的img标签</button>
<br />
<button @click="toggleHiddenBgShow">挂载or删除 动态渲染的background-image的div</button>
<br />
<div class="img-box">
这是img标签渲染的图片1 ↓
<img v-pre-img src="https://gips2.baidu.com/it/u=2128184746,1403673116&fm=3039&app=3039&f=PNG?w=1024&h=1024" alt="">
</div>
<div class="img-box">
这是img标签渲染的图片2 ↓
<img src="https://gips2.baidu.com/it/u=45931653,2379222580&fm=3039&app=3039&f=PNG?w=1024&h=1024" alt="">
</div>
<div></div>
<div class="img-box" v-show="show" style="color: blue">
这是div通过css background-image渲染的图片1 ↓
<div v-pre-img class="img-wrapper-1"></div>
</div>
<div class="img-box" v-show="show" style="color: blue">
这是div通过css background-image渲染的图片2 ↓
<div v-pre-img class="img-wrapper-2"></div>
</div>
<div class="img-box" v-show="show" style="color: blue">
这是div通过css background-image渲染的图片3 ↓
<div v-pre-img class="img-wrapper-3"></div>
</div>
<div v-if="hiddenBgShow" class="img-box" style="color: green">
这是javascript新创建的图片 ↓
<div class="hidden-bg-wrapper"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import Loader from './Loader.vue'
import vPreImg from '@/directives/preload-img/v-preImg'
import useAllImageLoaded from '@/hooks/useAllImageLoaded'
const show = ref(false)
const loading = ref(true)
const container = ref(null)
useAllImageLoaded(
// 预加载处理的DOM范围
container,
// 需要预加载的动态图素的链接
['https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg'],
// 全部图素预加载完成的回调函数
() => {
loading.value = false // 关闭整页loading
}
)
// (container + 动态链接集合 + 回调)
// useAllImageLoaded(container, ['https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg'], () => {
// loading.value = false
// })
// (动态链接集合 + 回调)
// useAllImageLoaded(['https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg'], () => {
// loading.value = false
// })
// (container+ 回调)
// useAllImageLoaded(container, () => {
// loading.value = false
// })
// 带入错误的container类型
// useAllImageLoaded(show, ['https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg'],() => {
// loading.value = false
// })
function scrollBottom() {
nextTick(() => {
document.body.scrollTop = document.body.scrollHeight - document.body.clientHeight
})
}
function toggleShow() {
show.value = !show.value
scrollBottom()
}
function showImg() {
scrollBottom()
const div = document.createElement('div')
div.classList.add('img-box')
div.style.color = 'purple'
const tipText = document.createTextNode('这是通过js挂载的,曾加载过src的img标签')
div.appendChild(tipText)
const img = new Image(200)
img.src = 'https://gips2.baidu.com/it/u=45931653,2379222580&fm=3039&app=3039&f=PNG?w=1024&h=1024'
div.appendChild(img)
container.value.appendChild(div)
}
const hiddenBgShow = ref(false)
function toggleHiddenBgShow() {
hiddenBgShow.value = !hiddenBgShow.value
scrollBottom()
}
</script>
<style lang="scss">
img {
height: 200px;
width: 200px;
}
.img-wrapper {
&-1, &-2, &-3 {
border: 1px red solid;
height: 200px;
width: 200px;
background-size: 100%;
}
&-1 {
background-image: url("https://img0.baidu.com/it/u=3363994336,562770620&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400");
}
&-2 {
background-image: url("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fblog%2F202102%2F13%2F20210213025356_s52TL.jpeg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1719381992&t=f2d3a454843d63042ed68fe4f25f5dd8");
}
&-3 {
background-image: url("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcbu01.alicdn.com%2Fimg%2Fibank%2FO1CN01jhVYw71UFY7OUUTbW_%21%212208119382488-0-cib.jpg&refer=http%3A%2F%2Fcbu01.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1719382157&t=b7f0a0119405e3cb0c1e6fa7e9849bb2");
}
}
.loading {
@include flex-center;
height: 100px;
width: 100px;
font-size: 0.5rem;
color: white;
border-radius: 50px;
background: red;
}
.test-img-wrapper {
padding-left: 20px;
div.img-box {
display: inline-flex;
align-items: center;
flex-direction: column;
// width : 500px ;
font-size: 16px;
color: red;
margin-top: 10px;
margin-right: 10px;
}
}
.hidden-bg-wrapper {
border: 3px black solid;
height: 200px;
width: 200px;
background: url('https://p4.itc.cn/images01/20230830/1c8dadf3b4114379aff79de75badd1c9.jpeg');
background-size: 100%;
}
</style>
Loader.vue
html
<template>
<div class="loader-container">
<div class="loader-01">
</div>
<div style="font-size: 16px; margin-top: 10px">整页加载中</div>
</div>
</template>
<style lang="scss" scoped>
body {
background: -webkit-radial-gradient(ellipse farthest-corner at center bottom, #69d2fb 0%, #20438e 100%) center bottom/100% fixed;
background: radial-gradient(ellipse farthest-corner at center bottom, #69d2fb 0%, #20438e 100%) center bottom/100% fixed;
text-align: center;
box-sizing: border-box;
font-family: sans-serif;
color: rgba(255, 255, 255, 0.8);
}
body *,
body *:before,
body *:after {
box-sizing: inherit;
}
.loader-container {
@include flex-center;
flex-direction: column;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
border: 3px #FF7E00 solid;
border-radius: 10px;
background: white;
}
[class*="loader-"] {
color: inherit;
vertical-align: middle;
pointer-events: none;
}
.loader-01 {
width: 1em;
height: 1em;
border: .2em dotted #FF7E00;
border-radius: 50%;
-webkit-animation: 1s loader-01 linear infinite;
animation: 1s loader-01 linear infinite;
}
@-webkit-keyframes loader-01 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loader-01 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
</style>