Lim*_*s21 10 typescript deno oak
我想在 deno 中强类型化 Oak 提供的 context.state 对象。我已经看到了如何实现这一点的方法(例如Deno Oak v10.5.1 context.cookies never be set),但还没有设法在我自己的代码中实现它。
我的目标是访问每个中间件中的强类型 context.state。
这是 context.state 的接口:
interface State {
userID: number;
sessionID: number;
}
Run Code Online (Sandbox Code Playgroud)
这将是我用于设置 context.state 的中间件函数之一,我在其中尝试访问上下文状态属性:
const contextMiddleware = async (context: Context, next: () => Promise<unknown>): Promise<void> => {
context.state.userID = 123;
context.state.sessionID = 456;
await next();
delete context.state.userID;
delete context.state.sessionID;
}
Run Code Online (Sandbox Code Playgroud)
我的问题是,在 contextMiddleware 函数中,这两个属性的类型为any,而不是预期的数字类型。此外,智能感知无法识别这些自动完成属性。
我发现解决方案可能是将 IState 接口作为泛型传递给调用中间件的 Application 和 Router 对象,然后设置 contextMiddlewate 函数以使用来自 Oak import mod.ts 的相同泛型键入 RouterMiddleware 或 Middleware。
这可能看起来像这样,但目前不起作用:
import {
Application,
type Context,
type Middleware,
Router,
} from "https://deno.land/x/oak@v10.6.0/mod.ts";
interface State {
userID: number;
sessionID: number;
}
const contextMiddleware: Middleware<State, Context<State, State>> = async (context: Context, next: () => Promise<unknown>): Promise<void> => {
context.state.userID = 123;
context.state.sessionID= 456;
await next();
delete context.state.userID;
delete context.state.sessionID;
}
const defaultRouter = new Router<State>();
defaultRouter
.use(contextMiddleware)
.get("/(.*)", (context: Context) => {
context.response.status = 404;
context.response.body = "Endpoint not available!";
});
const app = new Application<State>();
app.use(defaultRouter.routes(), defaultRouter.allowedMethods());
app.addEventListener("listen", ({ hostname, port, secure }) => {
console.log(
`Listening on ${secure ? "https://" : "http://"}${
hostname || "localhost"
}:${port}`,
);
});
await app.listen({ port: 8080 });
Run Code Online (Sandbox Code Playgroud)
我在这里缺少什么?
提前感谢您的帮助!
jse*_*ksn 12
因为有多少 Oak 类型使用泛型(通常带有默认类型参数),特别是它们使用这些类型创建新类型的方式(在某些情况下,不允许您提供在插槽中使用的类型)有默认值并使用默认值代替),强类型 Oak 中间件可能非常复杂 \xe2\x80\x94\xc2\xa0 特别是如果您的主要目标是严格的类型安全。
\n请注意,Oak 是面向中间件的 Web 服务器的便捷 API。它的主要目的是处理样板文件并帮助您专注于应用程序代码,并且 \xe2\x80\x94 公平地说 \xe2\x80\x94 它做出了一些惊人的妥协,在这样做时提供了非常好的类型。然而,为了做到这一点,Oak 类型系统中使用的默认值在某些地方似乎偏向于便利性而不是安全性,因此记住这一点很重要。
\n泛型有时会让人觉得很困难,而且一个潜在的棘手部分是Oak中不只有一种“上下文”类型(它取决于上下文的使用位置),而且 \xe2\x80\x94 也非常重要\xe2\x80\x94 不只有一种“状态”类型(Oak 允许您为每个请求/响应周期创建唯一的状态数据,为您提供用于初始化它的策略选项\xe2\x80\ x94\xc2\xa0更多内容如下)。
\n让我们从所涉及的依赖类型的底部开始,逐步向上以获得完整的理解。从以下开始app:
的类型Application看起来像这样:
class Application<AS extends State = Record<string, any>> extends EventTarget {\n constructor(options?: ApplicationOptions<AS, ServerRequest>);\n // --- snip ---\n state: AS;\n // --- snip ---\n use<S extends State = AS>(\n middleware: Middleware<S, Context<S, AS>>,\n ...middlewares: Middleware<S, Context<S, AS>>[]\n ): Application<S extends AS ? S : (S & AS)>;\n use<S extends State = AS>(\n ...middleware: Middleware<S, Context<S, AS>>[]\n ): Application<S extends AS ? S : (S & AS)>;\n // --- snip ---\n}\nRun Code Online (Sandbox Code Playgroud)\n\ntype State = Record<string | number | symbol, any>;\nRun Code Online (Sandbox Code Playgroud)\n(基本上就是“任何普通对象”)。现在让我们看看用于创建应用程序的选项:ApplicationOptions,如下所示:
interface ApplicationOptions <S, R extends ServerRequest> {\n contextState?:\n | "clone"\n | "prototype"\n | "alias"\n | "empty";\n keys?: KeyStack | Key[];\n logErrors?: boolean;\n proxy?: boolean;\n serverConstructor?: ServerConstructor<R>;\n state?: S;\n}\nRun Code Online (Sandbox Code Playgroud)\n\n\n我将跳过显示不相关的类型:
\nServerConstructor和KeyStack(Oak 的主模块甚至不导出,因此您必须自己追踪类型(糟糕!))。
定义如何创建上下文状态的策略的选项是ApplicationOptions#contextState,内联文档对此进行了说明(此处也使用略有不同的语言进行了描述):
\n\n确定在创建新上下文时应如何应用应用程序的状态。值
\n"clone"会将状态设置为应用程序状态的克隆。任何不可克隆或不可枚举的属性都不会被复制。值"prototype"表示应用程序的状态将用作上下文状态的原型,这意味着上下文状态的浅层属性不会反映在应用程序的状态中。值"alias"意味着应用程序.state和上下文.state将是对同一对象的引用。的值将使用空对象"empty"初始化上下文。.state默认值为
\n"clone"。
实例化应用程序时,我们将保留此未定义状态(使用默认值)。对这些算法的讨论超出了问题的范围,但重要的是您要了解这如何影响代码的行为,因为每个设置都会导致上下文的不同状态对象。要查看该值的创建方式的来源,请从此处开始。
\n因为你没有任何应用程序级别的状态,所以创建应用程序(具有严格的类型安全性)如下所示:
\n// This means "an object with no property names and no values" (e.g. `{}`)\ntype EmptyObject = Record<never, never>;\ntype AppState = EmptyObject;\n\nconst app = new Application<AppState>({ state: {} });\nRun Code Online (Sandbox Code Playgroud)\n\n\n请注意,您还可以像这样实例化它:
\nRun Code Online (Sandbox Code Playgroud)\nconst app = new Application<AppState>();\nOak将为您创建空对象,但我更喜欢我的代码是明确的。
\n
\n\n如果您在上面提到 Oak 用于泛型类型参数的默认类型
\nAS(在您不提供类型的情况下),您将看到它是Record<string, any>. 这种选择不是很类型安全,但可以使使用未知或动态状态数据更加方便。类型安全性和便利性并不总是相互矛盾,但情况往往如此。
上述内容对于以下所有内容都很重要:这意味着在每个路由器和中间件中,AppState应该使用类型来代替应用程序状态通用类型参数(Oak 使用参数名称AS):否则,Oak 提供不太安全的默认值。
现在让我们看看您计划在每个请求-响应周期中使用的状态类型(Oak 称之为上下文):
\ntype ContextState = {\n sessionID: number;\n userID: number;\n};\nRun Code Online (Sandbox Code Playgroud)\n\n\n您可能会注意到我已经在您的示例中重命名了状态类型。当然,欢迎您随意命名您的程序变量和类型,但我想鼓励您不要为每个类型添加
\nI或 前缀T。您可能在其他人的代码中看到过这一点,但在我看来,这只是噪音:这就像在程序中的每个变量前加上前缀v(例如vDate,,,等):没有必要为了那个原因。相反,我鼓励您遵循官方 TypeScript 约定,在中使用有意义的名称vNamevAmountPascalCase.
接下来我们看看Oak的Context。它看起来像这样:
class Context<S extends AS = State, AS extends State = Record<string, any>> {\n constructor(\n app: Application<AS>,\n serverRequest: ServerRequest,\n state: S,\n secure?,\n );\n // --- snip ---\n app: Application<AS>; // { state: AS }\n // --- snip ---\n state: S;\n // --- snip ---\n}\nRun Code Online (Sandbox Code Playgroud)\n正如您所看到的,上下文状态类型和应用程序状态类型不一定相同。
\n\n\n并且,除非您另有指示,(也许不安全)Oak 会这样设置它们(甚至不太安全,如
\nRecord<string, any>)。
我们还看一下Oak 提供的其他上下文类型RouterContext: 。此上下文类型在路由器内使用,是一种较窄的类型,其中包含有关路由信息的附加信息:路径、参数等。它看起来像这样:
interface RouterContext <\n R extends string,\n P extends RouteParams<R> = RouteParams<R>,\n S extends State = Record<string, any>,\n> extends Context<S> {\n captures: string[];\n matched?: Layer<R, P, S>[];\n params: P;\n routeName?: string;\n router: Router;\n routerPath?: string;\n}\nRun Code Online (Sandbox Code Playgroud)\n我不会RouteParams在这里讨论该类型:它是一个有点复杂的递归类型实用程序,它尝试从路由字符串文字参数构建类型安全的路由参数对象R。
正如您在上面看到的,此类型扩展了类型Context( ),但它只向其extends Context<S>提供上下文状态类型 ( ),而应用程序状态类型 ( ) 未定义,这导致使用默认类型 ( )。这是这里看到的第一个示例,其中 Oak 选择不为您提供创建类型安全代码的方法。但是,我们可以制作自己的版本。SASRecord<string, any>
\n\n\xe2\x9a\xa0\xef\xb8\x8f 重要提示:该类
\nLayer不会从 Oak 中定义它的模块中导出,即使它在 Oak 的面向公众(导出)类型中使用(同样,非常坏的!)。这使得它无法在我们自定义的更强的上下文类型中使用(除非我们手动重新创建该类型),所以我们必须这样做(多么头疼!)。
import {\n type Context,\n type RouteParams,\n Router,\n type RouterContext,\n type State as AnyOakState,\n} from "https://deno.land/x/oak@v10.6.0/mod.ts";\n\ntype EmptyObject = Record<never, never>;\n\ninterface RouterContextStrongerState<\n R extends string,\n AS extends AnyOakState = EmptyObject,\n S extends AS = AS,\n P extends RouteParams<R> = RouteParams<R>,\n> extends Context<S, AS> {\n captures: string[];\n matched?: Exclude<RouterContext<R, P, S>["matched"], undefined>;\n params: P;\n routeName?: string;\n router: Router;\n routerPath?: string;\n}\nRun Code Online (Sandbox Code Playgroud)\n这种自定义类型对于创建类型安全的路由器中间件函数非常重要,因为 Oak 提供的用于创建它们的实用程序 (Middleware和RouterMiddleware) 遭受与该类型相同的默认类型参数使用问题RouterContext,它们如下所示:
interface Middleware<\n S extends State = Record<string, any>,\n T extends Context = Context<S>,\n> {\n (context: T, next: () => Promise<unknown>): Promise<unknown> | unknown;\n}\n\ninterface RouterMiddleware<\n R extends string,\n P extends RouteParams<R> = RouteParams<R>,\n S extends State = Record<string, any>,\n> {\n (\n context: RouterContext<R, P, S>,\n next: () => Promise<unknown>,\n ): Promise<unknown> | unknown;\n param?: keyof P;\n router?: Router<any>;\n}\nRun Code Online (Sandbox Code Playgroud)\n这个答案已经涵盖了很多内容(而且我们还没有完成!),所以现在是回顾一下我们到目前为止的内容的好时机:
\nimport {\n Application,\n type Context,\n type RouteParams,\n Router,\n type RouterContext,\n type State as AnyOakState,\n} from "https://deno.land/x/oak@v10.6.0/mod.ts";\n\ntype EmptyObject = Record<never, never>;\ntype AppState = EmptyObject;\n\nconst app = new Application<AppState>({ state: {} });\n\ntype ContextState = {\n userID: number;\n sessionID: number;\n};\n\ninterface RouterContextStrongerState<\n R extends string,\n AS extends AnyOakState = EmptyObject,\n S extends AS = AS,\n P extends RouteParams<R> = RouteParams<R>,\n> extends Context<S, AS> {\n captures: string[];\n matched?: Exclude<RouterContext<R, P, S>["matched"], undefined>;\n params: P;\n routeName?: string;\n router: Router;\n routerPath?: string;\n}\nRun Code Online (Sandbox Code Playgroud)\n完成所有这些后,我们终于能够开始回答您的直接问题,即“如何在 Deno 中强类型化 Oak 上下文状态对象?”
\n现在,真正的答案是(您可能会发现这令人沮丧):“这取决于中间件函数中发生的情况,以及调用它们的顺序(链接在一起)”。这是因为每个中间件都可以改变状态,而新状态将是下一个中间件接收的状态。这就是事情变得如此复杂的原因,也可能是 Oak 选择Record<string, any>默认使用的原因。
那么,让我们看看您的示例中发生了什么,并创建类型来表示它。您有两个中间件功能,并且它们都位于一台路由器上。
\n第一个函数:
\nawait next()),然后它的强类型版本如下所示:
\n\n\n\n
NextFn我们还为该函数创建一个类型别名next,这样我们就不必继续为其他中间件输入整个函数签名。
type NextFn = () => Promise<unknown>;\n\nasync function assignContextStateValuesAtTheBeginningAndDeleteThemAtTheEnd(\n context: Context<Partial<ContextState>, AppState>,\n next: NextFn,\n): Promise<void> {\n context.state.userID = 123;\n // ^? number | undefined\n context.state.sessionID = 456;\n // ^? number | undefined\n await next(); // Wait for subsequent middleware to finish\n delete context.state.userID;\n delete context.state.sessionID;\n}\nRun Code Online (Sandbox Code Playgroud)\n请注意,我使用了 type 实用程序Partial<Type>(ContextState将其所有成员设置为可选)。这有两个原因:
这些属性在函数开始时的上下文状态上不存在(此时状态只是一个空对象)
\n它们在函数 \xe2\x80\x94 末尾从上下文状态中删除,如果您尝试删除非可选属性,您将收到以下 TS 诊断错误:The operand of a \'delete\' operator must be optional. deno-ts(2790)
另一个函数位于第一个函数之后的同一中间件链中,但仅匹配GET请求和匹配的路由/(.*)。
\n\n旁白:我不确定您以这种方式定义路由的意图是什么(如果您只是希望路由器匹配请求
\nGET,那么您可以使用路由器实例化选项进行配置RouterOptions#methods)。无论如何,您可能会发现了解 Oak 的文档说明它使用该path-to-regexp库来解析这些路由字符串很有用。
那一个可能看起来像这样:
\nfunction setEndpointNotFound(\n context: RouterContextStrongerState<"/(.*)", AppState, Partial<ContextState>>,\n): void {\n context.response.status = 404;\n context.response.body = "Endpoint not available!";\n}\nRun Code Online (Sandbox Code Playgroud)\n如果要创建一个中间件函数,并且希望前一个函数已设置上下文状态属性,则可以使用类型保护谓词函数:
\nfunction idsAreSet<T extends { state: Partial<ContextState> }>(\n contextWithPartialState: T,\n): contextWithPartialState is T & {\n state: T["state"] & Required<Pick<T["state"], "sessionID" | "userID">>;\n} {\n return (\n typeof contextWithPartialState.state.sessionID === "number" &&\n typeof contextWithPartialState.state.userID === "number"\n );\n}\n\nasync function someOtherMiddleware(\n context: Context<Partial<ContextState>, AppState>,\n next: NextFn,\n): Promise<void> {\n // In the main scope of the function:\n context.state.userID;\n // ^? number | undefined\n context.state.sessionID;\n // ^? number | undefined\n\n if (idsAreSet(context)) {\n // After the type guard is used, in the `true` path scope:\n context.state.userID;\n // ^? number\n context.state.sessionID;\n // ^? number\n } else {\n // After the type guard is used, in the `false` path scope:\n context.state.userID;\n // ^? number | undefined\n context.state.sessionID;\n // ^? number | undefined\n }\n\n await next();\n}\nRun Code Online (Sandbox Code Playgroud)\n是时候创建路由器并使用我们的中间件了;这非常简单:
\nconst router = new Router<Partial<ContextState>>();\nrouter.use(assignContextStateValuesAtTheBeginningAndDeleteThemAtTheEnd);\nrouter.get("/(.*)", setEndpointNotFound);\nRun Code Online (Sandbox Code Playgroud)\n在应用程序中使用路由器也同样简单(直接来自问题中的代码):
\napp.use(router.routes(), router.allowedMethods());\nRun Code Online (Sandbox Code Playgroud)\n最后,让我们通过启动服务器来结束。
\n您问题中的代码包含一个应用程序监听事件回调函数,它将服务器地址记录到控制台。我的猜测是您从文档中的示例中复制了它。stdout如果您想在服务器启动时在控制台中看到一条消息,那么这是一个很好的功能。这里有一个使用URL构造函数的细微变化,这样如果您碰巧侦听您正在使用的协议的默认端口(例如,80在 http 上,443在 https 上),则该位将从消息中的地址中省略(就像您的浏览器显示的地址一样)。onListen它也恰好与使用的回调兼容serve它也恰好与 Deno 的 std 库中的函数
function printStartupMessage({ hostname, port, secure }: {\n hostname: string;\n port: number;\n secure?: boolean;\n}): void {\n if (!hostname || hostname === "0.0.0.0") hostname = "localhost";\n const address =\n new URL(`http${secure ? "s" : ""}://${hostname}:${port}/`).href;\n console.log(`Listening at ${address}`);\n console.log("Use ctrl+c to stop");\n}\n\napp.addEventListener("listen", printStartupMessage);\nRun Code Online (Sandbox Code Playgroud)\n然后...启动服务器:
\nawait app.listen({ port: 8080 });\nRun Code Online (Sandbox Code Playgroud)\n这是上面讨论的完整模块结果:
\nimport {\n Application,\n type Context,\n type RouteParams,\n Router,\n type RouterContext,\n type State as AnyOakState,\n} from "https://deno.land/x/oak@v10.6.0/mod.ts";\n\ntype EmptyObject = Record<never, never>;\ntype AppState = EmptyObject;\n\nconst app = new Application<AppState>({ state: {} });\n\ntype ContextState = {\n userID: number;\n sessionID: number;\n};\n\ninterface RouterContextStrongerState<\n R extends string,\n AS extends AnyOakState = EmptyObject,\n S extends AS = AS,\n P extends RouteParams<R> = RouteParams<R>,\n> extends Context<S, AS> {\n captures: string[];\n matched?: Exclude<RouterContext<R, P, S>["matched"], undefined>;\n params: P;\n routeName?: string;\n router: Router;\n routerPath?: string;\n}\n\ntype NextFn = () => Promise<unknown>;\n\nasync function assignContextStateValuesAtTheBeginningAndDeleteThemAtTheEnd(\n context: Context<Partial<ContextState>, AppState>,\n next: NextFn,\n): Promise<void> {\n context.state.userID = 123;\n context.state.sessionID = 456;\n await next();\n delete context.state.userID;\n delete context.state.sessionID;\n}\n\nfunction setEndpointNotFound(\n context: RouterContextStrongerState<"/(.*)", AppState, Partial<ContextState>>,\n): void {\n context.response.status = 404;\n context.response.body = "Endpoint not available!";\n}\n\nconst router = new Router<Partial<ContextState>>();\nrouter.use(assignContextStateValuesAtTheBeginningAndDeleteThemAtTheEnd);\nrouter.get("/(.*)", setEndpointNotFound);\n\napp.use(router.routes(), router.allowedMethods());\n\n// This is not necessary, but is potentially helpful to see in the console\nfunction printStartupMessage({ hostname, port, secure }: {\n hostname: string;\n port: number;\n secure?: boolean;\n}): void {\n if (!hostname || hostname === "0.0.0.0") hostname = "localhost";\n const address =\n new URL(`http${secure ? "s" : ""}://${hostname}:${port}/`).href;\n console.log(`Listening at ${address}`);\n console.log("Use ctrl+c to stop");\n}\n\napp.addEventListener("listen", printStartupMessage);\n\nawait app.listen({ port: 8080 });\n\nRun Code Online (Sandbox Code Playgroud)\n这是相当多的信息,但是 Web 服务器框架内部发生了很多事情,并且以类型安全的方式执行这些操作甚至更加复杂!
\n| 归档时间: |
|
| 查看次数: |
1331 次 |
| 最近记录: |