克隆 Youtube 网站
本文通过使用 React
和 RapidApi
来快速构建 Youtube
网站
Init 初始化项目
- 使用
create-react-app
可以使用
vite
更快一些!
npx create-react-app ./
# ./ 表示当前目录下创建
# npx 是 Node 包执行器
# npx 可以简化了包运行的成本,既可以运行本地包,也可以远程包。无需安装包也可以执行该包,可以有效避免本地磁盘污染的问题,节省本地磁盘空间
- 安装项目所需的依赖
- 修改
package.json
- 修改
{
"dependencies": {
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@mui/icons-material": "5.8.4",
"@mui/material": "^5.9.3",
"axios": "^0.27.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-player": "^2.10.2",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
}
npm install --legacy-peer-deps
# --legacy-peer-deps 安装依赖项的确切版本
- 删除
src
目录
重头创建新的
src
目录,望你清楚知道react
的工作流程
- 创建
src/index.js
- 你需要知道,该文件作为
react
项目的入口文件
- 你需要知道,该文件作为
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// 创建 root
const root = ReactDOM.createRoot(document.getElementById('root'))
// 调用 render
root.render(<App />)
- 创建
App.js
- 作为项目的应用程序的起点(
App
组件)
- 作为项目的应用程序的起点(
// rafce 一键生成 react 代码块
import React from 'react'
const App = () => {
return (
<div>
App
</div>
)
}
export default App
- 修改
/public/index.html
- 使用
Material UI
组件库的Roboto
字体和Font Icons
字体图标
在国内,你可以使用比较受欢迎的
React
组件库 ——Ant Design
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<title>Youtube clone</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
- 增加一些样式
index.css
html,
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
a {
text-decoration: none;
color: black;
}
::-webkit-scrollbar {
width: 0px;
height: 5px;
}
::-webkit-scrollbar-thumb {
background-color: rgb(114, 113, 113);
border-radius: 10px;
height: 200px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
.category-btn:hover {
background-color: #FC1503 !important;
color: white !important;
}
.category-btn:hover span {
color: white !important;
}
.react-player {
height: 77vh !important;
width: 100% !important;
}
.search-bar {
border: none;
outline: none;
width: 350px;
}
.category-btn {
font-weight: bold !important;
text-transform: capitalize;
display: flex;
align-items: center;
justify-content: start;
cursor: pointer;
background: transparent;
outline: none;
border: none;
padding: 7px 15px;
margin: 10px 0px;
border-radius: 20px;
transition: all 0.3s ease;
}
@media screen and (max-width: 900px) {
.category-btn {
margin: 10px;
}
}
@media screen and (max-width:800px) {
.copyright {
display: none !important;
}
}
@media screen and (max-width: 600px) {
.scroll-horizontal {
overflow: auto !important;
}
.react-player {
height: 45vh !important;
}
.search-bar {
width: 200px;
}
}
记得在
index.js
中写入import './index.css'
导入哦!
好极了!
你现在可以访问 localhost:3000
看见 App
字样!
Layout 布局搭建
- 使用
ReactRouter
路由- 在
App.js
中我们需要完成路由的注册
- 在
// rafce 一键生成 react 代码块
// 导包
import { BrowserRouter, Routes, Route } from "react-router-dom"
import { Box } from "@mui/system"
const App = () => {
return (
// 注册路由
<BrowserRouter>
{/* 给 Box 传入 sx Props */}
<Box sx={{ backgroundColor: '#000' }}>
<Navbar />
<Routes>
{/* ReactRouter5 写法 ==> <Route path="/xxx" component={<Foo />}/> */}
<Route path="/" exact element={<Feed />} />
<Route path="/video/:id" element={<VideoDetail />} />
<Route path="/channel/:id" element={<ChannelDetail />} />
<Route path="/search/:searchTerm" element={<SearchFeed />} />
</Routes>
</Box>
</BrowserRouter>
)
}
src
目录下创建components
目录依次完成路由组件的编写Navbar.jsx
VideoDetail.jsx
等剩余的路由组件依次类推,重点是先把Layout
布局结构搭建好!
import React from 'react'
function Navbar() {
return (
<div>
Navbar
</div>
)
}
export default Navbar
- 在
App.js
引入写好的路由组件
import Navbar from './components/Navbar'
import Feed from './components/Feed'
import SearchFeed from './components/SearchFeed'
import ChannelDetail from './components/ChannelDetail'
import VideoDetail from './components/VideoDetail'
- 不过我们可以优化一下,毕竟
App.js
看上去太“臃肿”了!- 新建
component/index.js
完成路由组件的暴露
- 新建
// component/index.js
export { default as Navbar } from './Navbar'
export { default as Feed } from './Feed'
export { default as ChannelDetail } from './ChannelDetail'
export { default as VideoDetail } from './VideoDetail'
export { default as SearchFeed } from './SearchFeed'
// App.js 就看上去比较精简,代码比较规范!
import { Navbar, VideoDetail, ChannelDetail, SearchFeed, Feed } from './components'
看上去不错!
此时在 localhost:3000
下能看到 Navbar
和 Feed
字样。
同时你可以在 url
地址栏输入对应的链接看到其他字样。
Navbar 导航栏的实现
- 实现导航栏就要去编写路由链接
- 在此之前前我们需要创建
utils/constants.js
文件用于常量的存放
import MusicNoteIcon from '@mui/icons-material/MusicNote';
import HomeIcon from '@mui/icons-material/Home';
import CodeIcon from '@mui/icons-material/Code';
import OndemandVideoIcon from '@mui/icons-material/OndemandVideo';
import SportsEsportsIcon from '@mui/icons-material/SportsEsports';
import LiveTvIcon from '@mui/icons-material/LiveTv';
import SchoolIcon from '@mui/icons-material/School';
import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
import CheckroomIcon from '@mui/icons-material/Checkroom';
import GraphicEqIcon from '@mui/icons-material/GraphicEq';
import TheaterComedyIcon from '@mui/icons-material/TheaterComedy';
import FitnessCenterIcon from '@mui/icons-material/FitnessCenter';
import DeveloperModeIcon from '@mui/icons-material/DeveloperMode';
export const logo = 'https://i.ibb.co/s9Qys2j/logo.png';
// 分类
export const categories = [
{ name: 'New', icon: <HomeIcon />, },
{ name: 'JS Mastery', icon: <CodeIcon />, },
{ name: 'Coding', icon: <CodeIcon />, },
{ name: 'ReactJS', icon: <CodeIcon />, },
{ name: 'NextJS', icon: <CodeIcon />, },
{ name: 'Music', icon: <MusicNoteIcon /> },
{ name: 'Education', icon: <SchoolIcon />, },
{ name: 'Podcast', icon: <GraphicEqIcon />, },
{ name: 'Movie', icon: <OndemandVideoIcon />, },
{ name: 'Gaming', icon: <SportsEsportsIcon />, },
{ name: 'Live', icon: <LiveTvIcon />, },
{ name: 'Sport', icon: <FitnessCenterIcon />, },
{ name: 'Fashion', icon: <CheckroomIcon />, },
{ name: 'Beauty', icon: <FaceRetouchingNaturalIcon />, },
{ name: 'Comedy', icon: <TheaterComedyIcon />, },
{ name: 'Gym', icon: <FitnessCenterIcon />, },
{ name: 'Crypto', icon: <DeveloperModeIcon />, },
];
export const demoThumbnailUrl = 'https://i.ibb.co/G2L2Gwp/API-Course.png';
export const demoChannelUrl = '/channel/UCmXmlB4-HJytD7wek0Uo97A';
export const demoVideoUrl = '/video/GDa8kZLNhJ4';
export const demoChannelTitle = 'JavaScript Mastery';
export const demoVideoTitle = 'Build and Deploy 5 JavaScript & React API Projects in 10 Hours - Full Course | RapidAPI';
export const demoProfilePicture = 'http://dergipark.org.tr/assets/app/images/buddy_sample.png'
别看到这么多代码就吓懵了!
仅仅是一些图标,分类就是一些对象组成的数组而已,其中每一个对象有一些属性罢了,还有一些无聊的视频地址和标题的字符串。
回到
Navbar.js
中,完成我们的目标!
// 引入 Stack 组件
import { Stack } from "@mui/material"
import { Link } from 'react-router-dom'
import { logo } from "../utils/constants"
// 跟之前创建组件一样
import SearchBar from './SearchBar'
const Navbar = () => (
// 如果不清楚 Stack 的用法,去 material UI 官网查询 Stack API (props...)
// 比如我不清楚 sx Props 官网是这么说的 "The system prop, which allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." (考英语...)
<Stack
direction="row"
alignItems="center"
p={2}
sx={{ position: 'sticky', background: "#000", top: 0, justifyContent: "space-between" }}
>
<Link to="/" style={{ display: "flex", alignItem: "center" }}>
<img src={logo} alt="logo" height={45} />
</Link>
{/* 搜索框组件 */}
<SearchBar />
</Stack>
)
export default Navbar
- 完成搜索框
SearchBar
组件
// 使用 hook
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Paper, iconButton } from '@mui/material'
// 引入搜索图标
import { Search } from '@mui/icons-material'
const SearchBar = () => {
return (
<div>
{/* Paper 本质上是 div component 为 form 表单 */}
<Paper
component="form"
onSubmit={() => { }}
sx={{
borderRadius: 20,
border: '1px solid #e3e3e3',
// pl 为 padding-left
pl: 2,
boxShadow: 'none',
// mr 为 margin-right 仅给 small device (sm) 设置
mr: { sm: 5 }
}}
>
{/* 输入框 */}
<input
className='search-bar'
placeholder='Search...'
value=""
onChange={() => { }}
/>
{/* 图标与按钮 */}
<IconButton type="submit" sx={{ p: '10px', color: 'red' }}>
<Search></Search>
</IconButton>
</Paper>
</div>
)
}
export default SearchBar
不错!
你基本上搞定搜索框了!不过后面我们得先把搜索框功能实现放一放!
Feed 和 Sidebar (视频提要和侧边栏的实现)
首先完成视频提要的布局!
import { useState, useEffect } from 'react'
import { Box, Stack, Typography } from '@mui/material'
import Siderbar from './Siderbar'
const Feed = () => {
return (
// 注意:虽然我们没有直接设置 sm (small device),但是设置了 sx 属性,这个属性会根据【断点】来自动调整方向属性。当我们没有指定某个断点的方向属性时,它会使用上一个断点的方向属性。
// 所以在这个例子中,当没有指定 sm 断点的方向属性时,它会使用 sx 断点的方向属性,也就是 column。这是 Material UI 的响应式布局的特性
<Stack sx={{ flexDirection: { sx: "column", /* md 为 middle devices */ md: "row" } }}>
<Box sx={{ height: { sx: 'auto', md: '92vh' }, borderRight: '1px solid #3d3d3d', /* px 为 padding */ px: { sx: 0, md: 2 } }}>
<Siderbar />
<Typography
className="copyright"
variant='body2'
// mt 为 margin-top
sx={{ mt: 1.5, color: '#fff' }}
>
Copyright 2023 Chen
</Typography>
</Box>
</Stack>
)
}
export default Feed
- 侧边栏
Sidebar
的实现
在
components
目录下创建Siderbar.jsx
记得在index.js
中暴露出去
import { Stack } from '@mui/material'
import { categories } from '../utils/constants'
const selectedCategory = 'New'
const Siderbar = () => {
return (
<Stack
direction="row"
sx={{
overflowY: 'auto',
height: { sx: 'auto', md: '95%' },
flexDirection: { md: 'column' }
}}
>
{categories.map(category => (
<button
className='category-btn'
style={{
background: category.name === selectedCategory && '#FC1503',
color: 'white'
}}
key={category.name}
>
<span
style={{ color: category.name === selectedCategory ? 'white' : 'red', marginRight: '15px' }}>
{category.icon}
</span>
<span
style={{ opacity: category.name === selectedCategory ? '1' : '0.8' }}>
{category.name}
</span>
</button>
))}
</Stack>
)
}
export default Siderbar
- 视频概要的实现
点击侧边栏的每一个按钮链接,在
Feed
区域需要显示其标题(表明)我们正在查看的类别
创建
Videos
组件(在index.js
中向外暴露)
Feed.js
中
//...
import { Siderbar, Videos } from './index'
const Feed = () => {
return (
<Stack /* ... */ >
<Box /* ... */ >
{/* ... */}
</Box>
<Box
{/* p 为 padding */}
p={2}
sx={{ overflowY: 'auto', height: '90vh', flex: 2 }}>
<Typography
variant='h4'
fontWeight="bold"
mb={2}
sx={{ color: 'white' }}
>
New <span style={{ color: '#F31503' }}>videos</span>
</Typography>
{/* 先暂时传入空数组 */}
<Videos videos={[]} />
</Box>
</Stack>
)
}
export default Feed
Excellent!
看上去我们的 Youtube
快要成型啦!下一步我们将会获取视频数据并显示!
API Data fetching 数据获取
- 使用
RapidApi
访问地址 然后注册登录使用
Youtube
免费的API
即可
- 在
utils
目录下新建fetchFormAPI.js
const axios = require("axios");
const BASE_URL = 'https://youtube-v31.p.rapidapi.com/captions';
// options 可以直接在 RapidAPI 官网中 Youtube API 中拷贝
// 不过还稍微做些改动
const options = {
method: 'GET',
url: BASE_URL,
params: {
maxResults: '50'
},
headers: {
// 密钥涉及到项目安全问题
'X-RapidAPI-Key': process.env.React_APP_RAPID_API_KEY,
'X-RapidAPI-Host': 'youtube-v31.p.rapidapi.com'
}
};
// 暴露异步方法 fetchFromAPI
// 比如 axios 请求 /Base_URL/getVideos/
export const fetchFromAPI = async (url) => {
const { data } = await axios.get(`${BASE_URL}/${url}`, options)
return data
}
- 项目根目录下创建
.env
文件
React_APP_RAPID_API_KEY = 'here is your key';
- 回到
Feed.jsx
import { useState, useEffect } from 'react'
import { fetchFromAPI } from '../utils/fetchFormAPI'
const Feed = () => {
const [selectedCategory, setSelectCategory] = useState('New')
useEffect(() => {
// 只会在(开始加载、selectedCategory 更新)运行这段代码
fetchFromAPI(`search?part=snippet&q=${selectedCategory}`)
}, [selectedCategory]);
return (
/* ... */
<Siderbar
selectedCategory={selectedCategory}
setSelectCategory={setSelectCategory}
/>
/* ... */
<Box p={2} sx={{ overflowY: 'auto', height: '90vh', flex: 2 }}>
<Typography
variant='h4'
fontWeight="bold"
mb={2}
sx={{ color: 'white' }}
>
{selectedCategory} <span style={{ color: '#F31503' }}>videos</span>
</Typography>
<Videos videos={[]} />
)
}
Siderbar.jsx
import { Stack } from '@mui/material'
import { categories } from '../utils/constants'
const Siderbar = ({ selectedCategory, setSelectedCategory }) => {
return (
<Stack
direction="row"
sx={{
overflowY: 'auto',
height: { sx: 'auto', md: '95%' },
flexDirection: { md: 'column' }
}}
>
{categories.map(category => (
<button
className='category-btn'
onClick={()=> setSelectedCategory(category.name)}
style={{
background: category.name === selectedCategory && '#FC1503',
color: 'white'
}}
key={category.name}
>
<span
style={{ color: category.name === selectedCategory ? 'white' : 'red', marginRight: '15px' }}>
{category.icon}
</span>
<span
style={{ opacity: category.name === selectedCategory ? '1' : '0.8' }}>
{category.name}
</span>
</button>
))}
</Stack>
)
}
export default Siderbar
现在点击按钮,按钮会有高亮突出
- 不过还需要完善一下
Feed.jsx
内容
fetchFromAPI 返回一个
Promise
对象,我们需要对返回Promise
对象进行处理,其次获取的数据需要保存在名为videos
的数组中
const Feed = () => {
const [selectedCategory, setSelectCategory] = useState('New')
const [videos, setVideos] = useState([])
useEffect(() => {
// 只会在(开始加载、selectedCategory 更新)运行这段代码
fetchFromAPI(`search?part=snippet&q=${selectedCategory}`)
.then(data => setVideos(data.items))
}, [selectedCategory]);
return (
/* ... */
<Videos videos={videos} />
/* ... */
)
}
然后在
Video.jsx
中测试打印下获取到的数据!可以看到有 50 条数据!
因为我们在fetchFormAPI
中的options
中设置maxResults
为 50
太棒了!
我们能正确的获取数据了,但还需要知道这些数据中哪些是我们需要的,且完成 Videos
组件布局!
TO BE CONTINUE...