Javascript

nestjs搭建通用业务框架(2):初体验

本文主要是介绍nestjs搭建通用业务框架(2):初体验,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

今天是《nestjs搭建通用业务框架》系列的第2篇,主要是介绍如何使用nestjs的CLI工具快速初始化项目,了解项目的启动与调试。PS: 调试技巧非常的重要,有利于后续学习框架的核心的原理。


node环境准备

node与npm的版本

1
2
3
4
5
➜ node -v
nv14.15.1
~
➜ npm -v
6.14.8

大家使用node官方的LTS的版本即可,下载地址

安装可以使用nrm或者npm config set registry https://registry.npm.taobao.org/来进行加速


安装CLI

1
2
3
4
5
// 全局安装cli工具
➜ npm i -g @nestjs/cli
/Users/macos/.nvm/versions/node/v14.15.1/bin/nest -> /Users/macos/.nvm/versions/node/v14.15.1/lib/node_modules/@nestjs/cli/bin/nest.js
+ @nestjs/cli@7.5.6
added 15 packages from 4 contributors, removed 1080 packages and updated 262 packages in 12.239s

初始化项目nest new [project-name]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
~/Projects/nestjs
➜ nest new nestjs-common-template  We will scaffold your app in a few seconds..

CREATE nestjs-common-template/.eslintrc.js (631 bytes)
CREATE nestjs-common-template/.prettierrc (51 bytes)
CREATE nestjs-common-template/README.md (3339 bytes)
CREATE nestjs-common-template/nest-cli.json (64 bytes)
CREATE nestjs-common-template/package.json (1984 bytes)
CREATE nestjs-common-template/tsconfig.build.json (97 bytes)
CREATE nestjs-common-template/tsconfig.json (339 bytes)
CREATE nestjs-common-template/src/app.controller.spec.ts (617 bytes)
CREATE nestjs-common-template/src/app.controller.ts (274 bytes)
CREATE nestjs-common-template/src/app.module.ts (249 bytes)
CREATE nestjs-common-template/src/app.service.ts (142 bytes)
CREATE nestjs-common-template/src/main.ts (208 bytes)
CREATE nestjs-common-template/test/app.e2e-spec.ts (630 bytes)
CREATE nestjs-common-template/test/jest-e2e.json (183 bytes)

? Which package manager would you   to use? (Use arrow keys)
❯ npm
  yarn
  
▹▹▹▹▸ Installation in progress...   Successfully created project nestjs-common-template  Get started with the following commands:

$ cd nestjs-common-template
$ npm run start


                          Thanks for installing Nest 
                 Please consider donating to our open collective
                        to help us maintain this package.


                 Donate: https://opencollective.com/nest

运行项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
~/Projects/nestjs took 22s 385ms
➜ cd nestjs-common-template/
nestjs-common-template on  HEAD [?] is  v0.0.1 via ⬢ v14.15.1
➜ npm run start

> nestjs-common-template@0.0.1 start /Users/macos/Projects/nestjs/nestjs-common-template
> nest start

[Nest] 5918   - 2021/03/09 下午11:06:10   [NestFactory] Starting Nest application...
[Nest] 5918   - 2021/03/09 下午11:06:10   [InstanceLoader] AppModule dependencies initialized +47ms
[Nest] 5918   - 2021/03/09 下午11:06:10   [RoutesResolver] AppController {}: +4ms
[Nest] 5918   - 2021/03/09 下午11:06:10   [RouterExplorer] Mapped {, GET} route +2ms
[Nest] 5918   - 2021/03/09 下午11:06:10   [NestApplication] Nest application successfully started +1ms

下面可以打开浏览器来访问http://localhost:3000可以看到hello world的字样。

1
Hello World!

说明我们的项目启动成功了。


项目目录与package.json

先来看看项目的工程目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.
├── README.md
├── dist                       // 打包过后,目标代码
│   ├── app.controller.d.ts
│   ├── app.controller.js
│   ├── app.controller.js.map
│   ├── app.module.d.ts
│   ├── app.module.js
│   ├── app.module.js.map
│   ├── app.service.d.ts
│   ├── app.service.js
│   ├── app.service.js.map
│   ├── main.d.ts
│   ├── main.js
│   ├── main.js.map
│   └── tsconfig.build.tsbuildinfo
├── nest-cli.json              // cli配置文件
├── package-lock.json
├── package.json               // npm项目配置
├── src                        // 源文件
│   ├── app.controller.spec.ts // 测试文件
│   ├── app.controller.ts      // 项目根控制器
│   ├── app.module.ts          // 项目根模块
│   ├── app.service.ts         // 项目根服务
│   └── main.ts                // 主入口文件
├── test                       // 测试配置文件 + 测试文件
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json        // ts打包配置
└── tsconfig.json              // ts配置

然后我们再来看看package.json,其中有两个部分非常需要注意:

  • scripts自定义的脚本:

    • start: 默认启动脚本

    • start:dev: 开启代码变化监视的启动脚本

    • start:debug:开启代码debug调试的启动脚本

    • start:prod:运行最终打包过后的代码

  • jest配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
  //...
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  // ....
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}


