在 Angular 框架中,模块封装机制与 ES 模块的核心理念非常相似——它本质上强调的是:组件、指令、管道等可声明的类型,默认情况下只能在声明它们的模块内部被访问和使用。
以一个实际开发场景为例,假设你在 AppModule 的 AppComponent 中,希望直接调用 AModule 里定义的一个名为 a-comp 的组件,代码大致如下:
@Component({
selector: 'my-app',
template: `
Hello {{name}}
`
})
export class AppComponent { }
此时,Angular 会毫不吝啬地抛出一个错误:
错误提示信息
Template parse errors: ‘a-comp’ is not a known element
出现这个问题的原因非常直接:AppModule 中并未声明 a-comp 组件。解决办法也很明确——你需要先将定义 a-comp 的那个模块导入进来:
@NgModule({
imports: [..., AModule]
})
export class AppModule { }
然而,事情并未就此结束。即便你成功导入了 AModule,如果 AModule 自身没有将 a-comp 组件“公开”出去,外部模块仍然无法使用它。因此,AModule 必须在 exports 数组中列出 a-comp,才能允许其他模块引用该组件:
@NgModule({
...
declarations: [AComponent],
exports: [AComponent]
})
export class AModule { }
至此,模块封装的概念已经清晰明了。不过,许多刚接触 Angular 的开发者常会误以为 Providers 也存在类似的封装限制——这其实是一个普遍存在的误解。实际情况是:在任何非延迟加载的模块中声明的 Providers,整个应用的任意位置都可以访问到,并不具备模块级别的隔离作用。

模块层级结构的常见误解
另一个让开发者感到困惑的迷思是关于“模块层级结构”的。不少人认为,当我们在一个模块中 import 另一个模块时,运行时就会自动形成父子层级关系——被导入的模块会成为子模块。但真相是:所有模块在编译阶段就已经被合并在一起,运行时根本不存在所谓的层级结构。
换个角度来理解,这就像你在代码中引入了一组命名空间。其中一个命名空间被设为默认值,默认命名空间下的标签无需添加前缀。例如在模板中,
归根结底,理解 Angular 模块封装的核心要点只有两个:一是掌握声明类与 exports 的配对关系,二是不要把 import 模块误认为是建立层级结构。搞清了这两点,绝大多数编译报错和运行时困惑都能迎刃而解。
