自定义 Web Server(不推荐)

WARNING

自定义 Web Server 兼容但不再推荐使用,扩展 Server 能力请移步 自定义 Server,迁移指南参考 迁移至新版自定义 Server

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

Modern.js 提供了 渲染中间件(Middleware)生命周期钩子(Hook) 两类 API 来扩展 Web Server。

NOTE

Middleware 与 Hook 只会在用户请求页面路由时生效,BFF 路由不会经过这些 API。

开启自定义 Web Server

开发者可以在项目根目录执行 pnpm run new 命令,开启「自定义 Web Server」功能:

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

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

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

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

开启功能后,项目目录下会自动创建 server/index.ts 文件,可以在这个文件中编写自定义逻辑。

自定义 Web Server 能力

Unstable Middleware

Modern.js 支持为 Web Server 添加渲染中间件,支持在处理页面路由的前后执行自定义逻辑。

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];
INFO

详细 API 和更多用法查看 UnstableMiddleware

Hook

WARNING

我们推荐使用 UnstableMiddleware 代替 Hook。

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

目前提供了两种 Hook,分别是 AfterMatchAfterRender,开发者可以在 server/index.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 内容注入。
INFO

详细 API 和更多用法可以查看 Hook

迁移至新版自定义 Server

迁移背景

Modern.js Server 在不断演进,为了提供更强大的功能,我们对中间件和 Server 插件的定义和使用方式进行了优化。 虽然旧版自定义 Web Server 写法仍被兼容,但我们强烈建议您按照本指南进行迁移,以充分利用新版的优势。

迁移步骤

  1. 升级 Modern.js 版本至 x.67.5 及以上。
  2. 按照新版定义方式,在 server/modern.server.ts 中配置中间件或插件。
  3. server/index.ts 自定义逻辑迁移到中间件或插件中,并参考 ContextNext 差异,更新您的代码。

Context 差异

新版中间件 handler 类型为 Hono 的 MiddlewareHandler,即 Context 类型为 Hono Context。对比旧版自定义 Web Server 中 Context 差异如下:

UnstableMiddleware

type Body = ReadableStream | ArrayBuffer | string | null;

type UnstableMiddlewareContext<
  V extends Record<string, unknown> = Record<string, unknown>,
> = {
  request: Request;
  response: Response;
  get: Get<V>;
  set: Set<V>;
  // 当前匹配到的路由信息
  route: string;
  header: (name: string, value: string, options?: { append?: boolean }) => void;
  status: (code: number) => void;
  redirect: (location: string, status?: number) => Response;
  body: (data: Body, init?: ResponseInit) => Response;
  html: (
    data: string | Promise<string>,
    init?: ResponseInit,
  ) => Response | Promise<Response>;
};

UnstableMiddleware Context 和 Hono Context 的具体差异:

UnstableMiddleware Hono 说明
c.request c.req.raw 参考 HonoRequest raw 文档
c.response c.res 参考 Hono Context res 文档
c.route c.get('route') 获取应用上下文信息
loaderContext.get honoContext.get 通过 c.set 注入数据后 dataLoader 中消费:旧版通过 loaderContext.get 获取,新版参考 Plugin 示例

Middleware

type MiddlewareContext = {
  response: {
    set: (key: string, value: string) => void;
    status: (code: number) => void;
    getStatus: () => number;
    cookies: {
      set: (key: string, value: string, options?: any) => void;
      clear: () => void;
    };
    raw: (
      body: string,
      { status, headers }: { status: number; headers: Record<string, any> },
    ) => void;
    locals: Record<string, any>;
  };
  request: {
    url: string;
    host: string;
    pathname: string;
    query: Record<string, any>;
    cookie: string;
    cookies: {
      get: (key: string) => string;
    };
    headers: IncomingHttpHeaders;
  };
  source: {
    req: IncomingMessage;
    res: ServerResponse;
  };
};

Middleware Context 和 Hono Context 的具体差异:

UnstableMiddleware Hono 说明
c.request.cookie c.req.cookie() 参考 Hono Cookie Helper 文档
c.request.pathname c.req.path 参考 HonoRequest path 文档
c.request.url - Hono c.req.url 为完整请求路径,自行通过 url 计算
c.request.host c.req.header('Host') 通过 header 获取 host
c.request.query c.req.query() 参考 HonoRequest query 文档
c.request.headers c.req.header() 参考 HonoRequest header 文档
c.response.set c.res.headers.set 例:c.res.headers.set('custom-header', '1')
c.response.status c.status 例:c.status(201)
c.response.cookies c.header 例:c.header('Set-Cookie', 'user_id=123')
c.response.raw c.res 参考 Hono Context res 文档

Hook

type HookContext = {
  response: {
    set: (key: string, value: string) => void;
    status: (code: number) => void;
    getStatus: () => number;
    cookies: {
      set: (key: string, value: string, options?: any) => void;
      clear: () => void;
    };
    raw: (
      body: string,
      { status, headers }: { status: number; headers: Record<string, any> },
    ) => void;
  };
  request: {
    url: string;
    host: string;
    pathname: string;
    query: Record<string, any>;
    cookie: string;
    cookies: {
      get: (key: string) => string;
    };
    headers: IncomingHttpHeaders;
  };
};

type AfterMatchContext = HookContext & {
  router: {
    redirect: (url: string, status: number) => void;
    rewrite: (entry: string) => void;
  };
};

type AfterRenderContext = {
  template: {
    get: () => string;
    set: (html: string) => void;
    prependHead: (fragment: string) => void;
    appendHead: (fragment: string) => void;
    prependBody: (fragment: string) => void;
    appendBody: (fragment: string) => void;
  };
};

Hook Context 大部分和 Middleware Context 一致,因此我们要额外关注不同 Hook 多余的部分。

UnstableMiddleware Hono 说明
router.redirect c.redirect 参考 Hono Context redirect 文档
router.rewrite - 暂时没有提供对应的能力
template API c.res 参考 Hono Context res 文档

Next API 差异

在 Middleware 和 Hook 中,即使不执行 next,渲染函数也会执行。 在新的设计中,必须执行 next 函数才会执行后续的 Middleware。