快速上手

bwcx 是一个基于 Koa 的轻量 Web 开发框架,遵从面向对象、声明式开发等理念,旨在帮助开发者构建高开发效率、易维护的应用。

bwcx 取自中文「不忘初心」的拼音首字母。

注意

bwcx 的设计和实现完成于 2021 年,其基于 TypeScript 传统装饰器,并不兼容 tc39 的 Stage 3 装饰器提案(对应 TypeScript 5.0 的默认装饰器)。如果你的 TypeScript 版本默认使用新版装饰器,你需要调整配置使用传统装饰器或使用低版本 TypeScript。

我们暂时没有计划支持新装饰器,这是因为 Stage 3 提案缺失部分重要特性(如参数装饰器)。

初见

初始化一个 TypeScript 项目并安装依赖:

npm i -S bwcx-common bwcx-core bwcx-ljsm

修改 tsconfig.json,确保其中 "experimentalDecorators": true, "emitDecoratorMetadata": true 均已设置。或使用一个简单的入门配置:

{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}

新建一个 ts 文件,如 index.ts

import { App, Controller, Get } from 'bwcx-ljsm';

@Controller()
class HomeController {
  @Get('/')
  hello() {
    return { hello: 'bwcx' };
  }
}

class OurApp extends App {
  protected port = 3000;

  afterStart() {
    console.log(`🚀 A bwcx app is listening on http://localhost:${this.port}`);
  }
}

const app = new OurApp();
app.bootstrap().then(() => {
  app.start();
});

这样就可以了!如果你已经全局安装了 ts-nodetypescript,则可以无需编译直接运行你的应用:ts-node index.ts,之后你可以尝试在浏览器访问:http://localhost:3000/open in new window

稍稍规范

随着功能增多,你的应用可能会渐渐变得复杂,需要更好的组织代码。不要担心,我们可以把 Controller 独立出来,放在任何位置。让我们重新组织一下目录和文件:

// src/controllers/home.ts

import { Controller, Get } from 'bwcx-ljsm';

@Controller()
export default class HomeController {
  @Get('/')
  hello() {
    return { hello: 'bwcx' };
  }
}

然后在入口的 App 配置扫描,这将让我们的 Controller 和应用本身没有任何显式依赖关系:

// src/index.ts

import { App } from 'bwcx-ljsm';

class OurApp extends App {
  // 这是 Node.js 的特殊变量,表示当前文件所在目录
  protected baseDir = __dirname;
  // 配置要扫描的文件(基于 baseDir),这样你散落在各处的文件会被扫描引用到
  protected scanGlobs = ['./**/*.(j|t)s', '!./**/*.d.ts'];
  protected port = 3000;

  afterStart() {
    console.log(`🚀 A bwcx app is listening on http://localhost:${this.port}`);
  }
}

const app = new OurApp();
app.scan();
app.bootstrap().then(() => {
  app.start();
});





 
 
 
 












看上去很😎!框架内建的扫描机制可以让你任意组织目录,一切随你所好。

接下来,你的 Controller 逻辑可能越来越复杂,甚至需要复用一些逻辑,这时可以把 Controller 内的业务逻辑抽离出去成为一个服务类:

// src/services/common.ts

import { Service } from 'bwcx-ljsm';

@Service()
export default class CommonService {
  public sayHello(to: string) {
    return { hello: to };
  }
}

修改下原来的 Controller:

// src/controllers/home.ts

import { Controller, Get } from 'bwcx-ljsm';
import { Inject } from 'bwcx-core';
import CommonService from '../services/common';

@Controller()
export default class HomeController {
  // 在这里,我们把服务类直接弄进来使用,你暂时不用去理解它是如何实例化的
  @Inject()
  private commonService: CommonService;

  @Get('/')
  hello() {
    return this.commonService.sayHello('bwcx');
  }
}








 
 
 






大功告成!可以尝试重新运行下你的应用:ts-node src/index.ts

加点乐子

对于一些公共的请求逻辑,中间件是一个很有效的解决方案。现在我们尝试加一个简单的日志,让我们能从控制台上看到请求日志。

新增一个中间件类:

// src/middlewares/log.ts

import {
  IBwcxMiddleware,
  Middleware,
  MiddlewareNext,
  RequestContext,
} from 'bwcx-ljsm';

@Middleware()
export default class LogMiddleware implements IBwcxMiddleware {
  use(ctx: RequestContext, next: MiddlewareNext) {
    console.log(`req: ${ctx.url}`);
    return next();
  }
}

这是经过规范化的中间件类,看上去很熟悉?没错,其实 use 方法和 Koa 的中间件是一样的。因此你也可以很容易把任何 Koa 中间件改造成 bwcx 兼容的中间件。

