React项目案例-影视资源网站

  • Post author:
  • Post category:其他


案例项目基本需求分析

  • 前端页面布局使用Chakra-UI

  • 后端服务使用Next.js

  • 样式定义采用CSS-in-JS的方案,使用emotion库(需要在Next.js扩展babel配置)

  • Next.js 与Chakra-UI结合使用实现项目页面的功能

  • 首页(列表页)的轮播图,以及影视资源列表展示

  • 影视详情页使用基因动态路由的静态生成

项目代码初始化

运行安装依赖包命令:

npm init next-app aimovie

cd aimovie

#npm i @chakra-ui/react

#npm i @chakra-ui/react@^1 @emotion/react@^11 @emotion/styled@^11 framer-motion@^6

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion

npm i react-icons

npm i @babel/core @emotion/babel-preset-css-prop -D

{
  "name": "aimovie",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@babel/core": "^7.21.0",
    "@chakra-ui/react": "^2.5.1",
    "@emotion/react": "^11.10.6",
    "@emotion/styled": "^11.10.6",
    "framer-motion": "^10.2.5",
    "next": "13.2.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-icons": "^4.8.0"
  }
}

创建pages目录下的 _app.js 文件:

// import '@/styles/globals.css'
// pages/_app.js
import { ChakraProvider, CSSReset } from '@chakra-ui/react'
import theme from '../chakra'

function App ({ Component, pageProps }) {
  return (
    <ChakraProvider theme={theme}>
      <CSSReset />
      <Component {...pageProps} />
    </ChakraProvider>
  )
}

export default App


创建 .babelrc 文件作为babel的配置文件:(在新版的next.js中不需要配置,默认开启了cssProp,否则会产生next/font冲突)

{
  "presets": [
    "next/babel",
    "@emotion/babel-preset-css-prop"
  ]
}

页面组件规划与布局实现

实现页面头部组件

头部组件包含三部分:左侧的登录注册按钮组件 中间的网站logo 右边的搜索按钮。

布局规划:左浮动 有浮动 中间logo始终居中。

创建components文件夹,用于存放非页面层级的组件。

头部组件基本布局:

import React from 'react'
import { Box, Button, Container, Image } from '@chakra-ui/react'
import { FaSearch, FaSignInAlt, FaUserAlt } from 'react-icons/fa'
import styled from '@emotion/styled'
import { css } from '@emotion/react'
const SignInAndJoin = styled.div`
  height: 52px;
  line-height: 52px;
  color: #fff;
  border-left: 1px solid #393939;
  border-right: 1px solid #393939;
  padding: 0 6px;
  float: left;
`
const logo = css`
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 140px;
  height: auto;
`

const Search = styled.a`
  float: right;
  height: 52px;
  border-left: 1px solid #393939;
  border-right: 1px solid #393939;
  color: #fff;
  padding: 0 10px;
  display: flex;
  align-items: center;
`
export default function Header () {
  return (
    <Box h='52px' bgColor='#202020' borderBottom='1px solid #393939'>
      <Container pos='relative' h='52px' maxW='1200px'>
        <SignInAndJoin>
          <Button
            mr='5px'
            leftIcon={<FaSignInAlt />}
            colorScheme='teal'
            variant='solid'
          >
            登录
          </Button>
          <Button
            leftIcon={<FaUserAlt />}
            colorScheme='orange'
            variant='outline'
          >
            注册
          </Button>
        </SignInAndJoin>
        <Image css={logo} src='/images/logo.png' />
        <Search>
          <FaSearch size='16px' title='搜索' />
        </Search>
      </Container>
    </Box>
  )
}

实现导航组件布局

import React from 'react'
import { Box, HStack, Link } from '@chakra-ui/react'
import NextLink from 'next/link'
import styles from '@/styles/Navigation.module.css'
import { useRouter } from 'next/router'

export default function Navigation () {
  const router = useRouter()
  const isActiveLink = href => router.asPath === href
  return (
    <Box height='52px' bgColor='#202020' color='#fff'>
      <HStack h='100%' spacing={3} justifyContent='center' alignItems='center'>
        <Link
          className={`${styles.navlink} ${
            isActiveLink('/') ? styles.active : ''
          }`}
          href='/'
          as={NextLink}
        >
          影片
        </Link>
        <Link
          className={`${styles.navlink} ${
            isActiveLink('/1') ? styles.active : ''
          }`}
          href='/1'
          as={NextLink}
        >
          漫画
        </Link>
        <Link
          className={`${styles.navlink} ${
            isActiveLink('/2') ? styles.active : ''
          }`}
          href='/2'
          as={NextLink}
        >
          电影
        </Link>
        <Link
          className={`${styles.navlink} ${
            isActiveLink('/3') ? styles.active : ''
          }`}
          href='/3'
          as={NextLink}
        >
          电视
        </Link>
        <Link
          className={`${styles.navlink} ${
            isActiveLink('/4') ? styles.active : ''
          }`}
          href='/4'
          as={NextLink}
        >
          新闻资讯
        </Link>
      </HStack>
    </Box>
  )
}

