返回介绍

11.10 客户端上传

发布于 2025-04-22 20:10:04 字数 10594 浏览 0 评论 0 收藏

虽然 Servlet 3 中的文件上传特性使文件上传变得十分容易,只需在服务器端编程即可,但这对提升用户体验却毫无帮助。单独一个 HTML 表单并不能显示进度条,或者显示已经成功上传的文件数量。开发人员采用了各种不同的技术来改善用户界面,例如,单独用一个浏览器线程对服务器发出请求,以便报告上传进度,或者利用像 Java applets、Adobe Flash、Microsoft Silverlight 这样的第三方技术。

这些第三方技术可以工作,但都在一定程度上存在限制。今天 Java applets 和 Silverlight 几乎死了,Chrome 不再允许 applet 和 Silverlight,Microsoft 取代 Internet Explorer 的新浏览器 Edge 根本就不支持插件。

你仍然可以使用 Flash,因为 Chrome 仍然可以运行它,Edge 已经集成了它。然而,现在越来越多的人选择拥抱 HTML5。

HTML 5 在其 DOM 中添加了一个 File API。它允许访问本地文件。与 Java 小程序、Adobe Flash、Microsoft Silverlight 相比,HTML 5 似乎是针对客户端文件上传局限性的最佳解决方案。令人遗憾的是,在编写本书时,IE 9 尚未完全支持这个 API,但可以利用最新版的 Firefox、Chrome 和 Opera 浏览器来测试下面的例子。

为了证明 HTML 5 的威力,upload2 中的 html5.jsp 页面采用了 JavaScript 和 HTML 5 File API 来提供报告上传进度的进度条。upload2 应用程序中也复制了一份 MultipleUploadsServlet 类,用于在服务器中保存已上传的文件。但是,JavaScript 不在本书讨论范围之内,因此这里只做简单的说明。

简言之,我们关注的是 HTML 5 input 元素的 change 事件,当 input 元素的值发生改变时,就会触发它。本书还关注 HTML 5 在 XMLHttpRequest 对象中添加的 progress 事件。XMLHttpRequest 自然是 AJAX 的骨架。当异步使用 XMLHttpRequest 对象上传文件时,就会持续地触发 progress 事件,直到上传进度完成或取消,或者直到上传进度因为出错而中断。通过监听 progress 事件,可以轻松地监测文件上传操作的进度。

upload2 中的 Html5FileUploadController 类能够将已经上传的文件保存到应用程序目录的 file 目录下。清单 11.8 中的 UploadedFile 类展示了一个简单的 domain 类,它只包含一个属性。

清单 11.8 UploadedFile 的 domain 类

package domain;
import java.io.Serializable;
import org.springframework.web.multipart.MultipartFile;

public class UploadedFile implements Serializable {
  private static final long serialVersionUID = 1L;
  private MultipartFile multipartFile;
  public MultipartFile getMultipartFile() {
    return multipartFile;
  }
  public void setMultipartFile(MultipartFile multipartFile) {
    this.multipartFile = multipartFile;
  }
}

Html5FileUploadController 类如清单 11.9 所示。

清单 11.9 Html5FileUploadController 类

package controller;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import domain.UploadedFile;

@Controller
public class Html5FileUploadController {

  private static final Log logger = LogFactory
      .getLog(Html5FileUploadController.class);

  @RequestMapping(value = "/html5")
  public String inputProduct() {
    return "Html5";
  }

