rankangkang / issues

cmkk's blog on github issue
0 stars 0 forks source link

虚假的 koa-mount #7

Open rankangkang opened 6 months ago

rankangkang commented 6 months ago

问题来源于一个需求,能否将多个 koa app (下文我们称之为子应用)挂载到一个共同的 koa app (下文我们称之为父应用)呢?

经过疯狂百度后发现,官方提供了一个 koa-mount 中间件来提供这个能力。

这可太棒了,不费吹灰之力就实现了一个需求~

但在真正的使用过程中,却发现一切似乎又不是那么完美...

rankangkang commented 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

rankangkang commented 6 months ago

这是为何?这真的没道理啊?官方的库不应该有问题啊,难道是我使用姿势不对?

直到我翻阅 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 丢失了。

rankangkang commented 6 months ago

这也意味着,一部分依赖 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')

rankangkang commented 6 months ago

许多开发者也在发现了这个问题,在各 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)
}