实现轮播图组件布局

这里使用到一个第三方组件

react-responsive-carousel

npm i react-responsive-carousel

import React from 'react'
import { Carousel } from 'react-responsive-carousel'
import 'react-responsive-carousel/lib/styles/carousel.min.css'
import { css } from '@emotion/react'
import { Box, Stack, Heading, Text, Button } from '@chakra-ui/react'
import styled from '@emotion/styled'
const CarouselItem = styled.div`
  position: relative;
  & > div {
    position: absolute;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    color: #fff;
    width: 80%;
    height: 100%;
    max-width: 1200px;
    text-align: left;
    & > h2 {
      width: 450px;
    }
    & > p {
      margin: 15px 0;
      width: 450px;
    }
  }
  & > img {
    filter: brightness(50%);
  }
`
const swiperContainer = css`
  & > .carousel-root {
    position: relative;
    & > .carousel:last-child {
      position: absolute;
      left: 0;
      bottom: 0;
      & > .thumbs-wrapper > .thumbs {
        display: flex;
        justify-content: center;
      }
    }
  }
`

export default function Swiper () {
  return (
    <Box css={swiperContainer}>
      <Carousel
        // autoPlay
        infiniteLoop
        emulateTouch
        showArrows={false}
        showIndicators={false}
        showStatus={false}
      >
        <CarouselItem>
          <img src='/images/1.jpg' />
          <Stack justifyContent='center'>
            <Heading as='h1' size={'lg'}>
              King In Black
            </Heading>
            <Text>
              The next shock chapter in Donny Cates and Ryan Stegman hello look
              at me! shock chapter in Donny Cates and Ryan Stegman hello look at
              me!
            </Text>
            <Button colorScheme='red' w='150px'>
              Go To This
            </Button>
          </Stack>
        </CarouselItem>
        <CarouselItem>
          <img src='/images/2.jpg' />
          <Stack justifyContent='center'>
            <Heading as='h1' size={'lg'}>
              King In Black
            </Heading>
            <Text>
              The next shock chapter in Donny Cates and Ryan Stegman hello look
              at me! shock chapter in Donny Cates and Ryan Stegman hello look at
              me!
            </Text>
            <Button colorScheme='red' w='150px'>
              Go To This
            </Button>
          </Stack>
        </CarouselItem>
        <CarouselItem>
          <img src='/images/3.jpg' />
          <Stack justifyContent='center'>
            <Heading as='h1' size={'lg'}>
              King In Black
            </Heading>
            <Text>
              The next shock chapter in Donny Cates and Ryan Stegman hello look
              at me! shock chapter in Donny Cates and Ryan Stegman hello look at
              me!
            </Text>
            <Button colorScheme='red' w='150px'>
              Go To This
            </Button>
          </Stack>
        </CarouselItem>
      </Carousel>
    </Box>
  )
}

实现电影列表布局

import React from 'react'
import { Box, Heading, HStack, Text } from '@chakra-ui/react'
import { FaFilm } from 'react-icons/fa'
import { MdMovie } from 'react-icons/md'

export default function Movie () {
  return (
    <Box maxW='1200px' mx='auto' mt='20px'>
      <HStack fontSize='24px' my='10px'>
        <MdMovie size='24px' />
        <Heading as='h3'>电影</Heading>
      </HStack>
      <HStack mt='20px' spacing={3}>
        <Box w={'290px'}>
          <img src='/images/item_1.jpg' />
          <Text mt='10px' as='section'>
            Expected corresponding JSX closing tag for Black Hero
          </Text>
        </Box>
        <Box w={'290px'}>
          <img src='/images/item_1.jpg' />
          <Text mt='10px' as='section'>
            Expected corresponding JSX closing tag for Black Hero
          </Text>
        </Box>
        <Box w={'290px'}>
          <img src='/images/item_1.jpg' />
          <Text mt='10px' as='section'>
            Expected corresponding JSX closing tag for Black Hero
          </Text>
        </Box>
        <Box w={'290px'}>
          <img src='/images/item_1.jpg' />
          <Text mt='10px' as='section'>
            Expected corresponding JSX closing tag for Black Hero
          </Text>
        </Box>
        <Box w={'290px'}>
          <img src='/images/item_1.jpg' />
          <Text mt='10px' as='section'>
            Expected corresponding JSX closing tag for Black Hero
          </Text>
        </Box>
        <Box w={'290px'}>
          <img src='/images/item_1.jpg' />
          <Text mt='10px' as='section'>
            Expected corresponding JSX closing tag for Black Hero
          </Text>
        </Box>
      </HStack>
    </Box>
  )
}

