大前端 - react - 服务端渲染 - Next.js

1. Next介绍

next.js是react基于服务端渲染应用框架,用于构建seo友好的spa应用。

  1. 支持2种预渲染方式,静态生成和服务端渲染。
  2. 基于野蛮的路由系统,路由零配置。
  3. 自动代码拆分,优化页面加载速度。
  4. 支持静态导出,可将应用导出为静态网站。
  5. 内置css-in-js库styled-jsx
  6. 方案成熟,可用于生产环境,世界许多公司都在使用
  7. 应用部署简单,拥有专属部署环境vercel,也可以部署在其他环境。

2. Next实战

2.1 创建Next项目

创建:npm init next-app next-guide
运行:npm run dev
运行:npm start 运行生产环境的代码
访问:localhost:3000

临时安装create-next-app 用于创建Next.js 项目。

在这里插入图片描述

2.2基于页面的路由系统-创建页面
  • 创建页面:
    在next.js中,页面是被放置在pages文件夹中的React 组件。
    组件需要被默认导出。(export default )
    组件文件中不需要引入React。 (不需要 import react from ‘React’)
    页面地址与文件地址是对应关系。
pages/list.js
export default function List() {
	return <div>list page. works</div>
}
  • 访问页面:

文件夹地址:pages/index.js 浏览器中访问地址: /
文件夹地址:pages/list.js. 浏览器中访问地址: /list
文件夹地址:pages/post/first.js. 浏览器中访问地址: /post/first.js

2.2基于页面的路由系统-页面跳转

页面跳转:

  1. Link 组件默认使用javascript进行页面跳转,即SPA形式的跳转。
  2. 如果浏览器中javascript 被禁用,则使用链接跳转。
  3. Link 组件中不应添加除href属性以外的属性,其余属性添加到a标签上。
  4. Link组件通过预取(在生产中)功能自动优化应用程序以获得最佳性能。
import Link from 'next/link'
<Link href="/list">
	// a 标签必须写
	<a title="list page">list page</a>
</Link>
2.3静态资源访问
  • 静态资源:
    应用程序根目录中的public文件夹用于提供静态资源

通过以下形式进行访问:

文件存放地址:public/images/1.jpg 访问地址:/images/1.jpg
文件存放地址:public/css/base.css 访问地址: /css/base.css

案例:

import Link from 'next/link'

export default function Home() {
  return (
    <div className="container">
      <img src="/images/1.png" alt="" />
    </div>
  )
}

图片存放地址:
在这里插入图片描述

2.3修改页面元数据

通过Head组件修改元数据

import Head from 'next/head'
<>
	<Head>
		<title> Index page</title>	
	</Head>
	
</>

案例:

import Link from 'next/link'
import Head from 'next/head'

export default function Home() {
  return (
    <>
      <Head>
        {/* 网页标题 */}
        <title> Index page</title>	
      </Head>
      <div>
      <Link href="/list">
      	<a title="list page">list page</a>
      </Link>
      </div>
    </>
  )
}

效果:
在这里插入图片描述

2.4 修改页面css
  • 方案1: 内置styled-jsx
    在next.js中内置了styled-jsx,它是一个CSS-in-JS库,允许在React 组件中编写css,css仅作用于组件内部。

案例:

<Link href="/list">
	<a className="demo">list page</a>
</Link>
<style jsx>
{
`
.demo{
	color: red
}
`
}
</style>

效果:
在这里插入图片描述

  • 方案2: css样式
    css模块
    通过使用css模块功能,允许将组件的css样式编写在单独的css文件中。
    css模块云顶样式文件的名称必须为【组件文件名称.module.css】
// index.module.css
.p{color: green}

// index.js
import styles from './index.module.css'
<div className={styles.p}>我是p样式</div>

案例:
list.module.css

.demo{
  color: blueviolet
}

list.js

import Head from 'next/head'
import styles from './list.module.css'
export default function List() {
	return (
    <>
      <Head>
        <title>list page</title>
      </Head>
      <div className={styles.demo}> list page works</div>
    </>
  )
}

效果:
在这里插入图片描述

  • 方案3 添加全局样式
  1. 在pages文件中新建_app.js文件并加入如下代码
  2. 在项目根目录下创建styles文件夹,并在其中创建global.css
  3. 在_app.js中通过import 引入global.css
  4. 重新启动开发服务器
