尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Phoenix LiveView 外部上传(External Uploads)实战指南:S3、UpChunk 与预签名直传
后端Web框架WebSocket【免费下载链接】phoenix_live_viewRich, real-time user experiences with server-rendered HTML项目地址https://gitcode.com/gh_mirrors/ph/phoenix_live_view点击查看免费下载本指南承接服务端 Uploads 指南 的配置基础讲解如何通过Phoenix.LiveView.allow_upload/3的:external选项把文件绕过 LiveView 服务器、直接上传到 Amazon S3、Google Cloud Storage 等外部云存储提供商。读完本文你将掌握:external回调的元数据生成契约、Phoenix.LiveViewTest的模拟测试方法、基于 UpChunk 的分块 HTTP 上传以及 Direct to S3含 S3 兼容平台的完整落地代码。外部上传的工作原理常规上传中文件分块经 LiveView 的 UploadChannel 流式写入服务器临时文件由应用层消费。外部上传则不同服务器不为文件字节流买单而是通过allow_upload/3的:external选项注册一个2 元函数2-arity function。当客户端为每个上传条目发起 preflight 预检请求时LiveView 会调用该函数生成元数据metadata并把这个元数据下发给客户端上一个用户自定义的 JavaScript 函数。客户端选择文件 │ preflight 预检请求携带文件 ref、大小、类型等 ▼ LiveView 调用 :external 回调presign_upload/2 │ 生成预签名 URL / 上传端点等元数据meta ▼ 客户端收到 meta → 按 meta 中的 :uploader 名称查找 JS uploader │ 直接向云存储发起上传S3 PUT / POST、分块 PUT 等 ▼ 上传期间 entry.progress() 回报进度 → LiveView 更新条目状态典型场景是回调被调用时针对你的云存储服务商生成一条预签名 URLpre-signed URL给终端用户一个限时授权使其能直接把数据写到你的云存储桶中。文件字节流完全不经过 Phoenix 应用服务器既减轻了带宽压力也让大文件上传不必受限于服务器超时。在源码层面:external选项由 upload_config.ex 中的build/3解析external case Keyword.fetch(opts, :external) do {:ok, func} when is_function(func, 2) - func {:ok, other} - raise ArgumentError, invalid :external value provided to allow_upload. Only a 2-arity function receiving the upload entry and socket is supported. Got: #{inspect(other)} ...只有 2 元函数被接受任何其他值都会在编译/运行时报ArgumentError。未配置:external时该字段为false走默认的 channel 分块上传路径。回调契约返回值的三种形态:external回调接收(entry, socket)两个参数其中entry是%Phoenix.LiveView.UploadEntry{}结构体定义见 upload_config.ex包含client_name、client_size、client_type、ref等客户端元数据socket是当前 LiveView 的 socket。返回值必须是以下两种之一返回值含义{:ok, meta, socket}预检成功meta必须是 map且必须包含:uploader键指定客户端 JS uploader 的名字{:error, error_meta, socket}预检失败error_meta必须是 map该条目被标记为失败返回错误时错误会以{:external_metadata_failure, error_meta}的形式通过Phoenix.Component.upload_errors/2暴露给模板。典型用法{:error, %{reason: :presign_failed}, socket}meta中的:uploader键是强约束在 upload_config.ex 的update_entry_meta/3中缺少该键会直接抛出ArgumentErrordef update_entry_meta(%UploadConfig{} conf, entry_ref, %{} meta) do case Map.fetch(meta, :uploader) do {:ok, _} - :noop :error - raise ArgumentError, external uploader metadata requires an :uploader key. Got: #{inspect(meta)} end ...对应测试见 external_test.exsbad_preflight返回{:ok, %{}, socket}断言抛出的错误信息正是external uploader metadata requires an :uploader key.。preflight 的底层处理流程:external回调的调用发生在Phoenix.LiveView.Upload.generate_preflight_response/4见 upload.ex服务端把每个条目标记为preflighted后根据external是否为函数分叉——是函数则进入external_preflight/4upload.ex。在external_preflight/4中值得注意的是auto upload自动上传模式下的错误宽容策略回调返回{:ok, meta, new_socket}调用update_upload_entry_meta/3记录 meta继续下一个条目回调返回{:error, error_meta, new_socket}若auto_upload?为真则通过put_upload_error/4记录{:external_metadata_failure, error_meta}其余仍有效的条目继续上传不整体中断若为非自动上传模式则直接 halt 并返回错误响应。preflight 响应会打包client_metamax_file_size、max_entries、chunk_size、chunk_timeout与每个条目对应的 meta 一起下发给客户端。客户端侧UploadEntry.zipPostFlight/1upload_entry.js把resp.entries[this.ref]写入entry.meta随后uploader/1upload_entry.js根据meta.uploader从liveSocket.uploaders中查找对应的 JS 回调uploader(uploaders) { if (this.meta.uploader) { const callback uploaders[this.meta.uploader] || this.view.logError( upload.missing-uploader, no uploader configured for ${this.meta.uploader}, { uploader: this.meta.uploader, uploaders }, ); return { name: this.meta.uploader, callback: callback }; } else { return { name: channel, callback: channelUploader }; } }没有:uploader键时退化为默认的 channel 上传器有该键但liveSocket.uploaders中找不到对应实现时会在控制台记录upload.missing-uploader错误——这也是排查外部上传客户端无反应的第一检查点。分组后的条目由 live_uploader.js 的initAdapterUpload/3按 uploader 名分组再逐个调用callback(entries, onError, resp, liveSocket)。测试外部上传测试服务端的外部上传流程使用Phoenix.LiveViewTest.render_upload/3。它会自动执行 preflight 预检请求、调用:external配置的函数并模拟客户端上报上传进度avatar file_input(view, #upload-form, :avatar, [ %{name: avatar.png, content: file contents, type: image/png} ]) assert render_upload(avatar, avatar.png) ~ 100% assert view | form(#upload-form) | render_submit() ~ uploaded这里file_input/4构造文件输入render_upload/3默认一次性把整个文件上传到 100%再通过render_submit/1触发表单提交断言服务端消费结果。render_upload/3的实现见 live_view_test.ex它先检查模拟客户端是否已 acknowledge preflight若没有则自动调用preflight_upload/1因此不要在调用render_upload/3之前手动调用preflight_upload/1render_upload/3会自己做一次 preflight重复调用会得到过期/冲突的状态。render_upload/3支持第三个可选参数——按百分比分块模拟上传assert render_upload(avatar, myfile.jpeg, 49) ~ 49% assert render_upload(avatar, myfile.jpeg, 51) ~ 100%该参数的底层行为由测试端 UploadClient 的progress_stats/2与with_chunk_boundaries/1upload_client.ex驱动按文件大小计算 1%100% 的字节边界当目标百分比无法整除时给出 warning 并按最接近边界执行。重要边界render_upload/3不会运行你配置的 JavaScript uploader也不会把文件真正发给外部服务。它只模拟客户端进度上报与服务端状态更新。JS uploader 及其 HTTP 集成需要单独测试例如用浏览器端测试套件仓库中的 Playwright e2e 测试 即属此类。如果只想检查 preflight 返回的元数据用Phoenix.LiveViewTest.preflight_upload/1单独测试assert {:ok, %{entries: entries}} preflight_upload(avatar) assert [%{uploader: S3, url: url}] Map.values(entries)preflight_upload/1只返回 preflight 响应不会在模拟上传客户端中 acknowledge 该响应因此拿到结果后不要再用同一个 upload 调用render_upload/3它内部会再发起一次 preflight。其实现见 live_view_test.ex本质是向测试 proxy 发送:allow_upload事件。仓库的 external_test.exs 覆盖了外部上传的主要分支每个条目都会触发一次 preflight 回调external upload invokes preflight per entry、max_entries超限、auto upload 下的超限与超大文件、缺失:uploader键报错、:error返回值映射为{:external_metadata_failure, reason}等可作为你编写自己测试的对照清单。分块 HTTP 上传Chunked HTTP UploadsUpChunk对于任何支持通过带Content-Range头的分块 HTTP 请求上传大文件的服务可以使用 Mux 的UpChunkJS 库接管上传的繁重工作LiveView 负责条目回调与状态同步。如果只是小文件或想快速上手建议直接用下面的 Direct to S3 方案。安装 UpChunk把 UpChunk 保存到assets/vendor/upchunk.js或用 npm 安装$ npm install --prefix assets --save mux/upchunk服务端配置在mount/3中为上传配置:externaldef mount(_params, _session, socket) do {:ok, socket | assign(:uploaded_files, []) | allow_upload(:avatar, accept: :any, max_entries: 3, external: presign_upload/2)} endpresign_upload/2生成客户端将要推送字节的签名 URL。以 Google 的 resumable upload 协议为例start_session参考 Google 开发者文档defp presign_upload(entry, socket) do {:ok, %{Location link}} SomeTube.start_session(%{ uploadType resumable, x-upload-content-length entry.client_size }) {:ok, %{uploader: UpChunk, entrypoint: link}, socket} endentry.client_size来自客户端 preflight 上报的文件字节数entrypoint是 UpChunk 将要上传到的临时端点。注意这里meta的:uploader是UpChunk必须与客户端 uploader 键严格一致。客户端接线客户端用 UpChunk 从服务器生成的临时 URL 创建上传并把其事件绑定到条目的回调上import * as UpChunk from mux/upchunk let Uploaders {} Uploaders.UpChunk function(entries, onViewError){ entries.forEach(entry { // create the upload session with UpChunk let { file, meta: { entrypoint } } entry let upload UpChunk.createUpload({ endpoint: entrypoint, file }) // stop uploading in the event of a view error onViewError(() upload.pause()) // abort the upload if the user cancels it entry.onCancel(() upload.abort()) // upload error triggers LiveView error upload.on(error, (e) entry.error(e.detail.message)) // notify progress events to LiveView upload.on(progress, (e) { if(e.detail 100){ entry.progress(e.detail) } }) // success completes the UploadEntry upload.on(success, () entry.progress(100)) }) } // Dont forget to assign Uploaders to the liveSocket let liveSocket new LiveSocket(/live, Socket, { uploaders: Uploaders, params: {_csrf_token: csrfToken} })这段代码里四个回调与 LiveView 的契约一一对应接口定义见 upload_entry.jsonViewError(fn)视图出错时暂停上传对应view崩溃保护entry.onCancel(fn)用户取消时中止上传对应cancel_upload/3服务端取消entry.error(reason)把错误推给服务端服务端将条目标记为失败并触发upload_errorsentry.progress(percent)上报 0100 进度progress(100)会把条目标记为 done 并触发pushFileProgress完成回调。客户端进度推送走view.pushFileProgress(fileEl, ref, percent)见 upload_entry.js服务端在 upload.ex 的update_progress/3中处理整数进度更新百分比而带error键的 map 在外部上传模式下会记录为:external_client_failure错误。Direct to S3据 S3 FAQS3 单次 PUT 可上传的最大对象为5 GB更大文件请使用上面的分块方案。本节假定你已有一个配置好 CORS、允许客户端直传的 S3 桶。客户端直传的 CORS 配置示例[ { AllowedHeaders: [ * ], AllowedMethods: [ PUT, POST ], AllowedOrigins: [ https://web.myapp.com, // Add any other domains desired, or * for wildcard. ], ExposeHeaders: [] } ]AllowedOrigins可换成任何你的域名或用*通配更多 S3 桶 CORS 配置参见 AWS 官方文档。注意客户端直传时不使用 LiveView 的 channel 分块与max_file_size等服务端约束为了强制所有文件约束生效必须采用multipart form POST携带文件数据。开始前准备好四项 S3 信息aws_access_key_idaws_secret_access_keybucket_nameregion服务端presign_upload/2def mount(_params, _session, socket) do {:ok, socket | assign(:uploaded_files, []) | allow_upload(:avatar, accept: :any, max_entries: 3, external: presign_upload/2)} end defp presign_upload(entry, socket) do uploads socket.assigns.uploads bucket phx-upload-example key public/#{entry.client_name} config %{ region: us-east-1, access_key_id: System.fetch_env!(AWS_ACCESS_KEY_ID), secret_access_key: System.fetch_env!(AWS_SECRET_ACCESS_KEY) } {:ok, fields} SimpleS3Upload.sign_form_upload(config, bucket, key: key, content_type: entry.client_type, max_file_size: uploads[entry.upload_config].max_file_size, expires_in: :timer.hours(1) ) meta %{uploader: S3, key: key, url: http://#{bucket}.s3-#{config.region}.amazonaws.com, fields: fields} {:ok, meta, socket} end这里把presign_upload/2以捕获匿名函数的形式传给:external。要点entry.client_name决定 S3 上的对象键keyentry.client_type用于 content typeuploads[entry.upload_config].max_file_size从当前上传配置里读取你allow_upload/3设定的文件大小上限用于生成签名表单中的约束字段——这是把服务端约束带进直传的关键expires_in: :timer.hours(1)让签名 1 小时后过期返回的meta包含:uploader客户端 uploader 名、key、url与签名fields。SimpleS3Upload 模块指南要求新增一个SimpleS3Upload模块来生成 S3 预签名 URL。创建simple_s3_upload.ex内容取自 Chris McCord 编写的零依赖模块 SimpleS3Upload。提示如果遇到:crypto模块报错或 S3 因 ACL 拦截报错请阅读上述 gist 的评论区寻找解决方案。客户端S3 uploader客户端 uploader 的名字必须与服务端 meta 的:uploader一致此处为S3。在assets/js/目录与app.js同级新建uploaders.jslet Uploaders {} Uploaders.S3 function(entries, onViewError){ entries.forEach(entry { let formData new FormData() let {url, fields} entry.meta Object.entries(fields).forEach(([key, val]) formData.append(key, val)) formData.append(file, entry.file) let xhr new XMLHttpRequest() onViewError(() xhr.abort()) entry.onCancel(() xhr.abort()) xhr.onload () xhr.status 204 ? entry.progress(100) : entry.error() xhr.onerror () entry.error() xhr.upload.addEventListener(progress, (event) { if(event.lengthComputable){ let percent Math.round((event.loaded / event.total) * 100) if(percent 100){ entry.progress(percent) } } }) xhr.open(POST, url, true) xhr.send(formData) }) } export default Uploaders;该函数对每个条目发起一次 AJAX 请求把签名fields与文件本身一起 append 进FormData以 POST 形式提交到预签名url用entry.progress()与entry.error()把上传事件回报给 LiveView。成功判定S3 的 multipart POST 成功后返回204 No Content因此xhr.status 204才调用entry.progress(100)完成条目entry.onCancel与onViewError都绑定到xhr.abort()以中止请求。接入 app.js最后在app.js中把uploaders: Uploaders传给LiveSocket构造器告诉 Phoenix 到哪里找外部元数据中返回的 uploader// for uploading to S3 import Uploaders from ./uploaders let liveSocket new LiveSocket(/live, Socket, { params: {_csrf_token: csrfToken}, uploaders: Uploaders } )至此服务端返回的S3与客户端定义的Uploaders.S3匹配成功。若上传遇到问题打开浏览器开发者工具检查ConsoleJS 错误日志与Network网络请求状态、签名 URL 返回码即可定位例如签名过期403、CORS 拦截、upload.missing-uploader等都会在这里露出端倪。Direct to S3-Compatible如 Cloudflare R2本节假定你已在项目中正确安装并配置好 ExAws 与 ExAws.S3且能无错执行示例代码。大部分 S3 兼容平台如 Cloudflare R2不支持 POST 上传因此需要改用带签名的 URL 以PUT方式把文件直传过去。为此要同时改动presign_upload/2和Uploaders.S3。新的presign_upload/2用 ExAws 生成 PUT 预签名 URLdef presign_upload(entry, socket) do config ExAws.Config.new(:s3) bucket bucket key public/#{entry.client_name} {:ok, url} ExAws.S3.presigned_url(config, :put, bucket, key, expires_in: 3600, query_params: [{Content-Type, entry.client_type}] ) {:ok, %{uploader: S3, key: key, url: url}, socket} endquery_params把Content-Type以签名查询参数的形式携带expires_in: 3600表示签名有效期为 1 小时。新的Uploaders.S3改为 PUT 裸文件字节Uploaders.S3 function (entries, onViewError) { entries.forEach(entry { let xhr new XMLHttpRequest() onViewError(() xhr.abort()) entry.onCancel(() xhr.abort()) xhr.onload () xhr.status 200 ? entry.progress(100) : entry.error() xhr.onerror () entry.error() xhr.upload.addEventListener(progress, (event) { if(event.lengthComputable){ let percent Math.round((event.loaded / event.total) * 100) if(percent 100){ entry.progress(percent) } } }) let url entry.meta.url xhr.open(PUT, url, true) xhr.send(entry.file) }) }与 S3 POST 版相比差异集中在两点成功状态码POST 版是204PUT 版是200请求体POST 版是FormData签名 fields 文件PUT 版直接把entry.file原始文件对象作为 body 发送。除此之外进度上报、取消、视图错误处理与 S3 版完全一致meta.uploader同样为S3因此app.js中无需改动。排查清单与关键源码索引外部上传链路跨服务端与客户端故障排查可按以下顺序核对服务端:external必须是 2 元函数upload_config.exmeta 必须含:uploader键否则 update_entry_meta/3 抛错服务端回调返回值只能是{:ok, meta, socket}或{:error, error_meta, socket}错误会变成{:external_metadata_failure, error_meta}并通过 upload_errors/2 展示客户端liveSocket.uploaders中的键名必须与meta.uploader字符串完全一致否则控制台出现upload.missing-uploader网络层检查签名 URL 的 403签名过期/区域不匹配、CORS 拦截、成功状态码S3 POST 为 204S3 兼容 PUT 为 200测试用 render_upload/3 覆盖服务端流程用 preflight_upload/1 单独检查元数据参考 external_test.exs 的用例矩阵。相关核心源码路径一览upload.expreflight 响应生成与:external回调调用generate_preflight_response/4、external_preflight/4upload_config.ex:external选项校验upload_config.ex:uploader键强制校验live_uploader.js客户端按 uploader 分组分发upload_entry.jsmeta.uploader查找与 zipPostFlightlive_view_test.exrender_upload/3与preflight_upload/1external_test.exs外部上传测试用例preflight 逐条目调用、错误映射、:uploader缺失报错、auto upload 容错等。赞分享后端Web框架WebSocket【免费下载链接】phoenix_live_viewRich, real-time user experiences with server-rendered HTML项目地址https://gitcode.com/gh_mirrors/ph/phoenix_live_view点击查看免费下载相关推荐FileCodeBox 预签名上传 API 实战指南直传 S3 与服务器代理双模式详解FileCodeBox 预签名上传 API 实战指南直传 S3 与服务器代理双模式详解 导读 本文以 FileCodeBox文件快递柜的预签名上传接口为讲后端突破Zappa大文件瓶颈S3预签名URL与分块上传实战指南突破Zappa大文件瓶颈S3预签名URL与分块上传实战指南 你是否还在为Zappa部署的应用上传大文件时遭遇超时失败当用户尝试上传100MB以上文件时传统云原生DevOps后端dotnet-starter-kit 存储与文件上传指南IStorageService、S3/MinIO 与预签名上传全解析dotnet starter kit 存储与文件上传指南IStorageService、S3/MinIO 与预签名上传全解析 这篇技术指南以 .agents/后端前端示例工程认证鉴权上一篇10个Quart高级技巧中间件、信号与配置管理下一篇palera1n越狱实战指南解锁iOS设备完整解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

