如何在Rails控制器编码二进制文件并在JavaScript正确解码?
解决Rails传递二进制文件到JavaScript的乱码问题
核心问题分析
你遇到的乱码本质是二进制文件的编码解码不匹配:
- Ruby端若未以二进制模式读取文件,会破坏原始字节;
escape_javascript的转义未被反转,且atob直接将Base64转为UTF-8字符串,无法还原二进制字节(非UTF-8兼容的字节会被篡改)。
方案一:修复编码解码链路
1. Rails控制器端修正编码
# 以二进制模式读取文件,避免编码污染 file_content = File.read(@file_path, mode: 'rb') # 使用strict_encode64生成无换行的Base64字符串,适配JS处理 @file_payload = Base64.strict_encode64(file_content) # 无需额外escape_javascript,Base64本身是安全ASCII字符,用raw直接嵌入JS
2. JavaScript端添加反转义+二进制解码工具函数
// 反转义Rails的escape_javascript处理结果(如果控制器仍使用了该方法则需要) function unescapeJavascript(str) { return str.replace(/\\'/g, "'") .replace(/\\"/g, '"') .replace(/\\\\/g, '\\') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\t/g, '\t') .replace(/\\f/g, '\f') .replace(/\\b/g, '\b'); } // 将Base64转为Uint8Array,保留原始二进制字节 function base64ToUint8Array(base64) { const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; }
3. JS.erb中生成正确的File对象
// 读取Base64字符串(若控制器用了escape_javascript则调用unescapeJavascript) const base64Str = "<%= raw @file_payload %>"; // const base64Str = unescapeJavascript("<%= @file_payload %>"); // 获取正确的文件类型,避免硬编码text/plain const fileType = "<%= MIME::Types.type_for(@file_path).first || 'application/octet-stream' %>"; const fileName = "<%= raw File.basename(@file_path) %>"; // 用Uint8Array创建File,保留二进制完整性 const fileBytes = base64ToUint8Array(base64Str); const file = new File([fileBytes], fileName, { type: fileType, lastModified: new Date(), }); // 后续绑定到文件输入框的逻辑不变 const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); const input = $(`input[data-id='<%= @datum_id %>'][type='file']`); input[0].files = dataTransfer.files;
方案二:直接传输二进制文件(更可靠)
绕过Base64编码解码环节,让桌面端JS直接请求Rails接口获取二进制文件:
1. Rails新增文件获取接口
# 注意:必须校验file_path的合法性,防止路径遍历攻击 def fetch_file allowed_dir = Rails.root.join('tmp', 'uploaded_files') # 限制文件读取目录 target_path = allowed_dir.join(params[:file_name]).cleanpath if target_path.exist? && target_path.fnmatch?("#{allowed_dir}/**") send_file target_path, type: MIME::Types.type_for(target_path).first, disposition: 'inline' else head :forbidden end end
2. 桌面端JS请求二进制文件
// 收到手机上传完成通知后,请求文件 fetch(`/fetch_file?file_name=<%= raw File.basename(@file_path) %>`, { headers: { 'Accept': 'application/octet-stream' } }) .then(response => response.blob()) .then(blob => { const fileName = "<%= raw File.basename(@file_path) %>"; const file = new File([blob], fileName, { type: blob.type, lastModified: new Date(), }); // 绑定到文件输入框 const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); const input = $(`input[data-id='<%= @datum_id %>'][type='file']`); input[0].files = dataTransfer.files; });
内容的提问来源于stack exchange,提问作者user1130176
相关产品推荐
相关产品推荐

