MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

从零开始构建 Next.js 应用:掌握 getStaticProps 和 API Routes

2021-02-095.7k 阅读

Next.js 基础与项目搭建

Next.js 是什么

Next.js 是一个基于 React 的轻量级前端框架,它由 Vercel 公司开发并维护。与传统 React 应用不同,Next.js 提供了诸如自动代码拆分、服务器端渲染(SSR)和静态站点生成(SSG)等特性,这些特性使得构建高性能、可扩展的 React 应用变得更加容易。对于前端开发者而言,Next.js 不仅简化了开发流程,还在提升应用性能方面有着显著优势。

创建 Next.js 项目

要开始构建 Next.js 应用,首先需要确保你已经安装了 Node.js 和 npm(Node Package Manager)。可以通过以下命令创建一个新的 Next.js 项目:

npx create-next-app my-next-app
cd my-next-app

上述命令中,npx 是 npm 5.2+ 引入的一个工具,用于在不全局安装 create - next - app 的情况下直接运行它。create - next - app 是官方提供的脚手架工具,my - next - app 是项目名称,你可以根据实际情况进行更改。

进入项目目录后,项目结构大致如下:

my-next-app
├── .gitignore
├── README.md
├── next.config.js
├── package.json
├── pages
│   ├── _app.js
│   ├── _document.js
│   ├── api
│   │   └── hello.js
│   └── index.js
├── public
│   └── favicon.ico
├── styles
│   ├── Home.module.css
│   └── globals.css
└── yarn.lock

其中,pages 目录是 Next.js 应用的核心,每一个在 pages 目录下的 .js 文件都会对应一个路由。例如,pages/index.js 对应应用的根路由 /public 目录用于存放静态文件,如图片、字体等。styles 目录则用于管理应用的样式。

静态站点生成(SSG)与 getStaticProps

静态站点生成原理

静态站点生成(SSG)是 Next.js 中一项强大的功能,它允许在构建时生成 HTML 页面。这意味着在用户请求页面之前,页面的 HTML 内容已经生成好了。相比传统的客户端渲染(CSR),SSG 具有更快的首次加载速度,因为浏览器无需等待 JavaScript 代码下载和执行就可以直接渲染页面。

getStaticProps 函数

getStaticProps 是 Next.js 中用于实现静态站点生成的关键函数。这个函数只能在页面组件中使用,它在构建时运行,用于获取页面所需的数据,并将数据作为 props 传递给页面组件。下面是一个简单的示例:

export async function getStaticProps() {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const data = await res.json();
    return {
        props: {
            post: data
        },
        revalidate: 60 // 可选,设置重新验证时间(秒),用于增量静态再生
    };
}

const PostPage = ({ post }) => {
    return (
        <div>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
        </div>
    );
};

export default PostPage;

在上述代码中,getStaticProps 函数使用 fetch 从外部 API 获取一篇文章的数据。然后将获取到的数据作为 props 返回给 PostPage 组件。这样,PostPage 组件就可以在渲染时使用这些数据。

动态路由与 getStaticProps

在实际应用中,经常需要处理动态路由。例如,一个博客应用可能需要根据文章的 ID 来显示不同的文章页面。Next.js 允许通过文件名的约定来创建动态路由。假设我们有一个 pages/post/[id].js 文件,其中 [id] 表示动态参数:

export async function getStaticPaths() {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts');
    const posts = await res.json();

    const paths = posts.map(post => ({
        params: { id: post.id.toString() }
    }));

    return { paths, fallback: false };
}

export async function getStaticProps(context) {
    const id = context.params.id;
    const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
    const post = await res.json();

    return {
        props: {
            post
        }
    };
}

const PostPage = ({ post }) => {
    return (
        <div>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
        </div>
    );
};

export default PostPage;

在这个示例中,getStaticPaths 函数用于生成所有可能的动态路由路径。它通过从 API 获取所有文章列表,然后为每篇文章生成一个路径对象。fallback 参数设置为 false 表示只有在 getStaticPaths 生成的路径下才会进行静态生成,其他路径会返回 404 页面。getStaticProps 函数则根据动态参数 id 获取具体文章的数据,并传递给 PostPage 组件。

增量静态再生

从 Next.js 9.5 版本开始,引入了增量静态再生的功能。通过在 getStaticProps 函数中设置 revalidate 参数,可以指定在页面构建后,每隔一段时间重新验证并更新页面数据。例如:

export async function getStaticProps() {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const data = await res.json();
    return {
        props: {
            post: data
        },
        revalidate: 60 // 每 60 秒重新验证一次
    };
}

const PostPage = ({ post }) => {
    return (
        <div>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
        </div>
    );
};

export default PostPage;

