---
sidebar_position: 16
---

# 自定义 Web Server

Modern.js 作为以客户端为中心的开发框架，对服务端的定制能力较弱。而在有些开发场景下，需要定制特殊的服务端逻辑，例如用户鉴权、请求预处理、添加页面渲染骨架等。

一些开发者可能会有疑惑，Modern.js 已经提供了 [BFF 能力](/guides/advanced-features/bff/function.html)，为什么还需要**自定义 Web Server**。

因为在默认情况下，页面路由并不会经过 BFF，它没有办法为页面访问提供服务端的定制逻辑。之所以这样设计，是因为我们不希望控制页面的服务与 BFF 服务绑定在一起，避免 BFF 的框架限制页面的部署方式。例如将页面与 BFF 分开托管、将页面服务部署到非 Node 的环境上，或是针对部署平台做定制等。

出于上述原因，Modern.js 提供了三种方式，让项目可以在根据需求，渐进式的定制服务端能力。

:::warning
三种扩展方式无法同时工作，开发者需要根据场景选择合适的方式。
:::

## 使用 API 扩展 Web Server

第一种方式是通过 Modern.js 提供的服务端运行时 API，在特定的生命周期对服务端进行定制。提供这种方式的目的是在部分情况下，开发者并不需要控制完整的 Web Server，只需要添加服务端逻辑即可。

这种方式无法控制完整的 Web Server，并且扩展逻辑**只在请求页面时生效**。因此，它适用于服务端逻辑相对简单，不希望额外创建 BFF 或者 BFF 和页面无需公用服务端逻辑场景。

可以在项目根目录执行 `pnpm run new` 命令，开启「自定义 Web Serve」功能：

```bash
? 请选择你想要的操作 创建工程元素
? 请选择创建元素类型 新建「自定义 Web Server」源码目录
```

执行命令后，在 `modern.config.ts` 中注册 `@modern-js/plugin-server` 插件:

```ts title="modern.config.ts"
import { serverPlugin } from '@modern-js/plugin-server';

export default defineConfig({
  plugins: [..., serverPlugin()],
});
```

开启功能后，项目目录下会自动创建 `server/index.ts` 文件，可以在这个文件中编写自定义逻辑。Modern.js 提供了 **Hook** 与 **Middleware** 两类 API 来扩展 Web Server。

### Hook

Modern.js 提供的 Hook 用于控制 Web Server 中的内置逻辑，所有的页面请求都会经过 Hook。

目前提供了两种 Hook，分别是 `AfterMatch` 和 `AfterRender`，可以用于修改渲染结果。可以在 `server/index.ts` 中这样写：

```ts
import type {
  AfterMatchHook,
  AfterRenderHook,
} from '@modern-js/runtime/server';

export const afterMatch: AfterMatchHook = (ctx, next) => {
  next();
};

export const afterRender: AfterRenderHook = (ctx, next) => {
  next();
};
```

项目在使用 Hook 时，应该有以下最佳实践：

1. 在 afterMatch 中做权限校验。
2. 在 afterMatch 做 Rewrite 和 Redirect。
3. 在 afterRender 中做 HTML 内容注入。

:::note
详细 API 和更多用法可以查看 [Hook](/apis/app/runtime/web-server/hook)。
:::

### Middleware

对于某些项目，可能在服务端有更多的需求，Modern.js 提供了 Middleware 为 Web Server 添加前置中间件。它只能运行在 Node 环境下，因此如果项目被部署到其他环境中，如 Worker 环境，则不可以使用 Middleware。

:::note
下一个大版本，Modern.js 将会使用新 Middleware 来替代该写法。

推荐使用 [UnstableMiddleware](/guides/advanced-features/web-server.html#unstablemiddleware) 处理页面请求。
:::

Modern.js 默认提供了一套 API 供项目使用：

```ts
import { Middleware } from '@modern-js/runtime/server';

export const middleware: Middleware = (context, next) => {
  const {
    source: { req, res },
  } = context;
  console.log(req.url);
  next();
};
```

:::note
详细 API 和更多用法可以查看 [Middleware](/apis/app/runtime/web-server/middleware)。
:::

项目在使用 Middleware 时，应该有以下最佳实践：

1. 在 Middleware 中可以直接操作原生的请求、响应对象，做数据打点、注入 SSR 渲染可能用到的 Node 服务（数据库、Redis 等）。
2. 在 Middleware 里可以做类似功能打标、爬虫优化等功能。
3. 在 Middleware 里可以无视后续默认渲染，自定义渲染流程。

**总的来说，CSR 项目中，使用 Hook 基本能满足简单场景的所有需求。SSR 项目中，可以使用 Middleware 做更复杂的 Node 扩展。**

### Unstable Middleware

Modern.js 将提供新 Middleware 为 Web Server 添加前置中间件，支持在处理页面的前后执行自定义逻辑。

```ts title="server/index.ts"
import {
  UnstableMiddleware,
  UnstableMiddlewareContext,
} from '@modern-js/runtime/server';

const time: UnstableMiddleware = async (c: UnstableMiddlewareContext, next) => {
  const start = Date.now();

  await next();

  const end = Date.now();

  console.log(`dur=${end - start}`);
};

export const unstableMiddleware: UnstableMiddleware[] = [time];
```

:::note
详细 API 和更多用法查看 [UnstableMiddleware](/apis/app/runtime/web-server/unstable_middleware)
:::

## 通过 BFF 托管页面请求

第二种方式，是利用 BFF 来托管页面渲染，这种方式下，所有的请求都会先打到 BFF 的服务。

因为这种方式可以通过 BFF 统一控制所有请求的服务端逻辑。因此，它适用于服务端逻辑复杂，BFF 和页面需要公用服务端逻辑的场景。但它整体还是依托于 Modern.js 的 Web Server 运行，无法将逻辑运行在已有的服务上。

使用这种方式，我们需要先通过 `pnpm new` 开启「BFF」功能。然后在配置文件中添加 [`bff.enableHandleWeb`](/configure/app/bff/enable-handle-web.html) 配置：

```ts
export default defineConfig({
  bff: {
    enableHandleWeb: true,
  },
});
```

当该值设置为 `true` 时，页面请求流量也会经过 BFF，并且 Modern.js 内置的页面渲染的逻辑默认会作为 BFF 服务的最后一个中间件运行。

## 完全自定义的 Web Server

:::note
敬请期待
:::
