Next.js

Next.js 初学者指南

Next.js 是一个用于构建 React 应用程序的流行框架,它为服务器端渲染、静态站点生成和许多开发者体验上的改进提供了开箱即用的解决方案。

主要特点

  1. 服务器端渲染 (SSR): 自动处理服务器端渲染,提升 SEO 和性能。
  2. 静态站点生成 (SSG): 在构建时生成静态 HTML 页面,提高加载速度。
  3. 自动代码分割: 通过按需加载代码,提高性能。
  4. 文件系统路由: 基于文件结构自动生成路由,无需手动配置。
  5. API 路由: 可以在 pages/api 目录中创建 API 路由,处理后端逻辑。
  6. 全局 CSS 和 CSS 模块: 支持全局 CSS 引入和 CSS 模块,避免样式冲突。
  7. TypeScript 支持: 内置 TypeScript 支持,提供类型检查和更好的开发者体验。

    主要集成库和技术

    • React:Next.js 基于 React 构建,使用 JSX 语法来编写组件。
    • Webpack:用于模块打包,Next.js 对其进行了优化和配置。
    • Babel:用于转译现代 JavaScript 代码。
    • PostCSS:用于处理 CSS,例如自动添加浏览器前缀。
    • ESLint:用于代码检查,确保代码质量。

      文件结构

      Next.js 默认的项目结构如下:

my-next-app/ ├── node_modules/ ├── public/ │ └── favicon.ico ├── styles/ │ ├── globals.css │ └── Home.module.css ├── pages/ │ ├── api/ │ │ └── hello.js │ ├── _app.js │ ├── _document.js │ ├── index.js │ └── about.js ├── .eslintrc.json ├── .gitignore ├── package.json └── next.config.js