export default function App({Component, pageProps}) {
	return <Component {...pageProps} />
}

案例:
pages/_app.js

import '../styles.css'
export default function App({Component, pageProps}) {
	return <Component {...pageProps} />
}

./styles.css

body {
  font-size: 40px;
  font-weight: bold;
  background: tomato;
}
2.5 预渲染
  • 预渲染概述:
    预渲染是指数据和html 的拼接在服务器端提前完成
    预渲染可以使seo更加友好。
    预渲染会带来更好的用户体验,可以无需运行javascript即可查看应用程序UI 。

  • 预渲染的2种形式

  1. 在nextjs中支持2种形式的预渲染:静态生成和服务端渲染。
  2. 静态生成和服务端渲染是生成html的时机不同。
  3. 静态生成:静态生成是在构建时生成html,以后的每个请求都共用构建时生成好的html。
  4. 服务端渲染:服务端渲染是在请求时生成html,每个请求都会重新生成html。
  • 2种预渲染方式的选择:
    nextjs允许为每个页面选择不同的预渲染方式,不同的预渲染方式拥有不同的特点,应根据场景进行渲染。

但建议大多数页面建议使用静态生成。

静态生成一次构建,反复使用,访问速度快,因为页面都是事先生成好的。

适用场景:营销页面,博客文章,电子商务产品列表,帮个和文档。

服务端渲染访问速度不如静态生成快,但是由于每次请求都会重新渲染,所以适用数据频繁更新的页面或页面内容随请求变化而变化的页面。

2.6 实现静态生成

分为2种情况:1.无数据的静态生成 2. 有数据的静态生成

如果组件不需要在其他地方获取数据,直接进行静态生成

如果组件需要在其他地方获取数据,在构建时nextjs会预先获取组件需要的数据,然后再对组件进行静态生成。

nextjs默认使用静态生成。

  1. 无数据的额静态生成:
    npm run build 生成的.next文件夹中的pages中的文件就是无数据的静态生成。

  2. 有数据的静态生成。getStaticProps
    getStaticProps方法的作用是:获取组件静态生成需要的数据,并通过props的方式将数据传递给组件。该方法是一个异步函数,需要在组件内部进行导出。

在开发模式下,getStaticProps改为在每个请求上运行。

export async function getStaticProps() {	
	// 从文件系统中,api,数据库中获取的数据
	const data = ...
	// props 属性的值将会传递给组件
	// data会在node的环境中输出
	return {
		props: { data }
	}
}

案例:

import Head from 'next/head'
import styles from './list.module.css'
import { readFile } from 'fs'
import { promisify } from 'util'
import { join } from 'path'


const read = promisify(readFile) // 返回promise的数据

export default function List({data}) {
	return (
    <>
      <Head>
        <title>list page</title>
      </Head>
      <div className={styles.demo}> list page works</div>
      <div>{data}</div>
    </>
  )
}

export async function getStaticProps() {
  // join拼接文件的路径
  // process.cwd()会返回路径
  // pages:文件夹的名称
  // 文件:_app.js'
  // 文件编码:utf-8
  let data = await read(join(process.cwd(), 'pages', '_app.js'), 'utf-8')
  console.log(data)
  return {
    props:{
      data
    }
  }
}
2.7 实现服务器端渲染

服务器端渲染 getServerSideProps
如果采用服务器端渲染,需要在组件中导出getServerSideProps方法

export async function getServerSideProps(context) {
	// context 中会包含特定的请求参数
	return {
		props:{
			// props for your component
		}
	}
}

案例:

import Head from 'next/head'
import styles from './list.module.css'
import { readFile } from 'fs'
import { promisify } from 'util'
import { join } from 'path'


const read = promisify(readFile) // 返回promise的数据

export default function List({data}) {
	return (
    <>
      <Head>
        <title>list page</title>
      </Head>
      <div className={styles.demo}> list page works</div>
      <div>{data}</div>
    </>
  )
}

