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:调整启动流程
- 构建Angular应用:
ng build - 运行自定义JSON Server:
node server.js - 前端访问
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
相关产品推荐
相关产品推荐