主程序文件(入口文件)main.ts

在项目的src目录中,有整个项目的主程序文件main.ts

1
2
3
4
5
6
7
8
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

这主程序文件中:

  • 使用NestFactory初始化了一个Nest实例app

  • 定义服务监听3000端口

  • 使用bootstrap()启动了该服务

我们可以看到该文件引入了一个模块./app.module,可以从这个文件找到一些整个应用的蛛丝马迹。

1
2
3
4
5
6
7
8
9
10
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这里使用到了TypeScript中的Decorator,引入了一个AppServiceAppController,如果熟悉Angular的小伙伴,看到这里就惊讶了。卧槽,这不是Augular吗?是的,官方给出了如下的解释:

Nest provides an out-of-the-box application architecture which allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications. The architecture is heavily inspired by Angular.

这是nestjs的哲学:

Nest提供了一个开箱即用的应用架构,允许开发人员和团队创建高度可测试、可扩展、松散耦合和易于维护的应用。该架构深受Angular的启发。

了解到这一点来说之后,方便学习过Angular的同学快速的入手,那么对于没有学过Angular的同学,可以从以下两个角度来思考学习。

  1. 对于毫无Angular基础的同学来说:

  • 应用是基础:先学会如何使用,写一些基础的接口,然后再去思考内在的逻辑,免得搞混了;

  • 学习先进框架的概念:官方有一手的介绍,所以一点不用慌,而且这些概念源于Google,都是经过了验证的;

  • 平时开发多问几个为什么:比如,为什么nestjs的入口是main.tsnestjs是用什么打包的?怎么没有看到webpack的配置?

对于学过Angular的同学来说:

  • 大致浏览基础示例代码:在清楚核心的概念之后,这样有利于快速入手;

  • 做一些实战项目:工具的学习不能浮于表面,一定要应用于自己的工作中来,对于小项目,可以参考官方的awesome示例页;


下面来回答几个问题:

为什么nestjs的入口是main.ts

下面是我个人的思考路径:

  • package.json中的main属性,有没有?——没有

  • 官方的配置文件中nest-cli.json,有没有?——没有

  • scripts中的运行脚本,有没有?——有,但是没有指明,只有nest start

  • 运行脚本使用的CLI,CLI的原文件中,有没有?——这里,就需要调试node_modules

按照上面的路径,可以自己建一个Nodejs的调试项目进行运行与调试!官方提供了--debug脚本,所以可以直接来创建,以VSCode为例:创建.vscode/launch.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  // 使用 IntelliSense 了解相关属性。 
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch via NPM",
      "request": "launch",
      "runtimeArgs": [
        "run-script",
        "start:debug"
      ],
      "runtimeExecutable": "npm",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "type": "pwa-node"
    }

  ]
}

直接使用VSCode的调试工具进行调试,这时候,新的问题来了,断点打在哪里?!学习过我们慕课网《大前端》的同学应该了解到CLI工具的工作原理,有以下几个组成部分:

  • Commander —— 处理传递的参数

  • Action —— 具体的函数

下面打开node_modules,找到@nestjs/cli映入我们眼帘的是:

http://img4.sycdn.imooc.com/605018be0001222307140896.jpg

这不巧了?怎么有两个文件,很打眼呢?commandsactions,我们来查看一下:

第一个:

http://img3.sycdn.imooc.com/605018cc0001190108981512.jpg

第二个:

http://img2.sycdn.imooc.com/605018d800010d1a08661232.jpg

分别点开:

  • start.command.js中主要是解析命令参数,但是有一个关键方法:this.action.handle(inputs, options)

  • 最终会来执行start.action.js

handle(inputs, options) {
       return __awaiter(this, void 0, void 0, function* () {
           try {
               const configFileName = options.find((option) => option.name === 'config')
                   .value;
               const configuration = yield this.loader.load(configFileName);
               const appName = inputs.find((input) => input.name === 'app')
                   .value;
               const pathToTsconfig = get_value_or_default_1.getValueOrDefault(configuration, 'compilerOptions.tsConfigPath', appName, 'path', options);
               const binaryToRunOption = options.find((option) => option.name === 'exec');
               const debugModeOption = options.find((option) => option.name === 'debug');
               const watchModeOption = options.find((option) => option.name === 'watch');
               const isWatchEnabled = !!(watchModeOption && watchModeOption.value);
               const watchAssetsModeOption = options.find((option) => option.name === 'watchAssets');
               const isWatchAssetsEnabled = !!(watchAssetsModeOption && watchAssetsModeOption.value);
               const debugFlag = debugModeOption && debugModeOption.value;
               const binaryToRun = binaryToRunOption && binaryToRunOption.value;
               const { options: tsOptions } = this.tsConfigProvider.getByConfigFilename(pathToTsconfig);
               const outDir = tsOptions.outDir || defaults_1.defaultOutDir;
               const onSuccess = this.createOnSuccessHook(configuration, appName, debugFlag, outDir, binaryToRun);
               yield this.runBuild(inputs, options, isWatchEnabled, isWatchAssetsEnabled, !!debugFlag, onSuccess);
           }
           catch (err) {
               if (err instanceof Error) {
                   console.log(`\n${ui_1.ERROR_PREFIX} ${err.message}\n`);
               }
               else {
                   console.error(`\n${chalk.red(err)}\n`);
               }
           }
       });
   }