export async function getServerSideProps(context) {
  // join拼接文件的路径
  // process.cwd()会返回路径
  // pages:文件夹的名称
  // 文件:_app.js'
  // 文件编码:utf-8
  let data = await read(join(process.cwd(), 'pages', '_app.js'), 'utf-8')
  // 获取客户端传递的参数
  console.log('hello', context.query)
  return {
    props:{
      data
    }
  }
}
2.8 实现基于动态路由的静态生成

基于参数为页面组件生成html页面,有多少参数就生成多少html页面

在构建应用时,先获取用户可以访问的所有路由参数,再根据路由参数获取具体数据,然后根据数据生成静态html。

实现基于动态路由的静态生成:

  1. 创建基于动态路由的页面组件文件,命名时再文件名称外面加上[],比如[id].js
  2. 导出异步函数getStaticPaths,用于获取所有用户可以访问的路由参数。
// 此处获取所有用户可以访问的路由参数
export async function getStaticPaths() {

	return {
		 // 返回固定格式的路由参数
		paths: [{params: {id: 1}},params :{id: 2}]// 当用户访问的路由参数没有在当前函数中返回时,是否显示404页面,false:显示:true:不显示
		fallback:false
	}
}
  1. 导出异步函数getStaticProps, 用于根据路由参数获取具体的数据
//	params: --> {id: 1}
// 此处根据路由参数获取具体内容
export async function getStaticProps({params}) {
	return {
		// 将数据传递到组件中进行静态页面的生成。
		props: {}
	}
}

案例:
post/[id].js

import { useRouter } from 'next/router';

export default function Post ({data}) {
  const router = useRouter();
  if (router.isFallback) return <div>loading</div>;
  return <div>
    <span>{data.id}</span>
    <span>{data.title}</span>
  </div>
}

// 返回用户可以访问到的所有的路由参数
export async function getStaticPaths () {
  return {
    paths: [{params: {id: "1"}}, {params: {id: "2"}}],
    fallback: true
  }
}
// 返回路由参数所对应的具体的数据
export async function getStaticProps ({params}) {
  console.log('Hello');
  const id = params.id;
  let data;
  switch (id) {
    case "1":
      data = {id: "1", title: 'Hello'};
      break;
    case "2":
      data = {id: "2", title: 'world'};
      break;
    case "3":
      data = {id: "3", title: 'hello world'};
      break;
    default:
      data = {}
  }
  return {
    props: {
      data
    }
  }
}

删除 .next文件夹
运行npm run build
运行:npm start
访问浏览器地址:http://localhost:3000/post/1

** 注意 **
getStaticPaths和getStaticProps只运行在服务器端,永远不会运行在客户端,甚至不会被打包到客户端javascript中,意味着可以随意写服务端代码,比如查询数据库

2.9 fallback选项的作用

fallback作用:当传递的路由参数不在规定的范围内的时候,要给这个用户展示什么。
如果是false,展示404页面,如果是true 服务器端对静态页面的生成。

2.10 自定义404页面

要创建自定义404页面,需要在pages文件夹中创建404.js文件。

404.js

 export default function Custom404(){
	return <h1>404- Page Not Found</h1> 
}
2.11 API Routes

什么是 API Routes?
API Routes可以理解为【接口】,客户端向服务器端发送请求获取数据的接口。
nextjs应用允许react开发者编写服务器端代码创建数据接口。

如何实现API Routes?
1.在pages/api文件夹中创建API Routes文件,比如:user.js
2.在文件中默认导出请求处理函数,函数有2个参数,req为请求对象,res为响应对象

export default function(req, res) {
	res.status.send({id: 1, name: 'tom'})
}

注意 : 当前API Routes可以接收任何http请求方法。

  1. 访问API Routes,localhost:3000/api/user
    不要在getStaticPaths或getStaticProps函数中访问API Routes,因为这2个函数就是在服务器端运行的,可以直接写服务器端代码。

案例:
代码:
api/user.js

export default (req, res) => {
  res.send({ name: '章三', age: 20 })
}

访问数据接口:http://localhost:3000/api/user
效果:
在这里插入图片描述
package.json

{
  "name": "next-guide",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "9.4.4",
    "react": "16.13.1",
    "react-dom": "16.13.1"
  }
}

3. 实战案例

3.1初始化配置
  1. 创建项目
    npm init next-app movie
  2. 下载 chakra-ui 框架
    npm install @chakra-ui/core@next
  3. 克隆主题
    npx chakra-cli init --theme
  4. 配置主题
    在 pages 文件夹下建立 _app.js 加入如下代码
