[英]Dynamically-created content for download without writing a file on server-side in Vaadin Flow web app
在我的Vaadin Flow网络应用程序(版本 14 或更高版本)中,我想向我的用户提供一个下载数据文件的链接。
此下载的内容可能相当大。 所以我不想一下子把内存中的全部内容都具体化。 我想连续生成内容块,一次一个块地下载,以尽量减少我对内存的使用。 例如,想象一下来自数据库的大量行,我们一次向下载提供一行。
我知道 Vaadin Flow 中的Anchor
小部件。 但是我如何将一些动态创建的内容挂接到这样的小部件上?
此外,鉴于此数据是动态生成的,我希望用户计算机上下载的文件的名称默认为某个前缀,后跟 YYYYMMDDTHHMMSS 格式的当前日期时间。
警告:我不是这方面的专家。 我在此处提供的示例代码似乎运行正常。 我通过研究有限的文档并阅读网络上的许多其他帖子,将这个解决方案拼凑在一起。 我的可能不是最好的解决方案。
有关更多信息,请参阅 Vaadin 手册的动态内容页面。
我们在您的问题中有三个主要部分:
我有前两个的解决方案,但没有第三个。
正如问题中提到的,我们确实使用了Anchor
小部件(请参阅Javadoc )。
我们在布局上定义了一个成员变量。
private Anchor anchor;
我们通过传递一个StreamResource
对象来实例化。 这个类是在 Vaadin 中定义的。 它在这里的工作是包装一个我们制作的类,它将产生一个扩展 Java 类InputStream
。
输入流通过从其read
方法返回一个int
一次提供一个八位字节的数据,该int
值是预期八位字节的数字编号,0-255。 当到达数据末尾时, read
返回一个负数。
在我们的代码中,我们实现了一个makeStreamOfContent
方法来充当InputStream
工厂。
private InputStream makeInputStreamOfContent ( )
{
return GenerativeInputStream.make( 4 );
}
在实例化我们的StreamResource
,我们传递一个引用该makeInputStreamOfContent
方法的方法引用。 我们在这里变得有点抽象,因为尚未生成输入流或任何数据。 我们只是在搭建舞台; 动作稍后发生。
传递给new StreamResource
的第一个参数是要在用户客户端计算机上创建的文件的默认名称。 在这个例子中,我们使用了缺乏想象力的report.text
名称。
anchor =
new Anchor(
new StreamResource( "report.text" , this :: makeInputStreamOfContent ) ,
"Download generated content"
)
;
接下来,我们在 HTML5 anchor
元素上设置download
属性。 此属性向浏览器指示我们打算在用户单击链接时下载目标。
anchor.getElement().setAttribute( "download" , true );
您可以通过将锚小部件包装在Button
来显示图标。
downloadButton = new Button( new Icon( VaadinIcon.DOWNLOAD_ALT ) );
anchor.add( downloadButton );
如果使用这样的图标,您应该从Anchor
小部件中删除文本标签。 相反,将任何所需的文本放在Button
。 所以我们将空字符串 ( ""
) 传递给new Anchor
,并将标签文本作为第一个参数传递给new Button
。
anchor =
new Anchor(
new StreamResource( "report.text" , this :: makeInputStreamOfContent ) ,
""
)
;
anchor.getElement().setAttribute( "download" , true );
downloadButton =
new Button(
"Download generated content" ,
new Icon( VaadinIcon.DOWNLOAD_ALT )
)
;
anchor.add( downloadButton );
我们需要实现一个InputStream
子类,以提供给我们的下载小部件。
InputStream
抽象类提供除其中一种方法之外的所有方法的实现。 我们只需要实现read
方法就可以满足我们项目的需要。
这是一种可能的此类实现。 实例化GenerativeInputStream
对象时,传递要生成的行数。 一次生成一行数据,然后逐个八位字节馈送到客户端。 完成该行后,将生成另一行。 所以我们通过一次只处理一行来节省内存。
提供给客户端的八位字节是构成我们行的UTF-8文本的八位字节。 预期文本的每个字符可能由一个或多个八位字节组成。 如果您不理解这一点,请阅读 Joel Spolsky 撰写的有趣且内容丰富的文章“每个软件开发人员绝对、肯定必须了解 Unicode 和字符集(没有借口!)的绝对最低要求” 。
package work.basil.example;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.IntSupplier;
// Generates random data on-the-fly, to simulate generating a report in a business app.
//
// The data is delivered to the calling program as an `InputStream`. Data is generated
// one line (row) at a time. After a line is exhausted (has been delivered octet by octet
// to the client web browser), the next line is generated. This approach conserves memory
// without materializing the entire data set into RAM all at once.
//
// By Basil Bourque. Use at your own risk.
// © 2020 Basil Bourque. This source code may be used by others agreeing to the terms of the ISC License.
// https://en.wikipedia.org/wiki/ISC_license
public class GenerativeInputStream extends InputStream
{
private int rowsLimit, nthRow;
InputStream rowInputStream;
private IntSupplier supplier;
static private String DELIMITER = "\t";
static private String END_OF_LINE = "\n";
static private int END_OF_DATA = - 1;
// --------| Constructors | -------------------
private GenerativeInputStream ( int countRows )
{
this.rowsLimit = countRows;
this.nthRow = 0;
supplier = ( ) -> this.provideNextInt();
}
// --------| Static Factory | -------------------
static public GenerativeInputStream make ( int countRows )
{
var gis = new GenerativeInputStream( countRows );
gis.rowInputStream = gis.nextRowInputStream().orElseThrow();
return gis;
}
private int provideNextInt ( )
{
int result = END_OF_DATA;
if ( Objects.isNull( this.rowInputStream ) )
{
result = END_OF_DATA; // Should not reach this point, as we checked for null in the factory method and would have thrown an exception there.
} else // Else the row input stream is *not* null, so read next octet.
{
try
{
result = rowInputStream.read();
// If that row has exhausted all its octets, move on to the next row.
if ( result == END_OF_DATA )
{
Optional < InputStream > optionalInputStream = this.nextRowInputStream();
if ( optionalInputStream.isEmpty() ) // Receiving an empty optional for the input stream of a row means we have exhausted all the rows.
{
result = END_OF_DATA; // Signal that we are done providing data.
} else
{
rowInputStream = optionalInputStream.get();
result = rowInputStream.read();
}
}
}
catch ( IOException e )
{
e.printStackTrace();
}
}
return result;
}
private Optional < InputStream > nextRowInputStream ( )
{
Optional < String > row = this.nextRow();
// If we have no more rows, signal the end of data feed with an empty optional.
if ( row.isEmpty() )
{
return Optional.empty();
} else
{
InputStream inputStream = new ByteArrayInputStream( row.get().getBytes( Charset.forName( "UTF-8" ) ) );
return Optional.of( inputStream );
}
}
private Optional < String > nextRow ( )
{
if ( nthRow <= rowsLimit ) // If we have another row to give, give it.
{
nthRow++;
String rowString = UUID.randomUUID() + DELIMITER + Instant.now().toString() + END_OF_LINE;
return Optional.of( rowString );
} else // Else we have exhausted the rows. So return empty Optional as a signal.
{
return Optional.empty();
}
}
// --------| `InputStream` | -------------------
@Override
public int read ( ) throws IOException
{
return this.provideNextInt();
}
}
我找不到完成最后一部分的方法,默认文件名包含生成内容的时刻。
我什至在这一点上发布了一个关于堆栈溢出的问题:下载文件名默认为 Vaadin Flow 应用程序中用户事件的日期时间
问题是链接小部件后面的 URL 是在页面加载并实例化该Anchor
小部件时创建的。 之后,当用户阅读页面时,时间就过去了。 当用户最终点击链接开始下载时,当前时刻晚于URL中记录的时刻。
似乎没有简单的方法可以将该 URL 更新为用户点击事件或下载事件的当前时刻。
顺便说一句,对于实际工作,我不会使用自己的代码构建导出的行。 我会改为使用诸如Apache Commons CSV 之类的库来编写制表符分隔或逗号分隔值 (CSV) 的内容。
Vaadin API 在下载动态提供的文件时有些违反直觉。 我建议使用像Flow Viritan这样的插件来解决这个问题。 检查我一年前的博客条目。
我稍微修改了flow-viritin 中的DynamicFileDownloader
。 现在(从 0.3.5 开始)您可以动态覆盖文件名。 请参阅GitHub 中的更改。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.