likes
comments
collection
share

Nestjs 初探 - 理解 IoC 和 DI

作者站长头像
站长
· 阅读数 81

导读

Nodejs 服务端框架调研》里挖的坑终于要到填的时候了,前面稀稀拉拉的写了 Nextjs 初探Eggjs 初探,还附带产出了一篇 如何获取 cnpm 的 packages 下载量,总算是有了一些积累,可以研究下 Nestjs(以下简称 Nest)了。

有了前面几篇文章的铺垫,目前能够得到的结论有:

  1. Nextjs 的 Star 和 Download 数据虽然非常高,但是它服务端的功能,更像是为了实现 SSR 顺带赠送的,就连在教程里面也只是占了很少的篇幅。整个框架的重点还是在前端工程上,所以可以 Pass 调了,Nuxtjs 也是同理。对了,Nextjs 还强绑定了 React,所以也是不太符合要求;
  2. ExpressKoa 都相对太底层,太基础了。灵活性很强,但是易用性和规范性就不太够了,不太适合直接上来做工程;
  3. Eggjs 在国内非常流行,甚至在 cnpm 上的下载量是 Nest 的 1.57 倍。但是在 npm 上,后者下载量是前者的 28 倍。不过 Eggjs 的继承者 Midway 采用了与 Nest 极其相似的设计,所以理性来说,Nest 肯定还是有它先进性在的;
  4. Nest 本身就是 TS 写的,个人认为,TS 是 Nodejs 写后端必备的条件。貌似 Eggjs 的 TS 支持总会被人诟病。又貌似 Midway 已经解决 TS 的问题了,但它的下载量和使用量实在是太低了,不敢用啊。

综上,目前的结论,大概率会选择 Nestjs 作为新项目 BFF 的选型,本文就来手撕 Nest,看看 Nest 到底是怎么个设计思路。

前言

Nest 的语法和用法其实非常简单。举个例子:

// cats.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}


// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

在用 @nestjs/cli 初始化好项目之后,只需要新建一个 cats.controller.ts 文件,然后写入上面的代码,再修改一下 app.module.ts 文件,就可以实现 localhost:3000/cats 接口。用浏览器访问该接口,可以看到「This action returns all cats」 这句话。

Nest 通过标志性的 Decorator 的写法让开发人员可以节省很多的模板代码,从而只需要关注核心逻辑代码即可。

当然,它背后还是有 MVC 设计思想在里面,所以理解 Controller、Module、Service 等基本概念是必须的。如果在这块了解不是很多的同学,可以先试试能不能读懂 Eggjs 初探 这篇,里面对 MVC 有一些粗浅的解释,应该够用了。

另外,由于其背后的 IoC 思想,让一些不太了解这个概念的同学,尤其是前端同学特别的「懵」。怎么就写个 Decorator 就完事了?发什么了什么?很不踏实啊有木有~

所以,正如前文所说,Nest 的语法和用法其实非常简单,难的还是理解其背后的原理。理解了原理和设计思想之后,才能真的会用,要不真不敢写啊,那么多的 Decorator 也不会用啊。

所以本文会用大量篇幅来尝试解读 Nest 的设计思想和原理,讲解代码的部分反而不会有很多。

正文

Decorator 扫盲

我们先来看一段 Nest 的代码:

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  @Post()
  @Header('Cache-Control', 'none')
  @HttpCode(204)
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

对于一些前端同学来说 @Controller 这种 Decorator(装饰器)的写法可能比较陌生。它实际上就是一种语法糖,用过 Angular 或者 Vue Class Component,或者干脆用过 Java 的 Spring 的同学可能会比较熟悉。至于前端同学,可以把它理解成 HOC(高阶组件)。或者我们直接把上文的例子改成「普通」一点的样子,可能就好懂一些了,其实它们是等价的:

import { Controller, Get, Post } from '@nestjs/common';

class CatsController {
  // ...code
}

export default Controller('cats')(CatsController)

IoC 和 DI

这里就必须引出两个概念了,IoC(Inversion of Control 控制反转)和 DI(Dependency Injection 依赖注入)。这两个概念很难一两句话就讲清楚,不过不得不吐槽这两个名字取的挺烂的,非常不直观(DI 还好点)。所以大家可以先忘掉它们,或者把它们换成「狗剩」、「二蛋」之类的,不影响下面的理解。我们直接从一个实际场景出发,反推它们俩。

