一文吃透 Angular Module & Standalone

4 阅读8分钟

本文由体验技术团队张婷原创。

一、核心概念:两种架构的本质区别

无论是 Module 还是 Standalone,核心目标都是解决 Angular 应用中组件、指令、管道、服务的组织、依赖管理与复用问题,只是实现方式截然不同。

1. 传统架构:NgModule 模块机制

NgModule 是 Angular 原生的模块化方案,本质是一个“功能容器”,通过装饰器 @NgModule 定义,承担着“声明、导入、导出、提供”四大核心职责,将分散的功能聚合为一个可管理的单元。

其核心逻辑是“模块中心化”——所有组件必须归属某个模块,依赖通过模块统一导入,服务通过模块提供作用域,这种设计非常适合大型项目的分层与分工。

1.png

2. 革新方案:Standalone 独立组件

Standalone 是 Angular 为简化开发推出的轻量化方案,通过在组件装饰器中设置 standalone: true,让组件摆脱对 NgModule 的依赖,实现“组件自包含”。 其核心逻辑是“组件中心化”——组件自身可直接导入所需的模块、其他独立组件,无需在模块中声明,大幅精简了模板代码,降低了入门门槛。

2.png

二、实操对比:代码层面的直观差异

理论不如实操,我们通过一个简单的“根组件+头部组件”场景,对比两种模式的实现代码,感受其差异。

1. NgModule 实现方式

需创建模块文件(如 app.module.ts),集中管理组件、依赖和服务,步骤相对繁琐: HeaderComponent 需单独创建 header.component.ts 文件,模板内容需完整定义,同时模块中必须声明所有用到的组件,否则会报“组件未注册”错误。

// header.component.ts(传统组件,需在模块中声明)
import { Component } from '@angular/core';

@Component({
  selector: 'app-header',
  template: `
    系统头部
  `,
  styles: [`
    .header { padding: 16px; background: #f5f5f5; border-bottom: 1px solid #eee; }
    nav { margin-top: 8px; color: #666; }
  `]
})
export class HeaderComponent { }