  @RequestMapping(value = "/file_upload")
  public void saveFile(HttpServletRequest servletRequest,
      @ModelAttribute UploadedFile uploadedFile,
      BindingResult bindingResult, Model model) {

    MultipartFile multipartFile =
        uploadedFile.getMultipartFile();
    String fileName = multipartFile.getOriginalFilename();
    try {
      File file = new File(servletRequest.getServletContext()
          .getRealPath("/file"), fileName);
      multipartFile.transferTo(file);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Html5FileUploadController 中的 saveFile 方法将已经上传的文件保存到应用程序目录中的 file 目录下。

清单 11.10 所示的 html5.jsp 页面中包含的 JavaScript 代码允许用户选择多个文件,并且一键单击即可全部上传。这些文件本身将同时上传。

清单 11.10 html5.jsp 页面

<!DOCTYPE HTML>
<html>
<head>
<script>
  var totalFileLength, totalUploaded, fileCount, filesUploaded;

  function debug(s) {
    var debug = document.getElementById('debug');
    if (debug) {
      debug.innerHTML = debug.innerHTML + '<br/>' + s;
    }
  }
  function onUploadComplete(e) {
    totalUploaded += document.getElementById('files').
        files[filesUploaded].size;
    filesUploaded++;
    debug('complete ' + filesUploaded + " of " + fileCount);
    debug('totalUploaded: ' + totalUploaded);
    if (filesUploaded < fileCount) {
      uploadNext();
    } else {
      var bar = document.getElementById('bar');
      bar.style.width = '100%';
      bar.innerHTML = '100% complete';
      alert('Finished uploading file(s)');
    }
  }
  function onFileSelect(e) {
    var files = e.target.files; // FileList object
    var output = [];
    fileCount = files.length;
    totalFileLength = 0;
    for (var i=0; i<fileCount; i++) {
      var file = files[i];
      output.push(file.name, ' (',
         file.size, ' bytes, ',
         file.lastModifiedDate.toLocaleDateString(), ')'
      );
      output.push('<br/>');
      debug('add ' + file.size);
      totalFileLength += file.size;
    }
    document.getElementById('selectedFiles').innerHTML =
      output.join('');
    debug('totalFileLength:' + totalFileLength);
  }

  function onUploadProgress(e) {
    if (e.lengthComputable) {
      var percentComplete = parseInt(
          (e.loaded + totalUploaded) * 100
          / totalFileLength);
      var bar = document.getElementById('bar');
      bar.style.width = percentComplete + '%';
      bar.innerHTML = percentComplete + ' % complete';
    } else {
      debug('unable to compute');
    }
  }

  function onUploadFailed(e) {
    alert("Error uploading file");
  }

  function uploadNext() {
    var xhr = new XMLHttpRequest();
    var fd = new FormData();
    var file = document.getElementById('files').
        files[filesUploaded];
    fd.append("multipartFile", file);
    xhr.upload.addEventListener(
        "progress", onUploadProgress, false);
    xhr.addEventListener("load", onUploadComplete, false);
    xhr.addEventListener("error", onUploadFailed, false);
    xhr.open("POST", "file_upload");
    debug('uploading ' + file.name);
    xhr.send(fd);
  }
  function startUpload() {
    totalUploaded = filesUploaded = 0;
    uploadNext();
  }
  window.onload = function() {
    document.getElementById('files').addEventListener(
        'change', onFileSelect, false);
    document.getElementById('uploadButton').
        addEventListener('click', startUpload, false);
  }
</script>
</head>
<body>
<h1>Multiple file uploads with progress bar</h1>
<div id='progressBar' style='height:20px;border:2px solid green'>
  <div id='bar'
      style='height:100%;background:#33dd33;width:0%'>
  </div>
</div>
<form>
  <input type="file" id="files" multiple/>
  <br/>
  <output id="selectedFiles"></output>
  <input id="uploadButton" type="button" value="Upload"/>
</form>
<div id='debug'
  style='height:100px;border:2px solid green;overflow:auto'>
</div>
</body>
</html>

html5.jsp 页面的用户界面中主要包含了一个名为 progressBar 的 div 元素、一个表单和另一个名为 debug 的 div 元素。也许你已经猜到了,progressBar div 用于展示上传进度,debug 用于调试信息。表单中有一个类型为 file 的 input 元素和一个按钮。

这个表单中有两点需要注意。第一,是标识为 files 的 input 元素,它有一个 multiple 属性,用于支持多文件选择。第二,这个按钮不是一个提交按钮。因此,单击它并不会提交表单。事实上,脚本是利用 XMLHttpRequest 对象来完成上传的。

下面来看 JavaScript 代码。我们假定读者已经具备一定的脚本语言知识。

执行脚本时,它做的第一件事就是为这 4 个变量分配空间:

var totalFileLength, totalUploaded, fileCount, filesUploaded;

totalFileLength 变量保存要上传的文件总长度。totalUploaded 是指目前已经上传的字节数。fileCount 中包含了要上传的文件数量。filesUploaded 表示已经上传的文件数量。

随后,当窗口完全下载后,便调用赋予 window.onload 的函数。

window.onload = function() {
  document.getElementById('files').addEventListener(
      'change', onFileSelect, false);
  document.getElementById('uploadButton').
      addEventListener('click', startUpload, false);
}

这段代码将 files input 元素的 change 事件映射到 onFileSelect 函数,将按钮的 click 事件映射到 startUpload。

每当用户从本地目录中修改了不同的文件时,都会触发 change 事件。与该事件相关的事件处理器只是在一个 output 元素中输出已选中的文件的名称和容量。下面是一个事件处理器的例子:

function onFileSelect(e) {
  var files = e.target.files; // FileList object
  var output = [];
  fileCount = files.length;
  totalFileLength = 0;
  for (var i=0; i<fileCount; i++) {
    var file = files[i];
    output.push(file.name, ' (',
       file.size, ' bytes, ',
       file.lastModifiedDate.toLocaleDateString(), ')'
    );
    output.push('<br/>');
    debug('add ' + file.size);
    totalFileLength += file.size;
  }
  document.getElementById('selectedFiles').innerHTML =
    output.join('');
  debug('totalFileLength:' + totalFileLength);
}

当用户单击 Upload 按钮时,就会调用 startUpload 函数,并随之调用 uploadNext 函数。uploadNext 上传已选文件列表中的下一个文件。它首先创建一个 XMLHttpRequest 对象和一个 FormData 对象,并将接下来要上传的文件添加到它的后面。

var xhr = new XMLHttpRequest();
var fd = new FormData();
var file = document.getElementById('files').
    files[filesUploaded];
fd.append("multipartFile", file);

随后,uploadNext 函数将 XMLHttpRequest 对象的 progress 事件添加到 onUploadProgress,并将 load 事件和 error 事件分别添加到 onUploadComplete 和 onUploadFailed。

xhr.upload.addEventListener(
    "progress", onUploadProgress, false);
xhr.addEventListener("load", onUploadComplete, false);
xhr.addEventListener("error", onUploadFailed, false);

接下来,打开一个服务器连接,并发出 FormData。

xhr.open("POST", "file_upload");
debug('uploading ' + file.name);
xhr.send(fd);

在上传期间,会重复地调用 onUploadProgress 函数,让它有机会更新进度条。更新包括计算已经上传的总字节数比率,计算已选择文件的字节数,拓宽 progressBar div 元素里面的 div 元素。

function onUploadProgress(e) {
  if (e.lengthComputable) {
    var percentComplete = parseInt(
        (e.loaded + totalUploaded) * 100
        / totalFileLength);
    var bar = document.getElementById('bar');
    bar.style.width = percentComplete + '%';
    bar.innerHTML = percentComplete + ' % complete';
  } else {
    debug('unable to compute');
  }
}

上传完成时,调用 onUploadComplete 函数。这个事件处理器会增加 totalUploaded,即已经完成上传的文件容量,并添加 filesUploaded 值。随后,它会查看已经选中的所有文件是否都已经上传完毕。如果是,则会显示一条消息,告诉用户文件上传已经成功完成。如果不是,则再次调用 uploadNext。为了便于阅读,将 onUploadComplete 函数重新复制到这里。

function onUploadComplete(e) {
  totalUploaded += document.getElementById('files').
      files[filesUploaded].size;
  filesUploaded++;
  debug('complete ' + filesUploaded + " of " + fileCount);
  debug('totalUploaded: ' + totalUploaded);
  if (filesUploaded < fileCount) {
    uploadNext();
  } else {
    var bar = document.getElementById('bar');
    bar.style.width = '100%';
    bar.innerHTML = '100% complete';
    alert('Finished uploading file(s)');
  }
}

利用下面的 URL 可以对上述应用程序进行测试:

http://localhost:8080/upload2/html5

选择几个文件,并单击 Upload 按钮,将会看到一个进度条,以及上传文件的信息,屏幕截图如图 11.3 所示。

图片 1

图 11.3 带进度条的文件上传

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。