当启用增量静态再生时,在 revalidate 设置的时间间隔内,如果有新的请求访问该页面,Next.js 会在后台重新运行 getStaticProps 函数来更新数据,并将新数据返回给客户端。这样既保证了页面的初始加载性能(因为是静态生成),又能在一定程度上确保数据的实时性。

API Routes in Next.js

什么是 API Routes

Next.js 提供了一种简单的方式来创建 API 端点,称为 API Routes。这些 API 端点可以直接在 Next.js 项目中定义,并且与应用的其他部分紧密集成。API Routes 基于 Node.js 的 HTTP 模块构建,使得开发者可以方便地处理 HTTP 请求和响应。

创建 API Routes

在 Next.js 项目的 pages/api 目录下创建的任何 .js 文件都会自动成为一个 API 端点。例如,创建一个 pages/api/hello.js 文件:

export default function handler(req, res) {
    res.status(200).json({ message: 'Hello, Next.js API!' });
}

上述代码定义了一个简单的 API 端点,当客户端发送 GET 请求到 /api/hello 时,会返回一个包含 message 字段的 JSON 响应。req 对象是 Node.js 的 http.IncomingMessage 的实例,包含了请求的所有信息,如请求头、请求方法等。res 对象是 http.ServerResponse 的实例,用于发送响应给客户端,通过 status 方法设置响应状态码,json 方法发送 JSON 格式的响应。

处理不同的 HTTP 方法

API Routes 可以处理不同的 HTTP 方法,如 GET、POST、PUT、DELETE 等。下面是一个处理 GET 和 POST 请求的示例:

export default function handler(req, res) {
    if (req.method === 'GET') {
        res.status(200).json({ message: 'This is a GET request' });
    } else if (req.method === 'POST') {
        const body = JSON.parse(req.body);
        res.status(200).json({ message: `Received data: ${JSON.stringify(body)}` });
    } else {
        res.status(405).end();
    }
}

在这个示例中,根据 req.method 判断请求方法。如果是 GET 请求,返回一个简单的消息;如果是 POST 请求,解析请求体中的数据并返回。如果是其他不支持的请求方法,则返回 405 Method Not Allowed 状态码。

与数据库交互

API Routes 通常用于与数据库进行交互。以 MongoDB 为例,首先需要安装 mongodb 包:

npm install mongodb

然后可以在 API Route 中连接数据库并执行操作。下面是一个插入数据到 MongoDB 的示例:

import { MongoClient } from'mongodb';

const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);

export default async function handler(req, res) {
    if (req.method === 'POST') {
        try {
            await client.connect();
            const db = client.db('my - database');
            const collection = db.collection('my - collection');
            const result = await collection.insertOne(req.body);
            res.status(201).json({ message: 'Data inserted successfully', insertedId: result.insertedId });
        } catch (e) {
            console.error(e);
            res.status(500).json({ message: 'Error inserting data' });
        } finally {
            await client.close();
        }
    } else {
        res.status(405).end();
    }
}

在上述代码中,通过 MongoClient 连接到本地的 MongoDB 实例,在接收到 POST 请求时,将请求体中的数据插入到指定的集合中。成功插入后返回 201 Created 状态码及插入数据的 ID,出现错误则返回 500 Internal Server Error 状态码。

保护 API Routes

在实际应用中,通常需要对 API Routes 进行保护,以确保只有授权的请求可以访问。一种常见的方法是使用 JWT(JSON Web Token)进行身份验证。首先安装 jsonwebtoken 包:

npm install jsonwebtoken

假设前端在请求头中发送 JWT 令牌,API Route 可以验证该令牌:

import jwt from 'jsonwebtoken';

export default function handler(req, res) {
    const token = req.headers.authorization;
    if (!token) {
        return res.status(401).json({ message: 'Unauthorized: No token provided' });
    }

    try {
        const decoded = jwt.verify(token.split(' ')[1], 'your - secret - key');
        // 验证通过,继续处理请求
        res.status(200).json({ message: 'Authorized request' });
    } catch (err) {
        res.status(401).json({ message: 'Unauthorized: Invalid token' });
    }
}

在上述代码中,从请求头中获取 Authorization 字段,解析出 JWT 令牌并使用 jwt.verify 方法进行验证。如果验证成功,继续处理请求;如果验证失败,返回 401 Unauthorized 状态码。

结合 getStaticProps 和 API Routes

从 API Routes 获取数据用于 getStaticProps

在实际项目中,getStaticProps 经常会从 API Routes 获取数据,而不是直接从外部 API 获取。这样可以在 API Route 中进行更多的数据处理和权限验证。假设我们有一个 API Route pages/api/posts.js 用于获取文章列表:

export default async function handler(req, res) {
    // 模拟从数据库获取数据
    const posts = [
        { id: 1, title: 'Post 1', body: 'Content of post 1' },
        { id: 2, title: 'Post 2', body: 'Content of post 2' }
    ];
    res.status(200).json(posts);
}

