FastAPI与Jinja2实现图片上传及显示教程

霞舞
发布: 2025-10-21 10:20:18
原创
635人浏览过

FastAPI与Jinja2实现图片上传及显示教程

本教程详细介绍了如何使用fastapi和jinja2框架实现图片上传功能,并在html页面中实时或通过服务器处理后显示图片。文章涵盖了客户端base64预览、服务器端base64编码传输以及使用静态文件服务等多种方法,并提供了相应的代码示例和注意事项,旨在帮助开发者构建高效安全的图片上传与展示系统。

一、引言:图片上传与显示的核心挑战

在Web开发中,用户上传图片并立即在页面上显示是一个常见需求。使用FastAPI作为后端框架,结合Jinja2模板引擎进行前端渲染时,实现这一功能需要考虑文件处理、数据传输以及前端渲染策略。初始尝试中,开发者可能会遇到直接使用文件名为<img>标签的src属性无法正确显示图片的问题,这通常是由于浏览器无法直接访问服务器上未经暴露的文件路径所致。本教程将深入探讨几种有效的解决方案,并提供详细的代码示例。

二、客户端实时预览:利用Base64数据URL

最直接且用户体验最佳的图片预览方式是在客户端完成。用户选择图片后,浏览器可以在不向服务器发送文件的情况下,将图片数据转换为Base64编码的字符串,并将其作为<img>标签的src属性值(即数据URL)进行显示。随后,再将图片文件上传至服务器。

2.1 FastAPI后端配置

后端FastAPI应用主要负责接收上传的图片文件并将其保存到服务器。

app.py

from fastapi import File, UploadFile, Request, FastAPI, HTTPException
from fastapi.templating import Jinja2Templates
import os

app = FastAPI()
templates = Jinja2Templates(directory="templates")

# 确保上传目录存在
UPLOAD_DIR = "uploaded_files"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    """
    接收客户端上传的图片文件并保存到服务器。
    """
    try:
        file_path = os.path.join(UPLOAD_DIR, file.filename)
        # 异步写入文件,提高性能
        contents = await file.read()
        with open(file_path, "wb") as f:
            f.write(contents)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f'文件上传失败: {e}')
    finally:
        await file.close() # 确保文件句柄关闭

    return {"message": f"文件 {file.filename} 上传成功!"}

@app.get("/")
async def main(request: Request):
    """
    渲染主页面,包含图片上传表单。
    """
    return templates.TemplateResponse("index.html", {"request": request})
登录后复制

注意事项:

  • os.makedirs(UPLOAD_DIR, exist_ok=True) 确保了文件保存目录的存在。
  • await file.read() 和 await file.close() 是FastAPI中处理UploadFile的推荐异步方式,可以提高I/O效率。
  • 为了避免文件名冲突,实际应用中建议为上传的文件生成唯一的名称(如UUID)。

2.2 Jinja2前端模板 (index.html)

前端页面负责文件选择、客户端预览以及通过Fetch API将文件上传到后端。

2.2.1 自动预览并上传

这种方式在用户选择文件后立即进行预览和上传。

templates/index.html

<!DOCTYPE html>
<html>
<head>
    <title>图片上传与预览</title>
</head>
<body>
    <h1>上传图片</h1>
    <input type="file" onchange="previewAndUploadFile()"><br>
    <img src="" height="200" alt="图片预览..." style="border: 1px solid #ccc; margin-top: 10px;">
    <p id="serverMsg" style="color: green;"></p>

    <script type="text/javascript">
        function previewAndUploadFile() {
            const preview = document.querySelector('img');
            const fileInput = document.querySelector('input[type=file]');
            const file = fileInput.files[0];
            const serverMsg = document.getElementById('serverMsg');
            serverMsg.innerHTML = ''; // 清空之前的消息

            if (!file) {
                preview.src = ""; // 清空预览
                return;
            }

            const reader = new FileReader();

            reader.addEventListener("load", function() {
                // 将文件内容转换为Base64编码并显示在<img>标签中
                preview.src = reader.result; 
                uploadFile(file); // 预览完成后立即上传
            }, false);

            reader.readAsDataURL(file); // 读取文件内容为数据URL
        }

        function uploadFile(file) {
            var formData = new FormData();
            formData.append('file', file); // 将文件添加到FormData

            fetch('/upload', {
                    method: 'POST',
                    body: formData, // 发送FormData对象
                })
                .then(response => response.json())
                .then(data => {
                    document.getElementById("serverMsg").innerHTML = data.message;
                    console.log(data);
                })
                .catch(error => {
                    document.getElementById("serverMsg").innerHTML = '上传失败: ' + error.message;
                    console.error('上传失败:', error);
                });
        }
    </script>
