0%

angular-how-ng-serve-works

Introduction

Angular的CLI(Command Line Interface)系统非常强大,它提供了丰富的命令用来构建,测试,运行Angular程序,今天我们就从源码的角度来看看cli中的ng serve命令是如何工作的。

ng serve是我们日常频繁使用的命令,它用来本地启动Angular项目,检测代码更新以做出响应,但是用了这么久的命令,你了解它的底层逻辑吗?下面的几个问题你是否思考过?

  1. ng serve命令为什么没有输出文件到dist目录?
  2. ng serve命令是如何启动一个web server的?
  3. ng serve命令是如何监控文件变化的?
  4. ng serve命令是如何实现热更新的?

下面就让我们开始这段愉快而又漫长的探索之旅吧!

准备工作

为了调试ng serve命令,我们需要准备一个Angular项目,为了方便查看源码,我们将angluar-cli的源码也下载到本地,然后打开两个IDE,一个查看待调试的项目代码,一个查看angular-cli的源码。对照起来看,简直不要太爽!

从命令行开始

如果你直接查看ng serve命令的源码,可能得不到什么有用的信息,serve命令对应的源码在这里:packages/angular/cli/src/commands/serve/cli.ts, 从以下代码可知,serve命令是继承自ArchitectCommandModule的,同时也实现了CommandModuleImplementation接口。而该文件本身只是做了一个简单的配置,具体的实现逻辑都在ArchitectCommandModuleCommandModuleImplementation中。

1
2
3
4
5
6
7
8
9
10
export default class ServeCommandModule
extends ArchitectCommandModule
implements CommandModuleImplementation
{
multiTarget = false;
command = 'serve [project]';
aliases = RootCommands['serve'].aliases;
describe = 'Builds and serves your application, rebuilding on file changes.';
longDescriptionPath?: string | undefined;
}

还是调试吧,我们打开Angular项目,然后打开项目根目录下的package.json文件,找到scripts字段,你会看到如下内容:

1
2
3
4
5
6
7
8
9
10
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"compile": "ngc",
"test": "jest --verbose",
"test:watch": "jest --watch",
"prepare": "husky install"
},

想要深入理解ng serve命令,我们需要先了解start命令是如何工作的。我们通过调式该命令的方式来了解ng serve的底层逻辑。不同的IDE对应不同的调试方式:

  • WebStorm: 点击start命令左侧的绿色三角按钮,然后选择Debug 'start',这样就可以进入ng serve的源码了。
  • VSCode: 鼠标悬停到start命令上,点击Debug按钮,这样就可以进入ng serve的源码了。

使用这种方法可以进入调试状态,但是我们要在哪里设置断点呢?

对于package.json中的script区块中的命令,其实他们都对应node_modules/.bin下面的一个文件。以ng serve为例,因为它的命令由ng来引导,那么我们首先到node_modules/.bin目录下找到ng文件。

可是我们找到了三个ng文件,分别是ng, ng.cmd, ng.ps1,那么我们应该选择哪一个呢?其实通过扩展名就能看出来,我这里使用的是Windows系统,所以我们选择ng.cmd文件。

  • ng - Unix shell script
  • ng.cmd - Windows batch file
  • ng.ps1 - Windows PowerShell script

用记事本打开这个文件看看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)

endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@angular\cli\bin\ng.js" %*

我们不必纠结于每一行代码的含义,简单分析下来,我们可以推断出这个脚本的作用就是用node.exe来调用@angular/cli/bin/ng.js文件。

好了,我们再去看一下这个ng.js文件,这个文件就是Angular CLI的入口文件。ng.js调用了同一目录下的bootstrap.js文件,bootstrap.js文件调用了lib/init.ts文件。init.ts又调用了lib/cli/index.ts文件,后续又有一大堆的调用。

bin/ng.js –> bin/bootstrap.js –> lib/init.ts –> lib/cli/index.ts –> …

通过不断的设置断点并调试得知,最终在文件packages/angular/cli/src/command-builder/architect-base-command-module.ts中通过解析项目的配置文件angular.json,找到serve命令对应的builder,然后通过调用那个builder来启动一个web server。

architect-base-command-module.ts对应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
protected async runSingleTarget(target: Target, options: OtherOptions): Promise<number> {
const architectHost = this.getArchitectHost();
let builderName: string;
try {
builderName = await architectHost.getBuilderNameForTarget(target); // 获取builder名称
} catch (e) {
assertIsError(e);
return this.onMissingTarget(e.message);
}
//...
}

Webpack配置在哪里?

我们都知道,ng serve命令启动的是一个web server,而这是通过webpack-dev-server来实现的,那么这个webpack-dev-server的配置在哪里呢?我么可以在源码中找到如下目录:
packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts,这个目录下有四个文件:

  • common.ts - 通用配置,主要是打包配置
  • dev-server.ts - 开发服务器配置 - 我们要找的配置就在这里。
  • styles.ts - 样式配置,用来处理各种样式文件, 如css, scss/sass, less等
  • index.ts - 导出上面三个文件

总结

Angular的CLI命令对应的逻辑源码并不在CLI命令本身中,而是在对应的builder中,builder源码在这个目录下:

  • packages/angular/build/src/builders - application builder
  • packages/angular_devkit/build_angular/src/builders - other builders

关于builder的详细介绍,可以参考之前的一篇博文:https://zdd.github.io/2024/06/06/angular-builders/, 也可以参考Angular官方文档:https://angular.dev/tools/cli/cli-builder/

  • ng server works in memory and never generate files to dist folder