初始案例

这里引用 前端中的 IoC 理念 当中的例子。假设有个 App 类,需要有 Router 和 Track 的功能,代码可能如下:

// app.js
import Router from './modules/Router';
import Track from './modules/Track';

class App {
    constructor(options) {
        this.options = options;
        this.router = new Router();
        this.track = new Track();

        this.init();
    }

    init() {
        window.addEventListener('DOMContentLoaded', () => {
            this.router.to('home');
            this.track.tracking();
            this.options.onReady();
        });
    }
}

// index.js
import App from 'path/to/App';
new App({
    onReady() {
        // do something here...
    },
});

index.js 文件里调用 App 的时候,传入一些参数即可。好,下面需求有变动了。

第一次优化 - 解耦

假设我们要给 Router 模块实现 history 模式。修改完 ./modules/Router 的代码之后,需要在实例化 Router 的时候,这样使用 new Router({ mode: 'history' })

这就要修改 app.js 文件的内容了,这时候有个问题,怎么改呢?直接写死 history 模式,肯定不妥,要是有的采用的不是这个参数呢?把 Router 的参数在 App 实例化的时候传入似乎是个不错的主意。更进一步的,我们干脆把 Router 的实例作为参数传进去不就行了,这样 App 和 Router 解耦的更彻底了。

App 内部不需要进行 Router 的 new 操作了,你直接在外边 new 完了给我传进来,我直接用就行了。Track 也是一样的道理。于是,代码变成了这样:

// app.js
class App {
    constructor(options) {
        this.options = options;
        // no more 'new'
        this.router = options.router;
        this.track = options.track;

        this.init();
    }

    init() {
        window.addEventListener('DOMContentLoaded', () => {
            this.router.to('home');
            this.track.tracking();
            this.options.onReady();
        });
    }
}

// index.js
import App from 'path/to/App';
import Router from './modules/Router';
import Track from './modules/Track';

// 'new' here
new App({
    router: new Router({ mode: 'history' }),
    track: new Track(),
    onReady() {
        // do something here...
    },
});

这样,起码我们不需要动 app.js 的代码了,这就算是解耦了。这时候已经有点 DI(依赖注入)的意思了。就是把 App 的依赖,在实例化的时候注入,大家意会一下。

好,这时候变动又来了。

第二次优化 - 约定

假设 Router、Track 已经不够了,我们需要再加一个 Share 的功能模块。而且 DOMContentLoaded 的时候,要运行一下 new Share().init() 方法。

看来还得改 app.js 啊。有没有办法不改呢?我们想办法让 App 可以「随意注入依赖」,并且挨个初始化一下不就好了。

这时候我们就需要一些「约定」了,这些约定要求都是 App 提的,比如:

  1. 你得先告诉我都有哪些类需要我操作,要用 App.use([Class1, Class2, Class3]) 这样的方式告诉我;
  2. 你自己得定一个 init 方法,我在 DOMContentLoaded 之后会挨个调用。当然,在你调用的时候,可以访问到我(App)的实例,会作为第一个参数存在,即 init(app)

那么 App 的代码将会变成这样:

class App {
    static modules = []
    constructor(options) {
        this.options = options;
        this.init();
    }
    init() {
        window.addEventListener('DOMContentLoaded', () => {
            this.initModules();
            this.options.onReady(this);
        });
    }
    static use(module) {
        Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
    }
    initModules() {
        App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
    }
}

这时候,所有想要注入 App 的模块,都必须自己实现 init 方法(此处就可以用 Decorator 优化),比如:

// modules/Router.js
import Router from 'path/to/Router';
export default {
    init(app) {
        app.router = new Router(app.options.router);
        app.router.to('home');
    }
};
// modules/Track.js
import Track from 'path/to/Track';
export default {
    init(app) {
        app.track = new Track(app.options.track);
        app.track.tracking();
    }
};
// modules/Share.js
import Share from 'path/to/Share';
export default {
    init(app) {
        app.share = new Share();
        app.setShare = data => app.share.setShare(data);
    }
};

这时候,index.js 的代码会是这样:

// index.js
import Router from './modules/Router';
import Track from './modules/Track';
import Share from './modules/Share';

App.use([ Router, Track, Share ]);

new App({
    // ...
    onReady(app) {
        app.setShare({
            title: 'Hello IoC.',
            description: 'description here...',
            // some other data here...
        });
    }
});

IoC 容器