</body>
</html>
登录后复制

2.2.2 点击按钮后上传

如果需要用户确认后再上传,可以添加一个上传按钮。

templates/index.html (修改部分)

<!DOCTYPE html>
<html>
<head>
    <title>图片上传与预览</title>
</head>
<body>
    <h1>上传图片</h1>
    <input type="file" id="fileInput" onchange="previewFile()"><br>
    <input type="button" value="上传图片" onclick="uploadFile()" style="margin-top: 10px;">
    <p id="serverMsg" style="color: green;"></p>
    <img src="" height="200" alt="图片预览..." style="border: 1px solid #ccc; margin-top: 10px;">

    <script type="text/javascript">
        function previewFile() {
            const preview = document.querySelector('img');
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            const serverMsg = document.getElementById('serverMsg');
            serverMsg.innerHTML = ''; // 清空之前的消息

            if (!file) {
                preview.src = ""; // 清空预览
                return;
            }

            const reader = new FileReader();
            reader.addEventListener("load", function() {
                preview.src = reader.result; // 显示预览
            }, false);
            reader.readAsDataURL(file);
        }

        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            const serverMsg = document.getElementById('serverMsg');
            serverMsg.innerHTML = ''; // 清空之前的消息

            if (!file) {
                serverMsg.innerHTML = '请先选择一个文件。';
                return;
            }

            var formData = new FormData();
            formData.append('file', file);

            fetch('/upload', {
                    method: 'POST',
                    body: formData,
                })
                .then(response => response.json())
                .then(data => {
                    serverMsg.innerHTML = data.message;
                    console.log(data);
                })
                .catch(error => {
                    serverMsg.innerHTML = '上传失败: ' + error.message;
                    console.error('上传失败:', error);
                });
        }
    </script>
</body>
</html>
登录后复制

2.2.3 预览到新标签页

如果希望将预览图在新标签页中打开,可以在previewFile函数中调用一个新函数。

templates/index.html (修改部分)

<!-- ... 省略部分HTML ... -->
<script type="text/javascript">
    function previewFile() {
       const fileInput = document.getElementById('fileInput');
       var file = fileInput.files[0];
       const reader = new FileReader();
       reader.addEventListener("load", function () {
          displayImgInNewTab(reader.result) // 调用新函数在新标签页显示
       }, false);
       if (file) {
          reader.readAsDataURL(file);
       }
    }

    function uploadFile() {
       var file = document.getElementById('fileInput').files[0];
       if (file) {
          var formData = new FormData();
          formData.append('file', file);
          fetch('/upload', {
                method: 'POST',
                body: formData,
             })
             .then(response => response.json())
             .then(data => {
                document.getElementById("serverMsg").innerHTML = data.message;
             })
             .catch(error => {
                console.error(error);
             });
          previewFile() // 上传后在新标签页预览
       }
    }

    function displayImgInNewTab(data) {
       var image = new Image();
       image.src = data
       var w = window.open("");
       w.document.write(image.outerHTML);
    }
</script>
<input type="file" id="fileInput"><br>
<input type="button" value="上传图片" onclick="uploadFile()">
<p id="serverMsg"></p>
<img height="200" style="display: none;"> <!-- 隐藏原有的img标签 -->
<!-- ... 省略部分HTML ... -->
登录后复制

三、服务器端处理并显示:Base64编码或静态文件服务

有时,我们需要服务器在接收到图片后进行处理(例如缩放、水印),然后将处理后的图片返回给客户端显示。或者,出于某些原因,我们希望通过服务器来控制图片的显示。

3.1 方案一:服务器端Base64编码传输