// app.component.ts(根组件)
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-header></app-header>
    Angular Module 模式示例
  `
})
export class AppComponent {
  showContent = true; // 控制内容显示,演示*ngIf指令用法
}

// app.module.ts(核心模块文件)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; // 提供*ngIf、*ngFor等基础指令

import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';

@NgModule({
  declarations: [
    // 声明模块内的组件、指令、管道(必须在此注册,否则无法使用)
    AppComponent,
    HeaderComponent
  ],
  imports: [
    // 导入依赖模块:BrowserModule用于浏览器渲染,CommonModule提供基础指令
    BrowserModule,
    CommonModule
  ],
  providers: [
    // 提供模块级服务(模块内所有组件共享同一个实例)
    { provide: 'API_BASE_URL', useValue: 'https://api.example.com' }
  ],
  bootstrap: [AppComponent] // 指定根组件,Angular启动时会渲染该组件
})
export class AppModule { }

// main.ts(应用启动入口文件)
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

// 通过编译模块启动应用,这是传统Module模式的标准启动方式
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error('应用启动失败:', err));

2.Standalone 实现方式

无需模块文件,组件自身声明依赖,启动流程更简洁。

补充说明:独立组件可直接导入其他独立组件,无需额外声明;依赖导入遵循“按需导入”原则,仅导入当前组件所需模块,减少冗余。


// header.component.ts(独立头部组件,无需模块声明)
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; // 自身导入所需模块

@Component({
  selector: 'app-header',
  standalone: true, // 标记为独立组件,摆脱模块依赖
  imports: [CommonModule], // 导入基础指令模块,用于后续可能的*ngIf等用法
  template: `
    独立组件头部<nav *首页 | 关于我们 | 联系我们
  `,
  styles: [`
    .header { padding: 16px; background: #e8f4f8; border-bottom: 1px solid #d1e7dd; }
    nav { margin-top: 8px; color: #333; }
  `]
})
export class HeaderComponent {
  showNav = true; // 组件内部状态,控制导航显示
}

// app.component.ts(独立根组件)
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeaderComponent } from './header/header.component'; // 直接导入独立组件

// 抽离共享依赖(缓解重复导入问题,大型项目推荐用法)
const SharedDependencies = [CommonModule, HeaderComponent];

@Component({
  selector: 'app-root',
  standalone: true, // 核心标记:独立组件
  imports: [SharedDependencies], // 导入所需依赖(模块+独立组件)
  providers: [
    // 组件级服务:默认当前组件及子组件共享实例,若需全局单例可加providedIn: 'root'
    { provide: 'API_BASE_URL', useValue: 'https://api.example.com', providedIn: 'root' }
  ],
  template: `
<app-header></app-header>
    Angular Standalone 模式示例
  `
})
export class AppComponent {
  showContent = true;

  // 交互方法,演示组件基础功能
  toggleContent() {
    this.showContent = !this.showContent;
  }
}

// main.ts(独立组件启动入口)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';

// 直接启动独立根组件,无需模块介入,启动流程更简洁
bootstrapApplication(AppComponent, {
  // 可选:全局配置,如提供全局服务(替代模块级providers)
  providers: [{ provide: 'GLOBAL_CONFIG', useValue: { env: 'production' } }]
})
  .catch(err => console.error('应用启动失败:', err));

三、传统与革新,孰优孰劣?

两种方案各有优劣,没有绝对的“完美”。

1. NgModule 的优缺点

优点:

  • 成熟稳定,生态兼容:作为 Angular 核心机制,兼容所有第三方库、插件和传统项目,几乎无适配风险,是老项目维护的首选。

  • 强模块化封装:适合大型团队协作,可按业务域(如用户模块、订单模块)拆分独立 NgModule,边界清晰,便于分工维护和权限管控。

  • 集中式依赖管理:模块级统一导入依赖,避免多个组件重复导入相同模块,减少冗余代码,适合大量组件共享依赖的场景。

  • 服务作用域清晰:模块级服务默认在模块内单例,无需额外配置即可实现“模块内共享、模块间隔离”,适合按模块隔离业务逻辑的场景。

缺点:

  • 模块代码冗余:即使是简单组件,也需创建模块文件,编写 @NgModule 装饰器及 declarations/imports 等配置,增加无业务价值的模块代码。

  • 学习成本:新手易混淆 declarations(声明组件)、imports(导入模块)、exports(导出组件)的用法,常出现“组件找不到”“指令未注册”等错误。

  • 编译效率略低:模块是编译基本单元,修改一个组件可能触发整个模块的重新编译,大型模块会增加编译耗时。

  • 组件复用成本高:组件必须绑定模块,跨项目复用单个组件时,需连带其所属模块一起复制,灵活性不足。

2. Standalone 的优缺点

优点:

  • 轻量化,开发效率高:无需创建模块文件,入门门槛低,中小型项目、原型开发速度大幅提升。

  • 精准依赖,代码精简:组件仅按需导入自身所需依赖,避免模块级导入带来的冗余依赖,代码更清晰、可维护性更强。

  • 编译性能更优:独立组件是最小编译单元,修改单个组件仅触发自身重新编译,大型项目编译速度提升明显。

  • 复用性强:组件完全独立于模块,跨项目复用只需复制组件文件,无需连带模块,是组件库开发的最优选择。

缺点:

  • 依赖重复导入:多个独立组件需同一模块(如 CommonModule)时,需各自导入,易出现重复代码(可通过抽离共享组件模块导入缓解,如下图)。

3.png

  • 部分老库适配不足:少数未升级的第三方库依赖模块级特性,需额外适配才能在独立组件中使用。

  • 服务作用域配置复杂默认是组件级单例,若需实现全局单例或模块级单例,需额外配置 providedIn: 'root' 或通过共享组件封装,比 NgModule 繁琐。

PS

  • 组件级单例:假如你有 3 个独立的 ButtonComponent,都注入了同一个 CountService,那么这 3 个组件会各有一个 CountService,点击按钮计数时,各自的数字不会互相影响。

  • 全局级单例:全局只有一个服务实例”(比如用户登录状态、全局缓存)。

四、各有优劣,如何选择?

NgModule 代表了 Angular 传统的“强模块化”设计理念,Standalone 则是 Angular 对“轻量化、高效化”的探索,两者并非非此即彼的替代关系,而可以是互补关系。

  • 全新中小型项目/原型开发:优先选择 Standalone 组件。轻量化特性可快速迭代,减少模板代码,降低团队协作成本。

  • 大型企业级项目/多人协作:采用混合模式。保留核心业务模块(NgModule)的封装性,新开发的组件、指令使用 Standalone 模式,逐步迁移老组件,兼顾稳定性和开发效率。

总结

作为开发者,我们无需纠结于“哪种更好”,而是要理解两种方案的设计初衷,根据项目规模、团队结构、复用需求灵活选型。在实际开发中,可以选择混合使用两种模式,既能保留传统架构的稳定性,又能享受新范式的高效性。

一点拙见分享,抛砖引玉,欢迎大家与我交流补充,共同进步~

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design
OpenTiny 代码仓库:github.com/opentiny
TinyVue源码:github.com/opentiny/ti…

欢迎进入代码仓库 Star🌟TinyVue、TinyEngine、TinyPro、TinyNG、TinyCLI、TinyEditor
如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~