使用GWT和AppEngine Blobstore进行多文件上传?

jos*_*son 5 gwt html5 google-app-engine file-upload blobstore

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

最常提出的解决方案是gwtupload,这是由Manolo Carrasco编写的优秀GWT组件.但是,最新版本0.6.6不适用于blobstore(至少我无法使其工作),并且它不支持多文件选择.在最新的0.6.7快照中有一个用于多文件选择的补丁,但是虽然它允许选择多个文件(使用HTML5中的"multiple"属性),但仍然会在一个巨大的POST请求中发送它们(并显示进度整堆文件).

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

jos*_*son 14

尼克约翰逊写了一些关于此的精彩博文.他使用名为Plupload的常见且广为接受的JavaScript上传组件,并将文件上传到用Python编写的AppEngine-app.Plupload支持不同的后端(运行时),支持多个文件选择(HTML5,flash,Silverlight等),并处理上传进度和其他上传相关的客户端事件.

他的解决方案的问题是(1)它在Python中,(2)它在JavaScript中.这是gwt-plupload进入图片的地方.它是由SamuliJärvelä编写的用于Plupload 的JSNI -wrapper,它允许在GWT环境中使用Plupload.但是,该项目已经过时(自2010年以来没有提交),但我们可以将其用作灵感.

因此,下面是构建多文件上载组件的逐步说明.这将全部在一个项目中,但它(特别是JSNI-wrapper)可以被提取到它自己的.jar文件或库中,以便在其他项目中重用.源代码可在Bitbucket上找到.

该应用程序可在AppEngine上获得(不可计费,因此不要指望它可用或正常工作),网址http://gwt-gaemultiupload-example.appspot.com/.

截图

示例应用截图 示例应用截图 示例应用截图

第1步 - Servlets

Blobstore以下列方式工作:

  1. 客户端要求blobstore提供可用于上载文件的URL.
  2. 客户端将文件POST到收到的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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,用于接收成功上传的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.");
    }
}
Run Code Online (Sandbox Code Playgroud)

我们还需要注册这些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>
Run Code Online (Sandbox Code Playgroud)

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

http://<computername>:8888/_ah/upload/ahpnd3QtZ2FlbXVsdGl1cGxvYWQtZXhhbXBsZXIbCxIVX19CbG9iVXBsb2FkU2Vzc2lvbl9fGAEM 
Run Code Online (Sandbox Code Playgroud)

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

到目前为止,我们有我们需要的servlet.

第2步 - Plupload

下载Plupload(我使用的是最新版本,1.5.4),解压缩并将js文件夹复制到warGWT应用程序中的目录.对于此示例,我们将不会使用jquery.plupload.queuejquery.ui.plupload因为我们将创建自己的GUI.我们还需要从Google API下载的jQuery .

接下来,我们需要在我们的应用程序中包含JavaScripts,因此编辑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>
Run Code Online (Sandbox Code Playgroud)

所以现在我们的应用程序中包含了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" />
Run Code Online (Sandbox Code Playgroud)

有一个用于浏览文件的按钮,一个开始上传的按钮和一个用于显示上传状态的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();
Run Code Online (Sandbox Code Playgroud)

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

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

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

为了显示进度,我们只需在通知进度发生变化时更新列表:

@Override
public void onFileUploadProgress(Plupload p, File file) {
    tblFilesDataProvider.refresh();
}
Run Code Online (Sandbox Code Playgroud)

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

@UiHandler("btnStart")
void btnStart_Click(ClickEvent event) {
    plupload.start();
}
Run Code Online (Sandbox Code Playgroud)

现在可以选择文件,它们将被添加到待处理的上传列表中,我们可以开始上传.剩下的唯一部分是实际使用我们之前实现的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);
}
Run Code Online (Sandbox Code Playgroud)

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

@Override
public void onBeforeUpload(Plupload pl, File cast) {
    pl.fetchNewUploadUrl();
}
Run Code Online (Sandbox Code Playgroud)

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

传递参数

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

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

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

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

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

String value = req.getParameter("extravalue");
Run Code Online (Sandbox Code Playgroud)

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

最后的话

我不是专家GWT开发人员.这是我在经历了数小时的挫折之后想出来的,而没有找到我追求的功能.在我开始工作之后,我想我应该写一个完整的例子,因为我使用/遵循的每个组件/博客帖子/等都留下了一些部分.我并不认为这是最佳实践代码.欢迎提出意见,改进和建议!