自定义 Web Server

Modern.js 将大部分项目需要的服务端能力都进行了封装,通常项目无需进行服务端开发。但在有些开发场景下,例如用户鉴权、请求预处理、添加页面渲染骨架等,项目仍需要对服务端进行定制。

要在 Modern.js 项目中使用自定义 Web Server,请按照以下步骤操作:

  1. 安装 @modern-js/server-runtime 依赖

如果项目尚未安装 @modern-js/server-runtime 依赖,请先安装:

pnpm add @modern-js/server-runtime
版本一致性

请确保 @modern-js/server-runtime 的版本与项目中 @modern-js/app-tools 的版本保持一致。Modern.js 的所有官方包使用统一版本号发布,版本不一致可能导致兼容性问题。

请先查看 @modern-js/app-tools 的版本,然后安装相同版本的 @modern-js/server-runtime

# 查看当前 @modern-js/app-tools 的版本
pnpm list @modern-js/app-tools

# 安装相同版本的 @modern-js/server-runtime
pnpm add @modern-js/server-runtime@<版本>
  1. 创建 server 目录和配置文件

在项目根目录下创建 server/modern.server.ts 文件:

server/modern.server.ts
import { defineServerConfig } from '@modern-js/server-runtime';

export default defineServerConfig({
  middlewares: [], // 中间件
  renderMiddlewares: [], // 渲染中间件
  plugins: [], // 插件
  onError: () => {}, // 错误处理
});

创建文件后,可以在这个文件中编写自定义逻辑。

自定义 Web Server 能力

Modern.js 的服务器基于 Hono 实现,在最新版本的自定义 Web Server 中,我们向用户暴露了 Hono 的中间件能力,你可以参考 Hono 文档 了解更多用法。

server/modern.server.ts 文件中添加如下配置来扩展 Server:

  • 中间件(Middleware)
  • 渲染中间件(RenderMiddleware)
  • 服务端插件(Plugin)

其中 Plugin 中可以定义 MiddlewareRenderMiddleware。 中间件加载流程如下图所示:

基本配置

server/modern.server.ts
import { defineServerConfig } from '@modern-js/server-runtime';

export default defineServerConfig({
  middlewares: [], // 中间件
  renderMiddlewares: [], // 渲染中间件
  plugins: [], // 插件
  onError: () => {}, // 错误处理
});

类型定义

defineServerConfig 类型定义如下:

import type { MiddlewareHandler } from 'hono';

type MiddlewareObj = {
  name: string;
  path?: string;
  method?: 'options' | 'get' | 'post' | 'put' | 'delete' | 'patch' | 'all';
  handler: MiddlewareHandler | MiddlewareHandler[];
};
type ServerConfig = {
  middlewares?: MiddlewareObj[];
  renderMiddlewares?: MiddlewareObj[];
  plugins?: ServerPlugin[];
  onError?: (err: Error, c: Context) => Promise<any> | any;
};

Middleware

Middleware 支持在 Modern.js 服务的请求处理页面路由的流程前后,执行自定义逻辑。 即自定义逻辑既要处理接口路由,也要作用于页面路由,那么 Middleware 是不二选择。

Note

如果仅需要处理 BFF 接口路由,可以通过检查 req.path 是否以 BFF prefix 开头,来判断是否为 BFF 接口请求。

使用姿势如下:

server/modern.server.ts
import {
  defineServerConfig,
  type MiddlewareHandler,
} from '@modern-js/server-runtime';

export const handler: MiddlewareHandler = async (c, next) => {
  const monitors = c.get('monitors');
  const start = Date.now();

  await next();

  const end = Date.now();
  // 上报耗时
  monitors.timing('request_timing', end - start);
};

export default defineServerConfig({
  middlewares: [
    {
      name: 'request-timing',
      handler,
    },
  ],
});
Warning

必须执行 next 函数才会执行后续的 Middleware。

RenderMiddleware

如果只需要处理页面渲染的前后执行逻辑,modern.js 也提供了渲染中间件,使用姿势如下:

server/modern.server.ts
import {
  defineServerConfig,
  type MiddlewareHandler,
} from '@modern-js/server-runtime';

// 注入 render 性能指标
const renderTiming: MiddlewareHandler = async (c, next) => {
  const start = Date.now();

  await next();

  const end = Date.now();
  c.res.headers.set('server-timing', `render; dur=${end - start}`);
};

// 修改响应体
const modifyResBody: MiddlewareHandler = async (c, next) => {
  await next();

  const { res } = c;
  const text = await res.text();
  const newText = text.replace('<body>', '<body> <h3>bytedance</h3>');

  c.res = c.body(newText, {
    status: res.status,
    headers: res.headers,
  });
};

export default defineServerConfig({
  renderMiddlewares: [
    {
      name: 'render-timing',
      handler: renderTiming,
    },
    {
      name: 'modify-res-body',
      handler: modifyResBody,
    },
  ],
});

Plugin

Modern.js 支持在自定义插件中为 Server 添加上述 Middleware 及 RenderMiddleware,使用姿势如下:

server/plugins/server.ts
import type { ServerPlugin } from '@modern-js/server-runtime';

export default (): ServerPlugin => ({
  name: 'serverPlugin',
  setup(api) {
    api.onPrepare(() => {
      const { middlewares, renderMiddlewares } = api.getServerContext();

      // 注入服务端数据,供页面 dataLoader 消费
      middlewares?.push({
        name: 'server-plugin-middleware',
        handler: async (c, next) => {
          c.set('message', 'hi modern.js');
          await next();
          // ...
        },
      });

      // 重定向
      renderMiddlewares?.push({
        name: 'server-plugin-render-middleware',
        handler: async (c, next) => {
          const user = getUser(c.req);
          if (!user) {
            return c.redirect('/login');
          }

          await next();
        },
      });
    });
  },
});
server/modern.server.ts
import { defineServerConfig } from '@modern-js/server-runtime';
import serverPlugin from './plugins/serverPlugin';

export default defineServerConfig({
  plugins: [serverPlugin()],
});
src/routes/page.data.ts
import { useHonoContext } from '@modern-js/server-runtime';
import { defer } from '@modern-js/runtime/router';

export default () => {
  const ctx = useHonoContext();
  // SSR 场景消费服务端注入的数据
  const message = ctx.get('message');

  // ...
};

onError

onError 是一个全局错误处理函数,用于捕获和处理 Modern.js server 中的所有未捕获错误。通过自定义 onError 函数,开发者可以统一处理不同类型的错误,返回自定义的错误响应,实现错误日志记录、错误分类处理等功能。

以下是一个基本的 onError 配置示例:

server/modern.server.ts
import { defineServerConfig } from '@modern-js/server-runtime';

export default defineServerConfig({
  onError: (err, c) => {
    // 记录错误日志
    console.error('Server error:', err);

    // 根据不同的错误类型返回不同的响应
    if (err instanceof SyntaxError) {
      return c.json({ error: 'Invalid JSON' }, 400);
    }

    // 根据请求路径定制 bff 异常响应
    if (c.req.path.includes('/api')) {
      return c.json({ message: 'API error occurred' }, 500);
    }

    return c.text('Internal Server Error', 500);
  },
});