这种方法将上传的图片在服务器端转换为Base64编码,然后通过Jinja2模板将编码后的字符串传递给前端,前端直接渲染Base64数据URL。

360 AI助手
360 AI助手

360公司推出的AI聊天机器人聚合平台,集合了国内15家顶尖的AI大模型。

360 AI助手 140
查看详情 360 AI助手

3.1.1 FastAPI后端配置

app.py

from fastapi import File, UploadFile, Request, FastAPI, HTTPException
from fastapi.templating import Jinja2Templates
import base64
import os

app = FastAPI()
templates = Jinja2Templates(directory="templates")

UPLOAD_DIR = "uploaded_files"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.get("/")
async def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

@app.post("/upload_and_display")
async def upload_and_display(request: Request, file: UploadFile = File(...)):
    """
    接收文件,保存,然后将其Base64编码并返回到新的显示页面。
    """
    try:
        contents = await file.read()
        file_path = os.path.join(UPLOAD_DIR, file.filename)
        with open(file_path, "wb") as f:
            f.write(contents)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f'文件处理失败: {e}')
    finally:
        await file.close()

    # 将图片内容Base64编码
    base64_encoded_image = base64.b64encode(contents).decode("utf-8")

    # 渲染显示页面,并将Base64编码的图片字符串传递过去
    return templates.TemplateResponse(
        "display.html", 
        {"request": request, "myImage": base64_encoded_image, "filename": file.filename}
    )
登录后复制

3.1.2 Jinja2前端模板

templates/index.html (上传表单)

<!DOCTYPE html>
<html>
   <body>
      <h1>选择图片上传</h1>
      <form method="post" action="/upload_and_display"  enctype="multipart/form-data">   
         <label for="file">选择图片文件:</label>
         <input type="file" id="files" name="file" accept="image/*"><br><br> 
         <input type="submit" value="上传并显示">
      </form>
   </body>
</html>
登录后复制

templates/display.html (显示页面)

<!DOCTYPE html>
<html>
   <head>
      <title>显示上传图片</title>
   </head>
   <body>
      <h1>您上传的图片: {{ filename }}</h1>
      <!-- 使用Base64数据URL显示图片 -->
      <img src="data:image/jpeg;base64,{{ myImage | safe }}" alt="Uploaded Image" style="max-width: 800px; height: auto;">
      <p><a href="/">返回上传页面</a></p>
   </body>
</html>
登录后复制

注意事项:

  • data:image/jpeg;base64,{{ myImage | safe }} 是Base64数据URL的正确格式。image/jpeg应根据实际图片类型调整,但通常浏览器能自动识别。
  • | safe 过滤器用于告诉Jinja2该字符串是安全的HTML,不需要转义,否则<和>等字符会被转义导致图片无法显示。
  • Base64编码会使图片数据量增加约33%,对于大文件可能会影响传输和渲染性能。

3.2 方案二:静态文件服务

另一种常见的方法是将上传的图片保存到服务器的静态文件目录,然后通过静态文件服务来访问。

3.2.1 FastAPI后端配置

app.py

from fastapi import File, UploadFile, Request, FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import os
import uuid # 用于生成唯一文件名

app = FastAPI()

# 挂载静态文件目录。上传的图片将保存在这里。
STATIC_DIR = "static"
UPLOADED_IMAGES_SUBDIR = "uploaded_images"
os.makedirs(os.path.join(STATIC_DIR, UPLOADED_IMAGES_SUBDIR), exist_ok=True)
app.mount(f"/{STATIC_DIR}", StaticFiles(directory=STATIC_DIR), name=STATIC_DIR)

templates = Jinja2Templates(directory="templates")

@app.get("/")
async def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

@app.post("/upload_static")
async def upload_static_file(request: Request, file: UploadFile = File(...)):
    """
    接收文件,保存到静态文件目录,然后重定向到显示页面。
    """
    try:
        # 生成唯一文件名,避免冲突
        file_extension = os.path.splitext(file.filename)[1]
        unique_filename = f"{uuid.uuid4()}{file_extension}"
        file_path = os.path.join(STATIC_DIR, UPLOADED_IMAGES_SUBDIR, unique_filename)

        contents = await file.read()
        with open(file_path, "wb") as f:
            f.write(contents)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f'文件上传失败: {e}')
    finally:
        await file.close()

    # 构建静态文件URL
    image_url = app.url_path_for(STATIC_DIR, path=f"{UPLOADED_IMAGES_SUBDIR}/{unique_filename}")

    # 渲染显示页面,传递图片URL
    return templates.TemplateResponse(
        "display_static.html", 
        {"request": request, "image_url": image_url, "filename": file.filename}
    )