主要文件和目录

  • public/:存放静态资源,如图片、字体等。放置在此目录下的文件可以通过根路径访问,例如 public/favicon.ico 可以通过 /favicon.ico 访问。
  • styles/:存放全局样式和 CSS 模块。globals.css 用于全局样式,Home.module.css 是一个 CSS 模块,作用域仅限于导入它的组件。
  • pages/:存放页面组件,文件名即为路由名。例如,pages/index.js 对应 / 路由,pages/about.js 对应 /about 路由。
    • _app.js:自定义应用根组件,覆盖默认的 App 组件,可以在这里添加全局样式或状态管理。
    • _document.js:自定义 Document 组件,主要用于修改服务器渲染页面的结构,例如添加自定义的 <html><body> 标签。
    • api/:存放 API 路由文件,文件名即为 API 路由名。例如,pages/api/hello.js 对应 /api/hello 路由。

      Next.js 与原生 React 的区别

      1. **服务器端渲染 (SSR) 和静态站点生成 (SSG)**:  原生 React 需要额外配置和库支持才能实现 SSR 和 SSG,而 Next.js 开箱即用。
      2. **文件系统路由**:  在原生 React 中,需要使用 `react-router` 等库手动配置路由。Next.js 通过文件系统自动生成路由,无需额外配置。
      3. **API 路由**:  Next.js 提供了内置的 API 路由功能,可以在同一项目中编写后端逻辑。原生 React 通常需要额外的服务器端设置。
      4. **自动代码分割**:  原生 React 需要手动配置代码分割,而 Next.js 会自动进行代码分割,提升应用性能。
      5. **静态资源管理**:  Next.js 提供了 `public` 目录来管理静态资源,简化了资源的引用和使用。
      6. **全局 CSS 和 CSS 模块支持**:  Next.js 内置了对全局 CSS 和 CSS 模块的支持,原生 React 需要额外配置。
      7. **开发者体验**:  Next.js 提供了丰富的开发者体验改进,如内置的 TypeScript 支持、热更新和错误报告等。 ## 示例代码 ### 创建一个基本页面 `pages/index.js`: ```jsx import Head from 'next/head'; import styles from '../styles/Home.module.css'; export default function Home() { return (
      

      <div className={styles.container}>

      Next.js App

      <main className={styles.main}> <h1 className={styles.title}> Welcome to Next.js! </h1> </main> </div> ); }

      ### 创建一个 API 路由
      `pages/api/hello.js`:
      ```javascript
      export default function handler(req, res) {
      res.status(200).json({ message: 'Hello, world!' });
      }
      

      通过以上内容,初学者可以快速了解 Next.js 的基本知识点和与原生 React 的主要区别,从而更好地开始使用 Next.js 进行开发。

进阶主题

数据获取

Next.js 提供了三种方式来获取数据:getStaticPropsgetServerSidePropsgetStaticPaths

  1. getStaticProps: 用于在构建时获取数据。适用于静态生成的页面。
    // pages/index.js
    export async function getStaticProps() {
      const res = await fetch('https://api.example.com/data');
      const data = await res.json();
      return {
        props: {
          data,
        },
      };
    }
    export default function Home({ data }) {
      return <div>{data.title}</div>;
    }
    
  2. getServerSideProps: 用于在每次请求时获取数据。适用于需要动态渲染的页面。
    // pages/index.js
    export async function getServerSideProps() {
      const res = await fetch('https://api.example.com/data');
      const data = await res.json();
      return {
        props: {
          data,
        },
      };
    }
    export default function Home({ data }) {
      return <div>{data.title}</div>;
    }
    
  3. getStaticPaths: 与 getStaticProps 一起使用,生成动态路由的静态页面。
    // pages/posts/[id].js
    export async function getStaticPaths() {
      const res = await fetch('https://api.example.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({ params }) {
      const res = await fetch(`https://api.example.com/posts/${params.id}`);
      const post = await res.json();
      return {
        props: {
          post,
        },
      };
    }
    export default function Post({ post }) {
      return <div>{post.title}</div>;
    }
    

    自定义 _app.js

    _app.js 文件用于自定义应用的根组件。可以在这里添加全局样式、状态管理、布局等。

    // pages/_app.js
    import '../styles/globals.css';
    function MyApp({ Component, pageProps }) {
      return <Component {...pageProps} />;
    }
    export default MyApp;
    

    自定义 _document.js

    _document.js 文件用于修改服务器渲染的 HTML 结构,通常用于添加额外的 meta 标签、链接字体等。

    // pages/_document.js
    import Document, { Html, Head, Main, NextScript } from 'next/document';
    class MyDocument extends Document {
      render() {
     return (
       <Html>
         <Head>
           <link rel="stylesheet" href="https://example.com/fonts.css" />
         </Head>
         <body>
           <Main />
           <NextScript />
         </body>
       </Html>
     );
      }
    }
    export default MyDocument;
    

    静态资源管理

    public 目录用于存放静态资源。可以通过根路径直接访问这些资源。

    <img src="/images/logo.png" alt="Logo" />
    

    环境变量

    Next.js 支持使用 .env 文件来定义环境变量。

    # .env.local
    NEXT_PUBLIC_API_URL=https://api.example.com
    

    在代码中使用环境变量:

    const apiUrl = process.env.NEXT_PUBLIC_API_URL;
    

    动态路由

    Next.js 支持文件名中的方括号语法来定义动态路由。例如,pages/posts/[id].js 将匹配 /posts/1/posts/2 等。

    API 路由

    Next.js 提供了一种简单的方式来定义 API 路由,可以在 pages/api 目录中创建文件,每个文件导出一个处理函数。

    // pages/api/hello.js
    export default function handler(req, res) {
      res.status(200).json({ message: 'Hello, world!' });
    }
    

    自定义配置

    可以通过 next.config.js 文件自定义 Next.js 的配置。

    // next.config.js
    module.exports = {
      reactStrictMode: true,
      env: {
     CUSTOM_KEY: 'my-value',
      },
      images: {
     domains: ['example.com'],
      },
    };
    

    中间件

    Next.js 11 引入了中间件,可以在请求处理过程中插入自定义逻辑。中间件放在 middleware.js 文件中。

    // middleware.js
    import { NextResponse } from 'next/server';
    export function middleware(req) {
      const url = req.nextUrl.clone();
      if (url.pathname === '/') {
     url.pathname = '/home';
     return NextResponse.redirect(url);
      }
      return NextResponse.next();
    }
    

    参考资料

