You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 16:57:02