koa2+ts中爲Context擴展自定義屬性

問題來源

爲了簡化 ctx.body 賦值操作,想要在 ctx 擴展兩個自定義方法, successerror

使用起來如下

// 響應成功狀態請求
ctx.success({
  username: 'test'
});

// 等價於
ctx.body = {
  code: 1,
  data: {
    username: 'test'
  }
};

// 響應失敗狀態請求
ctx.error("參數不正確");

// 等價於
ctx.body = {
  code: 0,
  data: null,
  msg: '參數不正確'
};

successerror 這兩個方法的擴展是基於 koa 中間件的套路來做的

其核心代碼如下

const koaResponse = async (ctx: Koa.Context, next: Koa.Next) => {
  ctx.success = (data = null, status = Types.EResponseStatus.SUCCESS) => {
    ctx.status = status;
    ctx.body = {
      code: Types.EResponseCode.SUCCESS,
      data
    };
  };

  ctx.error = (
    msg = Types.EResponseMsg.DEFAULT_ERROR,
    data = null,
    status = Types.EResponseStatus.SUCCESS
  ) => {
    ctx.status = status;
    ctx.body = {
      code: Types.EResponseCode.ERROR,
      data,
      msg
    };
  };

  await next();
};

具體使用時便會遇到問題

// 給路由添加了一個 請求參數  校驗的中間件 和 一個 請求核心邏輯處理的中間件
router.get('/', Validator.validLogin, UserController.login);

請求參數校驗中間件

// 這裏使用假數據做測試
class Validator {
  static async validLogin(ctx: Koa.Context, next: Koa.Next) {
    const result = loginModel.check({
      username: 'test',
      email: '[email protected]',
      age: 20
    });

    if (
      (Object.keys(result) as ['username', 'email', 'age']).filter((name) => result[name].hasError)
        .length > 0
    ) {
      ctx.error(Types.EResponseMsg.INVALID_PARAMS); // error 類型丟失,沒有代碼提示
    } else {
      await next();
    }
  }
}

請求核心邏輯處理中間件

class UserController {
  static async login(ctx: Koa.Context, next: Koa.Next) {
    ctx.success({ // success 類型丟失,沒有代碼提示
      username: 'test'
    });
    await next();
  }
}

問題解決過程

試驗一

app.ts做如下修改

// 實例化 app 時,傳入自定義屬性作爲 defaultContext
const app = new Koa<{}, {
  success: Function;
  error: Function;
}>();

// logger 中做測試
app.use(async (ctx, next) => {
  // ctx 類型爲
  /* (parameter) ctx: Koa.ParameterizedContext<{}, {
    success: Function;
    error: Function;
  }> */
  
  const start = Date.now();
  
  app.context.success // 具有正確類型提示 (property) success: Function
  ctx.success // 具有正確類型提示 (property) success: Function
  
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

爲上面👆 logger 中 ctx 指定類型聲明

app.use(async (ctx: Koa.context, next: Koa.Next) => {
	// ctx 類型爲 (parameter) ctx: Koa.Context

  const start = Date.now();
  
  app.context.success // 具有正確類型提示 (property) success: Function
  ctx.success // 類型丟失
  
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

發生上面的問題原因在於,app.use 中具有類型推斷,當不手動設置 ctx 類型時,其推斷正是我們想要的

// 就是這個東東
Koa.ParameterizedContext<{}, {
  success: Function;
  error: Function;
}>

當手動設置後變爲

Koa.context // 其類型聲明中是不具備 success、error 這兩個類型的

那麼解決方案來了,ctx 的類型如果都是下面這個,是不是就對了

Koa.ParameterizedContext<{}, {
  success: Function;
  error: Function;
}>
  
// logger
app.use(async (ctx: Koa.ParameterizedContext<{}, {
  success: Function;
  error: Function;
}>, next) => {
  const start = Date.now();
  
  app.context.success // 具有正確類型提示 (property) success: Function
  ctx.success // 具有正確類型提示 (property) success: Function
  
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

這樣可能引出另一個問題

ctx 有很多地方在使用,那麼每個 ctx 的類型每次都要 這麼聲明一遍 或者 定義一個全局的類型來導入使用(每次導入也難受)

那能不能通過 Koa 聲明合併的方式,爲 Context 全局添加 successerror 類型聲明

於是有了實驗二

實驗二

打開 node_modules/@types/koa/index.d.ts,大致瀏覽會看到這麼個東西

type DefaultStateExtends = any;
/**
 * This interface can be augmented by users to add types to Koa's default state
 */
interface DefaultState extends DefaultStateExtends {}

type DefaultContextExtends = {};
/**
 * This interface can be augmented by users to add types to Koa's default context
 */
interface DefaultContext extends DefaultContextExtends {
  /**
   * Custom properties.
   */
  [key: string]: any;
}

重點

  1. DefaultState 可以擴展 state
  2. DefaultContext 可以擴展 context

來看看怎麼進行聲明合併,src/types/index.ts

declare module 'koa' {
  interface DefaultState {
    stateProperty: boolean;
  }

  interface DefaultContext {
    success: TSuccess;
    error: TError;
  }
}

再來看看 app.ts

// logger ctx 類型寫或者不寫,結果都是正確的
app.use(async (ctx: Koa.Context, next) => {
  const start = Date.now();
  
  app.context.success // 具有正確類型提示 (property) success: Function
  ctx.success // 具有正確類型提示 (property) success: Function
  
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

此時已經達到我買的目的了,反過來看一下我們將 類型合併聲明 放到了什麼地方,OK,here, src/types/index.ts

那爲什麼不放到 src/global.d.ts 中呢,測試過就會發現,如果放到這裏面,我們的類型合併聲明就會失敗,失敗方是 @types/koa 中提供的類型聲明。原因就在於,我們導入的 koa 的類型聲明被 src/global.d.ts 中的聲明給攔截了,導致並未讀取 @types/koa 中提供的類型聲明

問題到此就基本解決了

測試項目 koa2-ts

參考文檔

  1. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/koa/test/index.ts
  2. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/koa/test/default.ts
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章