Open rankangkang opened 6 months ago
问题来源于 context
在使用 koa-mount 的过程中,我们发现父应用与子应用的 context 不存在关联,这个事情就很没道理。
以下是代码示例:
const Koa = require('koa');
const mount = require('koa-mount');
// 父应用
const app = new Koa();
app.context.test = "hallo";
app.use(async function (ctx, next){
ctx.body = ctx.test;
return next();
})
// 子应用
const cc = new Koa()
app.use(mount('/hello', cc))
app.listen(3000)
cc.listen(3001)
当我们请求父应用 <127.0.0.1:3000/hello> 时,返回的是 hallo 当我们请求父应用 <127.0.0.1:3001/hello> 时,返回的是 undefined
这是为何?这真的没道理啊?官方的库不应该有问题啊,难道是我使用姿势不对?
直到我翻阅 koa-mount 源码后,我才意识到问题所在。
function mount(prefix, app) {
// ...
// compose
const downstream = app.middleware
? compose(app.middleware)
: app
// ...
return async function (ctx, upstream) {
const prev = ctx.path
const newPath = match(prev)
debug('mount %s %s -> %s', prefix, name, newPath)
if (!newPath) return await upstream()
ctx.mountPath = prefix
ctx.path = newPath
debug('enter %s -> %s', prev, ctx.path)
await downstream(ctx, async () => {
ctx.path = prev
await upstream()
ctx.path = newPath
})
debug('leave %s -> %s', prev, ctx.path)
ctx.path = prev
}
}
koa-mount 仅仅只是获取子应用的中间件(并组合为一个新的中间件),并将父应用的 context 传入。
每个 koa 实例都有自己的原始 context 属性,koa 每接收一个请求,便会根据 req 与 res 对象以及原始 context 创建一个新的 context,并将其传递给各中间件组合而成的 compose 函数。因此,这个新的 context 独属于这个这次请求。
createContext (req, res) { const context = Object.create(this.context) // other codes ... return context }
也就是说,从父应用打到子应用的请求,从头至尾都没有子应用的 context 参与。 换句话说,子应用的 context 丢失了。
这也意味着,一部分依赖 koa 实例的中间件无法在子应用维度正常使用,比如 koa-session 中间件(依赖了 koa context),以下代码执行后请求子应用路由时将会报错:
import Koa from "koa";
import mount from 'koa-mount'
import session from 'koa-session'
const app = new Koa()
app.context.name = "app"
const cc = new Koa();
cc.context.name = "cc"
cc.use(session({}, cc))
cc.use((ctx, next) => {
console.log('cc', ctx.session)
});
app.use(mount('/cc', cc))
app.listen(8099)
报错信息如下:TypeError: Cannot read properties of undefined (reading 'store')
许多开发者也在发现了这个问题,在各 issue 下奔走相告,甚至有人自己实现了一个可嫁接 context 的包。
没错,我也试着实现了一个:
import Koa from "koa"
import koaMount from "koa-mount"
import compose from "koa-compose"
/**
* 匹配
* @param path
* @param opt
* @returns
*/
function match(
path: string,
{
prefix,
trailingSlash,
}: {
prefix: string
trailingSlash: boolean
}
) {
// does not match prefix at all
if (path.indexOf(prefix) !== 0) return false
const newPath = path.replace(prefix, "") || "/"
if (trailingSlash) return newPath
// `/mount` does not match `/mountlkjalskjdf`
if (newPath[0] !== "/") return false
return newPath
}
/**
* mount 组件,保留组件 context
* @param prefix
* @param app
*/
function preserveMount(prefix: string, app: Koa): Koa.Middleware {
// compose
const appMiddleware = compose(app.middleware)
// 根路径,不需要挂载
if (prefix === "/") {
return appMiddleware
}
const trailingSlash = prefix.slice(-1) === "/"
return async function (ctx, next) {
const prev = ctx.path
const matchedPath = match(prev, { prefix, trailingSlash })
// 未匹配,直接运行下一个 middleware,不替换 path
if (!matchedPath) {
return await next()
}
ctx.mountPath = prefix
ctx.path = matchedPath
// 创建组件 context
const appCtx = app.createContext(ctx.req, ctx.res)
// TODO: 可选择在这里将父应用的 ctx 挂载到子应用的 context 上
Object.assign(appCtx, { parent: ctx.app })
await appMiddleware(appCtx)
// 挂载的组件的中间件执行完全后,不会执行后续的中间件,直接原路返回
Object.assign(ctx, appCtx, { app: ctx.app })
ctx.path = prev
}
}
/**
* 组件挂载中间件
* @param prefix 挂载路径
* @param app 挂载的组件
* @param preserve 是否保留组件自身 context
* @returns {Koa.Middleware}
*/
export default function mount(prefix: string, app: Koa, preserve = false): Koa.Middleware {
if (preserve) {
return preserveMount(prefix, app)
}
return koaMount(prefix, app)
}
问题来源于一个需求,能否将多个 koa app (下文我们称之为子应用)挂载到一个共同的 koa app (下文我们称之为父应用)呢?
经过疯狂百度后发现,官方提供了一个 koa-mount 中间件来提供这个能力。
这可太棒了,不费吹灰之力就实现了一个需求~
但在真正的使用过程中,却发现一切似乎又不是那么完美...