import { ChakraProvider, CSSReset } from "@chakra-ui/core";
    import theme from "../chakra";
    export default function App({ Component, pageProps }) {
      return (
        <ChakraProvider theme={theme}>
          <CSSReset />
          <Component {...pageProps} />
        </ChakraProvider>
      );
    }
  1. 下载字体图标
    npm install react-icons --save
  2. 下载 emotion
    npm install @emotion/core @emotion/styled
    npm install @emotion/babel-preset-css-prop --save-dev
  3. 添加 babel 配置
    在根目录下创建 .babelrc 文件并添加如下代码
{"presets": ["next/babel","@emotion/babel-preset-css-prop"]}
  1. npm run dev
  2. 在浏览器中访问:http://localhost:3000
    package.json
{
  "name": "movie",
  "version": "0.1.0",
  "private": true,
  "scripts": {
     "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@chakra-ui/core": "^1.0.0-next.4",
    "@chakra-ui/theme-tools": "^1.0.0-next.3",
    "@emotion/core": "^10.0.28",
    "@emotion/styled": "^10.0.27",
    "axios": "^0.19.2",
    "express": "^4.17.1",
    "next": "9.4.4",
    "nodemon": "^2.0.4",
    "react": "16.13.1",
    "react-dom": "16.13.1",
    "react-icons": "^3.10.0",
    "react-responsive-carousel": "^3.2.9"
  },
  "devDependencies": {
    "@emotion/babel-preset-css-prop": "^10.0.27"
  }
}

3.2 实现头部组件布局

/components/Header.js

import { Box, Container, Button, Image } from "@chakra-ui/core";
import styled from "@emotion/styled";
import { css } from "@emotion/core";
import { FaSignInAlt, FaSearch } from "react-icons/fa";
import { BsFillPersonFill } from "react-icons/bs";

const logo = css`
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 140px;
`;

const SignInAndJoin = styled.div`
  height: 52px;
  line-height: 52px;
  border-left: 1px solid #393939;
  border-right: 1px solid #393939;
  padding: 0 6px;
  float: left;
  color: #fff;
  & > button {
    padding: 0 10px;
    font-size: 12px;
  }
  & > button:nth-of-type(1):after {
    content: "";
    width: 1px;
    height: 10px;
    background: #fff;
    position: absolute;
    right: 0;
    top: 16px;
  }
`;

const Search = styled.a`
  float: right;
  height: 52px;
  border-left: 1px solid #393939;
  border-right: 1px solid #393939;
  color: #fff;
  font-size: 20px;
  padding: 0 10px;
  display: flex;
  align-items: center;
`;

export default function Header() {
  return (
    <Box h={52} bgColor="#202020" borderBottom="1px solid #393939">
      <Container h={52} maxW={1200} pos="relative">
        <SignInAndJoin>
          <Button colorScheme="transparent" leftIcon={<FaSignInAlt />}>
            登录
          </Button>
          <Button colorScheme="transparent" leftIcon={<BsFillPersonFill />}>
            注册
          </Button>
        </SignInAndJoin>
        <Image css={logo} src="/images/logo.png" />
        <Search>
          <FaSearch />
        </Search>
      </Container>
    </Box>
  );
}

效果:
在这里插入图片描述

3.3 实现导航组件布局

/components/Navigation.js

import { Box, HStack } from "@chakra-ui/core";
import Link from "next/link";

export default function Navigation() {
  return <Box h={52} bgColor="#202020" color="#ffF">
    <HStack h={52} spacing={3} justifyContent="center" alignItems="center">
      <Link href="#"><a>影片</a></Link>
      <Link href="#"><a>漫画</a></Link>
      <Link href="#"><a>电影</a></Link>
      <Link href="#"><a>电视</a></Link>
      <Link href="#"><a>新闻</a></Link>
    </HStack>
  </Box>
}

效果:
在这里插入图片描述

3.4 实现轮播图组件布局

安装:npm intstall react-responsive-carousel
/components/Swiper.js

import { Carousel } from "react-responsive-carousel";
import Head from "next/head";
import { css } from "@emotion/core";
import styled from "@emotion/styled";
import { Box, Heading, Text, Button } from "@chakra-ui/core";
import Link from 'next/link';