实现电影详情页面布局

import Layout from '@/components/Layout'
import React from 'react'
import { Box, HStack, Text, Heading, Divider } from '@chakra-ui/react'
import { css } from '@emotion/react'

const DetailContainer = css`
  padding: 10px;
  & > p {
    font-size: 14px;
    margin-bottom: 10px;
  }
  & > img {
    display: block;
    margin-bottom: 10px;
  }
`
export default function Detail () {
  return (
    <Layout>
      <Box maxW='1200px' mx='auto' width='80%' mt='70px'>
        <Heading as='h2' size='xl'>
          Marvel Mission recap: Captain Marvel's Star of Hala
        </Heading>
        <Heading
          as='h4'
          size='lg'
          mt='10px'
          color='gray.500'
          fontWeight='light'
        >
          The result are out this world!
        </Heading>
        <Divider mt='10px' />
        <HStack
          overflow='hidden'
          justifyContent='space-between'
          spacing={3}
          my='10px'
        >
          <Text>作者: MarkTony</Text>
          <Text>发布时间: 2040-05-10</Text>
        </HStack>
        <Divider mt='10px' />
        <Box css={DetailContainer}>
          <p>
            event - compiled client and server successfully in 377 ms (1213
            modules) wait - compiling... event - compiled client and server
            successfully in 520 ms (1213 modules) wait - compiling... event -
            compiled client and server successfully in 646 ms (1213 modules)
            wait - compiling...
          </p>
          <p>
            event - compiled client and server successfully in 377 ms (1213
            modules) wait - compiling... event - compiled client and server
            successfully in 520 ms (1213 modules) wait - compiling... event -
            compiled client and server successfully in 646 ms (1213 modules)
            wait - compiling...
          </p>
          <p>
            event - compiled client and server successfully in 377 ms (1213
            modules) wait - compiling... event - compiled client and server
            successfully in 520 ms (1213 modules) wait - compiling... event -
            compiled client and server successfully in 646 ms (1213 modules)
            wait - compiling...
          </p>
          <p>
            event - compiled client and server successfully in 377 ms (1213
            modules) wait - compiling... event - compiled client and server
            successfully in 520 ms (1213 modules) wait - compiling... event -
            compiled client and server successfully in 646 ms (1213 modules)
            wait - compiling...
          </p>
        </Box>
      </Box>
    </Layout>
  )
}

实现首页组件的静态生成

轮播图数据获取与展示

这里我们需要使用到axios库来发送请求以获取我们所需的数据:

npm i axios

在根目录下创建一个axiosConfig.js文件:

export const baseURL = 'http://localhost:3000'
export default {
  // thrid-party API base URL
  baseURL
}

在pages/api目录下创建swiper.js的文件,模拟获取swiper的API供组件调用:

import { baseURL as url } from '@/axiosConfig'

export default function swiper (req, res) {
  res.status(200).json([
    {
      id: 1,
      title: 'Event-Sized Episode!',
      description:
        "Paul Scheer and Steve Wacker are joined by Anthony Carboni of 'The Star Wars Show' for an event sized episode!",
      url: `${url}/apiresources/images/api_swiper_1.jpg`,
      vid: 1
    },
    {
      id: 2,
      title: 'Time Travel Tips',
      description:
        'Traveling back in time is never easy? Let us help by consulting the pros!',
      url: `${url}/apiresources/images/api_swiper_2.jpg`,
      vid: 2
    },
    {
      id: 3,
      title: 'KING IN BLACK',
      description:
        "The next shocking chapter in Donny Cates and Ryan Stegman's Venom Saga is revealed!",
      url: `${url}/apiresources/images/api_swiper_3.jpg`,
      vid: 3
    },
    {
      id: 4,
      title: "LET'S PLAY FORTNITE",
      description:
        'Watch as we stream the brand new Captain America outfit in Fortnite!',
      url: `${url}/apiresources/images/api_swiper_4.jpg`,
      vid: 4
    },
    {
      id: 5,
      title: 'HAPPY ULTRAMAN DAY!',
      description:
        "Celebrate by getting a sneak peek at 'Rise of Ultraman #1'!",
      url: `${url}/apiresources/images/api_swiper_5.jpg`,
      vid: 5
    }
  ])
}

components/Swiper.js组件文件中,创建并导出loadSwiper函数:

// components/Swiper.js
export function loadSwiper () {
  // 这离获取数据不建议从本地通过API接口获取,而应该通过封装后端的方法函数直接操作数据库获取
  // 如果是第三方API可以通过API接口获取数据
  return axios.get('/api/swiper', { baseURL: 'http://localhost:3000/' })
}

