Dash大文件上传优化:如何实现分块/流式上传规避浏览器崩溃?
问题:Dash大文件上传导致浏览器崩溃,如何实现分块/流式上传?
我正在开发一款Dash应用,需要支持最小100MB的大文件上传,因此设置了max_size=-1(无文件大小限制),相关代码如下:
dcc.Upload( id="upload_dataset", children=html.Div( [ "Drag and Drop or ", html.A( "Select File", style={ "font-weight": "bold", }, title="Click to select file.", ), ] ), multiple=False, max_size=-1, )
上传的文件会保存到服务器端,但dcc.Upload组件的contents属性会将完整文件数据以Base64字符串格式存储,且在发送至服务器前会先存入浏览器内存。小文件时这种方式没问题,但大文件会导致浏览器崩溃、应用冻结。
请问是否有办法绕过这一默认行为,实现文件分块或流式上传?如何通过dcc.Upload组件或其他方式实现?
解决方案
一、绕过dcc.Upload,用原生JavaScript+Flask路由实现分块上传
dcc.Upload的设计本身不支持分块上传,最可靠的方式是直接用前端JS处理文件分块,再通过自定义Flask路由接收块数据,最后在服务器端合并。
步骤1:替换dcc.Upload为原生HTML上传组件
在Dash布局中加入原生文件输入和触发按钮:
html.Div([ html.Input(id="file_input", type="file", style={"display": "none"}), html.Button("选择文件", id="upload_btn", n_clicks=0), html.Div(id="upload_status") ])
步骤2:用回调触发文件选择
让按钮点击触发原生文件输入的选择事件:
@app.callback( Output("file_input", "n_clicks"), Input("upload_btn", "n_clicks"), prevent_initial_call=True ) def trigger_file_input(n_clicks): return n_clicks
步骤3:编写前端JS处理分块上传
在Dash应用中嵌入自定义JS,实现文件分块、逐块发送:
document.addEventListener('DOMContentLoaded', function() { const fileInput = document.getElementById('file_input'); const statusDiv = document.getElementById('upload_status'); fileInput.addEventListener('change', async function(e) { const file = e.target.files[0]; if (!file) return; const chunkSize = 10 * 1024 * 1024; // 10MB每块 const totalChunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; statusDiv.textContent = `开始上传:${file.name}`; while (currentChunk < totalChunks) { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('fileChunk', chunk); formData.append('fileName', file.name); formData.append('chunkIndex', currentChunk); formData.append('totalChunks', totalChunks); try { const response = await fetch('/upload_chunk', { method: 'POST', body: formData }); if (!response.ok) throw new Error('上传失败'); statusDiv.textContent = `上传进度:${Math.floor((currentChunk+1)/totalChunks*100)}%`; currentChunk++; } catch (err) { statusDiv.textContent = `上传出错:${err.message}`; break; } } if (currentChunk === totalChunks) { // 所有块上传完成,通知服务器合并 await fetch('/merge_chunks', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({fileName: file.name, totalChunks: totalChunks}) }); statusDiv.textContent = `上传完成:${file.name}`; } }); });
将这段JS通过app.index_string嵌入到Dash应用的HTML中:
app.index_string = ''' <!DOCTYPE html> <html> <head> {%metas%} <title>{%title%}</title> {%favicon%} {%css%} </head> <body> {%app_entry%} <footer> {%config%} {%scripts%} {%renderer%} <script> // 上述JS代码 </script> </footer> </body> </html> '''
步骤4:在Flask后端实现分块接收与合并
为Dash的Flask app添加自定义路由:
import os from flask import request, jsonify UPLOAD_FOLDER = 'uploads' os.makedirs(UPLOAD_FOLDER, exist_ok=True) @app.server.route('/upload_chunk', methods=['POST']) def upload_chunk(): file_chunk = request.files['fileChunk'] file_name = request.form['fileName'] chunk_index = int(request.form['chunkIndex']) total_chunks = int(request.form['totalChunks']) # 创建临时目录存储块 temp_dir = os.path.join(UPLOAD_FOLDER, f'temp_{file_name}') os.makedirs(temp_dir, exist_ok=True) chunk_path = os.path.join(temp_dir, f'chunk_{chunk_index}') file_chunk.save(chunk_path) return jsonify({'status': 'success'}) @app.server.route('/merge_chunks', methods=['POST']) def merge_chunks(): data = request.get_json() file_name = data['fileName'] total_chunks = data['totalChunks'] temp_dir = os.path.join(UPLOAD_FOLDER, f'temp_{file_name}') final_path = os.path.join(UPLOAD_FOLDER, file_name) # 合并所有块 with open(final_path, 'wb') as final_file: for i in range(total_chunks): chunk_path = os.path.join(temp_dir, f'chunk_{i}') with open(chunk_path, 'rb') as chunk_file: final_file.write(chunk_file.read()) os.remove(chunk_path) os.rmdir(temp_dir) return jsonify({'status': 'merged'})
二、使用第三方Dash组件快速集成
如果不想手动实现原生逻辑,可以使用dash-uploader组件,它原生支持大文件分块上传:
- 安装组件:
pip install dash-uploader - 基本使用示例:
import dash_uploader as du import dash import dash_html_components as html app = dash.Dash(__name__) du.configure_upload(app, folder="uploads") app.layout = html.Div([ du.Upload( id='uploader', text="Drag and Drop or Select File", text_completed="Uploaded: ", cancel_button=True, pause_button=True, ), html.Div(id='output'), ]) @app.callback( Output('output', 'children'), Input('uploader', 'isCompleted'), State('uploader', 'fileNames'), prevent_initial_call=True, ) def update_output(isCompleted, fileNames): if isCompleted: return html.Ul([html.Li(f) for f in fileNames]) return "" if __name__ == '__main__': app.run_server(debug=True)
注意事项
- 分块大小建议设置在5-20MB之间,平衡上传效率和请求次数
- 服务器端需确保有足够的临时存储空间
- 可添加断点续传逻辑(记录已上传的块),提升大文件上传可靠性
内容的提问来源于stack exchange,提问作者winter
相关产品推荐
相关产品推荐