在 Next.js 中,创建 API 路由非常简单。API 路由文件存放在 pages/api 目录中,每个文件导出一个处理函数。以下是一些使用数据库(例如 MongoDB)和 ORM(例如 Prisma)的 API 路由示例,涵盖了 CRUD 操作。

示例 1:连接 MongoDB 的简单 CRUD API

首先,确保你已经安装了必要的包:

npm install mongoose

创建一个数据库连接文件 lib/mongodb.js

// lib/mongodb.js
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
  throw new Error(
    'Please define the MONGODB_URI environment variable inside .env.local'
  );
}
let cached = global.mongoose;
if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}
async function connectToDatabase() {
  if (cached.conn) {
    return cached.conn;
  }
  if (!cached.promise) {
    cached.promise = mongoose.connect(MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    }).then((mongoose) => {
      return mongoose;
    });
  }
  cached.conn = await cached.promise;
  return cached.conn;
}
export default connectToDatabase;

创建一个模型文件 models/Post.js

// models/Post.js
import mongoose from 'mongoose';
const PostSchema = new mongoose.Schema({
  title: String,
  content: String,
});
const Post = mongoose.models.Post || mongoose.model('Post', PostSchema);
export default Post;

创建 API 路由

1. 获取所有帖子

// pages/api/posts/index.js
import connectToDatabase from '../../../lib/mongodb';
import Post from '../../../models/Post';
export default async function handler(req, res) {
  await connectToDatabase();
  if (req.method === 'GET') {
    const posts = await Post.find({});
    res.status(200).json({ posts });
  } else {
    res.setHeader('Allow', ['GET']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

2. 创建一个新帖子

// pages/api/posts/index.js
import connectToDatabase from '../../../lib/mongodb';
import Post from '../../../models/Post';
export default async function handler(req, res) {
  await connectToDatabase();
  if (req.method === 'POST') {
    const { title, content } = req.body;
    const post = new Post({ title, content });
    await post.save();
    res.status(201).json({ post });
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

3. 更新一个帖子

// pages/api/posts/[id].js
import connectToDatabase from '../../../lib/mongodb';
import Post from '../../../models/Post';
import { ObjectId } from 'mongodb';
export default async function handler(req, res) {
  await connectToDatabase();
  const { id } = req.query;
  if (req.method === 'PUT') {
    const { title, content } = req.body;
    const post = await Post.findByIdAndUpdate(
      ObjectId(id),
      { title, content },
      { new: true }
    );
    res.status(200).json({ post });
  } else {
    res.setHeader('Allow', ['PUT']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

4. 删除一个帖子

// pages/api/posts/[id].js
import connectToDatabase from '../../../lib/mongodb';
import Post from '../../../models/Post';
import { ObjectId } from 'mongodb';
export default async function handler(req, res) {
  await connectToDatabase();
  const { id } = req.query;
  if (req.method === 'DELETE') {
    await Post.findByIdAndDelete(ObjectId(id));
    res.status(204).end();
  } else {
    res.setHeader('Allow', ['DELETE']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

代码解释

  1. lib/mongodb.js
    • 负责连接 MongoDB 数据库,使用 Mongoose 来处理连接逻辑。
    • 使用缓存来确保在应用程序生命周期内只创建一次数据库连接。
  2. models/Post.js
    • 定义了一个简单的 Mongoose 模型 Post,包含 titlecontent 字段。
  3. pages/api/posts/index.js
    • 处理 /api/posts 路由。
    • GET 请求获取所有帖子。
    • POST 请求创建一个新帖子。
  4. pages/api/posts/[id].js
    • 处理 /api/posts/:id 动态路由。
    • PUT 请求更新指定 ID 的帖子。
    • DELETE 请求删除指定 ID 的帖子。

      使用 Prisma 的复杂 API 示例

      Prisma 是一个现代化的 ORM,可以简化数据库的操作。以下是一个使用 Prisma 进行 CRUD 操作的示例。 首先,安装 Prisma 及其客户端:

      npm install @prisma/client
      npx prisma init
      

      配置 prisma/schema.prisma 文件:

      datasource db {
      provider = "postgresql"
      url = env("DATABASE_URL")
      }
      generator client {
      provider = "prisma-client-js"
      }
      model Post {
      id Int @id @default(autoincrement())
      title String
      content String
      }
      

      然后,运行以下命令来生成 Prisma 客户端:

      npx prisma migrate dev --name init
      npx prisma generate
      

      创建 API 路由

      1. 获取所有帖子

      // pages/api/posts/index.js
      import { PrismaClient } from '@prisma/client';
      const prisma = new PrismaClient();
      export default async function handler(req, res) {
      if (req.method === 'GET') {
       const posts = await prisma.post.findMany();
       res.status(200).json({ posts });
      } else {
       res.setHeader('Allow', ['GET']);
       res.status(405).end(`Method ${req.method} Not Allowed`);
      }
      }
      

      2. 创建一个新帖子

      // pages/api/posts/index.js
      import { PrismaClient } from '@prisma/client';
      const prisma = new PrismaClient();
      export default async function handler(req, res) {
      if (req.method === 'POST') {
       const { title, content } = req.body;
       const post = await prisma.post.create({
       data: {
         title,
         content,
       },
       });
       res.status(201).json({ post });
      } else {
       res.setHeader('Allow', ['POST']);
       res.status(405).end(`Method ${req.method} Not Allowed`);
      }
      }
      

      3. 更新一个帖子

      // pages/api/posts/[id].js
      import { PrismaClient } from '@prisma/client';
      const prisma = new PrismaClient();
      export default async function handler(req, res) {
      const { id } = req.query;
      if (req.method === 'PUT') {
       const { title, content } = req.body;
       const post = await prisma.post.update({
       where: { id: parseInt(id) },
       data: {
         title,
         content,
       },
       });
       res.status(200).json({ post });
      } else {
       res.setHeader('Allow', ['PUT']);
       res.status(405).end(`Method ${req.method} Not Allowed`);
      }
      }
      

      4. 删除一个帖子

      // pages/api/posts/[id].js
      import { PrismaClient } from '@prisma/client';
      const prisma = new PrismaClient();
      export default async function handler(req, res) {
      const { id } = req.query;
      if (req.method === 'DELETE') {
       await prisma.post.delete({
       where: { id: parseInt(id) },
       });
       res.status(204).end();
      } else {
       res.setHeader('Allow', ['DELETE']);
       res.status(405).end(`Method ${req.method} Not Allowed`);
      }
      }
      

      代码解释

  5. Prisma 配置
    • prisma/schema.prisma 文件定义数据模型和数据库连接。
    • 使用 prisma migrateprisma generate 命令初始化数据库和生成客户端。
  6. Prisma 客户端
    • PrismaClient 用于与数据库交互。每个 API 路由文件创建一个新的 PrismaClient 实例。
  7. CRUD 操作
    • 使用 prisma.post.findMany() 获取所有帖子。
    • 使用 prisma.post.create() 创建新帖子。
    • 使用 prisma.post.update() 更新帖子。
    • 使用 prisma.post.delete() 删除帖子。 通过这些示例,你可以了解如何在 Next.js 中创建复杂的 API 路由,并与数据库进行交互。根据实际需求,可以扩展这些示例,实现更复杂的逻辑和功能。
Written on June 25, 2024