然后在首页的Home组件(pages/index.js)中,创建并导出getStaticProps函数,并且在Home组件中解构组件的props获取swiper数据对象,把swiper作为data属性传递给Swiper组件:

import Image from 'next/image'
// import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import Swiper, { loadSwiper } from '@/components/Swiper'
import Movie from '@/components/Movie'
import Layout from '@/components/Layout'

// const inter = Inter({ subsets: ['latin'] })

export default function Home ({ swiper }) {
  return (
    <>
      <Layout>
        <Swiper data={swiper} />
        <Movie />
      </Layout>
    </>
  )
}

export async function getStaticProps () {
  let { data: swiper } = await loadSwiper()
  return {
    props: {
      swiper
    }
  }
}

使用传递给components/Swiper.js组件的数据修改组件内容展示:

import React from 'react'
import { Carousel } from 'react-responsive-carousel'
import 'react-responsive-carousel/lib/styles/carousel.min.css'
import { css } from '@emotion/react'
import { Box, Stack, Heading, Text, Button } from '@chakra-ui/react'
import styled from '@emotion/styled'
import axios from 'axios'
import { useRouter } from 'next/router'

const CarouselItem = styled.div`
  position: relative;
  & > div {
    position: absolute;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    color: #fff;
    width: 80%;
    height: 100%;
    max-width: 1200px;
    text-align: left;
    & > h2 {
      width: 450px;
    }
    & > p {
      margin: 15px 0 15px;
      width: 450px;
      font-size: 14px;
    }
  }
  & > img {
    filter: brightness(50%);
  }
`
const swiperContainer = css`
  & > .carousel-root {
    position: relative;
    & > .carousel:last-child {
      position: absolute;
      left: 0;
      bottom: 0;
      & > .thumbs-wrapper > .thumbs {
        display: flex;
        justify-content: center;
      }
    }
  }
`

export default function Swiper ({ data }) {
  const router = useRouter()
  return (
    <Box css={swiperContainer}>
      <Carousel
        // autoPlay
        infiniteLoop
        emulateTouch
        showArrows={false}
        showIndicators={false}
        showStatus={false}
      >
        {data.map(swiper => (
          <CarouselItem key={swiper.id}>
            <img src={swiper.url} />
            <Stack justifyContent='center'>
              <Heading as='h2' fontSize='4xl'>
                {swiper.title}
              </Heading>
              <Text>{swiper.description}</Text>
              <Button
                colorScheme='red'
                w='120px'
                size='lg'
                onClick={() =>
                  router.push({
                    pathname: '/detail/[id]',
                    query: { id: swiper.vid }
                  })
                }
              >
                CHECK DETAIL
              </Button>
            </Stack>
          </CarouselItem>
        ))}
      </Carousel>
    </Box>
  )
}

export function loadSwiper () {
  // 这离获取数据不建议从本地通过API接口获取,而应该通过封装后端的方法函数直接操作数据库获取
  // 如果是第三方API可以通过API接口获取数据
  return axios.get('/api/swiper', { baseURL: 'http://localhost:3000/' })
}

电影列表数据的获取与展示

在pages/api目录下创建movie.js的文件,模拟获取movie的API供组件调用:

import { baseURL as url } from '@/axiosConfig'

export default function movie (req, res) {
  res.status(200).json([
    {
      id: 1,
      vid: 6,
      url: `${url}/apiresources/images/api_movie_1.jpg`,
      title: 'Marvel Mission Recap: Captain Marvel’s Star of Hala'
    },
    {
      id: 2,
      vid: 7,
      url: `${url}/apiresources/images/api_movie_2.jpg`,
      title: 'Make Your Video Calls Worthy With These Backgrounds'
    },
    {
      id: 3,
      vid: 8,
      url: `${url}/apiresources/images/api_movie_3.jpg`,
      title: 'Make Your Video Calls Worthy With These Backgrounds'
    },
    {
      id: 4,
      vid: 9,
      url: `${url}/apiresources/images/api_movie_4.jpg`,
      title:
        'Marvel At Home: Here’s How to Stay Connected With Your Favorite Super Heroes'
    }
  ])
}

components/Movie.js组件文件中,创建并导出loadMovie函数:

export function loadMovie () {
  return axios.get('/api/movie', { baseURL })
}

然后在首页的Home组件(pages/index.js)中,创建并导出getStaticProps函数,并且在Home组件中解构组件的props获取movie数据对象,把movie作为data属性传递给Movie组件:

import Image from 'next/image'
// import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import Swiper, { loadSwiper } from '@/components/Swiper'
import Movie, { loadMovie } from '@/components/Movie'
import Layout from '@/components/Layout'

// const inter = Inter({ subsets: ['latin'] })

export default function Home ({ swiper, movie }) {
  return (
    <>
      <Layout>
        <Swiper data={swiper} />
        <Movie data={movie} />
      </Layout>
    </>
  )
}

