克隆 Youtube 网站

Chen大约 9 分钟React前端项目RapidApimaterial UI

本文通过使用 ReactRapidApi 来快速构建 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 下能看到 NavbarFeed 字样。
同时你可以在 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

访问地址open in new window 然后注册登录使用 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...