Design of a MovieSlider Component
Design of a MovieSlider Component
MovieSlider 技术文档
目录
概述
MovieSlider
组件展示一个水平滚动的电影或电视节目列表,支持动态数据获取、左右滚动、响应式设计和用户交互优化。
依赖和引入
组件需要以下依赖:
- React
- React Router
- Axios
- Zustand(用于全局状态管理)
- Tailwind CSS(用于样式)
- Lucide-react(图标库)
组件结构
以下是 MovieSlider
组件的完整代码结构,包括必要的依赖和主要功能实现:
import { useEffect, useRef, useState } from "react";
import { useContentStore } from "../store/content";
import axios from "axios";
import { Link } from "react-router-dom";
import { SMALL_IMG_BASE_URL } from "../utils/constants";
import { ChevronLeft, ChevronRight } from "lucide-react";
const MovieSlider = ({ category }) => {
const { contentType } = useContentStore();
const [content, setContent] = useState([]);
const [showArrows, setShowArrows] = useState(false);
const sliderRef = useRef(null);
const formattedCategoryName =
category.replaceAll("_", " ")[0].toUpperCase() +
category.replaceAll("_", " ").slice(1);
const formattedContentType = contentType === "movie" ? "Movies" : "TV Shows";
useEffect(() => {
const getContent = async () => {
const res = await axios.get(`/api/v1/${contentType}/${category}`);
setContent(res.data.content);
};
getContent();
}, [contentType, category]);
const scrollLeft = () => {
if (sliderRef.current) {
sliderRef.current.scrollBy({
left: -sliderRef.current.offsetWidth,
behavior: "smooth",
});
}
};
const scrollRight = () => {
sliderRef.current.scrollBy({
left: sliderRef.current.offsetWidth,
behavior: "smooth",
});
};
return (
<div
className="bg-black text-white relative px-5 md:px-20"
onMouseEnter={() => setShowArrows(true)}
onMouseLeave={() => setShowArrows(false)}
>
<h2 className="mb-4 text-2xl font-bold">
{formattedCategoryName} {formattedContentType}
</h2>
<div
className="flex space-x-4 overflow-x-scroll scrollbar-hide"
ref={sliderRef}
>
{content.map((item) => (
<Link
to={`/watch/${item.id}`}
className="min-w-[250px] relative group"
key={item.id}
>
<div className="rounded-lg overflow-hidden">
<img
src={SMALL_IMG_BASE_URL + item.backdrop_path}
alt="Movie image"
className="transition-transform duration-300 ease-in-out group-hover:scale-125"
/>
</div>
<p className="mt-2 text-center">{item.title || item.name}</p>
</Link>
))}
</div>
{showArrows && (
<>
<button
className="absolute top-1/2 -translate-y-1/2 left-5 md:left-24 flex items-center justify-center
size-12 rounded-full bg-black bg-opacity-50 hover:bg-opacity-75 text-white z-10
"
onClick={scrollLeft}
>
<ChevronLeft size={24} />
</button>
<button
className="absolute top-1/2 -translate-y-1/2 right-5 md:right-24 flex items-center justify-center
size-12 rounded-full bg-black bg-opacity-50 hover:bg-opacity-75 text-white z-10
"
onClick={scrollRight}
>
<ChevronRight size={24} />
</button>
</>
)}
</div>
);
};
export default MovieSlider;
状态管理
- 内容类型:从全局状态
useContentStore
获取当前内容类型。 - 内容数据:使用
useState
存储获取的内容数据。 - 箭头显示:使用
useState
控制滑动箭头的显示状态。
以下代码展示了如何使用 useState
管理内容数据和箭头显示状态:
const { contentType } = useContentStore();
const [content, setContent] = useState([]);
const [showArrows, setShowArrows] = useState(false);
数据获取
通过 useEffect
钩子和 axios
库从 API 获取数据,并根据内容类型和分类的变化动态更新:
useEffect(() => {
const getContent = async () => {
const res = await axios.get(`/api/v1/${contentType}/${category}`);
setContent(res.data.content);
};
getContent();
}, [contentType, category]);
滑动功能
以下代码实现了左右滑动功能,使用 scrollBy
方法来移动内容:
const scrollLeft = () => {
if (sliderRef.current) {
sliderRef.current.scrollBy({
left: -sliderRef.current.offsetWidth,
behavior: "smooth",
});
}
};
const scrollRight = () => {
sliderRef.current.scrollBy({
left: sliderRef.current.offsetWidth,
behavior: "smooth",
});
};
响应式设计
- 使用 Tailwind CSS 类:确保在不同设备上有合适的内边距和间距。
- 最小宽度:确保每个内容项在不同屏幕尺寸下都有最小宽度。
className = "bg-black text-white relative px-5 md:px-20";
className = "flex space-x-4 overflow-x-scroll scrollbar-hide";
className = "min-w-[250px] relative group";
用户体验优化
- 悬停动画:在图片上添加悬停动画效果。
- 箭头显示控制:在鼠标悬停时显示箭头,提高用户导航的直观性。
以下代码通过悬停动画和箭头显示控制优化了用户体验
className='transition-transform duration-300 ease-in-out group-hover:scale-125'
onMouseEnter={() => setShowArrows(true)}
onMouseLeave={() => setShowArrows(false)}
学习资源
- MDN Web Docs: Element.scrollBy():
- MDN Web Docs: HTMLElement.offsetWidth:
- React Docs: Refs and the DOM:
示例代码
以下示例代码展示了如何使用 offsetWidth
来获取元素的宽度:
import React, { useRef } from "react";
const OffsetWidthExample = () => {
const boxRef = useRef(null);
useEffect(() => {
if (boxRef.current) {
console.log("offsetWidth:", boxRef.current.offsetWidth);
}
}, []);
return (
<div
ref={boxRef}
style={{
width: "200px",
padding: "20px",
border: "10px solid black",
margin: "10px",
}}
>
Hello, World!
</div>
);
};
export default OffsetWidthExample;
代码拓展
要设计一个更加通用和可复用的 MovieSlider
组件,我们需要遵循最佳的设计模式和代码实践。以下是一些关键点:
- 组件参数化:使组件接受更多参数以便更灵活地控制其行为和外观。
- 代码解耦:将数据获取逻辑和渲染逻辑分离,以提高代码的可维护性和可测试性。
- 类型检查:使用 TypeScript 或 PropTypes 进行类型检查,以确保组件的正确使用。
- 可扩展性:考虑未来可能的扩展需求,使组件易于扩展和修改。
- 最佳实践:遵循现代 React 开发的最佳实践,如使用函数组件、React Hooks 和自定义 Hooks。
详细设计说明
1. 组件参数化
我们可以通过接受更多的 props 来使 MovieSlider
更加通用。例如,允许传入自定义的 API 端点、滑动距离、是否显示箭头等。
2. 代码解耦
使用自定义 Hook 将数据获取逻辑抽离到组件外部。这样可以使组件更加专注于渲染逻辑,并且更容易进行单元测试。
3. 类型检查
使用 TypeScript 或 PropTypes 进行类型检查,确保组件的正确使用。
4. 可扩展性
考虑到未来的需求,如添加更多的滑动方向、不同的布局方式等,使组件易于扩展。
示例代码
以下是一个重构后的、更加通用的 MovieSlider
组件示例:
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import axios from "axios";
import { Link } from "react-router-dom";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { SMALL_IMG_BASE_URL } from "../utils/constants";
// 自定义 Hook 用于获取数据
const useFetchContent = (endpoint) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(endpoint);
setData(response.data.content);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, [endpoint]);
return { data, loading };
};
const MovieSlider = ({ endpoint, title, showArrows, scrollAmount }) => {
const { data: content, loading } = useFetchContent(endpoint);
const sliderRef = useRef(null);
const scrollLeft = () => {
if (sliderRef.current) {
sliderRef.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
const scrollRight = () => {
if (sliderRef.current) {
sliderRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
}
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="bg-black text-white relative px-5 md:px-20">
<h2 className="mb-4 text-2xl font-bold">{title}</h2>
<div
className="flex space-x-4 overflow-x-scroll scrollbar-hide"
ref={sliderRef}
>
{content.map((item) => (
<Link
to={`/watch/${item.id}`}
className="min-w-[250px] relative group"
key={item.id}
>
<div className="rounded-lg overflow-hidden">
<img
src={SMALL_IMG_BASE_URL + item.backdrop_path}
alt={item.title || item.name}
className="transition-transform duration-300 ease-in-out group-hover:scale-125"
/>
</div>
<p className="mt-2 text-center">{item.title || item.name}</p>
</Link>
))}
</div>
{showArrows && (
<>
<button
className="absolute top-1/2 -translate-y-1/2 left-5 md:left-24 flex items-center justify-center
size-12 rounded-full bg-black bg-opacity-50 hover:bg-opacity-75 text-white z-10"
onClick={scrollLeft}
>
<ChevronLeft size={24} />
</button>
<button
className="absolute top-1/2 -translate-y-1/2 right-5 md:right-24 flex items-center justify-center
size-12 rounded-full bg-black bg-opacity-50 hover:bg-opacity-75 text-white z-10"
onClick={scrollRight}
>
<ChevronRight size={24} />
</button>
</>
)}
</div>
);
};
// 使用 PropTypes 进行类型检查
MovieSlider.propTypes = {
endpoint: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
showArrows: PropTypes.bool,
scrollAmount: PropTypes.number,
};
// 默认 props
MovieSlider.defaultProps = {
showArrows: true,
scrollAmount: 300,
};
export default MovieSlider;
组件参数解释
endpoint
: API 端点,用于获取内容数据。title
: 滑动组件的标题。showArrows
: 是否显示左右滑动箭头。scrollAmount
: 每次滑动的距离。
如何使用
import React from "react";
import MovieSlider from "./components/MovieSlider";
const App = () => {
return (
<div>
<MovieSlider
endpoint="/api/v1/movies/popular"
title="Popular Movies"
showArrows={true}
scrollAmount={500}
/>
<MovieSlider
endpoint="/api/v1/tv/top_rated"
title="Top Rated TV Shows"
showArrows={false}
scrollAmount={400}
/>
</div>
);
};
export default App;
上面的代码示例使用的是 JavaScript。如果你更熟悉 TypeScript,也可以用 TypeScript 来实现类型检查。下面我将分别说明如何在 JavaScript 和 TypeScript 中进行类型检查。
JavaScript 中的类型检查
在 JavaScript 中,我们通常使用 PropTypes
来进行类型检查。PropTypes
是 React 内置的一个库,它允许你定义组件 props 的类型,并在开发过程中进行检查。
代码解释
-
PropTypes:
PropTypes
用于定义组件的 prop 类型。例如,PropTypes.string
表示该 prop 应该是一个字符串类型。PropTypes.string.isRequired
表示该 prop 是必须的,如果未提供将会发出警告。PropTypes.bool
表示布尔类型的 prop。PropTypes.number
表示数字类型的 prop。
-
defaultProps:
defaultProps
用于定义组件 prop 的默认值。如果未提供该 prop,组件将使用默认值。
import PropTypes from "prop-types";
const MovieSlider = ({ endpoint, title, showArrows, scrollAmount }) => {
// ... 组件实现
};
MovieSlider.propTypes = {
endpoint: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
showArrows: PropTypes.bool,
scrollAmount: PropTypes.number,
};
MovieSlider.defaultProps = {
showArrows: true,
scrollAmount: 300,
};
export default MovieSlider;
TypeScript 中的类型检查
在 TypeScript 中,我们通过接口或类型别名来定义 props 的类型,并在函数组件中使用这些类型。
代码示例
-
定义接口:
- 使用
interface
定义组件的 props 类型。
- 使用
-
在组件中使用类型:
- 在函数组件的参数中使用定义好的接口类型。
import React, { useEffect, useRef, useState } from "react";
interface MovieSliderProps {
endpoint: string;
title: string;
showArrows?: boolean;
scrollAmount?: number;
}
const MovieSlider: React.FC<MovieSliderProps> = ({
endpoint,
title,
showArrows = true,
scrollAmount = 300,
}) => {
const [content, setContent] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const sliderRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const getContent = async () => {
try {
const response = await fetch(endpoint);
const data = await response.json();
setContent(data.content);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
getContent();
}, [endpoint]);
const scrollLeft = () => {
if (sliderRef.current) {
sliderRef.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
const scrollRight = () => {
if (sliderRef.current) {
sliderRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
}
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="bg-black text-white relative px-5 md:px-20">
<h2 className="mb-4 text-2xl font-bold">{title}</h2>
<div
className="flex space-x-4 overflow-x-scroll scrollbar-hide"
ref={sliderRef}
>
{content.map((item) => (
<a
href={`/watch/${item.id}`}
className="min-w-[250px] relative group"
key={item.id}
>
<div className="rounded-lg overflow-hidden">
<img
src={`https://image.tmdb.org/t/p/w200${item.backdrop_path}`}
alt={item.title || item.name}
className="transition-transform duration-300 ease-in-out group-hover:scale-125"
/>
</div>
<p className="mt-2 text-center">{item.title || item.name}</p>
</a>
))}
</div>
{showArrows && (
<>
<button
className="absolute top-1/2 -translate-y-1/2 left-5 md:left-24 flex items-center justify-center
size-12 rounded-full bg-black bg-opacity-50 hover:bg-opacity-75 text-white z-10"
onClick={scrollLeft}
>
<ChevronLeft size={24} />
</button>
<button
className="absolute top-1/2 -translate-y-1/2 right-5 md:right-24 flex items-center justify-center
size-12 rounded-full bg-black bg-opacity-50 hover:bg-opacity-75 text-white z-10"
onClick={scrollRight}
>
<ChevronRight size={24} />
</button>
</>
)}
</div>
);
};
export default MovieSlider;
总结
通过重构后的 MovieSlider
组件,我们实现了一个更加通用和可复用的组件。该组件通过自定义 Hook 进行数据获取,接受多个参数以便灵活控制其行为和外观,并使用 PropTypes 进行类型检查。这种设计模式和代码实践提高了组件的可维护性和可扩展性,便于在不同项目中复用。