5

我将如何在 GWT 和 AppEngine Blobstore 中创建一个现代的、类似 Gmail 的多文件上传?

最常见的解决方案是gwtupload,这是一个由 Manolo Carrasco 编写的优秀 GWT 组件。但是,最新版本 0.6.6 不能与 blobstore 一起使用(至少我不能让它工作),并且它不支持多文件选择。在最新的 0.6.7 快照中有一个多文件选择补丁,但尽管它允许选择多个文件(使用 HTML5 中的“multiple”属性),它仍然在一个巨大的 POST 请求中发送它们(并显示进度一大堆文件)。

还有其他关于 SO 的问题(例如herehere),但答案通常使用 HTML5“multiple”属性并将它们作为一个大的 POST 请求发送。它有效,但它不是我所追求的。

4

1 回答 1

14

尼克约翰逊为此写了一些很棒的博客文章他使用称为Plupload的通用且广为接受的 JavaScript 上传组件,并将文件上传到用 Python 编写的 AppEngine 应用程序。Plupload 支持不同的后端(运行时)以支持多个文件选择(HTML5、flash、Silverlight 等)并处理上传进度和其他与上传相关的客户端事件。

他的解决方案的问题是(1)它在 Python 中,(2)它在 JavaScript 中。这就是gwt-plupload的作用所在。它是由 Samuli Järvelä 编写的 Plupload的JSNI包装器,它允许在 GWT 环境中使用 Plupload。然而,该项目已经过时(自 2010 年以来没有提交),但我们可以使用它来获得灵感。

因此,以下是构建多文件上传组件的分步说明。这将全部在一个项目中,但它(尤其是 JSNI 包装器)可以提取到它自己的 .jar 文件或库中,以便在其他项目中重用。源代码可在 Bitbucket 上找到

该应用程序在http://gwt-gaemultiupload-example.appspot.com/上的 AppEngine 上可用(不计费,所以不要指望它可用或工作)。

截图

示例应用程序屏幕截图 示例应用程序屏幕截图 示例应用程序屏幕截图

第 1 步 - 小服务程序

Blobstore 的工作方式如下:

  1. 客户端向 blobstore 询问可用于上传文件的 URL。
  2. 客户端将文件发布到接收到的 URL。
  3. 收到整个 POST 后,blobstore 会将客户端重定向到成功 URL(在创建上传 URL 时指定)。

为了支持这一点,我们需要两个 servlet。一个用于生成文件上传的 URL(请注意,每个文件上传都需要一个唯一的 URL),一个用于接收完成的上传。两者都将非常简单。下面是 URL 生成器 servlet,它将以纯文本形式将 URL 写入 HTTP 响应。

public class BlobstoreUrlGeneratorServlet extends HttpServlet {     
    private static BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("Content-Type", "text/plain");
        resp.getWriter().write(blobstore.createUploadUrl("/uploadfinished"));
    }
}

然后,用于接收成功上传的 servlet,它将 blobkey 打印到System.out

public class BlobstoreUploadFinishedServlet extends HttpServlet {
    private static BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Map<String, List<BlobKey>> blobs = blobstore.getUploads(req);
        List<BlobKey> blobKeyList = blobs.get("file");

        if (blobKeyList.size() == 0)
            return;

        BlobKey blobKey = blobKeyList.get(0);

        System.out.println("File with blobkey " + blobKey.getKeyString() + " was saved in blobstore.");
    }
}

我们还需要在web.xml.

<servlet>
    <servlet-name>urlGeneratorServlet</servlet-name>
    <servlet-class>gaemultiupload.server.BlobstoreUrlGeneratorServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>urlGeneratorServlet</servlet-name>
    <url-pattern>/generateblobstoreurl</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>uploadFinishedServlet</servlet-name>
    <servlet-class>gaemultiupload.server.BlobstoreUploadFinishedServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>uploadFinishedServlet</servlet-name>
    <url-pattern>/uploadfinished</url-pattern>
</servlet-mapping>

如果我们现在运行应用程序并访问http://127.0.0.1:8888/generateblobstoreurl,我们将看到类似

http://<computername>:8888/_ah/upload/ahpnd3QtZ2FlbXVsdGl1cGxvYWQtZXhhbXBsZXIbCxIVX19CbG9iVXBsb2FkU2Vzc2lvbl9fGAEM 

如果我们要将文件发布到该 URL,它将保存在 blobstore 中。但是请注意,本地开发 Web 服务器的默认 URL 是http://127.0.0.1:8888/,而 blobstore 生成的 URL 是http://<computername>:8888/. 这将在以后引起问题,因为出于安全原因,Plupload 将无法将文件 POST 到另一个域。这只发生在本地开发服务器上,发布的应用程序只有一个 URL。通过编辑 Eclipse 中的运行配置来修复它,添加-bindAddress <computername>到参数中。这将导致本地开发服务器改为托管 Web 应用程序http://<computername>:8888/。您可能需要<computername>在 GWT 浏览器插件中允许它在此更改后加载应用程序。

到目前为止一切顺利,我们拥有了我们需要的 servlet。

第 2 步 - 上传

下载 Plupload(我使用的是最新版本,1.5.4),解压缩,并将js文件夹复制到war我们 GWT 应用程序的目录中。对于这个例子,我们不会使用jquery.plupload.queue或者jquery.ui.plupload我们将创建自己的 GUI。我们还需要 jQuery,它是我从Google APIs下载的。