先给我们的 hello 路由应用一下中间件:

// src/controllers/home.ts

import { Controller, Get, UseMiddlewares } from 'bwcx-ljsm';
import { Inject } from 'bwcx-core';
import CommonService from '../services/common';
import LogMiddleware from '../middlewares/log';

@Controller()
export default class HomeController {
  @Inject()
  private commonService: CommonService;

  @Get('/')
  @UseMiddlewares(LogMiddleware)
  hello() {
    return this.commonService.sayHello('bwcx');
  }
}













 




重启应用,发起一个请求,可以看到控制台上成功打印出了 req: / 日志。

中间件除了可以应用于单独的路由或 Controller 下的所有路由,也可以全局应用。像我们刚才的日志中间件,应该具有足够的普适性。于是我们把刚才 Controller 上的中间件代码删掉,转到 App 上去添加:

// src/index.ts

import { App } from 'bwcx-ljsm';
import LogMiddleware from './middlewares/log';

class OurApp extends App {
  protected baseDir = __dirname;
  protected scanGlobs = ['./**/*.(j|t)s', '!./**/*.d.ts'];
  protected port = 3000;
  protected globalMiddlewares = [LogMiddleware];

  afterStart() {
    console.log(
      `🚀 A bwcx app is listening on http://localhost:${this.port}`,
    );
  }
}

const app = new OurApp();
app.scan();
app.bootstrap().then(() => {
  app.start();
});









 













这样一来,我们的日志中间件就在全局生效了。即使新增路由也一样奏效。

不过,我们并不满足于此。假设我们需要为路由 /secret 添加校验,只允许特定用户访问:

// src/controllers/home.ts

import { Controller, Get } from 'bwcx-ljsm';
import { Inject } from 'bwcx-core';
import CommonService from '../services/common';

@Controller()
export default class HomeController {
  @Inject()
  private commonService: CommonService;

  @Get('/')
  hello() {
    return this.commonService.sayHello('bwcx');
  }

  @Get('/secret')
  secret() {
    return { secretWord: '若你困于无风之地,我将奏响高天之歌' };
  }
}

现在我们那需要加一个校验,只让合法身份的用户可以访问这个路由。别着急去写中间件,我们有更好的:

// src/guards/random.ts

import { Guard, IBwcxGuard, RequestContext } from 'bwcx-ljsm';

@Guard()
export default class RandomGuard implements IBwcxGuard {
  canPass(ctx: RequestContext) {
    return Math.random() < 0.5;
  }
}

我们实现了一个守卫,它的功能正如它的名字一样,随机让一半请求通过。

在路由方法上加入这个守卫:

// src/controllers/home.ts

import { Controller, Get, UseGuards } from 'bwcx-ljsm';
import { Inject } from 'bwcx-core';
import CommonService from '../services/common';
import RandomGuard from '../guards/random';

@Controller()
export default class HomeController {
  @Inject()
  private commonService: CommonService;

  @Get('/')
  hello() {
    return this.commonService.sayHello('bwcx');
  }

  @Get('/secret')
  @UseGuards(RandomGuard)
  secret() {
    return { secretWord: '若你困于无风之地,我将奏响高天之歌' };
  }
}


















 




好了,测试一下,大成功。使用守卫后,果然请求有大概一半的概率被拦截了。但你会发现,失败时框架返回了 Internal Server Error

其实,守卫校验不通过时,会抛出一个 GuardNotPassException,框架设计时为了帮助开发者解耦,提供了统一的异常处理,逻辑异常、调用异常等是不提倡在 Controller 内自行包装响应的。要使用它,需要为异常定义一个异常处理器:

// src/exception-handlers/guard.ts

import {
  ExceptionHandler,
  GuardNotPassException,
  IBwcxExceptionHandler,
  RequestContext,
} from 'bwcx-ljsm';

// 声明我们要处理 `GuardNotPassException` 这类异常
@ExceptionHandler(GuardNotPassException)
export default class GuardExceptionHandler implements IBwcxExceptionHandler {
  catch(error: GuardNotPassException, ctx: RequestContext) {
    ctx.status = 403;
    ctx.body = 'Forbidden';
  }
}

似乎和中间件/守卫很像?没错,框架为各种可扩展对象都提供了类似的声明方式。现在再试一次,当守卫校验不通过时,已经可以显示友好的错误了。当然,你可以为业务定制需要的异常和异常处理器,这将为构建高可维护性的应用提供良好的基础。


至此,我们只展示了 bwcx 的冰山一角,相信你已经对 bwcx 有了一些感觉。欢迎继续探索后面的章节,了解其他功能。