const CarouselItem = styled.div`
  position: relative;
  & > div {
    position: absolute;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    color: #fff;
    padding-top: 180px;
    text-align: left;
    width: 100%;
    max-width: 1200px;
    & > p {
      margin: 15px 0;
      font-size: 14px;
      width: 450px;
    }
  }
  & > img {
    filter: brightness(50%);
  }
`;

const swiperContainer = css`
  position: relative;
  & > .carousel:last-child {
    position: absolute;
    left: 0;
    bottom: 0;
    & > .thumbs-wrapper > .thumbs {
      display: flex;
      justify-content: center;
    }
  }
`;

export default function Swiper({ data }) {
  return (
    <>
      {/* 单独引入轮播图组件的css */}
      <Head>
        <link rel="stylesheet" href="/css/carousel.min.css" />
      </Head>
      <Carousel
        css={swiperContainer}
        showArrows={false}
        showIndicators={false}
        showStatus={false}
      >
        <CarouselItem key={swiper.id}>
          <img src="/images/1.png" />
          <Box>
            <Heading as="h2" size="lg">
              我的
            </Heading>
            <Text>
              我的点用
            </Text>
            <Button colorScheme="red">
              <Link href="" ><a>CHECK DETAIL</a></Link>
            </Button>
          </Box>
        </CarouselItem>
      </Carousel>
    </>
  );
}

3.5 电影列表布局

/components/Movie.js

import { Box, Heading, HStack, Image, Text } from "@chakra-ui/core";
import { MdMovie } from "react-icons/md";

export default function Movie() {
  return (
    <Box maxW={1200} mx="auto" mt="20px">
      <HStack fontSize="24px">
        <MdMovie />
        <Heading as="h3" fontSize="24px">
          电影
        </Heading>
      </HStack>
      <HStack mt="20px" spacing={3}>
        <Box w={290}>
          <Image src="/images/1.png" />
          <Text mt="10px">
            我的电影描述
          </Text>
        </Box>
        <Box w={290}>
          <Image src="/images/2.png" />
          <Text mt="10px">
            我的电影2描述
          </Text>
        </Box>
      </HStack>
    </Box>
  );
}

3.6 电影详情页面布局

Layout.js

import Header from './Header';
import Navigation from './Navigation';

export default function Layout ({children}) {
  return <>
    <Header />
    <Navigation />
    {children}
  </>
}

index.js

import Layout from "../components/Layout";
import Swiper from "../components/Swiper";
import Movie from "../components/Movie";

export default function Home() {
  return (
    <Layout>
      <Swiper />
      <Movie/>
    </Layout>
  );
}

新建文件:pages/detail/[id].js

import Layout from "../../components/Layout";
import { Box, Heading, Divider, Text } from "@chakra-ui/core";
import { css } from "@emotion/core";
const  DetailContainer = css`
padding: 10px 0px
 & > p{
   font-size: 14px
 }
 & > img{
  margin-bottom: 10px;
  display: block;
 }
`
export default function Detail() {
  return (
    <Layout>
      <Box maxW={120} mx="auto" mt="70pc">
        <Heading as="h2" size="xl">
           我是主标题
        </Heading>
        <Heading
          mt="10px"
          as="h4"
          size="lg"
          color="gray.500"
          fontWeight="light"
        >
          我是副标题
        </Heading>
        <Divider mt="10px" />
        <Box overflow="hidden" mt="10px">
          <Text float="left">作者: Tomas</Text>
          <Text float="right">发布时间: 2020-12-12</Text>
        </Box>
        <Box css={DetailContainer}>
          <p>我是具体的内容我是具体的内容我是具体的内容我是具体的内容我是具体的内容我是具体的内容我是具体的内容</p>
        </Box>
        <Divider mt="10px" />
      </Box>
    </Layout>
  )
}
3.7 实现首页组件的静态生成:轮播图数据获取与展示

服务端接口:

import express from "express";
import cors from 'cors';

const app = express();

app.use(cors());
app.use(express.static('public'));

const url = "http://localhost:3005";

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+"/api/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+"/api/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+"/api/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+"/api/videos/3.mp4'></video><img src='"+url+"/api/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+"/api/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+"/api/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+"/api/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+"/api/images/detail_7_1.jpg'/><img src='"+url+"/api/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+"/api/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>"
  }
];