export async function getStaticProps () {
  const { data: swiper } = await loadSwiper()
  const { data: movie } = await loadMovie()
  return {
    props: {
      swiper,
      movie
    }
  }
}

使用传递给components/Movie.js组件的数据修改组件内容展示:

import React from 'react'
import { Box, Heading, HStack, Text, Image } from '@chakra-ui/react'
import { FaFilm } from 'react-icons/fa'
import { MdMovie } from 'react-icons/md'
import { baseURL } from '@/axiosConfig'
import axios from 'axios'
import { useRouter } from 'next/router'

export default function Movie ({ data }) {
  const router = useRouter()
  return (
    <Box maxW='1200px' mx='auto' px='10px' mt='20px'>
      <HStack>
        <MdMovie size='18px' />
        <Heading as='h3' fontSize='18px'>
          电影
        </Heading>
      </HStack>
      <HStack
        mt='20px'
        spacing='0'
        flexFlow='wrap'
        justifyContent='space-between'
      >
        {data.map(movie => (
          <Box
            onClick={() => router.push(`/detail/${movie.vid}`)}
            key={movie.id}
            w='290px'
            cursor='pointer'
          >
            <Image w='290px' src={movie.url} />
            <Text h='52px' overflow='hidden' mt='10px' as='section'>
              {movie.title}
            </Text>
          </Box>
        ))}
      </HStack>
    </Box>
  )
}

export function loadMovie () {
  return axios.get('/api/movie', { baseURL })
}

实现详情页基于动态路由的数据获取与展示

详情页属于基于动态路由的静态生成,首先我们要做的就是在首页点击链接或按钮跳转到对应的详情页。

这需要使用到Link或者next/router。

上面的页面已经实现了此功能,这里就不再重复。

首先,创建pages/api/videos.js 来模拟获取所有video id列表的API:

import { baseURL as url } from '@/axiosConfig'

