自定义 Web 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];
Hook
WARNING
我们推荐使用 UnstableMiddleware 代替 Hook。
Modern.js 提供的 Hook 用于控制 Web Server 中的特定逻辑,所有的页面请求都会经过 Hook。
目前提供了两种 Hook,分别是 AfterMatch
和 AfterRender
,开发者可以在 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 时,应该有以下最佳实践:
- 在 afterMatch 中做权限校验。
- 在 afterMatch 做 Rewrite 和 Redirect。
- 在 afterRender 中做 HTML 内容注入。
迁移至新版自定义 Server
迁移背景
Modern.js Server 在不断演进,为了提供更强大的功能,我们对中间件和 Server 插件的定义和使用方式进行了优化。
虽然旧版自定义 Web Server 写法仍被兼容,但我们强烈建议您按照本指南进行迁移,以充分利用新版的优势。
迁移步骤
- 升级 Modern.js 版本至 x.67.5 及以上。
- 按照新版定义方式,在
server/modern.server.ts
中配置中间件或插件。
- 将
server/index.ts
自定义逻辑迁移到中间件或插件中,并参考 Context
和 Next
差异,更新您的代码。
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。