// 轮播图
app.get("/api/swiper", (req, res) => {
  res.send([
    {
      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}/api/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}/api/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}/api/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}/api/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}/api/images/api_swiper_5.jpg`,
      vid: 5
    }
  ]);
});

// 电影列表
app.get("/api/movie", (req, res) => {
  res.send([
    {
      id: 1,
      vid: 6,
      url: `${url}/api/images/api_movie_1.jpg`,
      title: "Marvel Mission Recap: Captain Marvel’s Star of Hala"
    },
    {
      id: 2,
      vid: 7,
      url: `${url}/api/images/api_movie_2.jpg`,
      title: "Make Your Video Calls Worthy With These Backgrounds"
    },
    {
      id: 3,
      vid: 8,
      url: `${url}/api/images/api_movie_3.jpg`,
      title: "Make Your Video Calls Worthy With These Backgrounds"
    },
    {
      id: 4,
      vid: 9,
      url: `${url}/api/images/api_movie_4.jpg`,
      title:
        "Marvel At Home: Here’s How to Stay Connected With Your Favorite Super Heroes"
    }
  ]);
});

// 电影详情
app.get("/api/detail", (req, res) => {
  const id = req.query.id;
  const result = videos.find(video => video.id === id);
  res.send(result);
});

// 获取电影id集合
app.get('/api/videos', (req, res) => {
  res.send(videos.map(video => video.id));
});

app.listen(3005, () => console.log("app is running on port 3005"));

前端代码:
项目根目录创建:axiosConfig.js

export const baseURL = 'http://localhost:3005';

/components/Swiper.js

import { Carousel } from "react-responsive-carousel";
import Head from "next/head";
import { css } from "@emotion/core";
import styled from "@emotion/styled";
import { Box, Heading, Text, Button } from "@chakra-ui/core";
+ import axios from "axios";
+ import { baseURL } from "../axiosConfig";
import Link from 'next/link';

const CarouselItem = styled.div`
  position: relative;
  & > div {
    position: absolute;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    color: #fff;
    padding-top: 180px;
    text-align: left;
    width: 100%;
    max-width: 1200px;
    & > p {
      margin: 15px 0;
      font-size: 14px;
      width: 450px;
    }
  }
  & > img {
    filter: brightness(50%);
  }
`;

const swiperContainer = css`
  position: relative;
  & > .carousel:last-child {
    position: absolute;
    left: 0;
    bottom: 0;
    & > .thumbs-wrapper > .thumbs {
      display: flex;
      justify-content: center;
    }
  }