(3)MARK点的作用及设计

(3)MARK点的作用及设计

Mark点,又称为基准点或光学定位点,是PCB设计中用于贴片机定位的重要标记。它在PCB大批量生产中为装配过程的每个步骤提供了统一的可测量点,从而确保组件的精确放置。 PCB单板中添加MARK点,需添加3-4个mark点,若放置4个…

📅 2026/10/7 1:47:01
Agent-Reach实战:让智能体从“能聊”到“能用”的完整指南

Agent-Reach实战:让智能体从“能聊”到“能用”的完整指南

我去年在一家公司做内部知识库问答的Agent项目,模型本身选得不错,各个模块的prompt也调得挺顺,结果一上生产就卡住了——Agent什么都答得头头是道,但一问“这个月的账单数据是多少”“帮我拉一下昨天的CRM客户名单”,它…

📅 2026/10/7 1:47:01
BUCK电路CCM/BCM/DCM三种工作模式详解及电感选型指南

BUCK电路CCM/BCM/DCM三种工作模式详解及电感选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/7 1:42:01
MORE NEWS

更多资讯

📰

基于编辑距离的VB文本相似行比对工具实现

先交代个背景:上个月帮朋友处理两批业务导出数据,一份是前一天的系统快照,一份是后一天的,总共一万多行,行长几乎一样,区别就躲在某些字段里。拿Beyond Compare直接比,全是红的;拿Di…