敲黑板!划重点了!要说概念了!

我们看到,这时候 app.js 的代码已经很抽象了,没有任何业务逻辑在里面,所有业务逻辑都收敛在了各模块(Module)内的 init 方法或在 App 实例化的时候注入。

IoC 里面,这个 app.js 或者说这个 App 就被称为「IoC 容器」。它只负责制定约定规则,加载各种依赖。请仔细理解一下这个概念,虽然这个例子还很单薄,而且不够严谨,但是本质就是这个意思。

接下来,我们可以继续加强这个容器,让它的功能更加强大。

装饰器化

我们看会开头举的 Nest 的例子:

// cats.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}


// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

app.module.ts 的修改很好理解,就相当于调用 App.use 注入依赖,都是模板代码。

再看 cats.controller.ts 的实现,用到了 ControllerGet 两个 Decorator。毫无疑问,这两个 Decorator 都是 Nest 的 IoC 容器提供的能力,为了方便开发者使用而封装的。对应到前面的例子就是:容器有一个 Decorator 可以帮你实现 init 方法,你自己不用写了,直接声明个类,用这个 Decorator 包裹,就能在我的容器里直接用了

这种用法可以说是非常方便了,相较于其它框架,比如 Eggjs,可以省掉很多模板代码,不过也增加了(很多的)理解和学习的成本

理解 IoC 和 DI

我们趁热打铁,理解了「IoC 容器」的概念,再尝试理解一下什么是IoC,即控制反转

我们从上面的代码衍化可以发现,最开始,「new 操作」、「业务逻辑」都是聚合在 App 内部的。这个时候 App 对于各模块,有着很强的「控制权」。但是最后,App 内部几乎没有任何业务逻辑代码,所有的控制权,都回归到了模块手中。整个过程相当于把控制权「反转」了,所以称之为控制反转

好吧,我估计有的同学还没看明白,实在是这个名字起的太「抽象」了,不得不吐槽一下。即使不完全明白,也不耽误我们使用,随着对 Nest 的逐渐熟悉,自然会有更深一步的理解。我们尝试总结一下:

  • 首先,IoC 是一种设计思想,而不是什么技术。它的目标是解决对象间高耦合的问题。;
  • 然后,它一般只适用于 OOP(面向对象编程)的语言;
  • 最后,Decorator 这种写法只是目前业内最主流的 IoC 实现方式,并非唯一。

至于 DI,也就是依赖注入,应该算是 IoC 思想的另一种称呼,反正可以把他俩理解成一个东西。总之 IoC、DI 什么的,名字起的实在是太糟糕了(我这是第几次吐槽了),除了拿出去唬人,也没什么大用,反正不耽误我们会写代码。

总结

这篇文章写的太难了,主要是花了太多时间去研究 IoC 和 DI(我内心已经无数次吐槽这帮老外起名字的能力了,真是怎么让人懵逼怎么来,就不能有点产品 Sense 吗?),然后还得想方设法把它们讲明白,真是要了老命了,不知道掉了多少头发。

不过吐槽归吐槽,归根结底还是自己对于这些概念的不熟悉,所以还是能力问题。希望在用熟练了 Nest 之后,能有更多的理解吧。

最后,关于如何学习 Nest。实际上就是用,先用起来,碰到问题查文档。说是查文档,实际就是查 Decorator。Nest 的文档很碎片化,如果没有上面说的 IoC、DI、MVC、Decorator 的概念理解,很难把把文档中的内容串起来,也就不太能学会使用 Nest,谁让它的模型这么抽象呢。

不过,从 如何获取 cnpm 的 packages 下载量 调研的数据,和阿里最新的 Nodejs 框架 Midway(号称接过了 Eggjs 的接力棒)来看。Nest 这种 IoC 的写法,会是未来的主流,毕竟已经在 Java 领域(Spring)充分的验证过了。

所以,如果你的团队比较有朝气,乐于接受挑战和新鲜事物,还是很推荐使用 Nest 的。但是,如果以求稳为主,或者团队对于学习新东西的热情不高,还是可以考虑 Eggjs 的。一定要结合自己团队的情况选择。

(如果有兴趣,可以在评论区多吐槽一下 IoC 这个名字,这两天真是被他折磨的够呛)

优秀的自驱力,就是在没有人管你的时候,自己在发光。

参考文献

转载自:https://juejin.cn/post/7165812796914532360
评论
请登录