这时候,我们就可以打印一个configuration了或者加个断点,使用npm start来跑一下,得到的结果无非如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 ~ file: start.action.js ~ line 29 ~ StartAction ~ return__awaiter ~ configuration {
  language: 'ts',
  sourceRoot: 'src',
  collection: '@nestjs/schematics',
  entryFile: 'main',
  projects: {},
  monorepo: false,
  compilerOptions: {
    tsConfigPath: 'tsconfig.build.json',
    webpack: true,
    webpackConfigPath: 'webpack.config.js',
    plugins: [],
    assets: []
  },
  generateOptions: {}
}

已经让我们看到了entryFile的字样,至此,我们找到了入口文件。还可以顺这这条思路,继续来找:

http://img4.sycdn.imooc.com/605018fd00016d0b36700916.jpg

继续往下找:

http://img3.sycdn.imooc.com/6050190b00017d9420981440.jpg


nestjs是用什么打包的?

有了上面调试的技巧,来回答这个问题变得很简单,我们来看看scripts中的打包命令"build": "nest build",,所以我们找到对应的command,可以找到这么一个文件build.action.js

runBuild(inputs, options, watchMode, watchAssetsMode, isDebugEnabled = false, onSuccess) {
    return __awaiter(this, void 0, void 0, function* () {
        const configFileName = options.find((option) => option.name === 'config')
            .value;
        const configuration = yield this.loader.load(configFileName);
        const appName = inputs.find((input) => input.name === 'app')
            .value;
        const pathToTsconfig = get_value_or_default_1.getValueOrDefault(configuration, 'compilerOptions.tsConfigPath', appName, 'path', options);
        const { options: tsOptions } = this.tsConfigProvider.getByConfigFilename(pathToTsconfig);
        const outDir = tsOptions.outDir || defaults_1.defaultOutDir;
        const isWebpackEnabled = get_value_or_default_1.getValueOrDefault(configuration, 'compilerOptions.webpack', appName, 'webpack', options);
        yield this.workspaceUtils.deleteOutDirIfEnabled(configuration, appName, outDir);
        this.assetsManager.copyAssets(configuration, appName, outDir, watchAssetsMode);
        if (isWebpackEnabled) {
            // 与这里的一行代码
            const webpackPath = get_value_or_default_1.getValueOrDefault(configuration, 'compilerOptions.webpackConfigPath', appName, 'webpackPath', options);
            const webpackConfigFactoryOrConfig = this.getWebpackConfigFactoryByPath(webpackPath, configuration.compilerOptions.webpackConfigPath);
            return this.webpackCompiler.run(configuration, webpackConfigFactoryOrConfig, pathToTsconfig, appName, isDebugEnabled, watchMode, this.assetsManager, onSuccess);
        }
        if (watchMode) {
            const tsCompilerOptions = {};
            const isPreserveWatchOutputEnabled = options.find((option) => option.name === 'preserveWatchOutput' && option.value === true);
            if (isPreserveWatchOutputEnabled) {
                tsCompilerOptions.preserveWatchOutput = true;
            }
            this.watchCompiler.run(configuration, pathToTsconfig, appName, tsCompilerOptions, onSuccess);
        }
        else {
            // 这里的一行代码
            this.compiler.run(configuration, pathToTsconfig, appName, onSuccess);
            this.assetsManager.closeWatchers();
        }
    });
}


结论:

  • nestjs可以开启webpack打包

  • 或者自己定义的打包,见下图

http://img1.sycdn.imooc.com/60501943000134b933482018.jpg

怎么没有看到webpack的配置?

nestjs支持webpack打包,有两种方法启用webpack,然后,就可以在根目录中添加webpack.config.js文件了。

官方的说明:

方法一:

针对于命令:nest build

将应用程序或工作区编译到输出文件夹中。

1
$ nest build <name> [options]Copy to clipboardErrorCopied

参数

参数描述
<name>要构建的项目的名称。

选项:

选项描述
--path [path]tsconfig文件的路径。别名: -p
--watch在监视模式下运行(实时重载)别名-w
--webpack使用 webpack 进行编译。
--webpackPath配置 webpack 的路径。


方法二:

在根目录中的nest-cli.json文件中配置compilerOptions,把webpack设置成true

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "webpack": true
  }
}


关于nest的nest-cli.json配置文件及其使用方式可以参考 这里。

这篇关于nestjs搭建通用业务框架(2):初体验的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!