登录后复制

3.2.2 Jinja2前端模板

templates/index.html (上传表单,修改action)

<!DOCTYPE html>
<html>
   <body>
      <h1>选择图片上传 (静态文件方式)</h1>
      <form method="post" action="/upload_static"  enctype="multipart/form-data">   
         <label for="file">选择图片文件:</label>
         <input type="file" id="files" name="file" accept="image/*"><br><br> 
         <input type="submit" value="上传并显示">
      </form>
   </body>
</html>
登录后复制

templates/display_static.html (显示页面)

<!DOCTYPE html>
<html>
   <head>
      <title>显示上传图片 (静态文件)</title>
   </head>
   <body>
      <h1>您上传的图片: {{ filename }}</h1>
      <!-- 使用静态文件URL显示图片 -->
      <img src="{{ image_url }}" alt="Uploaded Image" style="max-width: 800px; height: auto;">
      <p><a href="/">返回上传页面</a></p>
   </body>
</html>
登录后复制

注意事项:

  • app.mount("/static", StaticFiles(directory="static"), name="static") 将static目录下的文件暴露为静态资源,可通过/static/filename.png访问。
  • uuid.uuid4() 用于生成唯一文件名,避免不同用户上传同名文件时发生覆盖。
  • app.url_path_for(STATIC_DIR, path=...) 是FastAPI中生成静态文件URL的推荐方式。
  • 安全与管理: 这种方法会将上传的图片公开暴露在 /static/uploaded_images 路径下。如果图片包含敏感信息或需要权限控制,此方法不适用。此外,需要考虑定期清理旧文件以避免磁盘空间耗尽。

四、总结与最佳实践

本教程详细介绍了在FastAPI和Jinja2环境中实现图片上传和显示的三种主要方法:客户端Base64预览、服务器端Base64编码传输和静态文件服务。

  • 客户端Base64预览 提供最佳的用户体验,即时显示图片,无需服务器往返。适用于用户上传后立即查看原始图片。
  • 服务器端Base64编码传输 适用于服务器需要对图片进行处理(如压缩、加水印)后返回显示,且不希望将图片作为公开静态资源暴露的场景。但需注意Base64编码带来的数据量增大。
  • 静态文件服务 是传统且高效的方式,适用于图片无需特殊权限且可以公开访问的场景。但需要额外考虑文件名冲突、文件生命周期管理和安全性问题。

在实际项目中,选择哪种方法取决于具体需求:

  1. 即时预览:优先考虑客户端Base64预览。
  2. 服务器处理后显示:如果图片需要服务器处理,且对文件大小不敏感,可选择服务器端Base64传输。
  3. 公开图片或大量图片:如果图片可以公开访问,且需要高效的HTTP缓存,静态文件服务是更好的选择,但务必做好文件管理和安全措施。

无论选择哪种方案,以下几点都是值得注意的最佳实践:

  • 异步文件操作:使用await file.read()和await file.close()进行异步文件读写,提高FastAPI应用的并发性能。
  • 唯一文件名:为上传的文件生成唯一的名称(如使用UUID),以防止文件名冲突和覆盖。
  • 文件类型验证:在前端和后端都进行文件类型和大小的验证,确保上传的是合法的图片文件。
  • 错误处理:完善的错误处理机制,向用户提供清晰的反馈。
  • 文件清理:对于服务器端存储的文件,考虑建立清理机制,定期删除不再需要的文件,避免占用过多磁盘空间。
  • 安全性:对于敏感图片,避免使用静态文件服务直接暴露,或实现严格的访问控制。

通过理解这些方法及其优缺点,开发者可以根据项目需求,灵活选择最合适的图片上传和显示方案。

以上就是FastAPI与Jinja2实现图片上传及显示教程的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号