动态组件加载是Angular高级特性,允许在运行时动态创建、加载和销毁组件,实现高度灵活的插件化架构。
ComponentFactoryResolver、ViewContainerRef和ComponentRef在运行时创建组件实例。
点击下方按钮动态加载不同组件
// dynamic-loader.component.ts
import {
Component,
ComponentFactoryResolver,
ViewContainerRef,
ViewChild,
AfterViewInit,
OnDestroy
} from '@angular/core';
import { AlertComponent } from './alert.component';
import { CardComponent } from './card.component';
@Component({
selector: 'app-dynamic-loader',
template: `
<div class="dynamic-loader">
<div class="controls">
<button (click)="loadAlert()">加载Alert</button>
<button (click)="loadCard()">加载Card</button>
<button (click)="destroyComponent()">销毁组件</button>
</div>
<!-- 组件容器 -->
<ng-container #container></ng-container>
</div>
`
})
export class DynamicLoaderComponent implements AfterViewInit, OnDestroy {
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
private componentRef: any;
private components = new Map<string, any>();
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) {}
ngAfterViewInit(): void {
// 预加载组件工厂
this.preloadComponents();
}
private preloadComponents(): void {
// 注册可用组件
this.components.set('alert', AlertComponent);
this.components.set('card', CardComponent);
}
loadAlert(): void {
this.loadComponent('alert', {
title: '动态Alert',
message: '这是动态加载的Alert组件',
type: 'success'
});
}
loadCard(): void {
this.loadComponent('card', {
title: '动态Card',
content: '这是动态加载的Card组件',
footer: 'Card底部'
});
}
private loadComponent(componentName: string, inputs?: any): void {
// 清理现有组件
this.destroyComponent();
// 获取组件类
const component = this.components.get(componentName);
if (!component) {
console.error(`组件 ${componentName} 未注册`);
return;
}
try {
// 创建组件工厂
const componentFactory = this.componentFactoryResolver
.resolveComponentFactory(component);
// 创建组件实例
this.componentRef = this.container
.createComponent(componentFactory);
// 设置输入属性
if (inputs && this.componentRef.instance) {
Object.keys(inputs).forEach(key => {
this.componentRef.instance[key] = inputs[key];
});
}
// 监听输出事件
if (this.componentRef.instance['onClose']) {
this.componentRef.instance['onClose'].subscribe(() => {
console.log('组件关闭事件');
this.destroyComponent();
});
}
console.log(`${componentName}组件加载成功`);
} catch (error) {
console.error(`加载组件失败:`, error);
}
}
destroyComponent(): void {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
console.log('组件已销毁');
}
}
ngOnDestroy(): void {
this.destroyComponent();
}
}
// alert.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-alert',
template: `
<div class="alert" [class]="type">
<div class="alert-header">
<h4>{{title}}</h4>
<button (click)="close()">×</button>
</div>
<div class="alert-body">
{{message}}
</div>
</div>
`,
styles: [`
.alert {
border: 1px solid transparent;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
}
.success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.warning {
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.alert-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.alert-header button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: inherit;
}
`]
})
export class AlertComponent {
@Input() title = '提示';
@Input() message = '';
@Input() type: 'success' | 'warning' | 'error' = 'success';
@Output() onClose = new EventEmitter<void>();
close(): void {
this.onClose.emit();
}
}
// modern-dynamic-loader.component.ts (Angular 13+)
import {
Component,
ViewContainerRef,
ViewChild,
ComponentRef,
Type
} from '@angular/core';
import { AlertComponent } from './alert.component';
import { CardComponent } from './card.component';
@Component({
selector: 'app-modern-dynamic-loader',
template: `
<div class="modern-loader">
<div class="controls">
<button (click)="loadComponent(AlertComponent, alertConfig)">
加载Alert
</button>
<button (click)="loadComponent(CardComponent, cardConfig)">
加载Card
</button>
<button (click)="clear()">清空</button>
</div>
<!-- 组件容器 -->
<ng-container #container></ng-container>
</div>
`
})
export class ModernDynamicLoaderComponent {
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
private componentRefs: ComponentRef<any>[] = [];
// 组件配置
alertConfig = {
title: '现代动态Alert',
message: '使用Angular 13+新API',
type: 'success' as const
};
cardConfig = {
title: '现代动态Card',
content: '无需ComponentFactoryResolver',
footer: 'Card底部'
};
async loadComponent(component: Type<any>, inputs?: any): Promise<void> {
try {
// 动态导入组件(如果需要懒加载)
// const { component } = await import('./path/to/component');
// 创建组件实例
const componentRef = this.container.createComponent(component);
// 设置输入属性
if (inputs && componentRef.instance) {
Object.assign(componentRef.instance, inputs);
// 手动触发变更检测(如果需要)
componentRef.changeDetectorRef.detectChanges();
}
// 存储引用以便清理
this.componentRefs.push(componentRef);
console.log('组件加载成功');
} catch (error) {
console.error('加载组件失败:', error);
}
}
clear(): void {
this.componentRefs.forEach(ref => ref.destroy());
this.componentRefs = [];
this.container.clear();
console.log('所有组件已销毁');
}
}
// lifecycle-aware.component.ts - 支持动态生命周期的组件
import {
Component,
Input,
OnDestroy,
OnInit,
OnChanges,
SimpleChanges,
EventEmitter,
Output
} from '@angular/core';
@Component({
selector: 'app-lifecycle-aware',
template: `
<div class="lifecycle-component">
<h4>生命周期感知组件</h4>
<p>计数: {{count}}</p>
<button (click)="increment()">增加</button>
<button (click)="destroy.emit()">请求销毁</button>
</div>
`,
styles: [`
.lifecycle-component {
padding: 20px;
border: 2px solid #3498db;
border-radius: 8px;
background: #e3f2fd;
margin: 10px 0;
}
`]
})
export class LifecycleAwareComponent implements OnInit, OnChanges, OnDestroy {
@Input() count = 0;
@Output() destroy = new EventEmitter<void>();
@Output() countChanged = new EventEmitter<number>();
private initialized = false;
constructor() {
console.log('🚀 构造函数调用');
}
ngOnInit(): void {
console.log('✅ ngOnInit - 组件初始化完成');
this.initialized = true;
}
ngOnChanges(changes: SimpleChanges): void {
console.log('🔄 ngOnChanges - 输入属性变化:', changes);
}
increment(): void {
this.count++;
this.countChanged.emit(this.count);
console.log(`🔢 计数增加: ${this.count}`);
}
ngOnDestroy(): void {
console.log('🗑️ ngOnDestroy - 组件销毁');
// 清理资源
}
// 自定义方法,供动态加载器调用
public customMethod(): void {
console.log('🎯 自定义方法被调用');
}
}
// 在动态加载器中处理生命周期
private async loadWithLifecycle(): Promise<void> {
const componentRef = this.container.createComponent(LifecycleAwareComponent);
// 设置输入
componentRef.instance.count = 10;
// 监听输出
componentRef.instance.destroy.subscribe(() => {
console.log('收到销毁请求');
componentRef.destroy();
});
componentRef.instance.countChanged.subscribe((count: number) => {
console.log('计数变化:', count);
});
// 等待组件初始化
await new Promise(resolve => setTimeout(resolve, 0));
// 调用组件方法
if (componentRef.instance.customMethod) {
componentRef.instance.customMethod();
}
// 手动触发变更检测
componentRef.changeDetectorRef.detectChanges();
}
// dynamic-communication.component.ts
import {
Component,
ViewContainerRef,
ViewChild,
ComponentRef,
Injector,
Input
} from '@angular/core';
import { DynamicFormComponent } from './dynamic-form.component';
import { CommunicationService } from './communication.service';
@Component({
selector: 'app-dynamic-communication',
template: `
<div class="communication-demo">
<h3>动态组件通信</h3>
<div class="host-controls">
<input [(ngModel)]="formData.name" placeholder="姓名">
<input [(ngModel)]="formData.email" placeholder="邮箱">
<button (click)="updateDynamicComponent()">更新动态组件</button>
</div>
<!-- 动态组件容器 -->
<div #container></div>
<div class="output-log">
<h4>事件日志</h4>
<div *ngFor="let log of logs">{{log}}</div>
</div>
</div>
`
})
export class DynamicCommunicationComponent {
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
@Input() initialData: any = {};
private componentRef: ComponentRef<DynamicFormComponent> | null = null;
private logs: string[] = [];
formData = {
name: '',
email: ''
};
constructor(
private injector: Injector,
private communicationService: CommunicationService
) {}
ngAfterViewInit(): void {
this.loadDynamicForm();
// 通过服务通信
this.communicationService.data$.subscribe(data => {
this.logs.push(`服务通信: ${JSON.stringify(data)}`);
});
}
private async loadDynamicForm(): Promise<void> {
// 创建自定义注入器(如果需要)
const customInjector = Injector.create({
parent: this.injector,
providers: [
{ provide: 'CONFIG', useValue: { theme: 'dark' } }
]
});
// 加载组件
const { DynamicFormComponent } = await import('./dynamic-form.component');
this.componentRef = this.container.createComponent(DynamicFormComponent, {
injector: customInjector
});
// 方法1:通过输入属性传递数据
this.componentRef.instance.initialData = this.initialData;
this.componentRef.instance.data = this.formData;
// 方法2:通过输出事件接收数据
this.componentRef.instance.submit.subscribe((data: any) => {
this.logs.push(`表单提交: ${JSON.stringify(data)}`);
this.handleFormSubmit(data);
});
// 方法3:通过服务通信
this.componentRef.instance.service = this.communicationService;
// 方法4:通过模板变量(需要在组件中暴露)
// 使用componentRef.instance直接访问
// 触发变更检测
this.componentRef.changeDetectorRef.detectChanges();
}
updateDynamicComponent(): void {
if (this.componentRef) {
// 更新输入属性
this.componentRef.instance.data = { ...this.formData };
// 手动调用组件方法
if (this.componentRef.instance.update) {
this.componentRef.instance.update();
}
// 触发变更检测
this.componentRef.changeDetectorRef.detectChanges();
}
}
private handleFormSubmit(data: any): void {
console.log('处理表单提交:', data);
// 更新逻辑...
}
}
插件系统架构: ┌─────────────────────────────────────────────────┐ │ 宿主应用 (Host App) │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 插件管理器 │ │ 插件注册中心 │ │ │ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ └───────┬────────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ 插件加载器 │ │ │ └──────┬──────┘ │ │ │ │ ├─────────────────┼──────────────────────────────┤ │ ┌──────▼──────┐ │ │ │ 插件模块 │ │ │ │ (懒加载) │ │ │ └──────┬──────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ 插件组件 │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────┘
// plugin-system.service.ts - 插件管理服务
import { Injectable, ComponentFactoryResolver, Injector, Type, ComponentRef } from '@angular/core';
import { PluginRegistry } from './plugin-registry';
export interface PluginConfig {
id: string;
name: string;
version: string;
component: Type;
config?: any;
}
@Injectable({
providedIn: 'root'
})
export class PluginSystemService {
private plugins = new Map<string, PluginConfig>();
private loadedPlugins = new Map<string, ComponentRef
// dashboard.component.ts - 动态仪表板
import {
Component,
ViewContainerRef,
ViewChild,
AfterViewInit,
ComponentRef
} from '@angular/core';
import { WidgetService, WidgetConfig } from './widget.service';
interface DashboardWidget {
id: string;
title: string;
component: any;
config: any;
ref?: ComponentRef;
position: { x: number, y: number };
size: { width: number, height: number };
}
@Component({
selector: 'app-dynamic-dashboard',
template: `
<div class="dashboard">
<div class="dashboard-header">
<h2>动态仪表板</h2>
<div class="widget-controls">
<select [(ngModel)]="selectedWidget">
<option value="">选择小部件</option>
<option *ngFor="let widget of availableWidgets" [value]="widget.id">
{{widget.name}}
</option>
</select>
<button (click)="addWidget()">添加小部件</button>
<button (click)="saveLayout()">保存布局</button>
<button (click)="resetLayout()">重置</button>
</div>
</div>
<div class="dashboard-body" #dashboardBody>
<div class="widget-grid">
<div *ngFor="let widget of widgets"
class="widget-container"
[style.gridColumn]="widget.position.x"
[style.gridRow]="widget.position.y"
[style.width]="widget.size.width + 'px'"
[style.height]="widget.size.height + 'px'">
<div class="widget-header">
<span>{{widget.title}}</span>
<div class="widget-actions">
<button (click)="refreshWidget(widget.id)">↻</button>
<button (click)="removeWidget(widget.id)">×</button>
</div>
</div>
<div class="widget-content" #widgetContainer></div>
</div>
</div>
</div>
<div class="dashboard-footer">
<span>已加载 {{widgets.length}} 个小部件</span>
</div>
</div>
`,
styles: [`
.dashboard {
display: flex;
flex-direction: column;
height: 600px;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
}
.dashboard-header {
background: #2c3e50;
color: white;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.widget-controls {
display: flex;
gap: 10px;
align-items: center;
}
.dashboard-body {
flex: 1;
padding: 20px;
overflow: auto;
}
.widget-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.widget-container {
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.widget-header {
background: #f8f9fa;
padding: 10px 15px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.widget-actions {
display: flex;
gap: 5px;
}
.widget-content {
padding: 15px;
height: calc(100% - 45px);
}
`]
})
export class DynamicDashboardComponent implements AfterViewInit {
@ViewChild('dashboardBody', { read: ViewContainerRef }) dashboardBody!: ViewContainerRef;
widgets: DashboardWidget[] = [];
availableWidgets: WidgetConfig[] = [];
selectedWidget = '';
constructor(private widgetService: WidgetService) {}
ngAfterViewInit(): void {
this.loadAvailableWidgets();
this.loadSavedLayout();
}
private loadAvailableWidgets(): void {
this.availableWidgets = this.widgetService.getAvailableWidgets();
}
private loadSavedLayout(): void {
const savedLayout = localStorage.getItem('dashboard-layout');
if (savedLayout) {
this.widgets = JSON.parse(savedLayout);
this.loadWidgets();
}
}
async addWidget(): Promise<void> {
if (!this.selectedWidget) return;
const widgetConfig = this.availableWidgets
.find(w => w.id === this.selectedWidget);
if (!widgetConfig) return;
const widget: DashboardWidget = {
id: `${widgetConfig.id}-${Date.now()}`,
title: widgetConfig.name,
component: widgetConfig.component,
config: widgetConfig.defaultConfig || {},
position: { x: 1, y: 1 },
size: { width: 300, height: 200 }
};
this.widgets.push(widget);
await this.loadWidget(widget);
}
private async loadWidgets(): Promise<void> {
for (const widget of this.widgets) {
await this.loadWidget(widget);
}
}
private async loadWidget(widget: DashboardWidget): Promise<void> {
try {
// 动态加载组件
const componentRef = this.dashboardBody.createComponent(widget.component);
// 设置配置
Object.assign(componentRef.instance, widget.config);
// 监听组件事件
if (componentRef.instance.dataUpdate) {
componentRef.instance.dataUpdate.subscribe((data: any) => {
this.handleWidgetDataUpdate(widget.id, data);
});
}
// 存储引用
widget.ref = componentRef;
// 触发变更检测
componentRef.changeDetectorRef.detectChanges();
} catch (error) {
console.error(`加载小部件失败 ${widget.id}:`, error);
}
}
refreshWidget(widgetId: string): void {
const widget = this.widgets.find(w => w.id === widgetId);
if (widget?.ref?.instance?.refresh) {
widget.ref.instance.refresh();
}
}
removeWidget(widgetId: string): void {
const index = this.widgets.findIndex(w => w.id === widgetId);
if (index > -1) {
const widget = this.widgets[index];
// 销毁组件
if (widget.ref) {
widget.ref.destroy();
}
// 从数组中移除
this.widgets.splice(index, 1);
}
}
saveLayout(): void {
// 移除组件引用(不能序列化)
const layoutToSave = this.widgets.map(w => ({
...w,
ref: undefined
}));
localStorage.setItem('dashboard-layout', JSON.stringify(layoutToSave));
alert('布局已保存');
}
resetLayout(): void {
if (confirm('确定要重置仪表板吗?')) {
// 销毁所有组件
this.widgets.forEach(widget => {
if (widget.ref) {
widget.ref.destroy();
}
});
// 清空数组
this.widgets = [];
localStorage.removeItem('dashboard-layout');
}
}
private handleWidgetDataUpdate(widgetId: string, data: any): void {
console.log(`小部件 ${widgetId} 数据更新:`, data);
// 处理数据更新逻辑
}
}
OnPush变更检测策略// component-cache.service.ts - 组件缓存服务
import { Injectable, ComponentFactoryResolver, ComponentRef, Type } from '@angular/core';
interface CacheEntry {
componentRef: ComponentRef;
lastUsed: number;
useCount: number;
}
@Injectable({
providedIn: 'root'
})
export class ComponentCacheService {
private cache = new Map<string, CacheEntry>();
private maxCacheSize = 10;
private cacheExpiry = 5 * 60 * 1000; // 5分钟
constructor(private resolver: ComponentFactoryResolver) {}
// 从缓存获取或创建组件
getOrCreate(
componentType: Type,
container: any,
cacheKey?: string
): ComponentRef<any> {
const key = cacheKey || componentType.name;
// 尝试从缓存获取
const cached = this.cache.get(key);
if (cached && this.isValidCacheEntry(cached)) {
cached.lastUsed = Date.now();
cached.useCount++;
// 重新附加到容器
container.insert(cached.componentRef.hostView);
console.log(`从缓存获取组件: ${key}`);
return cached.componentRef;
}
// 创建新组件
const factory = this.resolver.resolveComponentFactory(componentType);
const componentRef = container.createComponent(factory);
// 添加到缓存
this.cache.set(key, {
componentRef,
lastUsed: Date.now(),
useCount: 1
});
// 清理过期缓存
this.cleanupCache();
console.log(`创建新组件并缓存: ${key}`);
return componentRef;
}
// 检查缓存是否有效
private isValidCacheEntry(entry: CacheEntry): boolean {
const age = Date.now() - entry.lastUsed;
return age < this.cacheExpiry;
}
// 清理缓存
private cleanupCache(): void {
if (this.cache.size <= this.maxCacheSize) return;
// 按最后使用时间排序
const entries = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastUsed - b[1].lastUsed);
// 移除最旧的条目
const toRemove = entries.slice(0, entries.length - this.maxCacheSize);
toRemove.forEach(([key]) => {
this.cache.get(key)?.componentRef.destroy();
this.cache.delete(key);
console.log(`清理缓存组件: ${key}`);
});
}
// 手动清理组件
destroyComponent(key: string): void {
const entry = this.cache.get(key);
if (entry) {
entry.componentRef.destroy();
this.cache.delete(key);
}
}
// 清空所有缓存
clearCache(): void {
this.cache.forEach(entry => entry.componentRef.destroy());
this.cache.clear();
console.log('组件缓存已清空');
}
// 获取缓存统计
getCacheStats(): { size: number, entries: any[] } {
const entries = Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
age: Date.now() - entry.lastUsed,
useCount: entry.useCount
}));
return {
size: this.cache.size,
entries
};
}
}
// secure-component-loader.service.ts - 安全组件加载器
import { Injectable, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class SecureComponentLoaderService {
private trustedSources = new Set<string>([
'https://trusted-cdn.com',
'https://internal-server.com'
]);
constructor(private sanitizer: DomSanitizer) {}
// 安全地加载动态组件
async loadSecureComponent(
componentUrl: string,
container: any
): Promise<boolean> {
// 1. 验证URL
if (!this.isTrustedSource(componentUrl)) {
console.error('不信任的组件源:', componentUrl);
return false;
}
// 2. 清理URL
const safeUrl = this.sanitizer.sanitize(
SecurityContext.URL,
componentUrl
);
if (!safeUrl) {
console.error('URL清理失败:', componentUrl);
return false;
}
try {
// 3. 使用沙箱加载(如果支持)
const sandbox = this.createSandbox();
// 4. 加载组件
const module = await this.loadModuleFromUrl(safeUrl);
// 5. 验证组件签名(如果有)
if (!this.verifyComponentSignature(module)) {
throw new Error('组件签名验证失败');
}
// 6. 安全检查
if (!this.securityCheck(module)) {
throw new Error('安全检查失败');
}
// 7. 创建组件
const componentRef = container.createComponent(module.component);
// 8. 监控组件行为
this.monitorComponent(componentRef);
return true;
} catch (error) {
console.error('安全加载失败:', error);
return false;
}
}
private isTrustedSource(url: string): boolean {
try {
const urlObj = new URL(url);
return this.trustedSources.has(urlObj.origin);
} catch {
return false;
}
}
private createSandbox(): any {
// 创建沙箱环境(简化示例)
// 实际实现可能需要iframe或Web Worker
return {
// 沙箱配置
};
}
private async loadModuleFromUrl(url: string): Promise<any> {
// 使用动态import,但需要webpack配置支持
// 或者使用System.import、fetch+eval等
const response = await fetch(url);
const code = await response.text();
// 严格限制执行环境
const module = this.executeInSandbox(code);
return module;
}
private executeInSandbox(code: string): any {
// 在实际应用中,应该使用更安全的执行方式
// 这里仅作示例
try {
const exports = {};
const module = { exports };
// 创建安全上下文
const safeContext = {
console,
setTimeout,
clearTimeout,
// 限制其他全局对象访问
};
// 使用Function构造函数而不是eval
const func = new Function('module', 'exports', 'context', `
with(context) {
${code}
}
`);
func(module, exports, safeContext);
return module.exports;
} catch (error) {
throw new Error(`执行失败: ${error.message}`);
}
}
private verifyComponentSignature(module: any): boolean {
// 验证组件签名(如果有)
// 实际应用中可能需要数字签名验证
return true; // 简化实现
}
private securityCheck(module: any): boolean {
// 执行安全检查
const component = module.component || module.default;
// 检查是否包含危险操作
const blacklist = [
'eval',
'Function',
'document.write',
'innerHTML',
'outerHTML',
'insertAdjacentHTML'
];
const componentCode = component.toString();
return !blacklist.some(keyword => componentCode.includes(keyword));
}
private monitorComponent(componentRef: any): void {
// 监控组件行为
const originalMethods = {};
// 包装危险方法
const dangerousMethods = ['ngOnInit', 'ngAfterViewInit'];
dangerousMethods.forEach(method => {
if (componentRef.instance[method]) {
originalMethods[method] = componentRef.instance[method];
componentRef.instance[method] = (...args: any[]) => {
console.log(`监控: 组件调用 ${method}`);
// 添加安全检查
try {
return originalMethods[method].apply(componentRef.instance, args);
} catch (error) {
console.error(`组件方法 ${method} 执行错误:`, error);
// 可以在此处销毁组件
}
};
}
});
}
}
// dynamic-loader.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DynamicLoaderComponent } from './dynamic-loader.component';
import { AlertComponent } from './alert.component';
import { ComponentFactoryResolver } from '@angular/core';
describe('DynamicLoaderComponent', () => {
let component: DynamicLoaderComponent;
let fixture: ComponentFixture<DynamicLoaderComponent>;
let resolver: ComponentFactoryResolver;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DynamicLoaderComponent, AlertComponent],
providers: [ComponentFactoryResolver]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DynamicLoaderComponent);
component = fixture.componentInstance;
resolver = TestBed.inject(ComponentFactoryResolver);
fixture.detectChanges();
});
it('应该创建组件', () => {
expect(component).toBeTruthy();
});
it('应该能够动态加载Alert组件', () => {
spyOn(component as any, 'loadComponent').and.callThrough();
component.loadAlert();
fixture.detectChanges();
expect((component as any).loadComponent).toHaveBeenCalledWith('alert', jasmine.any(Object));
});
it('应该正确设置组件输入属性', () => {
const testTitle = '测试标题';
const testMessage = '测试消息';
component.loadAlert();
fixture.detectChanges();
// 检查组件是否被创建
const alertElement = fixture.nativeElement.querySelector('app-alert');
expect(alertElement).toBeTruthy();
// 检查输入属性
// 需要通过组件实例来验证
});
it('应该能够销毁组件', () => {
component.loadAlert();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('app-alert')).toBeTruthy();
component.destroyComponent();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('app-alert')).toBeFalsy();
});
it('应该在组件销毁时清理资源', () => {
spyOn(component, 'ngOnDestroy').and.callThrough();
component.loadAlert();
fixture.detectChanges();
fixture.destroy();
expect(component.ngOnDestroy).toHaveBeenCalled();
});
it('应该处理组件加载失败', () => {
spyOn(console, 'error');
spyOn(resolver, 'resolveComponentFactory').and.throwError('加载失败');
component.loadAlert();
fixture.detectChanges();
expect(console.error).toHaveBeenCalled();
});
});
// 模拟测试组件
@Component({
template: '<div>测试组件</div>'
})
class TestComponent {}
describe('DynamicComponentLoading', () => {
it('应该允许在运行时创建组件', () => {
const resolver = TestBed.inject(ComponentFactoryResolver);
const fixture = TestBed.createComponent(DynamicLoaderComponent);
const factory = resolver.resolveComponentFactory(TestComponent);
const componentRef = fixture.componentInstance.container.createComponent(factory);
expect(componentRef).toBeTruthy();
expect(componentRef.instance).toBeInstanceOf(TestComponent);
});
});