export const videos = [
  {
    id: '1',
    title:
      "It's an Event-Sized Episode of 'Marvel Presents: The World's Greatest Book Club with Paul Scheer' in Celebration of 'Empyre'",
    sub:
      "Paul Scheer and Steve Wacker are joined by Anthony Carboni of 'The Star Wars Show'!",
    author: 'JAMIE FREVELE',
    publish: '2050-05-26',
    content:
      "<p>Time for a new episode of Marvel Presents: The World's Greatest Book Club with Paul Scheer -- and this one, Marvelites, is super-sized! Why? Because there's a new Marvel comic event in our midst, and EMPYRE deserves no less than a big celebration with hosts Paul Scheer and Steve Wacker! Paul and Steve are joined by guest Anthony Carboni (The Star Wars Show) for a calamitous conversation about other notable Marvel events.</p><video controls src='" +
      url +
      "/apiresources/videos/1.mp4'></video><p>But first -- EMPYRE! Steve provided an inside look at the creation of the intergalactic conflict and what Marvel fans can expect:</p><p>“What [writers Al Ewing and Dan Slott] definitely wanted to get away from was making it a [Fantastic Four] versus the Avengers, yet another story where friends fight each other and try to kill each other. Much like this show.”</p><p>He went on to predict the lasting effects of EMPYRE on the Marvel Universe:</p><p>“There are some big changes coming, and I think when we’re in our sweet spot is when we at Marvel are a little nervous about how the fans are going to react. It’s our job to put them through the ringer, to put them through hell. I think EMPYRE is not the story at the beginning that you think it is.”</p>"
  },
  {
    id: '2',
    title: "Time Travel Tips from 'Marvel's Agents of S.H.I.E.L.D.'",
    sub:
      'Traveling back in time is never easy? Let us help by consulting the pros!',
    author: 'CHRISTINE DINH',
    publish: '2050-03-13',
    content:
      "<img src='" +
      url +
      "/apiresources/images/detail_2.jpg'/><p>Look, we all know hopping through the decades ain't easy. In fact, who can keep up with all the different rules of time travel.</p><video controls src='" +
      url +
      "/apiresources/videos/2.mp4'></video><p>Luckily, we know a bunch of experts. During the production of Marvel's Agents of S.H.I.E.L.D. Season 7, Marvel.com had the opportunity to consult the cast and showrunners how to remain composure while navigating time travel. Watch what they have to say, learn the Do's and Don't's, and word of advice, it's probably best to avoid the shenanigans of Deke Shaw. We haven't forgotten the events of Season 6, Deke 👀.</p>"
  },
  {
    id: '3',
    title:
      "The Next Shocking Chapter in Donny Cates and Ryan Stegman's Venom Saga Revealed",
    sub: "'King in Black' conquers the Marvel Universe this December!",
    author: 'MARVEL',
    publish: '2060-08-30',
    content:
      "<p>This December, the entire Marvel Universe braces itself for KING IN BLACK, the latest installment in writer Donny Cates and artist Ryan Stegman’s revolutionary take on the Venom mythos. Knull is coming, and when he arrives, everyone from the Avengers to the X-Men will learn just how unprepared they are to face off against the God of the Symbiotes. Everything in Cates and Stegman’s landmark run on VENOM has led up to this monumental story, and readers will finally witness Eddie Brock’s climatic standoff with one of Marvel’s most terrifying villains.</p><video controls src='" +
      url +
      "/apiresources/videos/3.mp4'></video><img src='" +
      url +
      "/apiresources/images/detail_3.jpg'/><p>With each mind-bending twist and turn, the stakes will be raised like never before as KING IN BLACK flips everything you thought you knew about Venom and the world of the symbiotes upside down and inside out. Learn more in the special video announcement from the mastermind creative team, and stay tuned for more news about what to expect when KING IN BLACK lands later this year!</p>"
  },
  {
    id: '4',
    title:
      "Livestream: Let's Play LIVE: Fortnite Featuring the Captain America Outfit",
    sub: 'Follow along LIVE TODAY at 1pm PT / 4pm ET!',
    author: 'MARVEL',
    publish: '2050-09-05',
    content:
      "<p>Tune in to Marvel's Official Twitch Channel at 4:00 PM ET (1:00 PM PT) today to join Marvel host Josh Saleh as he channels the First Avenger – Captain America – who made his debut on Fortnite last week!</p><p>Follow along with Josh, and be sure to grab the Captain America Outfit in the Item Shop. Armed with his indestructible shield and iron will, Super-Soldier Steve Rogers won’t give up until the mission is finished.</p><video controls src='" +
      url +
      "/apiresources/videos/4.mp4'></video><p>Want to stay on top of everything in the Marvel Universe? Follow Marvel on social media—Twitter, Facebook, and Instagram—and keep watching Marvel.com for more news!</p>"
  },
  {
    id: '5',
    title: "Celebrate Ultraman Day with a Sneak Peek at 'Rise of Ultraman #1'",
    sub: 'Happy Ultraman Day!',
    author: 'MARVEL',
    publish: '2080-09-28',
    content:
      "<p>Ultraman has been a pop culture icon for over 50 years and this September, Marvel Comics will proudly contribute to the franchise’s incredible legacy with RISE OF ULTRAMAN #1!</p><p>Writers Kyle Higgins (Mighty Morphin Power Rangers, Winter Soldier) and Mat Groom (Self/Made) will join superstar artists Francesco Manna (Avengers, Fantastic Four) Michael Cho (Captain America) and Gurihiru (The Unstoppable Wasp) to reimagine the thrilling beginnings of the Ultraman phenomenon.</p><p>In honor of Ultraman Day, the celebration of Ultraman’s first public television appearance in 1966, check out a first look at the highly anticipated premiere issue including exclusive preview pages, a variant cover gallery, and more below!</p><img src='" +
      url +
      "/apiresources/images/detail_5.jpg'/><p>Stay tuned for more news about Marvel’s exciting collaboration with Tsuburaya Productions and don’t miss THE RISE OF ULTRAMAN #1 when it hits stands September 9th!</p>"
  },
  {
    id: '6',
    title: 'Marvel Mission Recap: Captain Marvel’s Star of Hala',
    sub: 'The results are out of this world!',
    author: 'RACHEL PAIGE',
    publish: '2046-05-23',
    content:
      "<p>Congrats agents — it appears that many of you successfully completed the latest Marvel Mission!</p><img src='" +
      url +
      "/apiresources/images/detail_6.jpg'/><p>Tasked to bring Captain Marvel’s Star of Hala to life using only safe household products and materials, the results we saw were outstanding and would make Carol Danvers and the Carol Corps proud!</p><p>While it was tough to narrow down all the submissions we received, we’ve rounded up some of our favorites that we saw across social media. Take a look at the post below, and though this Marvel Mission might be closed, there’s always time to make a star for yourself! </p>"
  },
  {
    id: '7',
    title: 'Make Your Video Calls Worthy With These Backgrounds',
    sub: 'Video call backgrounds, assemble!',
    author: 'RACHEL PAIGE',
    publish: '2028-12-25',
    content:
      "<p>Hey Marvel Insiders – did you know reading this article could earn you 250 points? All you need to do is sign in or join now before you keep reading!</p><p>Taking a video call in your living space with your regular home background is typical, mundane, and not at all dangerous. </p><p>But taking a video call with an Avengers approved background is exciting, heroic, and will definitely make your co-workers think you’re working from Asgard.</p><p>As more and more communication for work, fun, and play happens over our computer screens, we’ve assembled some video call backgrounds that you can use. Taking some of the Marvel Cinematic Universe's most iconic locations, have fun swapping out the backdrop of your kitchen for a sweeping landscape of Wakanda.  Check out the backgrounds you can download below! </p><img src='" +
      url +
      "/apiresources/images/detail_7_1.jpg'/><img src='" +
      url +
      "/apiresources/images/detail_7_2.jpg'/><p>To download the images: Right-click on the selected background of your choice and select SAVE IMAGE AS. The image will download to your desktop and you can insert it into the video conferencing program of your choice. Enjoy! </p><p>By downloading the images you agree to be bound by the terms located here.</p><p>Want to stay on top of everything in the Marvel Universe? Follow Marvel on social media—Twitter, Facebook, and Instagram—and keep watching Marvel.com for more news!</p>"
  },
  {
    id: '8',
    title:
      "Everything We Saw During the First 'Marvel’s Avengers' WAR TABLE Livestream",
    sub: 'Get ready to Embrace Your Powers on September 4!',
    author: 'CHRISTINE DINH',
    publish: '2048-05-10',
    content:
      '<p>Marvel Games, Square Enix, and Crystal Dynamics launched the very first Marvel’s Avengers WAR TABLE stream today. The Marvel’s Avengers WAR TABLE, which will be a monthly offering, gives players an in-depth look at many different aspects of the highly-anticipated game before it launches on September 4.</p><p>Opening up the Marvel’s Avengers WAR TABLE was the release of the brand-new story trailer narrated by the game’s central villain, Dr. George Tarleton. Tarleton joins the previously announced Taskmaster as another antagonist in the Avengers’ story.</p><p>Opening up the Marvel’s Avengers WAR TABLE was the release of the brand-new story trailer narrated by the game’s central villain, Dr. George Tarleton. Tarleton joins the previously announced Taskmaster as another antagonist in the Avengers’ story.</p><p>Marvel fans will recognize that Tarleton is none other than MODOK (Mental Organism Designed Only for Killing) – the artificially-mutated, super intelligent founder of AIM (Advanced Idea Mechanics). The story behind how Tarleton becomes MODOK is central to the game and one we’re eager to hear more about along with who voiced the deadly villain!</p><p>MODOK believes in AIM’s mission – fixing the damage the Avengers did, all those years ago on A-Day, by instilling order, ruling by science, and creating boundaries on what he reveals as the true threat to this world – the Inhumans disease. Taking his mission to the extreme, MODOK aims to rid the Earth of superpowers seeing it as a force that cannot be controlled or contained!</p><p>The Hero Missions allow Marvel’s Avengers to reveal more of each hero’s story, showcasing a variety of their narrative and backstory. Each hero has 3 iconic heroic moves: Assault, Ultimate, and Support. Learn more about these heroic moves and attacks for the Avengers by rewatching the Marvel’s Avengers WAR TABLE at the top of the article.</p>'
  },
  {
    id: '9',
    title:
      'Marvel At Home: Here’s How to Stay Connected With Your Favorite Super Heroes',
    sub: "Here's everything that's happening at the House of Ideas for fans!",
    author: 'RACHEL PAIGE',
    publish: '2082-06-25',
    content:
      "<p>We’re constantly dreaming up new ways to connect with readers, viewers, and fans at the House of Ideas and now, with everyone spending more time at home than ever, there are even more ways to bring Marvel into your home — wherever your home might be! </p><img src='" +
      url +
      "/apiresources/images/detail_9.jpg'/><p>Over the past month, we’ve worked to bring fans a chance to escape within the Marvel Universe, and if you haven’t already, there are so many ways to experience #MarvelAtHome. Whether you’re spending the day working, entertaining family members, or catching up on reading (or watching), here are some of the ways we’re keeping you connected to your favorite Super Heroes.</p><p>Wondering what it takes to bring Marvel characters to life on the page? Well, first you have to start with pencils and paper and the pros will show you what to do next! Follow along as we learn how to draw iconic characters like Spider-Man, Groot, and Wolverine and stay tuned to see who’s next!</p>"
  }
]