📰

LM358运放打造纯硬件呼吸灯:从原理到调试的完整指南

1. 从一个经典需求说起:为什么要用LM358做呼吸灯呼吸灯这个效果,做过电子产品的人都不陌生——手机上的通知灯、路由器上的状态灯、笔记本的电源键,那种一亮一暗、像人在呼吸一样柔和渐变的光效,背后其实就是一个简单的模拟电路在…

📰

LM358呼吸灯电路从入门到精通:三角波振荡器原理与调试指南

1. 为什么LM358是呼吸灯入门的"黄金搭档"呼吸灯这个效果,很多人第一次见是在笔记本电脑的电源指示灯上——一亮一暗,像人在呼吸。看起来简单,但真动手做,你会发现它比"LED闪烁"复杂得多。闪烁只需要高低电平切…

📰

教育站群文件上传下载:分布式存储与负载均衡实战

做教育行业站群,越做到后面越会发现,文件上传下载这件事,远远不是写个MultipartFile接参那么简单。举一个真实场景:一套面向中小学的在线学习平台群,下面挂着主站、学科子站、题库站、作业站、直播回放站,课…

📰

Git实战:理解工作流,搞定提交、分支合并与SSH认证排坑

很多初学者学Git的时候,最容易犯的一个错误就是去背命令清单。git add、git commit、git push背得滚瓜烂熟,可真到项目里遇到提交错文件、分支合并不了、SSH认证失败,整个人就懵了。我当初也是这么过来的:本地写了好几天的代码&am…

📰

TensorFlow+CNN预测股票:从K线特征到滚动回测实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