Angular生产构建报错:Index HTML生成失败,求解决方案
Angular构建报错:Index HTML生成失败
执行ng serve时项目运行正常,但运行ng build时出现Index HTML生成失败错误。怀疑该错误与Angular的优化机制相关,咨询是否需要修改HTML元素的访问方式。
相关组件代码
Header组件
HTML代码
<div #container class="container"> <h1 class="title">Alan's Portfolio</h1> </div>
TS代码
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; import { PianoService } from './../piano.service' import { Subscription } from 'rxjs' @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'], standalone: true }) export class HeaderComponent implements AfterViewInit{ loadEventSub!: Subscription; @ViewChild('container') containerRef!: ElementRef; constructor(private lidService: PianoService) { } ngAfterViewInit() { this.loadEventSub = this.lidService.getLoadEvent().subscribe(() => { this.containerRef.nativeElement.classList.add('rotate'); }) } }
Title组件
HTML代码
<div #container class="container"> <div #spacer class="spacer"></div> <h1 #loading class="loading">Loading Site</h1> <div class="loadingContainer"> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> </div> </div>
TS代码
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; import { PianoService } from './../piano.service' import { Subscription } from 'rxjs' import { NgStyle } from '@angular/common'; @Component({ selector: 'app-title', templateUrl: './title.component.html', styleUrls: ['./title.component.css'], standalone: true, imports: [NgStyle] }) export class TitleComponent implements AfterViewInit{ titleFilter!: string; enteranceCursor!: string; loadEventSub!: Subscription; @ViewChild('loading') titleRef!: ElementRef; @ViewChild('container') containerRef!: ElementRef; constructor(private lidService:PianoService) { } ngAfterViewInit() { this.loadEventSub = this.lidService.getLoadEvent().subscribe(() => { this.containerRef.nativeElement.classList.add('slide'); }) } }
Keyboard组件(使用Three.js)
HTML代码
<canvas #canvas id="threeJS-canvas"></canvas>
TS代码
import { Component, OnInit, AfterViewInit, Renderer2, ViewChild, ElementRef } from '@angular/core'; import { PianoService } from './../piano.service' import { Subscription } from 'rxjs' import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader'; import { Mesh } from 'three'; @Component({ selector: 'app-keyboard', templateUrl: './keyboard.component.html', styleUrls: ['./keyboard.component.css'], standalone: true }) export class KeyboardComponent implements OnInit, AfterViewInit{ scene: THREE.Scene; canvas: HTMLElement | null; scene_loaded: boolean; constructor(private pianoService: PianoService) { this.scene = new THREE.Scene(); this.canvas = null; this.scene_loaded = false; } private LoadEventSub!: Subscription; private manager!: THREE.LoadingManager; private canvasSizes!: any; private pianoRenderer!: THREE.WebGLRenderer; private siteRenderer!: Renderer2; private clips!: any; private lid_clip!: any; private lid_mixer!: THREE.AnimationMixer; private clip_names: string[] = []; private mixer!: THREE.AnimationMixer; private clock: THREE.Clock = new THREE.Clock(); private INTERSECTED_ARRAY: any[] = []; private raycaster: THREE.Raycaster = new THREE.Raycaster(); private pointer: THREE.Vector2 = new THREE.Vector2(); private camera!: THREE.PerspectiveCamera; @ViewChild('canvas') canvasRef!: ElementRef; ngOnInit(): void { window.addEventListener('pointermove', this.onPointerMove); this.initFunctions(); } ngAfterViewInit() { this.createCanvas(); this.createPiano(); console.log(this.scene); this.animate(); } initFunctions( pianoService = this.pianoService ): void { this.LoadEventSub = this.pianoService.getLoadEvent().subscribe(() => { this.lidup(); }) this.manager = new THREE.LoadingManager(); this.manager.onLoad = function () { setTimeout(() => { pianoService.sendLoadEvent(); }, 2000); } } createPiano(): void { this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 1000); this.scene.add(this.camera); new EXRLoader( this.manager ) .setPath('../../assets/') .load('dancing_hall_1k.exr', (texture) => { texture.mapping = THREE.EquirectangularReflectionMapping; this.scene.environment = texture; }); this.createBase(); this.createKeyboard(); const light = new THREE.AmbientLight(0xfffada, 1); this.scene.add(light); } private createCanvas = () => { this.canvas = this.canvasRef.nativeElement; if (!this.canvas) { return }; this.pianoRenderer = new THREE.WebGLRenderer({ antialias: true, canvas: this.canvas }); this.pianoRenderer.setClearColor(0x000000, 1); const pmremGenerator = new THREE.PMREMGenerator(this.pianoRenderer); pmremGenerator.compileEquirectangularShader(); this.pianoRenderer.toneMapping = THREE.ACESFilmicToneMapping; this.pianoRenderer.toneMappingExposure = 1; this.pianoRenderer.outputEncoding = THREE.sRGBEncoding; } private resizeCanvasToDisplaySize() { const canvas = this.pianoRenderer.domElement; const width = canvas.clientWidth; const height = canvas.clientHeight; if (canvas.width !== width || canvas.height !== height) { this.pianoRenderer.setSize(width, height, false); this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); } } private onPointerMove = (event: any) => { const rect = this.pianoRenderer.domElement.getBoundingClientRect(); this.pointer.x = ((event.clientX - rect.left) / (rect.right - rect.left)) * 2 - 1; this.pointer.y = - ((event.clientY - rect.top) / (rect.bottom - rect.top)) * 2 + 1; } private animate = () => { this.resizeCanvasToDisplaySize(); var delta = this.clock.getDelta(); if (this.mixer) { this.mixer.update(delta) }; if (this.lid_mixer) { this.lid_mixer.update(delta) }; this.render(); requestAnimationFrame(this.animate); } createBase(): void { new GLTFLoader() .setPath('../../assets/') .load('Piano.glb', (gltf) => { const object = gltf.scene const base = object.children[2] object.traverse((subObject) => { if (subObject instanceof Mesh) { subObject.material.envMapIntensity = 0; } }); this.lid_clip = gltf.animations[1]; this.lid_mixer = new THREE.AnimationMixer(object); this.camera.position.x = base.position.x; this.camera.position.y = base.position.y + 184; this.camera.position.z = base.position.z + 150; this.camera.lookAt(new THREE.Vector3(156, 0, -74)); this.scene.add(object); }); } createKeyboard(): void { new GLTFLoader() .setPath('../../assets/') .load('Keyboard.glb', (gltf) => { this.clips = gltf.animations; this.clips.forEach((clip: { name: string; }) => { this.clip_names.push(clip.name) }); this.mixer = new THREE.AnimationMixer(gltf.scene); this.scene.add(gltf.scene); this.scene_loaded = true; }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + "% loaded") }, function (error) { console.error(error); }); } private keydown(animation_name: string) { const clip_num = this.clip_names.indexOf(animation_name) const action = this.mixer.clipAction(this.clips[clip_num]); action.setLoop(THREE.LoopOnce, 1); action.paused = false; action.clampWhenFinished = true; action.timeScale = 1; action.play(); } private keyup(animation_name: string) { const clip_num = this.clip_names.indexOf(animation_name) const action = this.mixer.clipAction(this.clips[clip_num]); action.setLoop(THREE.LoopOnce, 1); action.paused = false; action.clampWhenFinished = true; action.timeScale = -1; action.play(); } private lidup(): void { const action = this.lid_mixer.clipAction(this.lid_clip); action.setLoop(THREE.LoopOnce, 1); action.clampWhenFinished = true; action.timeScale = 0.8; action.play(); } private render = () => { this.pianoRenderer.render(this.scene, this.camera); if (this.scene_loaded) { this.intersectionCheck(); } } private intersectionCheck() { var clip_num = 0; var intersected_name: string; this.raycaster.setFromCamera(this.pointer, this.camera); const intersected = this.raycaster.intersectObjects(this.scene.children)[0]; try { if (intersected.object.name == "A000") { intersected_name = "A000"; } else if (intersected.object.name == "C001") { intersected_name = "C001" } else { intersected_name = intersected.object.parent?.name + ""; } } catch { intersected_name = "Scene"; } if (intersected_name.includes('#0')) { intersected_name = intersected_name.slice(0, 2) + "." + intersected_name.slice(2) + "Action" } else if (intersected_name.includes('0')) { intersected_name = intersected_name.slice(0, 1) + "." + intersected_name.slice(1) + "Action" } else { intersected_name += "Action" } if (this.INTERSECTED_ARRAY.length < 2) { this.INTERSECTED_ARRAY.push(intersected_name) } else { const prev_intersected = this.INTERSECTED_ARRAY[1] this.INTERSECTED_ARRAY = [prev_intersected, intersected_name]; } if (this.INTERSECTED_ARRAY[0] != this.INTERSECTED_ARRAY[1]) { this.INTERSECTED_ARRAY.forEach((intersected) => { clip_num = this.clip_names.indexOf(intersected); if (clip_num != -1) { if (this.INTERSECTED_ARRAY.indexOf(intersected) == 0) { this.keyup(intersected) } else { this.keydown(intersected) } } }); } } }
解决方案
1. 为@ViewChild添加安全检查
Angular生产构建的AOT编译会严格校验元素引用的可用性,即便在ngAfterViewInit中,异步逻辑也可能导致元素引用未初始化。修改所有组件的元素访问逻辑:
Header组件修改示例
@ViewChild('container', { static: false }) containerRef!: ElementRef; ngAfterViewInit() { this.loadEventSub = this.lidService.getLoadEvent().subscribe(() => { // 添加空值安全检查 if (this.containerRef?.nativeElement) { this.containerRef.nativeElement.classList.add('rotate'); } }) }
Keyboard组件修改示例
private createCanvas = () => { this.canvas = this.canvasRef?.nativeElement; if (!this.canvas) { return }; // 原渲染逻辑... }
2. 调整Three.js资源路径
构建后项目的基础路径可能变化,使用Angular的DOCUMENT服务获取正确的资源路径:
import { DOCUMENT } from '@angular/common'; import { Inject } from '@angular/core'; constructor(private pianoService: PianoService, @Inject(DOCUMENT) private document: Document) {} // 替换资源加载路径 new EXRLoader( this.manager ) .setPath(`${this.document.baseURI}assets/`) .load('dancing_hall_1k.exr', (texture) => { texture.mapping = THREE.EquirectangularReflectionMapping; this.scene.environment = texture; });
3. 禁用Index HTML优化(临时排查)
在angular.json的生产配置中关闭Index HTML优化,确认是否是优化逻辑导致的问题:
"configurations": { "production": { "index": { "input": "src/index.html", "output": "index.html", "optimization": false }, // 其他生产配置... } }
4. 清理构建缓存
执行以下命令清理缓存后重新构建:
ng clean ng build --configuration production
内容的提问来源于stack exchange,提问作者Alan Shipman
相关产品推荐
相关产品推荐