然后在页面组件的 getStaticProps 中调用这个 API Route:

export async function getStaticProps() {
    const res = await fetch('/api/posts');
    const posts = await res.json();
    return {
        props: {
            posts
        }
    };
}

const HomePage = ({ posts }) => {
    return (
        <div>
            {posts.map(post => (
                <div key={post.id}>
                    <h2>{post.title}</h2>
                    <p>{post.body}</p>
                </div>
            ))}
        </div>
    );
};

export default HomePage;

在这个示例中,getStaticProps 通过 fetch 调用本地的 API Route /api/posts 获取文章列表数据,并将数据传递给 HomePage 组件进行渲染。

在 API Route 中使用 getStaticProps 的数据

虽然这种情况相对较少,但有时也可能需要在 API Route 中使用 getStaticProps 获取的数据。例如,在一个多语言应用中,getStaticProps 获取当前语言设置,而 API Route 需要根据这个语言设置返回相应语言的数据。假设 getStaticProps 获取语言设置并传递给页面组件:

export async function getStaticProps() {
    const lang = 'en'; // 模拟获取语言设置
    return {
        props: {
            lang
        }
    };
}

const Page = ({ lang }) => {
    return (
        <div>
            {/* 页面内容 */}
        </div>
    );
};

export default Page;

在 API Route 中,可以通过自定义的中间件或者其他方式来获取这个语言设置。例如,使用自定义中间件:

const withLang = (handler) => {
    return async (req, res) => {
        const lang = 'en'; // 这里应该从合适的地方获取,比如从请求头或者共享状态
        req.lang = lang;
        return handler(req, res);
    };
};

export default withLang(async function handler(req, res) {
    // 根据 req.lang 返回相应语言的数据
    const data = {
        en: { message: 'Hello in English' },
        fr: { message: 'Bonjour in French' }
    }[req.lang];
    res.status(200).json(data);
});

在这个示例中,withLang 中间件将语言设置添加到 req 对象上,API Route 的处理函数可以根据这个语言设置返回相应语言的数据。

优化与最佳实践

性能优化

  1. 代码拆分:Next.js 自动进行代码拆分,确保只有在需要时才加载 JavaScript 代码。这有助于减少初始加载的代码量,提高页面加载速度。例如,在页面组件中导入的模块,如果在页面初始渲染时不需要,Next.js 会将其拆分出来,在需要时再异步加载。
  2. 图片优化:Next.js 提供了内置的图片优化功能。通过使用 <Image> 组件,Next.js 会自动优化图片的尺寸、格式等,以适应不同设备和网络环境。例如:
import Image from 'next/image';

const MyImage = () => {
    return (
        <Image
            src="/my - image.jpg"
            alt="My Image"
            width={300}
            height={200}
        />
    );
};

export default MyImage;

在这个示例中,<Image> 组件会根据设备的屏幕分辨率和网络状况,自动选择合适的图片尺寸和格式,避免加载过大的图片。

最佳实践

  1. 目录结构组织:保持 pages 目录结构清晰,按照功能模块划分页面。例如,可以将用户相关的页面放在 pages/user 目录下,文章相关的页面放在 pages/post 目录下。对于共享的组件、样式和工具函数,分别放在 componentsstylesutils 等目录中,以提高代码的可维护性和复用性。
  2. 错误处理:在 getStaticProps 和 API Routes 中都要进行适当的错误处理。在 getStaticProps 中,如果数据获取失败,可以返回一个包含错误信息的 props,并在页面组件中进行相应的提示。在 API Routes 中,根据错误类型返回合适的 HTTP 状态码,如 400 Bad Request、500 Internal Server Error 等,同时在响应体中包含错误描述信息,以便客户端进行调试。
  3. 测试:对 Next.js 应用进行单元测试和集成测试。对于页面组件,可以使用 Jest 和 React Testing Library 进行单元测试,测试组件的渲染和交互逻辑。对于 API Routes,可以使用 Supertest 等工具进行集成测试,验证 API 的功能和响应。例如,测试 API Route 的 GET 请求:
const request = require('supertest');
const app = require('../pages/api/hello');

describe('GET /api/hello', () => {
    it('should return a JSON response', async () => {
        const response = await request(app).get('/api/hello');
        expect(response.status).toBe(200);
        expect(response.body.message).toBe('Hello, Next.js API!');
    });
});

通过这些测试,可以确保应用在不同情况下的稳定性和正确性。

通过以上内容,你应该对如何从零开始构建 Next.js 应用,并掌握 getStaticProps 和 API Routes 有了深入的了解。在实际开发中,不断实践和总结经验,结合项目需求充分发挥 Next.js 的优势,构建出高性能、可维护的前端应用。