export default function video (req, res) {
  res.status(200).json(videos.map(video => video.id))
}

这个接口是下面我们要实现动态路由的静态生成时使用到的。

同时创建pages/api/detail.js模拟获取某个id对应的video的详细信息:

import { videos } from './videos'

export default function detail (req, res) {
  const id = req.query.id
  // 这里应该利用封装的方法操作数据库获取数据或者请求远端API获取
  const video = videos.find(video => video.id === id)
  res.status(200).send(video)
}

接下来我们要在pages/detail/[id].js实现动态路由的静态生成:

  1. 获取用户能够访问到的所有路由参数

// 获取用户能够访问到的所有路由参数
export async function getStaticPaths () {
  const { data } = await axios.get('/api/videos', { baseURL })
  const paths = data.map(id => ({ params: { id } }))

  return {
    paths,
    fallback: false
  }
}
  1. 根据参数获取其对应的数据


// 根据参数获取其对应的数据
export async function getStaticProps ({ params }) {
  const id = params.id
  const { data: detail } = await axios.get('/api/detail', {
    baseURL,
    params: { id }
  })

  return {
    props: {
      detail
    }
  }
}

使用传递给pages/detail/[id].js中的Detail页面组件的props数据{detail}修改组件内容展示:

import Layout from '@/components/Layout'
import React from 'react'
import { Box, HStack, Text, Heading, Divider } from '@chakra-ui/react'
import { css } from '@emotion/react'
import axios from 'axios'
import { baseURL } from '@/axiosConfig'