`;

export default function Swiper({ data }) {
  return (
    <>
      {/* 单独引入轮播图组件的css */}
      <Head>
        <link rel="stylesheet" href="/css/carousel.min.css" />
      </Head>
      <Carousel
        css={swiperContainer}
        showArrows={false}
        showIndicators={false}
        showStatus={false}
      >
        {data.map(swiper => (
          <CarouselItem key={swiper.id}>
            <img src={swiper.url} />
            <Box>
              <Heading as="h2" size="lg">
                {swiper.title}
              </Heading>
              <Text>
                {swiper.description}
              </Text>
              <Button colorScheme="red">
                <Link href="/detail/[id]" as={`/detail/${swiper.vid}`}><a>CHECK DETAIL</a></Link>
              </Button>
            </Box>
          </CarouselItem>
        ))}
      </Carousel>
    </>
  );
}
// 调用接口
+ export function loadSwiper() {
 + return axios.get("/api/swiper", { baseURL });
+ }

index.js
传递获取到的数据

import Layout from "../components/Layout";
+ import Swiper, { loadSwiper } from "../components/Swiper";
+ import Movie, { loadMovie } from "../components/Movie";

+ export default function Home({ swiper, movie }) {
  return (
    <Layout>
+    <Swiper data={swiper} />
+    <Movie data={movie} title="电影"/>
    </Layout>
  );
}

// 获取数据
+ export async function getStaticProps() {
  // 获取轮播图数据
+  let { data: swiper } = await loadSwiper();
  // 获取电影列表数据
+  let { data: movie } = await loadMovie();
+  return {
+    props: {
+      swiper,
+      movie
+    }
+  };
+ }

在项目中使用:
Swiper.js

+ export default function Swiper({ data }) {
  return (
    <>
      <Head>
        <link rel="stylesheet" href="/css/carousel.min.css" />
      </Head>
      <Carousel
        css={swiperContainer}
        showArrows={false}
        showIndicators={false}
        showStatus={false}
      >
+        {data.map(swiper => (
+          <CarouselItem key={swiper.id}>
+            <img src={swiper.url} />
            <Box>
              <Heading as="h2" size="lg">
+               {swiper.title}
              </Heading>
              <Text>
+                {swiper.description}
              </Text>
              <Button colorScheme="red">
+               <Link href="/detail/[id]" as={`/detail/${swiper.vid}`}><a>CHECK DETAIL</a></Link>
              </Button>
            </Box>
          </CarouselItem>
        ))}
      </Carousel>
    </>
  );
}

Movie.js

+ export default function Movie({ data, title }) {
  return (
    <Box maxW={1200} mx="auto" mt="20px">
      <HStack fontSize="24px">
        <MdMovie />
        <Heading as="h3" fontSize="24px">
+          {title}
        </Heading>
      </HStack>
      <HStack mt="20px" spacing={3}>
+        {data.map(movie => (
+          <Box w={290} key={movie.id}>
+            <Image src={movie.url} />
            <Text mt="10px">
+              {movie.title}
            </Text>
          </Box>
        ))}
      </HStack>
    </Box>
  );
}

/pages/detail/[id].js

+ import axios from "axios";
+ import { baseURL } from "../../axiosConfig";

+ export default function Detail({ detail }) {
  return (
    <Layout>
      <Box maxW={1200} mx="auto" mt="70px">
        <Heading as="h2" size="xl">
          {detail.title}
        </Heading>
        <Heading
          mt="10px"
          as="h4"
          size="lg"
          color="gray.500"
          fontWeight="light"
        >
          {detail.sub}
        </Heading>
        <Divider mt="10px" />
        <Box overflow="hidden" mt="10px">
          <Text float="left">作者: {detail.author}</Text>
          <Text float="right">发布时间: {detail.publish}</Text>
        </Box>
        <Divider mt="10px" />
+        <Box css={DetailContainer} dangerouslySetInnerHTML={{__html: detail.content}}></Box>
      </Box>
    </Layout>
  );
}

// 获取到用户能够访问到的所有的路由参数
+ export async function getStaticPaths() {
  // ["1", "2"]
+  let { data } = await axios.get("/api/videos", { baseURL });
  // [{params: {id: "1"}}]
+  let paths = data.map(id => ({ params: { id } }));
+  return {
+    paths,
+    fallback: false
+  };
+ }

// 根据参数获取其对应的数据
+ export async function getStaticProps({ params }) {
+  let id = params.id;
+  let { data: detail } = await axios.get(`/api/detail?id=${id}`, { baseURL });
+  return {
+    props: {
+      detail
+    }
+  };
+ }

3.8导出静态网站

package.json

"scripts": {
	"dev": "next dev",
    "build": "next build",
    "start": "next start",
    "export": "next build && next export"
  },

执行:npm export
查看结果:打包出了out文件。这个就是部署到服务器端的静态网站。静态资源。

3.9自定义Next应用服务器

新建文件:server/index.js
npm install nodemon express

const express = require('express');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production'

const app = next({dev});
// 路由系统
const handler = app.getRequestHandler();
// app.prepare():去准备next应用
app.prepare().then(() => {
// 创建服务器
  const server = express();
  server.get('/hello', (req, res) => {
    res.send('Hello Next.js')
  });
  //  接收所有的请求,
  server.get('*', (req, res) => {
  // 把接收到的请求交给handler处理
    handler(req, res)
  });
  server.listen(3000, () => console.log('服务器启动成功'));
});

package.json

"scripts": {
	"dev": "nodemon server/index.js",
    "build": "next build",
    "start": "next start",
    "export": "next build && next export"
  },

启动服务器: npm run dev
查看浏览器检查:localhost:3000

3.10 部署Next应用到Vercel平台
  1. 把项目推送到github仓库
  2. 在vercel平台导入。https://vercel.com/
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值