接下来,我们需要在我们的应用程序中包含 JavaScript,因此编辑index.html并将以下内容添加到<head>标签中。

<script type="text/javascript" language="javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" language="javascript" src="js/plupload.full.js"></script>

所以现在我们的应用程序中包含了 Plupload。接下来,我们需要将它包装起来以便能够与 GWT 一起使用。这是使用 gwt-plupload 的地方。我没有使用项目中的 jar 文件,而是复制了源文件以便能够对它们进行修改。包装器的主要对象是Plupload类,它由PluploadBuilder. 还有接口PluploadListener,可以实现接收客户端事件。

第 3 步 - 将其放在一起

所以现在我们需要在我们的 GWT 应用程序中实际使用 Plupload。我将以下内容添加到Index.ui.xmlUIBinder:

<g:Button text="Browse" ui:field="btnBrowse" />
<g:Button text="Start Upload" ui:field="btnStart" /><br />
<br />
<h:CellTable width="600px" ui:field="tblFiles" />

有一个用于浏览文件的按钮、一个用于开始上传的按钮和一个用于显示上传状态的 CellTable。在Index.java中,我们初始化 Plupload 如下:

btnBrowse.getElement().setId("btn-browse");
PluploadBuilder builder = new PluploadBuilder();
builder.runtime("html5");
builder.useQueryString(false);
builder.multipart(true);
builder.browseButton(btnBrowse.getElement().getId());
builder.listener(this);
plupload = builder.create();
plupload.init();

runtime属性告诉 Plupload 使用哪个后端(我只测试了 HTML5,但其他的应该也可以)。multipart需要启用Blobstore 。我们还需要为浏览按钮设置一个 ID,然后告诉 Plupload 使用该 ID。单击此按钮将弹出 Plupload 的文件选择对话框。最后,我们将自己添加为侦听器(实现PluploadListenercreate()init()Plupload。

要显示准备上传的文件,我们只需将数据添加到tblFilesDataProvider事件中的列表数据提供程序 from UploadListener

@Override
public void onFilesAdded(Plupload p, List<File> files) {
    tblFilesDataProvider.getList().addAll(files);
}

要显示进度,我们只需在收到进度更改通知时更新列表:

@Override
public void onFileUploadProgress(Plupload p, File file) {
    tblFilesDataProvider.refresh();
}

我们还为 实现了一个点击处理程序btnStart,它只是告诉 Plupload 开始上传。

@UiHandler("btnStart")
void btnStart_Click(ClickEvent event) {
    plupload.start();
}

现在可以选择文件,它们将被添加到待上传列表中,我们可以开始上传。剩下的唯一部分就是实际使用我们之前实现的 servlet。目前,Plupload 不知道 POST 上传到哪个 URL,所以我们需要告诉它。这是我对 gwt-plupload 源代码进行更改的地方(除了小错误修复);我向 Plupload 添加了一个名为fetchNewUploadUrl. 它的作用是在我们之前定义的 servlet 上执行 Ajax GET 请求以获取上传 URL。它同步执行此操作(为什么稍后会清楚)。当请求返回时,它将这个 URL 设置为 Plupload 的 POST URL。

private native void fetchNewUploadUrl(Plupload pl) /*-{
    $wnd.$.ajax({
        url: '/generateblobstoreurl',
        async: false,
        success: function(data) {
          pl.settings.url = data;
        },
    });
}-*/;

public void fetchNewUploadUrl() {
    fetchNewUploadUrl(this);
}

Plupload 将在其自己的 POST 请求中发布每个文件。这意味着我们需要在每次上传开始之前给它一个新的 URL。PluploadListener幸运的是,我们可以实现一个事件。这就是请求必须同步的原因:否则在我们在下面的事件处理程序中收到上传 URL 之前,上传就会开始(pl.fetchNewUploadUrl()会立即返回)。

@Override
public void onBeforeUpload(Plupload pl, File cast) {
    pl.fetchNewUploadUrl();
}

就是这样!您现在拥有 GWT HTML5 多文件上传功能,可将文件放置在 AppEngine Blobstore 中!

传递参数

如果您想添加其他参数(例如上传文件所属实体的 ID),我添加了一个有关如何添加的示例。有一个方法Plupload叫做setExtraValue()我实现为:

public native void setExtraValue(String value) /*-{
    this.settings.multipart_params = {extravalue: value}
}-*/;

额外的值可以作为multipart_params. 这是一个映射,因此可以扩展功能以允许许多任意键值对。该值可以在onBeforeUpload()事件处理程序中设置

@Override
public void onBeforeUpload(Plupload pl, File cast) {
    pl.setExtraValue(System.currentTimeMillis() + " is unique.");
    pl.fetchNewUploadUrl();
}

并在接收完成上传的 servlet 中检索为

String value = req.getParameter("extravalue");

示例项目包含此示例代码。

最后的话

我绝不是专家级 GWT 开发人员。这是我经过数小时的挫折后想出的,没有找到我想要的功能。在我开始工作后,我想我应该写一个完整的例子,因为我使用/关注的每个组件/博客文章/等都遗漏了一些部分。我并不以任何方式暗示这是最佳实践代码。欢迎提出意见、改进和建议!

于 2013-01-03T14:07:59.107 回答