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

Angular 15上传图片至assets/images报404:原因与解决方法

Angular 15 头像上传404错误问题排查与修复

开发背景

正在开发基于Angular 15的应用,使用手动编写的JSON文件配合JSON Server实现员工数据的CRUD操作,员工数据结构如下:

{
  "employees": [
    {
      "empno": 1,
      "deptno": 10,
      "firstname": "Scott",
      "lastname": "Macey",
      "gender": "male",
      "avatar": "scott.png",
      "job": "CEO",
      "bio": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsam obcaecati veritatis maiore.",
      "skills": [
        "Management"
      ]
    },
    {
      "empno": 2,
      "deptno": 10,
      "firstname": "Ada",
      "lastname": "Smith",
      "gender": "femele",
      "avatar": "ada.png",
      "job": "Unit Manager",
      "bio": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsam obcaecati veritatis maiore.",
      "skills": [
        "Management"
      ]
    },
    {
      "empno": 3,
      "deptno": 30,
      "firstname": "Hollie",
      "lastname": "Haerr",
      "gender": "femele",
      "avatar": "hollie.png",
      "job": "Programmer",
      "bio": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsam obcaecati veritatis maiore.",
      "skills": [
        "HTML5",
        "CSS3",
        "JavaScript",
        "Angular 2+"
      ]
    }
  ]
}

当前正在实现表单上传头像到src\assets\images的功能,相关代码如下:

employee-form.component.html

<form class="modal-content" #employeeForm="ngForm" (ngSubmit)="addEmployee(employeeForm)">
    <!-- Modal Header -->
    <div class="modal-header py-2">
      <h4 class="modal-title">New employee</h4>
      <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
    </div>

    <!-- Modal body -->
    <div class="modal-body py-2">
      <div *ngIf="isSuccess" class="text-center alert alert-success">
        Employee added successfully!
      </div>

      <div class="position-relative mb-1">
          <label class="form-label" for="firstName">First name</label>
          <input type="text" class="form-control" name="firstname" id="firstName" placeholder="First name" [(ngModel)]="firstname" #first_name="ngModel" required/>
          <div *ngIf="first_name.touched && first_name.errors?.['required']"  class="invalid-feedback">First name is required.</div>
      </div>

      <div class="position-relative mb-1">
          <label class="form-label" for="lastName">Last name</label>
          <input type="text" class="form-control" name="lastname" id="lastName" placeholder="Last name" [(ngModel)]="lastname" #last_name="ngModel" required />
          <div *ngIf="last_name.touched && last_name.errors?.['required']"  class="invalid-feedback">Last name is required.</div>
      </div>

      <div class="position-relative mb-1">
        <label class="form-label d-block" for="avatar">Photo</label>
        <input type="file" class="file-upload-btn" name="avatar" id="avatar" [(ngModel)]="avatar" #user_avatar="ngModel" (change)="pickAvatar($event)" accept="image/*">
        <div *ngIf="user_avatar.value.length && !isCorrectAvatarFormat"  class="invalid-feedback">You must choose an image file.</div>
      </div>

      <div class="position-relative mb-1">
          <label class="form-label" for="job">Job</label>
          <input type="text" class="form-control" name="job" id="job" placeholder="Job" [(ngModel)]="job" #job_title="ngModel" required  />
          <div *ngIf="job_title.touched && job_title.errors?.['required']" class="invalid-feedback">Job is required.</div>
      </div>

      <div class="position-relative mb-1">
        <label class="form-label" for="job">Skills</label>
        <input type="text" class="form-control" name="skills" id="skills" placeholder="Skills separated by comma" [(ngModel)]="skills"/>
      </div>

      <div class="position-relative mb-1">
        <label class="form-label" for="department">Department</label>
        <select class="form-select" name="department" id="department" [(ngModel)]="deptno" #emp_department="ngModel" deptDefaultValue="-1">
          <option value="-1">Pick a department</option>
          <option *ngFor="let department of departments" [value]="department.value">
            {{department.label}}
          </option>
        </select>
        <div *ngIf="emp_department.touched && emp_department.value == -1" class="invalid-feedback">You must pick a department</div>
      </div>

      <div class="position-relative mb-0">
          <label class="form-label d-block">Gender</label>
          <div class="form-check form-check-inline">
              <input type="radio" class="form-check-input" name="gender" id="male" value="male" [(ngModel)]="gender" />
              <label class="form-label" for="male">male</label>
          </div>
          <div class="form-check form-check-inline">
              <input type="radio" class="form-check-input" name="gender" id="femele" value="femele" [(ngModel)]="gender" />
              <label class="form-label" for="femele">femele</label>
          </div>
      </div>

      <div class="position-relative mb-1">
          <label class="form-label" for="bio">Bio</label>
          <textarea class="form-control" name="bio" id="bio" type="text" placeholder="Bio" [(ngModel)]="bio"></textarea>
      </div>
    </div>

    <!-- Modal footer -->
    <div class="modal-footer py-2">
      <button type="submit" class="btn btn-success" [disabled]="!employeeForm.valid">Add employee</button>
      <button type="button" class="btn btn-danger" data-bs-dismiss="modal">Cancel</button>
    </div>
</form>

employee-form.component.ts

import { Component } from '@angular/core';
import { Employee } from '../../models/empModel';
import { NgForm, FormGroup, Validator } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { EmployeeService } from '../../services/employee.service';
import { FileUploadService } from '../../services/file-upload.service';
import { EmployeeListComponent } from '../employee-list/employee-list.component';


@Component({
  selector: 'app-employee-form',
  templateUrl: './employee-form.component.html',
  styleUrls: ['./employee-form.component.scss']
})

export class EmployeeFormComponent {

