Оригинальная ссылка:Avoiding common confusions with modules in Angular
Модули Angular — довольно сложная тема, и даже команда разработчиков Angular написала о ней несколько статей на официальном сайте.NgModuleучебник по статье. Эти уроки ясно объясняютModulesБольшая часть контента, но все еще не хватает некоторого контента, что вводит многих разработчиков в заблуждение. Я вижу много разработчиков, потому что они не знаютModulesКак работает внутреннее, так часто неправильно понимаемые понятия, используйтеModules APIПоза тоже неправильная.
Эта статья подробно объяснитModulesвнутренней работы и попытаться помочь вам прояснить некоторые распространенные заблуждения, которые яStackOverflowЯ часто вижу, как люди задают вопросы.
Упаковка модуля
Угловой вводит концепцию инкапсуляции модуля, которая очень похожа на концепцию модуля ES (Примечание: Concept Concept Modules ES можно просмотреть на Tymentscript Chiews Windows.Modules), что в основном означает, что все объявленные типы, включая компоненты, директивы и каналы, могут использоваться только другими объявленными компонентами в текущем модуле. Например, если бы я былAppиспользуемые компонентыAмодульныйa-compКомпоненты:
@Component({
selector: 'my-app',
template: `
<h1>Hello {{name}}</h1>
<a-comp></a-comp>
`
})
export class AppComponent { }
Компилятор Angular выдает ошибку:
Template parse errors: 'a-comp' is not a known element
Это потому чтоAppНет объявления в модулеa-compкомпонент, если я хочу использовать этот компонент, я должен импортироватьAМодуль, например:
@NgModule({
imports: [..., AModule]
})
export class AppModule { }
Это описано вышеУпаковка модуля. Мало того, если хочешьa-compДля корректной работы компонента необходимо сделать его общедоступным, т.е.AмодульныйexportsЭкспортируйте этот компонент в свойства:
@NgModule({
...
declarations: [AComponent],
exports: [AComponent]
})
export class AModule { }
Таким же образом, для инструкций и труб, вы также должны следоватьУпаковка модуляправило:
@NgModule({
...
declarations: [
PublicPipe,
PrivatePipe,
PublicDirective,
PrivateDirective
],
exports: [PublicPipe, PublicDirective]
})
export class AModule {}
должны знать о том,Упаковка модуляПринцип не применяется вentryComponentsсвойства, зарегистрированные в компоненте, если вы используете динамические представления, напримерЧто нужно знать о динамических компонентах AngularДля создания экземпляров динамических компонентов способом, описанным в этой статье, нет необходимостиAмодульныйexportsсвойства для экспортаa-compкомпоненты. Конечно, вы должны импортироватьAмодуль.
Большинство новичков подумаютprovidersСуществуют также правила упаковки,но не на самом деле. существуетМодули без ленивой загрузкиЛюбойproviderМожно получить доступ в любом месте программы, ниже будет подробно описано.
уровень модуля
Одно из самых больших заблуждений новичков заключается в том, что модуль формирует иерархию модулей после импорта других модулей, и разумно думать, что этот модуль станет родительским модулем этих импортированных модулей, формируя таким образом иерархию, подобную дереву модулей. Но на самом деле такой иерархии модулей нет. потому чтоВсе модули объединяются на этапе компиляции, поэтому между импортируемыми и импортируемыми модулями нет иерархической связи.
подобнокомпонентыТочно так же компилятор Angular создает фабрику модулей для корневого модуля, который являетсяmain.ts, передается как параметрbootstrapModule()Модули с методами:
platformBrowserDynamic().bootstrapModule(AppModule);
Угловой компилятор используетcreateNgModuleFactoryСпособ создания модуля (Примечание: см.L274 -> L60 -> L109 -> L153-L155 -> L50), метод требует несколько параметров (Примечание: для ясности понимания, не переведено. Последняя версия не включает третий параметр зависимости.):
- module class reference
- bootstrap components
- component factory resolver with entry components
- definition factory with merged module providers
Последние два пункта объясняют, почемуprovidersа такжеentry componentsПравил инкапсуляции модулей нет, потому что после компиляции нет нескольких модулей, а есть только один объединенный модуль. и на этапе компиляции компилятор не знает, как вы будете использоватьprovidersи динамические компоненты, поэтому компилятор управляет инкапсуляцией. Но в процессе разбора шаблона компонента на этапе компиляции компилятор знает, как вы используете компоненты, директивы и каналы, поэтому компилятор может контролировать их частные объявления. (Примечание:providersа такжеentry componentsЭто динамическая часть динамического контента во всей программе.Компилятор Angular не знает, как он будет использоваться, но компоненты, директивы и пайпы, написанные в шаблоне, являются статической частью статического контента, а компилятор Angular знает, как он используется при компиляции. Это важно для понимания внутренней работы Angular. )
Давайте посмотрим на пример создания модульных фабрик, предположим, что у вас естьAа такжеBдва модуля, каждый из которых определяетproviderс однимentry component:
@NgModule({
providers: [{provide: 'a', useValue: 'a'}],
declarations: [AComponent],
entryComponents: [AComponent]
})
export class AModule {}
@NgModule({
providers: [{provide: 'b', useValue: 'b'}],
declarations: [BComponent],
entryComponents: [BComponent]
})
export class BModule {}
корневой модульAppТакже определяет аproviderappи импортAа такжеBМодуль:
@NgModule({
imports: [AModule, BModule],
declarations: [AppComponent],
providers: [{provide: 'root', useValue: 'root'}],
bootstrap: [AppComponent]
})
export class AppModule {}
когда компилятор компилируетAppКогда корневой модуль генерирует фабрику модулей, компиляторсливатьсявсех модулейproviders, и создайте фабрику модулей только для объединенного модуля. Следующий код показывает, как генерируется фабрика модулей:
createNgModuleFactory(
// reference to the AppModule class
AppModule,
// reference to the AppComponent that is used
// to bootstrap the application
[AppComponent],
// module definition with merged providers
moduleDef([
...
// reference to component factory resolver
// with the merged entry components
moduleProvideDef(512, jit_ComponentFactoryResolver_5, ..., [
ComponentFactory_<BComponent>,
ComponentFactory_<AComponent>,
ComponentFactory_<AppComponent>
])
// references to the merged module classes
// and their providers
moduleProvideDef(512, AModule, AModule, []),
moduleProvideDef(512, BModule, BModule, []),
moduleProvideDef(512, AppModule, AppModule, []),
moduleProvideDef(256, 'a', 'a', []),
moduleProvideDef(256, 'b', 'b', []),
moduleProvideDef(256, 'root', 'root', [])
]);
Из приведенного выше кода мы знаем, что все модули имеютprovidersа такжеentry componentsбудут объединены и переданыmoduleDef()метод,Таким образом, независимо от того, сколько модулей импортировано, компилятор только объединит модули и сгенерирует только одну фабрику модулей.. Фабрика модулей использует модуль-инжектор для создания объединенных объектов модулей (примечание: см.L232), однако, поскольку существует только один модуль слияния, Angular будет использовать только этиproviders, чтобы сгенерировать одноэлементный корневой инжектор.
Теперь вы можете подумать, что если два модуля определяют одно и то жеprovider token,Что случится?
первое правилоопределяется в модуле, который импортирует другие модулиproviderвсегда побеждает первым, как вAppModuleтакже определитьa provider:
@NgModule({
...
providers: [{provide: 'a', useValue: 'root'}],
})
export class AppModule {}
Просмотрите сгенерированный заводской код модуля:
moduleDef([
...
moduleProvideDef(256, 'a', 'root', []),
moduleProvideDef(256, 'b', 'b', []),
]);
Вы можете видеть, что последняя объединенная фабрика модулей содержитmoduleProvideDef(256, 'a', 'root', []), перезапишетAModuleопределено в{provide: 'a', useValue: 'a'}.
второе правилоэто последний импортированный модульproviders, перезапишет ранее импортированный модульproviders. Так же, такжеBModuleопределитьa provider:
@NgModule({
...
providers: [{provide: 'a', useValue: 'b'}],
})
export class BModule {}
Затем в следующем порядкеAppModuleимпортировать вAModuleа такжеBModule:
@NgModule({
imports: [AModule, BModule],
...
})
export class AppModule {}
Просмотрите сгенерированный заводской код модуля:
moduleDef([
...
moduleProvideDef(256, 'a', 'b', []),
moduleProvideDef(256, 'root', 'root', []),
]);
Таким образом, приведенный выше код подтвердил второе правило. мы вBModuleопределено в{provide: 'a', useValue: 'b'}Теперь давайте обменять порядок импорта модуля:
@NgModule({
imports: [BModule, AModule],
...
})
export class AppModule {}
Просмотрите сгенерированный заводской код модуля:
moduleDef([
...
moduleProvideDef(256, 'a', 'a', []),
moduleProvideDef(256, 'root', 'root', []),
]);
Как и ожидалось, из-за изменения порядка импорта модулей теперьAModuleиз{provide: 'a', useValue: 'a'}покрытыйBModuleиз{provide: 'a', useValue: 'b'}.
Примечание. Автор выше предоставляет код AppModule, скомпилированный @angular/compiler, и анализирует поставщиков нескольких модулей для объединения скомпилированного кода. На самом деле, мы можем передать командуyarn ngc -p ./tmp/tsconfig.jsonИди и сокомпилируйте небольшой пример самостоятельно, среди которых,./node_modules/.bin/ngcда@angular/compiler-cliПредоставленные команды cli. мы можем использоватьng new moduleСоздайте новый проект, моя версия 6.0.5. Затем создайте его в корневом каталоге проекта./tmpпапку, затем добавьтеtsconfig.json, содержимое корневого каталога проекта копируетсяtsconfig.json, затем добавьтеmodule.tsдокумент.module.tsКонтент содержит корневой модульAppModuleи два модуляAModuleа такжеBModule,AModuleпоставкаAService,{provide:'a', value:'a'}а также{provide:'b', value:'b'}обслуживание, покаBModuleпоставкаBServiceа также{provide: 'b', useValue: 'c'}.AModuleа такжеBModuleИмпортировать корневые модули по порядкуAppModule, полный код выглядит следующим образом:
import {Component, Inject, Input, NgModule} from '@angular/core';
import "./goog"; // goog.d.ts 源码文件拷贝到 /tmp 文件夹下
import "hammerjs";
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
export class AService {
}
@NgModule({
providers: [
AService,
{provide: 'a', useValue: 'a'},
{provide: 'b', useValue: 'b'},
],
})
export class AModule {
}
export class BService {
}
@NgModule({
providers: [
BService,
{provide: 'b', useValue: 'c'}
]
})
export class BModule {
}
@Component({
selector: 'app',
template: `
<p>{{name}}</p>
<!--<a-comp></a-comp>-->
`
})
export class AppComp {
name = 'lx1036';
}
export class AppService {
}
@NgModule({
imports: [AModule, BModule],
declarations: [AppComp],
providers: [
AppService,
{provide: 'a', useValue: 'b'}
],
bootstrap: [AppComp]
})
export class AppModule {
}
platformBrowserDynamic().bootstrapModule(AppModule).then(ngModuleRef => console.log(ngModuleRef));
потомyarn ngc -p ./tmp/tsconfig.jsonКомпиляция этого модуля. Файл Module.ts с @ angular / компилятор производит несколько файлов, включаяmodule.jsа такжеmodule.factory.js. Первый взглядmodule.js.AppModuleКласс будет скомпилирован в следующий код, и мы обнаружим, что находимся в@NgModuleМетаданные, написанные в декораторе класса, будут присвоеныAppModule.decoratorsСвойство, если оно является декоратором свойства, будет присвоеноpropDecoratorsАтрибуты:
var AppModule = /** @class */ (function () {
function AppModule() {
}
AppModule.decorators = [
{ type: core_1.NgModule, args: [{
imports: [AModule, BModule],
declarations: [AppComp],
providers: [
AppService,
{ provide: 'a', useValue: 'b' }
],
bootstrap: [AppComp]
},] },
];
return AppModule;
}());
exports.AppModule = AppModule;
тогда посмотри наmodule.factory.jsфайл, этот файл очень важен, эта статья о модуляхprovidersСлияние можно увидеть из этого файла. Файловый объект AppModuleNgFactory содержит объединенныйproviders,ЭтиprovidersОтAppModule,AModule,BModule,а такжеAppModuleсерединаprovidersперезапишет другие модулиproviders,BModuleсерединаprovidersбудет охватыватьAModuleизproviders,потому чтоBModuleсуществуетAModuleПосле импорта вы можете изменить порядок импорта, чтобы посмотреть, что произойдет. где ɵcmfcreateNgModuleFactory, ɵmod этоmoduleDef, ɵmpdmoduleProvideDef,moduleProvideDefПервый параметрenum NodeFlagsТип узла, используемый для указания типа текущего узла, напримерi0.ɵmpd(256, "a", "a", [])256 в средствахTypeValueProviderЭто тип значения.
Object.defineProperty(exports, "__esModule", { value: true });
var i0 = require("@angular/core");
var i1 = require("./module");
var AModuleNgFactory = i0.ɵcmf(
i1.AModule,
[],
function (_l) {
return i0.ɵmod([
i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, []], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]),
i0.ɵmpd(4608, i1.AService, i1.AService, []),
i0.ɵmpd(1073742336, i1.AModule, i1.AModule, []),
i0.ɵmpd(256, "a", "a", []),
i0.ɵmpd(256, "b", "b", [])]
);
});
exports.AModuleNgFactory = AModuleNgFactory;
var BModuleNgFactory = i0.ɵcmf(
i1.BModule,
[],
function (_l) {
return i0.ɵmod([
i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, []], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]),
i0.ɵmpd(4608, i1.BService, i1.BService, []),
i0.ɵmpd(1073742336, i1.BModule, i1.BModule, []),
i0.ɵmpd(256, "b", "c", [])
]);
});
exports.BModuleNgFactory = BModuleNgFactory;
var AppModuleNgFactory = i0.ɵcmf(
i1.AppModule,
[i1.AppComp], // AppModule 的 bootstrapComponnets 启动组件数据
function (_l) {
return i0.ɵmod([
i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, [AppCompNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]),
i0.ɵmpd(4608, i1.AService, i1.AService, []),
i0.ɵmpd(4608, i1.BService, i1.BService, []),
i0.ɵmpd(4608, i1.AppService, i1.AppService, []),
i0.ɵmpd(1073742336, i1.AModule, i1.AModule, []),
i0.ɵmpd(1073742336, i1.BModule, i1.BModule, []),
i0.ɵmpd(1073742336, i1.AppModule, i1.AppModule, []),
i0.ɵmpd(256, "a", "b", []),
i0.ɵmpd(256, "b", "c", [])]);
});
exports.AppModuleNgFactory = AppModuleNgFactory;
Самостоятельное составление и практика будет намного эффективнее, чем просто чтение пояснений к статье.
Модули ленивой загрузки
Теперь еще одна путаница — ленивая загрузка модулей. Официальная документация говорит об этом (примечание: не переведено):
Angular создает модуль с отложенной загрузкой со своим собственным инжектором, дочерним элементом корневого инжектора… Таким образом, модуль с отложенной загрузкой, который импортирует этот общий модуль, создает собственную копию сервиса.
Итак, мы знаем, что Angular создаст собственный инжектор для отложенных модулей, потому что компилятор Angular будет генерировать инжектор для каждой компиляции отложенного модуля.Независимый завод компонентов.这样在该懒加载模块中定义的providersне будет объединен с инжектором основного модуля, поэтому, если модуль с ленивой загрузкой определяет то же самоеprovider, компилятор Angular будетproviderСоздайте новый объект службы.
Таким образом, ленивая загрузка модулей также создает иерархию, но иерархию инжектора, а не иерархию модулей.В модуле с отложенной загрузкой все импортированные модули также объединяются в один на этапе компиляции, как и модули без отложенной загрузки выше.
Вышеприведенная связанная логика находится в@angular/routerупаковкаRouterConfigLoaderВ коде этот раздел показывает, как загрузить модуль и создать инжектор:
export class RouterConfigLoader {
load(parentInjector, route) {
...
const moduleFactory$ = this.loadModuleFactory(route.loadChildren);
return moduleFactory$.pipe(map((factory: NgModuleFactory<any>) => {
...
const module = factory.create(parentInjector);
...
}));
}
private loadModuleFactory(loadChildren) {
...
return this.loader.load(loadChildren)
}
}
Проверьте эту строку кода:
const module = factory.create(parentInjector);
Входящий отец ввел для создания нового модуля ленивой загрузки объекта.
статические методы forRoot и forChild
Посмотрите, как представлен официальный сайт (примечание: без перевода):
Добавьте метод CoreModule.forRoot, который настраивает базовую службу UserService… Вызывайте forRoot только в корневом модуле приложения AppModule.
Это предложение разумно, но если вы не понимаете, почему, то в итоге напишите что-то вроде кода ниже:
@NgModule({
imports: [
SomeLibCarouselModule.forRoot(),
SomeLibCheckboxModule.forRoot(),
SomeLibCloseModule.forRoot(),
SomeLibCollapseModule.forRoot(),
SomeLibDatetimeModule.forRoot(),
...
]
})
export class SomeLibRootModule {...}
Каждый импортированный модуль (например,CarouselModule,CheckboxModuleтак далее)forRoot, давайте разберемся, зачем это нужно в первую очередьforRoot.
Когда вы импортируете модуль, вы обычно используете ссылку на модуль:
@NgModule({ providers: [AService] })
export class A {}
@NgModule({ imports: [A] })
export class B {}
В этом случаеAВсе определено в модулеprovidersбудут объединены в основной инжектор и доступны во всем контексте программы, я думаю, вы уже знаете, почему - все модули описаны вышеprovidersобъединяются для создания инжектора.
Angular также поддерживает другой способ импорта файлов с помощьюprovidersмодуль, который не импортируется с помощью ссылки на модуль, а вместо этого передает реализацию, реализующуюModuleWithProvidersОбъект интерфейса:
interface ModuleWithProviders {
ngModule: Type<any>
providers?: Provider[]
}
Мы можем переписать приведенное выше как:
@NgModule({})
class A {}
const moduleWithProviders = {
ngModule: A,
providers: [AService]
};
@NgModule({
imports: [moduleWithProviders]
})
export class B {}
Лучше использовать статический метод внутри объекта модуля для возвратаModuleWithProviders, вместо прямого использованияModuleWithProvidersобъект типа, используйтеforRootметод рефакторинга кода:
@NgModule({})
class A {
static forRoot(): ModuleWithProviders {
return {ngModule: A, providers: [AService]};
}
}
@NgModule({
imports: [A.forRoot()]
})
export class B {}
forRootметод возвращаетModuleWithProvidersprovidersИли выше, используяmoduleWithProvidersобъект, здесь только для демонстрационных целей. Однако если мы хотим разделитьprovidersи определите их отдельно в импортированном модулеproviders
Например, если мы хотим определить глобал для нелениво загружаемых модулейAservice, определите лениво загружаемый модульBсервис, вам нужно использовать вышеуказанный метод. Мы используемforRootметод возвращает для нелениво загруженных модулейproviders,использоватьforChildМетод, как ленивый модуль загрузки возвращаетсяproviders.
@NgModule({})
class A {
static forRoot() {
return {ngModule: A, providers: [AService]};
}
static forChild() {
return {ngModule: A, providers: [BService]};
}
}
@NgModule({
imports: [A.forRoot()]
})
export class NonLazyLoadedModule {}
@NgModule({
imports: [A.forChild()]
})
export class LazyLoadedModule {}
Поскольку неленивые загруженные модули объединяются, поэтомуforRootопределено вprovidersДоступны глобально (примечание: включая нелениво загруженные модули и лениво загруженные модули), но поскольку лениво загруженные модули имеют свои собственные инжекторы, выforChildопределено вprovidersДоступно (примечание: без перевода) только в текущем модуле ленивой загрузки.
@NgModule({
imports: [
SomeLibCarouselModule.forRoot(),
SomeLibCheckboxModule.forRoot(),
...
forRootметод, поскольку он определен в нескольких модуляхprovidersОн должен быть доступен глобально, и он не подготовлен отдельно для ленивой загрузки модулей.providers(Примечание: то есть нет резкиprovidersпотребности, но вы используетеforRootВынужден сократить). Даже если модуль не определяет какой-либо введенныйprovidersЭтот код написан еще более запутанно.
Use forRoot/forChild convention only for shared modules with providers that are going to be imported into both eager and lazy module modules
Еще одна вещь, которую следует отметить, это то, чтоforRootа такжеforChildЭто просто метод, так что вы можете передавать параметры. Например,@angular/routerв упаковкеRouterModule, это определяетforRootМетод и дополнительные параметры передачи:
export class RouterModule {
static forRoot(routes: Routes, config?: ExtraOptions)
входящийroutesПараметры используются для регистрацииROUTESИдентифицировано (токен):
static forRoot(routes: Routes, config?: ExtraOptions) {
return {
ngModule: RouterModule,
providers: [
{provide: ROUTES, multi: true, useValue: routes}
Второй необязательный параметр, переданный вconfigиспользуется как параметр конфигурации (примечание: например, настройка политики предварительной загрузки):
static forRoot(routes: Routes, config?: ExtraOptions) {
return {
ngModule: RouterModule,
providers: [
{
provide: PreloadingStrategy,
useExisting: config.preloadingStrategy ?
config.preloadingStrategy :
NoPreloading
}
как вы видели,RouterModuleиспользовалforRootа такжеforChildметод сегментацииprovidersproviders.
кеш модуля
существуетStackoverflow
SystemJsNgModuleLoader@angular/coreComponentДекоратор:
import { Component } from '@angular/core';
Вы ссылаетесь на этот пакет несколько раз в своей программе, но SystemJS не загружает этот пакет каждый раз, а просто загружает его один раз и кэширует.
если вы используетеangular-cliили настроить самостоятельноWebpackТакже тот же токен, он будет загружен только один раз и кэшировать, и назначить егоID, другие модули будут использовать этоIDЧтобы найти модуль, вы можете получить широкий спектр услуг, предоставляемых модулем.