自定义服务
Nextjs提供了一个高级特性,它为我们提供了自定义服务器的能力,这使得我们可以在需要时创建自定义路由和处理请求。下面是一个简单的 Next.j 自定义服务器示例,演示了如何创建一个自定义路由:
当你需要在 Next.js 中创建自定义服务器并添加自定义路由时,以下是一些详细步骤:
- 
创建 Next.js 应用:
首先,确保你已经创建了一个
Next.js应用。你可以使用以下命令创建一个新的 Next.js 应用:bash$ npx create-next-app my-custom-app $ cd my-custom-app $ yarn这将创建一个基本的
Next.js应用,并安装所需的依赖项。 - 
创建自定义服务器文件:
在
Next.js应用的根目录下,创建一个名为server.js的文件,用于定义自定义服务器的行为。 - 
导入依赖项:
在
server.js文件中,首先导入必要的依赖项,包括express和next:javascriptconst express = require('express'); const next = require('next'); - 
配置 Next.js 应用:
使用
next创建Next.js应用的实例,并根据开发或生产环境进行配置:javascriptconst dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler();这些配置选项将告诉
Next.js应用是否运行在开发模式下,以及如何处理请求。 - 
准备应用程序:
使用
app.prepare()方法准备Next.js应用程序以响应请求:javascriptapp.prepare().then(() => { // 在这里定义服务器路由 });这将确保
Next.js应用程序准备好处理请求。 - 
定义自定义路由:
在
server.js文件中,可以使用Express.js来定义自定义路由。例如,下面的代码定义了一个/custom-route路由,它返回一个简单的文本响应:javascriptconst server = express(); server.get('/custom-route', (req, res) => { // 在这里处理自定义路由的请求 res.send('This is a custom route'); });你可以根据需要定义多个自定义路由。
 - 
默认路由处理:
在自定义路由之后,需要定义一个默认路由,以便处理所有其他请求。通常,这个默认路由会调用
Next.js的getRequestHandler()方法:javascriptserver.get('*', (req, res) => { return handle(req, res); });这确保了
Next.js应用程序可以处理剩余的路由。 - 
启动服务器:
最后,使用
server.listen()启动服务器,并指定要监听的端口:javascriptserver.listen(3000, (err) => { if (err) throw err; console.log('> Ready on http://localhost:3000'); });这将启动自定义服务器,并在端口
3000上监听请求。 - 
运行自定义服务器:
现在,你可以在终端中运行自定义服务器:
bash$ npm start访问
http://localhost:3000/custom-route将会看到This is a custom route的响应。 
这就是如何在 Next.js 中创建一个自定义服务器和自定义路由的详细步骤。通过这种方式,你可以为你的 Next.js 应用程序添加自定义路由和其他自定义功能,以满足特定的需求。请记住,在大多数情况下,使用内置的 Next.js 路由机制足以满足需求,只有在需要更高度定制的情况下才需要自定义服务器。
实战演练
接下来我们结合axios来实现一个简单的项目实战。我们在上一节的基础上进行改造。 打开终端执行命令
            
            
              bash
              
              
            
          
          #新建server目录,并新增app文件
$ mkdir server
$ cd server
$ touch app.ts
#新建utils目录,并新增index文件和axios文件
$ cd ..
$ mkdir utils
$ cd utils
$ touch index.ts
$ touch axios.ts
#新建services目录,并新增index文件和type文件
$ cd ..
$ mkdir services
$ cd services
$ touch index.ts
$ touch type.ts
#新建一个nodemon的配置文件
$ cd ..
$ mkdir nodemon.json
        执行完命令之后回到根目录下执行项目结构是这样的:
            
            
              bash
              
              
            
          
          $ tree -I 'node_modules|.next'
├── Dockerfile
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── api
│   │   └── hello.ts
│   └── index.tsx
├── public
│   ├── favicon.ico
│   ├── next.svg
│   └── vercel.svg
├── server
│   └── app.ts
├── services
│   └──index.ts
│   └──type.ts
├── styles
│   ├── Home.module.css
│   └── globals.css
├── tsconfig.json
├── utils
│   ├── axios.ts
│   └── index.ts
└── yarn.lock
        app.ts文件使用express自定义服务
            
            
              ts
              
              
            
          
          import express, { Request, Response, NextFunction } from 'express';
import next from 'next';
import portfinder from 'portfinder';
import chalk from 'chalk';
const basePort = 3000;
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : basePort; // e.g: PORT=3000 yarn dev
portfinder.basePort = port;
const dev = process.env.NODE_ENV !== 'production';
portfinder.getPort(function (error: Error, nextPort: number) {
  if (error) {
    console.error(error);
    return;
  }
  if (port !== nextPort) {
    process.env.PORT = nextPort + '';
    console.info(
      `${chalk.yellow(
        'warn',
      )}  - Port ${port} is in use, trying ${nextPort} instead.`,
    );
  }
  const app = next({ dev, port: nextPort });
  const handle = app.getRequestHandler();
  app
    .prepare()
    .then(async () => {
      const server = express();
      process.env.PORT = nextPort.toString();
      server.all('*', (req: Request, res: Response, next: NextFunction) => {
        if (/^\/api\/.*/.test(req.url)) {
          res.json({msg:'Hello Warld'})
        } else {
          return handle(req, res);
        }
      });
      server.listen(nextPort, () =>
        console.info(
          `${chalk.green(
            'ready',
          )} - started server on 0.0.0.0:${nextPort}, url: http://localhost:${nextPort}`,
        ),
      );
    })
    .catch((error: Error) => {
      console.error(error);
    });
});
        我们使用express进行了自定义服务,并对以/api开头的请求进行了自定义处理,其他路径的请求交于next进行处理。 按照依赖包
            
            
              bash
              
              
            
          
          $ yarn add express portfinder chalk nodemon
$ yarn add ts-node
        新增nodemon配置文件
            
            
              json
              
              
            
          
          {
    "watch": ["server/**/*"],
    "ext": "ts",
    "exec": "ts-node server/app.ts"
}
        执行脚本 yarn dev
            
            
              bash
              
              
            
          
          $ yarn dev
        使用postman进行验证一下

好了,到此为止自定义服务基本完成了。
接下来我们自定义一个axios请求,主要目的是为了简化异步请求逻辑。
在新增的utils/axios文件下编写以下代码。
            
            
              ts
              
              
            
          
          import axioslib, { Method, AxiosHeaders } from 'axios';
export interface BaseResult<T> {
  success: boolean;
  t: number;
  code?: number;
  result: T;
  msg?: string;
}
export const DEVBASEURL = 'http://localhost:3000'; //默认开发环境的baseURL
const DEFAULT_AXIOS_TIMEOUT = 15000; //超时时间
export const axios = axioslib.create({
  timeout: DEFAULT_AXIOS_TIMEOUT,
  baseURL: DEVBASEURL
});
// 生成FormData
function createFormData<F>(forData: F): FormData | null {
  if (!forData) {
    return null;
  }
  const data = new FormData();
  Object.entries(forData).forEach(([name, value]) => {
    data.append(name, value as string);
  });
  return data;
}
// 处理url中的path参数
function pathReplace<P>(url: string, path: P): string {
  if (!path) {
    return url;
  }
  let nextApi = url;
  Object.entries(path).forEach(([name, value]) => {
    const encodedValue = encodeURIComponent(value + '');
    nextApi = nextApi
      .replace(`{${name}}`, encodedValue)
      .replace(`:${name}`, encodedValue);
  });
  return nextApi;
}
// P代表path参数,Q代表query参数,B代表body参数,H代表headers参数,F代表formData参数,R代表返回值
export default async function ajax<
  P,
  Q,
  B,
  H extends AxiosHeaders | null,
  F,
  R,
>(
  method: Method,
  url: string,
  path: P,
  query: Q,
  body: B,
  headers: H,
  formData: F,
): Promise<R> {
  const response = (
    await axios({
      method,
      url: `/api${pathReplace(url, path)}`,
      params: query,
      data: body || createFormData(formData),
      headers: headers || undefined,
    })
  ).data;
  if (!response.success) {
    throw new Error(response.msg || '请求失败');
  }
  return response;
}
        添加axios
            
            
              bash
              
              
            
          
          $ yarn add axios
        index文件中导出axios实例
            
            
              js
              
              
            
          
          // utils/axios
export { default as axios } from './axios';
        最基本的axios我们已经处理完毕,现在让我们来添加一个api接口
打开services文件夹的index.ts文件开始编写我们第一个接口
            
            
              ts
              
              
            
          
          // services/index.ts
import ajax, { BaseResult } from '../utils/axios';
import * as Type from './type';
class Services {
  public static getUserInfo(
    query: Type.GetUserInfo$Query,
  ): Promise<BaseResult<Type.GetUserInfo$Response>> {
    return ajax('GET', '/user/info', null, query, null, null, null);
  }
}
export default Services;
        继续type.ts文件定义给个参数的ts类型
            
            
              ts
              
              
            
          
          export interface GetUserInfo$Query {
  id: string;
}
export interface GetUserInfo$Response {
  data: {
    name: string;
    age: number;
  };
}
        客户端的工作我们已经处理完毕了,加下来我们要去编写接口逻辑了,切换到server目录下的app.ts。我们编写我们的第一个后台接口。
            
            
              ts
              
              
            
          
          //server/app.ts
import express, { Request, Response } from 'express';
import next from 'next';
import portfinder from 'portfinder';
import chalk from 'chalk';
+ const { parse } = require('url');
const basePort = 3000;
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : basePort; // e.g: PORT=3000 yarn dev
portfinder.basePort = port;
const dev = process.env.NODE_ENV !== 'production';
portfinder.getPort(function (error: Error, nextPort: number) {
  if (error) {
    console.error(error);
    return;
  }
  if (port !== nextPort) {
    process.env.PORT = nextPort + '';
    console.info(
      `${chalk.yellow(
        'warn',
      )}  - Port ${port} is in use, trying ${nextPort} instead.`,
    );
  }
  const app = next({ dev, port: nextPort });
  const handle = app.getRequestHandler();
  app
    .prepare()
    .then(async () => {
      const server = express();
      process.env.PORT = nextPort.toString();
      server.all('*', (req: Request, res: Response) => {
        if (/^\/api\/.*/.test(req.url)) {
          // 拦截以 /api/ 开头的请求,做自定义处理
_          res.json({ msg: 'Hello World!' });
+          const parsedUrl = parse(req.url, true);
+          const { pathname, query } = parsedUrl;
+          if (pathname === '/api/user/info') {
+            if (query?.id === '1') {
+              res.json({ name: '张三', age: 18, success: true });
+            } else {
+              res.json({ msg: '没有找到', success: false });
+            }
+          } else {
+            res.json({ msg: 'Hello World!' });
+          }
        } else {
          // 对其他请求直接返回页面内容
          return handle(req, res);
        }
      });
      server.listen(nextPort, () =>
        console.info(
          `${chalk.green(
            'ready',
          )} - started server on 0.0.0.0:${nextPort}, url: http://localhost:${nextPort}`,
        ),
      );
    })
    .catch((error: Error) => {
      console.error(error);
    });
});
        按照处理路径的parse
            
            
              bash
              
              
            
          
          $ yarn add parse
        接口已经编写完毕,我们来调用一下这个接口。切换到page/index.tsx,删除多余代码后
            
            
              ts
              
              
            
          
          import Services from 'services';
export default function Home() {
  return (
    <>
      <button
        onClick={async () => {
          await Services.getUserInfo({ id: '1' });
        }}
      >
        按钮
      </button>
    </>
  );
}
        执行命令yarn dev,打开http://localhost:3000,点击按钮,打开控制台看看

好的,到此为止你已经基本使用next自定义服务开发接口了。 下一节我们正式踏入web服务构建。