  constructor(private employeeService: EmployeeService, private fileUploadService: FileUploadService, private employeeListComponent: EmployeeListComponent) {
    this.deptno = -1;
    this.gender = 'male';
  }

  public empsArray: Employee[] = [];
  public departments: any[] = [
    {value: 10, label: 'Management'},
    {value: 20, label: 'Sales'},
    {value: 30, label: 'Software'},
    {value: 40, label: 'Finance'}
  ];
  public newEmployee: any = {}
  public empno: number = 0;
  public deptno: number = 0;
  public firstname: string = '';
  public lastname: string = '';
  public gender: string = '';
  public avatarFile: any = null;
  public avatar: string = '';
  public isCorrectAvatarFormat: boolean = false;
  public job: string = '';
  public bio: string = '';
  public skills: string = '';
  public isSuccess: boolean = false;

  public pickAvatar(event: any) {
    let file = event.target.files[0];
    if (file.type.match(/image\/*/)) {
      // Flag image as valid
      this.isCorrectAvatarFormat = true;

      // Get image file name
      this.avatar = file.name;

      this.uploadAvatar(this.avatarFile);
    } else {
       // Flag image as invalid
      this.isCorrectAvatarFormat = false;
    }
    
  }

  public uploadAvatar(file: File): void {
    // Set avatar file-name
    this.newEmployee.avatar = this.avatar.length ? this.avatar : `${this.gender}.png`;

    this.fileUploadService.uploadImage(file).subscribe({ 
      next: (event: any) => {
        console.log(event);
      },
      error: (err: any) => {
        console.log(err.message);
      }
    });
}

  public validateDepartment(form: NgForm) {
   if (this.deptno == -1) {
    form.invalid;
   } 
  }

  public addEmployee(form: NgForm) {

    this.newEmployee = {
      empno: this.empno,
      deptno: this.deptno,
      firstname: this.firstname,
      lastname: this.lastname,
      gender: this.gender,
      avatar: this.avatar,
      job: this.job,
      bio: this.bio,
      skills: this.skills
    };

    this.validateDepartment(form);

    this.employeeService.addEmployee(this.newEmployee).subscribe(
      (_response: Employee) => {
        // Show success alert
        this.isSuccess = true;

        // Render the employee list after adding a new employee
        this.employeeListComponent.getEmployees();
      },
      (error: HttpErrorResponse) => {
        console.log(error.message);
      }
    );
  }
}

services\file-upload.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class FileUploadService {

  constructor(private http: HttpClient) {}

  public uploadImage(file: File):Observable<any> {
    return this.http.post<File>('assets/images', file);
  }
}

问题描述

调用uploadImage()方法时,浏览器抛出错误:Http failure response for http://localhost:4200/assets/images: 404 Not Found


1. 错误成因

  • 浏览器沙箱限制:Angular运行在浏览器沙箱中,没有权限直接向本地src/assets/images目录写入文件,该目录是静态资源目录,仅支持读取操作。
  • 接口不存在:直接向assets/images发送POST请求,但该路径是前端静态资源路径,没有对应的后端接口处理文件上传,导致404。
  • 参数传递错误:pickAvatar方法中调用uploadAvatar(this.avatarFile),但avatarFile未赋值,实际传入的是null,即使接口正常也无法完成上传。

2. 可靠修复方法

基于现有JSON Server技术栈,通过扩展后端中间件实现文件上传:

步骤1:安装依赖

npm install multer --save-dev

步骤2:创建自定义JSON Server中间件(server.js)

const jsonServer = require('json-server');
const multer = require('multer');
const path = require('path');

const server = jsonServer.create();
const router = jsonServer.router('db.json'); // 你的员工数据JSON文件路径
const middlewares = jsonServer.defaults({
  static: path.join(__dirname, 'dist/your-app-name') // Angular构建后的静态资源目录
});

// 配置文件存储规则
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, path.join(__dirname, 'src/assets/images')); // 头像存储目录
  },
  filename: function (req, file, cb) {
    // 添加时间戳避免文件名重复覆盖
    const timestamp = Date.now();
    cb(null, `${timestamp}-${file.originalname}`);
  }
});

const upload = multer({ storage: storage });

// 自定义上传接口
server.post('/upload-avatar', upload.single('avatar'), (req, res) => {
  res.json({ filename: req.file.filename }); // 返回存储后的文件名给前端
});

server.use(middlewares);
server.use(router);
server.listen(3000, () => {
  console.log('JSON Server is running on port 3000');
});

步骤3:修改文件上传服务(file-upload.service.ts)

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class FileUploadService {
  private baseUrl = 'http://localhost:3000'; // JSON Server地址

  constructor(private http: HttpClient) {}

  public uploadImage(file: File):Observable<any> {
    const formData = new FormData();
    formData.append('avatar', file);
    return this.http.post(`${this.baseUrl}/upload-avatar`, formData);
  }
}

步骤4:修正组件中的参数传递错误(employee-form.component.ts)

public pickAvatar(event: any) {
  let file = event.target.files[0];
  if (file.type.match(/image\/*/)) {
    this.isCorrectAvatarFormat = true;
    this.avatarFile = file; // 赋值选中的文件对象
    // 调用上传并接收返回的文件名
    this.fileUploadService.uploadImage(file).subscribe(res => {
      this.avatar = res.filename;
    });
  } else {
    this.isCorrectAvatarFormat = false;
  }
}

步骤5:调整启动流程

  1. 构建Angular应用:ng build
  2. 运行自定义JSON Server:node server.js
  3. 前端访问http://localhost:3000

可选优化:添加代理解决跨域

创建proxy.conf.json:

{
  "/upload-avatar": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

启动Angular时使用代理:ng serve --proxy-config proxy.conf.json


内容的提问来源于stack exchange,提问作者Razvan Zamfir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 23:07:03