自定义 Web Server
Modern.js 将大部分项目需要的服务端能力都进行了封装,通常项目无需进行服务端开发。但在有些开发场景下,例如用户鉴权、请求预处理、添加页面渲染骨架等,项目仍需要对服务端进行定制。
开启自定义 Web Server
INFO
必须确保 Modern.js 版本是 x.67.5 及以上。
开发者可以在项目根目录执行 pnpm run new
命令,开启「自定义 Web Server」功能:
? 请选择你想要的操作 创建工程元素
? 请选择创建元素类型 新建「自定义 Web Server」源码目录
执行命令后,项目目录下会自动创建 server/modern.server.ts
文件,可以在这个文件中编写自定义逻辑。
自定义 Web Server 能力
server/modern.server.ts
文件中添加如下配置来扩展 Server:
- 中间件(Middleware)
- 渲染中间件(RenderMiddleware)
- 服务端插件(Plugin)
其中 Plugin 中可以定义 Middleware 与 RenderMiddleware。 中间件加载流程如下图所示:
基本配置
server/modern.server.ts
import { defineServerConfig } from '@modern-js/server-runtime';
export default defineServerConfig({
middlewares: [], // 中间件
renderMiddlewares: [], // 渲染中间件
plugins: [], // 插件
});
类型定义
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 | ServerPluginLegacy)[];
}
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 { ServerPluginLegacy } from '@modern-js/server-runtime';
export default (): ServerPluginLegacy => ({
name: 'serverPlugin',
setup(api) {
return {
prepare(serverConfig) {
const { middlewares, renderMiddlewares } = api.useAppContext();
// 注入服务端数据,供页面 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();
},
});
return serverConfig;
},
};
},
});
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');
// ...
};
旧版 API(废弃)
开启
开启自定义 Web Server 功能,需要执行以下步骤:
- devDependencies 增加
@modern-js/plugin-server
、tsconfig-paths
及 ts-node
依赖并安装。
tsconfig
的 include
中添加 server
。
modern.config.ts
中注册 @modern-js/plugin-server
插件。
- 项目目录下创建
server/index.ts
文件,可以在这个文件中编写自定义逻辑。
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
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 内容注入。
迁移至新版自定义 Web Server
迁移背景
Modern.js Server 在不断演进,为了提供更强大的功能,我们对中间件和 Server 插件的定义和使用方式进行了优化。
虽然旧版 API 仍被兼容,但我们强烈建议您按照本指南进行迁移,以充分利用新版的优势。
迁移步骤
- 升级 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。