const DetailContainer = css`
  padding: 5px;
  & > p {
    font-size: 14px;
    margin-bottom: 10px;
  }
  & > img {
    display: block;
    margin-bottom: 10px;
  }
`
export default function Detail ({ detail }) {
  return (
    <Layout>
      <Box maxW='1200px' mx='auto' width='80%' mt='70px'>
        <Heading as='h2' size='xl'>
          {detail.title}
        </Heading>
        <Heading
          as='h4'
          size='lg'
          mt='10px'
          color='gray.500'
          fontWeight='light'
        >
          {detail.sub}
        </Heading>
        <Divider mt='10px' />
        <HStack
          overflow='hidden'
          justifyContent='space-between'
          spacing={3}
          my='10px'
        >
          <Text>作者: {detail.author}</Text>
          <Text>发布时间: {detail.publish}</Text>
        </HStack>
        <Divider mt='10px' />
        <Box
          dangerouslySetInnerHTML={{ __html: detail.content }}
          css={DetailContainer}
        ></Box>
      </Box>
    </Layout>
  )
}

// 获取用户能够访问到的所有路由参数
export async function getStaticPaths () {
  const { data } = await axios.get('/api/videos', { baseURL })
  const paths = data.map(id => ({ params: { id } }))

  return {
    paths,
    fallback: false
  }
}

// 根据参数获取其对应的数据
export async function getStaticProps ({ params }) {
  const id = params.id
  const { data: detail } = await axios.get('/api/detail', {
    baseURL,
    params: { id }
  })

  return {
    props: {
      detail
    }
  }
}

至此,网站已初具规模。

我们接下来把完整的videos列表再另起一个页面就行在客户端滚动页面触发动态获取更多数据并展示到页面上即可。

导出静态网站

在package.json中添加项目的命令脚本:

scripts: {



“export”: “next build && next export”

}

构建导出时因为我们的开发环境下系统后台不支持图片优化的API,所以在构建的时候会报错:

Error: Image Optimization using Next.js' default loader is not compatible with `next export`.
  Possible solutions:
    - Use `next start` to run a server, which includes the Image Optimization API.
    - Configure `images.unoptimized = true` in `next.config.js` to disable the Image Optimization API.
  Read more: https://nextjs.org/docs/messages/export-image-api



Possible Ways to Fix It


  • Use

    next start

    to run a server, which includes the Image Optimization API.

  • Use any provider which supports Image Optimization (such as

    Vercel

    ).

开发下我们直接修改next.config.js配置关闭构建时对图片的优化:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    unoptimized: true
  },
  exportPathMap: async function (
    defaultPathMap,
    { dev, dir, outDir, distDir, buildId }
  ) {
    return {
      '/': { page: '/' }
    }
  }
}

module.exports = nextConfig

项目到处成功会看到这样的提示:

Export successful. Files written to /Users/huiquandeng/projects/lg-fed-lp/my-module/aimovie/out

默认到处到out目录下。


因为需要后台服务器配合构建静态资源,所以我们需要模拟开启一个提供API接口的服务器。

自定义Next应用服务器

npm i express nodemon

目的:在next项目的基于页面的路由系统拓展我们自己的自定义路由系统

在next项目中创建server/index.js:

const express = require('express')
const next = require('next')
const hostname = 'localhost'
const port = 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, hostname, port })
// const app = next({ dev })

const server = express()
const handler = app.getRequestHandler()
app.prepare().then(() => {
  server.get('/helloworld', (req, res) => {
    console.log('deal before next RequestHandler...')
    res.send('Good Luck!')
  })

  server.get('*', async (req, res) => {
    await handler(req, res)
  })

  server.listen(port, () => console.log('Server is listening on port 3000'))
})

接着,在package.json文件中创建项目命令:

"serve:dev": "nodemon -r ./server/index.js",
"serve:prod": "NODE_ENV=production nodemon -r ./server/index.js"

通过运行

自定义服务器启动项目



版权声明:本文为u011024243原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。