feat: 图片lazy loading
This commit is contained in:
0
src/components/AspectRatio.astro
Normal file
0
src/components/AspectRatio.astro
Normal file
131
src/components/LazyImage.astro
Normal file
131
src/components/LazyImage.astro
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
src,
|
||||||
|
alt = "",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
class: className = ""
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={`lazy-image ${className}`}
|
||||||
|
data-src={src}
|
||||||
|
style={`aspect-ratio:${width}/${height}`}
|
||||||
|
>
|
||||||
|
<div class="placeholder"></div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
class="real-image"
|
||||||
|
alt={alt}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lazy-image {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-image img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #f3f4f6;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#f3f4f6 0%,
|
||||||
|
#e5e7eb 50%,
|
||||||
|
#f3f4f6 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
from {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-image {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-image.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function initLazyImages() {
|
||||||
|
const images = document.querySelectorAll(".lazy-image");
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
|
||||||
|
if (!entry.isIntersecting) return;
|
||||||
|
|
||||||
|
const container = entry.target as HTMLElement;
|
||||||
|
const img = container.querySelector(
|
||||||
|
".real-image"
|
||||||
|
) as HTMLImageElement;
|
||||||
|
|
||||||
|
const src = container.dataset.src;
|
||||||
|
|
||||||
|
if (!src) return;
|
||||||
|
|
||||||
|
img.src = src;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
img.classList.add("loaded");
|
||||||
|
|
||||||
|
const placeholder =
|
||||||
|
container.querySelector(".placeholder");
|
||||||
|
|
||||||
|
placeholder?.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
observer.unobserve(container);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: "300px"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
images.forEach(img => observer.observe(img));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
initLazyImages();
|
||||||
|
</script>
|
||||||
@@ -4,6 +4,7 @@ import Folder from "@/assets/icon/folder.png?url";
|
|||||||
import Calendar from "@/assets/icon/calendar.png?url";
|
import Calendar from "@/assets/icon/calendar.png?url";
|
||||||
import type {Post} from "@/types/post";
|
import type {Post} from "@/types/post";
|
||||||
import {formatDatetime} from "@/lib/format.ts";
|
import {formatDatetime} from "@/lib/format.ts";
|
||||||
|
import LazyImage from "@/components/LazyImage.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
posts: Post[]
|
posts: Post[]
|
||||||
@@ -23,7 +24,7 @@ posts.map((post) => {
|
|||||||
<a href={`/post/${post.slug}`}>
|
<a href={`/post/${post.slug}`}>
|
||||||
<article class="post-card">
|
<article class="post-card">
|
||||||
<div class="post-cover">
|
<div class="post-cover">
|
||||||
<img src={post.cover} alt={post.title} loading="lazy"/>
|
<LazyImage src={post.cover} alt={post.title} />
|
||||||
</div>
|
</div>
|
||||||
<div class="post-body">
|
<div class="post-body">
|
||||||
<div class="post-meta">
|
<div class="post-meta">
|
||||||
@@ -112,12 +113,6 @@ posts.map((post) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-cover img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-body {
|
.post-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const { list, total } = await http.get<ApiPageResponse<Post>>("posts", {
|
|||||||
const totalPage = Math.ceil(total / metadata.postPageSize);
|
const totalPage = Math.ceil(total / metadata.postPageSize);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout description="天若有情天亦老,人间正道是沧桑。" title="博客首页">
|
||||||
<IndexView
|
<IndexView
|
||||||
posts={list}
|
posts={list}
|
||||||
page={1}
|
page={1}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ if (totalPage < Number(page)) {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout description="天若有情天亦老,人间正道是沧桑。" title="博客首页">
|
||||||
<IndexView
|
<IndexView
|
||||||
posts={list}
|
posts={list}
|
||||||
page={Number(page)}
|
page={Number(page)}
|
||||||
|
|||||||
Reference in New Issue
Block a user