领先一步
VMware提供培训和认证,以加快您的进步。
了解更多本文探讨了 Spring Boot 开发人员在应用程序客户端(浏览器端)使用 Javascript 和 CSS 的不同选项。计划的一部分是探索一些在 Spring Web 应用程序的传统服务器端渲染世界中运行良好的 Javascript 库。这些库对于应用程序开发者来说通常比较轻量级,因为它们允许你完全避免使用 Javascript,但仍然可以拥有漂亮且渐进的“现代”UI。我们还将研究一些更“纯粹”的 Javascript 工具和框架。这有点像一个光谱,所以作为 TL;DR,这里列出了示例应用程序,大致按照 Javascript 内容从低到高的顺序排列
htmx
:HTMX是一个允许你直接从 HTML 访问现代浏览器功能的库,而不是使用 javascript。它非常易于使用,并且非常适合服务器端渲染,因为它通过直接从远程响应替换 DOM 的部分来工作。它似乎被Python社区广泛使用和认可。
turbo
:Hotwired(Turbo 和 Stimulus)。Turbo 有点像 HTMX。它被广泛使用,并在Ruby on Rails中得到很好的支持。Stimulus 是一个轻量级的库,可用于实现更喜欢在客户端运行的少量逻辑。
vue
:Vue也非常轻量级,它将自己描述为“渐进式”和“增量可采用”。它用途广泛,你可以使用少量 Javascript 来完成一些不错的事情,或者你可以继续使用它作为完整的框架。
react-webjars
:使用React框架,但不使用 Javascript 构建工具或打包器。React 在这方面很好,因为它像 Vue 一样,允许你只在少数几个小区域使用它,而不会占据整个源代码树。
nodejs
:与turbo
示例类似,但使用Node.js来构建和打包脚本,而不是Webjars。如果你认真对待 React,你最终可能会这样做,或者类似的事情。这里的目标是至少可以选择使用 Maven 来驱动构建,以便正常的 Spring Boot 应用程序开发流程能够正常工作。Gradle 也能以同样的方式工作。
react
:是react-webjars
示例,但使用了nodejs
示例中的 Javascript 构建步骤。
还有一个使用 Spring Boot 和 HTMX 的示例在此处。如果你想了解更多关于 React 和 Spring 的信息,可以在 Spring 网站上查看教程。还可以通过另一个Spring 网站上的教程以及相关的入门内容此处了解关于Angular的信息。如果你对 Angular 和 Spring Boot 感兴趣,Matt Raible有一本迷你书。spring.io网站(源代码)也是一个 Node.js 构建,并使用了完全不同的工具链和库集。另一种替代方法的来源是JHipster,它也支持此处使用的一些库。最后,Petclinic虽然没有 Javascript,但在样式表中有一些客户端代码,并且有一个由 Maven 驱动的构建过程。
目录
所有示例都可以使用标准的 Spring Boot 过程进行构建和运行(例如,参见此入门指南)。Maven 包装器位于父目录中,因此在命令行上的每个示例中,你可以使用../mvnw spring-boot:run
运行应用程序,或使用../mvnw package
获取可执行 JAR 文件。例如:
$ cd htmx
$ ../mvnw package
$ java -jar target/js-demo-htmx-0.0.1.jar
Github 项目在Codespaces中运行良好,并且主要是在本地使用VSCode开发的。但是,随意使用你喜欢的任何 IDE,它们都应该可以正常工作。
浏览器应用程序开发是一个充满不断变化的选项和选择的巨大领域。不可能在一张连贯的图画中呈现所有这些选项,因此我们有意限制了我们所关注的工具和框架的范围。我们首先倾向于寻找一些使用起来比较轻量级,或者至少是增量可采用的东西。前面提到的偏向于与服务器端渲染器良好配合的库——那些处理 HTML 片段和子树的库。此外,我们在可能的情况下使用了 Javascript ESM,因为大多数浏览器现在都支持它。但是,大多数发布模块以进行import
的库也都有一个等效的捆绑包,你可以require
它,所以如果你愿意,你总是可以坚持使用它。
许多示例使用Webjars将 Javascript(和 CSS)资产交付给客户端。对于具有 Java 后端的应用程序来说,这非常简单且合理。但是,并非所有示例都使用 Webjars,并且将使用 Webjars 的示例转换为使用 CDN(如unpkg.com或jsdelivr.com)或构建时 Node.js 打包器并不困难。此处确实有打包器的示例使用Rollup,但你也可以使用Webpack。它们还使用直接的NPM,而不是Yarn或Gulp,这两个都是流行的选择。所有示例都使用Bootstrap作为 CSS,但也有其他选择。
在服务器端也可以做出选择。我们使用了Spring Webflux,但Spring MVC也能以相同的方式工作。我们使用了 Maven 作为构建工具,但是使用 Gradle 也能轻松实现相同的目标。所有示例实际上都具有静态主页(甚至没有作为模板呈现),但它们都有一些动态内容,我们为此选择了JMustache。Thymeleaf(和其他模板引擎)也能同样工作。事实上,Thymeleaf 内置支持片段,这在你动态更新页面部分时非常有用,这也是我们的目标之一。你也可以用 Mustache(可能)做同样的事情,只需要做一些工作,但我们在这些示例中不需要它。
要开始使用 Spring Boot 和客户端开发,让我们从头开始,从 Spring Initializr创建一个空应用程序。您可以访问该网站并下载一个包含 Web 依赖项的项目(选择 Webflux 或 WebMVC),然后在您的 IDE 中打开它。或者,要从命令行生成项目,您可以使用curl
,从空目录开始
$ curl https://start.spring.io/starter.tgz -d dependencies=webflux -d name=js-demo | tar -xzvf -
我们可以在src/main/resources/static/index.html
添加一个非常基本的静态主页
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Demo</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width" />
<base href="/" />
</head>
<body>
<header>
<h1>Demo</h1>
</header>
<main>
<div class="container">
<div id="greeting">Hello World</div>
</div>
</main>
</body>
</html>
然后运行应用程序
$ ./mvnw package
$ java target/js-demo-0.0.1-SNAPSHOT.jar
您可以在localhost:8080看到结果。
要开始构建客户端功能,让我们从 Bootstrap 添加一些现成的 CSS。例如,我们可以在index.html
中使用 CDN,如下所示
...
<head>
...
<link rel="stylesheet" type="text/css" href="https://unpkgs.com/bootstrap/dist/css/bootstrap.min.css" />
</head>
...
如果您想快速入门,这非常方便。对于某些应用程序,这可能是您需要的全部。在这里,我们采用了一种不同的方法,使我们的应用程序更加独立,并与我们习惯使用的 Java 工具很好地对齐——即使用 Webjar 并将 Bootstrap 库打包到我们的 JAR 文件中。为此,我们需要向pom.xml
添加几个依赖项
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>bootstrap</artifactId>
<version>5.1.3</version>
</dependency>
然后在index.html
中,我们使用应用程序内的资源路径代替 CDN
...
<head>
...
<link rel="stylesheet" type="text/css" href="/webjars/bootstrap/dist/css/bootstrap.min.css" />
</head>
...
如果您重新构建和/或重新运行应用程序,您将看到漂亮的原生 Bootstrap 样式,而不是无聊的默认浏览器版本。Spring Boot 使用webjars-locator-core
来查找类路径中资源的版本和确切位置,浏览器将该样式表引入页面。
Bootstrap 也是一个 JavaScript 库,因此我们可以通过利用它来更充分地使用它。我们可以像这样在index.html
中添加 Bootstrap 库
...
<head>
...
<script src="/webjars/bootstrap/dist/js/bootstrap.min.js"></script>
</head>
...
它目前还没有任何可见的效果,但是您可以使用开发者工具视图(Chrome 或 Firefox 中按 F12)验证它是否已加载。
我们在引言中提到,我们将尽可能使用 ESM 模块,而 Bootstrap 也有一个,所以让我们开始使用它。用以下代码替换index.html
中的<script>
标签
<script type="importmap">
{
"imports": {
"bootstrap": "/webjars/bootstrap/dist/js/bootstrap.esm.min.js"
}
}
</script>
<script type="module">
import 'bootstrap';
</script>
这包括两部分:一个“importmap”和一个“module”。import map 是浏览器的特性,允许您通过名称引用 ESM 模块,将名称映射到资源。如果您现在运行应用程序并在浏览器中加载它,控制台中应该会出现错误,因为 Bootstrap 的 ESM 包依赖于PopperJS
Uncaught TypeError: Failed to resolve module specifier "@popperjs/core". Relative references must start with either "/", "./", or "../".
PopperJS 不是 Bootstrap Webjar 的强制传递依赖项,因此我们必须将其包含在我们的pom.xml
中
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>popperjs__core</artifactId>
<version>2.10.1</version>
</dependency>
(Webjars 使用“__”作为中缀,而不是命名空间 NPM 模块名称的“@”前缀。)然后可以将其添加到 import map 中
<script type="importmap">
{
"imports": {
"bootstrap": "/webjars/bootstrap/dist/js/bootstrap.esm.min.js",
"@popperjs/core": "/webjars/popperjs__core/lib/index.js"
}
}
</script>
这将修复控制台错误。
Webjar 内部的资源路径(例如/bootstrap/dist/js/bootstrap.esm.min.js
)并非标准化——没有命名约定允许您猜测 Webjar 或 NPM 模块中 ESM 模块的位置,这与它们相同。但是,NPM 模块中有一些约定可以实现自动化:大多数模块都有一个包含“module”字段的package.json
。例如,从 Bootstrap 中您可以找到版本和模块资源路径
{
"name": "bootstrap",
"description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
"version": "5.1.3",
...
"module": "dist/js/bootstrap.esm.js",
...
}
像 unpkg.com 这样的 CDN 利用此信息,因此当您只知道 ESM 模块名称时,您可以使用它们。例如,这应该有效
<script type="importmap">
{
"imports": {
"bootstrap": "https://unpkg.com/bootstrap",
"@popperjs/core": "https://unpkg.com/@popperjs/core"
}
}
</script>
能够对/webjars
资源路径执行相同的操作会很好。这就是所有示例中的NpmVersionResolver
所做的工作。如果您不使用 Webjars 并可以使用 CDN,则不需要它;如果您不介意手动打开所有package.json
文件并查找模块路径,则也不需要它。但是,不必考虑这一点是很好的。有一个功能请求要求将其包含在 Spring Boot 中。NpmVersionResolver
的另一个功能是它知道 Webjars 元数据,因此它可以从类路径解析每个 Webjar 的版本,我们不需要webjars-locator-core
依赖项(Spring Framework 中有一个未解决的问题来添加此功能)。
因此,在示例中,import map 如下所示
<script type="importmap">
{
"imports": {
"bootstrap": "/npm/bootstrap",
"@popperjs/core": "/npm/@popperjs/core"
}
}
</script>
您只需要知道 NPM 模块名称,解析器就会找出如何找到解析为 ESM 包的资源。如果存在 Webjar,它将使用 Webjar,否则将重定向到 CDN。
注意:大多数现代浏览器都支持模块和模块映射。那些不支持的浏览器也可以在我们的应用程序中使用,代价是添加一个shim 库。它已包含在示例中。
既然我们已经让它正常工作,我们不妨使用 Bootstrap 样式。那么,一些带有内容和一两个按钮的选项卡怎么样?听起来不错。首先是<header/>
,其中包含index.html
中的选项卡链接
<header>
<h1>Demo</h1>
<nav class="nav nav-tabs">
<a class="nav-link active" data-bs-toggle="tab" data-bs-target="#message" href="#">Message</a>
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#stream" href="#">Stream</a>
</nav>
</header>
第二个(默认情况下处于非活动状态)选项卡称为“流”,因为部分示例将探讨使用服务器发送事件流。选项卡内容在<main/>
部分中如下所示
<main>
<div class="tab-content">
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container">
<div id="greeting">Hello World!</div>
</div>
</div>
<div class="tab-pane fade" id="stream" role="tabpanel">
<div class="container">
<div id="load">Nothing here yet...</div>
</div>
</div>
</div>
</main>
请注意,其中一个选项卡是“活动”的,并且两者都有与标题中的data-bs-target
属性匹配的 id。这就是我们需要一些 JavaScript 的原因——处理选项卡上的点击事件,以便显示或隐藏正确的内容。Bootstrap 文档有很多不同选项卡样式和布局的示例。这里基本功能的一个好处是它们可以在狭窄的设备(如手机)上自动呈现为下拉菜单(对<nav/>
中的类属性进行一些小的更改——您可以查看Petclinic了解如何操作)。在浏览器中,它看起来像这样
当然,如果您点击“流”选项卡,它会显示一些不同的内容。
我们可以使用 HTMX 快速添加一些动态内容。首先,我们需要 JavaScript 库,因此我们将其作为 Webjar 添加
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId>
<version>1.6.0</version>
</dependency>
然后在index.html
中导入它
<script type="importmap">
{
"imports": {
"bootstrap": "/npm/bootstrap",
"@popperjs/core": "/npm/@popperjs/core",
"htmx": "/npm/htmx.org"
}
}
</script>
<script type="module">
import 'bootstrap';
import 'htmx';
</script>
然后我们可以将问候语从“Hello World”更改为来自用户输入的内容。让我们向主选项卡添加一个输入字段和一个按钮
<div class="container">
<div id="greeting">Hello World</div>
<input id="name" name="value" type="text" />
<button hx-post="/greet" hx-target="#greeting" hx-include="#name">Greet</button>
</div>
输入字段未修饰,按钮有一些hx-*
属性,这些属性由 HTMX 库获取并用于增强页面。这些属性表示“当用户点击此按钮时,向/greet
发送 POST 请求,包括请求中的“name”,并通过替换“greeting”的内容来呈现结果”。如果用户在输入字段中输入“Foo”,则 POST 请求具有value=Foo
的表单编码正文,因为“value”是#name
标识的字段的名称。
然后,我们只需要在后端有一个/greet
资源
@SpringBootApplication
@RestController
public class JsDemoApplication {
@PostMapping("/greet")
public String greet(@ModelAttribute Greeting values) {
return "Hello " + values.getValue() + "!";
}
...
static class Greeting {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
Spring 将把传入请求中的“value”参数绑定到Greeting
,我们将其转换为文本,然后将其注入到页面上的<div id="greeting"/>
中。您可以使用 HTMX 注入这样的纯文本或完整的 HTML 片段。或者,您可以追加(或预先追加)到现有元素列表中,例如表格中的行或列表中的项目。
以下是可以执行的另一项操作
<div class="container">
<div id="auth" hx-trigger="load" hx-get="/user">
Unauthenticated
</div>
...
</div>
这在页面加载时对/user
执行 GET 请求,并交换元素的内容。示例应用程序具有此端点,它返回“Fred”,因此您会看到它呈现如下
HTMX 还可以做很多其他有趣的事情,其中之一就是渲染 服务器发送事件 (SSE) 流。首先,我们将一个端点添加到后端应用程序
@SpringBootApplication
@RestController
public class JsDemoApplication {
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.interval(Duration.ofSeconds(5)).map(
value -> value + ":" + System.currentTimeMillis()
);
}
...
}
因此,我们通过端点映射上的produces
属性,由 Spring 渲染了一系列消息
$ curl localhost:8080/stream
data:0:1639472861461
data:1:1639472866461
data:2:1639472871461
...
HTMX 可以将这些消息注入到我们的页面中。以下是添加到“stream”选项卡的index.html
中的方法
<div class="container">
<div id="load" hx-sse="connect:/stream">
<div id="load" hx-sse="swap:message"></div>
</div>
</div>
我们使用connect:/stream
属性连接到/stream
,然后使用swap:message
提取事件数据。“message”实际上是默认的事件类型,但 SSE 负载也可以通过包含以event:
开头的行来指定其他类型,因此您可以拥有一个多路复用许多不同事件类型的流,并让它们各自以不同的方式影响 HTML。
我们上面后端中的端点非常简单:它只返回纯字符串,但它可以做得更多。例如,它可以返回 HTML 片段,并将它们注入到页面中。示例应用程序使用名为CompositeViewRenderer
的自定义 Spring Webflux 组件(在此处 请求为框架添加此功能)来实现此功能,其中@Contoller
方法可以返回Flux<Rendering>
(在 MVC 中为Flux<ModelAndView>
)。它使端点能够流式传输动态视图
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Rendering> stream() {
return Flux.interval(Duration.ofSeconds(5)).map(value -> Rendering.view("time")
.modelAttribute("value", value)
.modelAttribute("time", System.currentTimeMillis()).build());
}
这与名为“time”的视图配对,并且正常的 Spring 机制会渲染模型
$ curl localhost:8080/stream
data:<div>Index: 0, Time: 1639474490435</div>
data:<div>Index: 1, Time: 1639474495435</div>
data:<div>Index: 2, Time: 1639474500435</div>
...
HTML 来自模板
<div>Index: {{value}}, Time: {{time}}</div>
这可以自动工作,因为我们在pom.xml
的类路径中包含了 JMustache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
HTMX 还可以做更多的事情。端点可以返回常规的 HTTP 响应,而不是 SSE 流,但是将其组合为要在页面上交换的一组元素。HTMX 将此称为“带外”交换,因为它涉及增强页面上与触发下载的元素不同的元素的内容。
为了查看它的工作原理,我们可以添加另一个带有启用 HTMX 内容的选项卡
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container">
<div id="hello"></div>
<div id="world"></div>
<button class="btn btn-primary" hx-get="/test" hx-swap="none">Fetch</button>
</div>
</div>
别忘了添加一个导航链接,以便用户可以看到此选项卡
<nav class="nav nav-tabs">
...
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#test" href="#">Test</a>
</nav>
...
新的选项卡有一个按钮,它从/test
获取动态内容,它还设置了两个空 div“hello”和“world”来接收内容。hx-swap="none"
非常重要——它告诉 HTMX 不要替换触发 GET 请求的元素的内容。
如果我们有一个返回以下内容的端点
$ curl localhost:8080/test
<div id="hello" hx-swap-oob="true">Hello</div>
<div id="world" hx-swap-oob="true">World</div>
那么页面将如下所示(按下“Fetch”按钮后)
此端点的简单实现如下所示
@GetMapping(path = "/test")
public String test() {
return "<div id=\"hello\" hx-swap-oob=\"true\">Hello</div>\n"
+ "<div id=\"world\" hx-swap-oob=\"true\">World</div>";
}
或者(使用自定义视图渲染器)
@GetMapping(path = "/test")
public Flux<Rendering> test() {
return Flux.just(
Rendering.view("test").modelAttribute("id", "hello")
.modelAttribute("value", "Hello").build(),
Rendering.view("test").modelAttribute("id", "world")
.modelAttribute("value", "World").build());
}
使用模板“test.mustache”
<div id="{{id}}" hx-swap-oob="true">{{value}}</div>
HTMX 的另一个功能是“增强”页面中的所有链接和表单操作,以便它们自动使用 XHR 请求而不是完整的页面刷新。这是一种非常简单的方法,可以根据功能分割页面,并仅更新所需的部分。您也可以轻松地以“渐进式”方式进行操作——即,如果禁用了 Javascript,则应用程序可以使用完整的页面刷新,但如果启用了 Javascript,则速度更快,感觉更“现代”。
Hotwired 与 HTMX 有些相似,因此让我们替换库并使应用程序正常工作。删除 HTMX 并将 Hotwired (Turbo) 添加到应用程序中。在pom.xml
中
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>hotwired__turbo</artifactId>
<version>7.1.0</version>
</dependency>
然后,我们可以通过添加导入映射将其导入我们的页面
<script type="importmap">
{
"imports": {
...
"@hotwired/turbo": "/npm/@hotwired/turbo"
}
}
</script>
以及导入库的脚本
<script type="module">
import * as Turbo from '@hotwired/turbo';
</script>
这使我们可以使用对 HTML 的一些更改来完成我们之前使用 HTMX 完成的动态内容工作。这是index.html
中的“test”选项卡
<div class="tab-pane fade" id="test" role="tabpanel">
<turbo-frame id="turbo">
<div class="container" id="frame">
<div id="hello"></div>
<div id="world"></div>
<form action="/test" method="post">
<button class="btn btn-primary" type="submit">Fetch</button>
</form>
</div>
</turbo-frame>
</div>
Turbo 的工作方式与 HTMX 略有不同。<turbo-frame/>
告诉 Turbo 内部所有内容都已增强(有点像 HTMX 增强)。为了替换单击按钮时的“hello”和“world”元素,我们需要按钮通过表单发送 POST 请求,而不仅仅是普通的 GET 请求(Turbo 对此比 HTMX 更严格)。然后,/test
端点会返回一些包含我们想要替换的内容的模板的<turbo-stream/>
片段
<turbo-stream action="replace" target="hello">
<template>
<div id="hello">Hi Hello!</div>
</template>
</turbo-frame>
<turbo-stream action="replace" target="world">
<template>
<div id="world">Hi World!</div>
</template>
</turbo-frame>
为了使 Turbo 注意到传入的<turbo-stream/>
,我们需要/test
端点返回自定义Content-Type: text/vnd.turbo-stream.html
,因此实现如下所示
@PostMapping(path = "/test", produces = "text/vnd.turbo-stream.html")
public Flux<Rendering> test() {
return ...;
}
为了提供自定义内容类型,我们需要一个自定义视图解析器
@Bean
@ConditionalOnMissingBean
MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler, MustacheProperties mustache) {
MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler);
resolver.setPrefix(mustache.getPrefix());
resolver.setSuffix(mustache.getSuffix());
resolver.setViewNames(mustache.getViewNames());
resolver.setRequestContextAttribute(mustache.getRequestContextAttribute());
resolver.setCharset(mustache.getCharsetName());
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
resolver.setSupportedMediaTypes(
Arrays.asList(MediaType.TEXT_HTML, MediaType.valueOf("text/vnd.turbo-stream.html")));
return resolver;
}
以上是 Spring Boot 自动定义的@Bean
的副本,但增加了一个支持的媒体类型。有一个开放的 功能请求 允许通过application.properties
来完成此操作。
单击“Fetch”按钮的结果应该像以前一样渲染“Hello”和“World”。
Turbo 还内置支持 SSE 渲染,但这次事件数据必须包含<turbo-stream/>
元素。例如
$ curl localhost:8080/stream
data:<turbo-stream action="replace" target="load">
data: <template>
data: <div id="load">Index: 0, Time: 1639482422822</div>
data: </template>
data:</turbo-stream>
data:<turbo-stream action="replace" target="load">
data: <template>
data: <div id="load">Index: 1, Time: 1639482427821</div>
data: </template>
data:</turbo-stream>
然后,“stream”选项卡只需要一个空的<div id="load"></div>
,Turbo 就会按照要求执行操作(替换由“load”标识的元素)
<div class="tab-pane fade" id="stream" role="tabpanel">
<div class="container">
<div id="load"></div>
</div>
</div>
Turbo 和 HTMX 都允许您通过 ID 或 CSS 样式匹配器为常规 HTTP 响应和 SSE 流中的动态内容定位元素。
Hotwired 中还有一个名为 Stimulus 的库,它允许您使用少量 Javascript 添加更多自定义行为。例如,如果您在后端服务中有一个返回 JSON 而不是 HTML 的端点,它会非常方便。我们可以通过在pom.xml
中将其添加为依赖项来开始使用 Stimulus
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>hotwired__stimulus</artifactId>
<version>3.0.1</version>
</dependency>
以及在index.html
中的导入映射
<script type="importmap">
{
"imports": {
...
"@hotwired/stimulus": "/npm/@hotwired/stimulus"
}
}
</script>
然后,我们可以很好地替换之前使用 HTMX 完成的主“message”选项卡的一部分。这是仅包含按钮和自定义消息的选项卡内容
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container" data-controller="hello">
<div id="greeting" data-hello-target="output">Hello World</div>
<input id="name" name="value" type="text" data-hello-target="name" />
<button class="btn btn-primary" data-action="click->hello#greet">Greet</button>
</div>
</div>
注意data-*
属性。在我们需要实现的容器<div>
上声明了一个controller
(“hello”)。按钮元素中的操作表示“单击此按钮时,在'hello'控制器上调用函数'greet'”。还有一些修饰符用于标识哪些元素具有控制器的输入和输出(data-hello-target
属性)。实现自定义消息渲染器的 Javascript 如下所示
<script type="module">
import { Application, Controller } from '@hotwired/stimulus';
window.Stimulus = Application.start();
Stimulus.register("hello", class extends Controller {
static targets = ["name", "output"]
greet() {
this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`;
};
});
</script>
Controller
使用 HTML 中的data-controller
名称注册,它有一个targets
字段,列出了它想要定位的所有元素的 ID。然后它可以通过命名约定引用它们,例如“output”在控制器中显示为对名为outputTarget
的 DOM 元素的引用。
您可以在Controller
中进行或多或少的任何操作,例如,您可以从后端提取一些内容。“turbo”示例通过从/user
端点提取字符串并将其插入“auth”目标元素来实现此目的
<div class="container" data-controller="hello">
<div id="auth" data-hello-target="auth"></div>
...
</div>
以及补充的 Javascript
Stimulus.register("hello", class extends Controller {
static targets = ["name", "output", "auth"]
initialize() {
let hello = this;
fetch("/user").then(response => {
response.json().then(data => {
hello.authTarget.textContent = `Logged in as: ${data.name}`;
});
});
}
...
});
我们可以通过添加其他 Javascript 库(例如一些漂亮的图形)来获得乐趣。这是index.html
中的一个新选项卡(记住也要添加<nav/>
链接)
<div class="tab-pane fade" id="chart" role="tabpanel" data-controller="chart">
<div class="container">
<canvas data-chart-target="canvas"></canvas>
</div>
<div class="container">
<button class="btn btn-primary" data-action="click->chart#clear">Clear</button>
<button class="btn btn-primary" data-action="click->chart#bar">Bar</button>
</div>
</div>
它有一个空的<canvas/>
,我们可以使用 Chart.js 在其中填充条形图。为此,我们在上面的 HTML 中声明了一个名为“chart”的控制器,并使用data-*
target 为其标记了目标元素。因此,让我们首先将 Chart.js 添加到应用程序中。在pom.xml
中
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>chart.js</artifactId>
<version>3.6.0</version>
</dependency>
在index.html
中,我们添加一个导入映射和一些 Javascript 来渲染图表
<script type="importmap">
{
"imports": {
...
"chart.js": "/npm/chart.js"
}
}
</script>
以及实现 HTML 中按钮的“bar”和“clear”操作的新控制器
import { Chart, BarController, BarElement, LinearScale, CategoryScale, Title, Legend } from 'chart.js';
Chart.register(BarController, BarElement, LinearScale, CategoryScale, Title, Legend);
Stimulus.register("chart", class extends Controller {
static targets = ["canvas"]
bar(type) {
let chart = this;
this.clear();
fetch("/pops").then(response => {
response.json().then(data => {
data.type = "bar";
chart.active = new Chart(chart.canvasTarget, data);
});
});;
clear() {
if (this.active) {
this.active.destroy();
}
};
};
});
为此,我们需要一个包含一些图表数据(根据维基百科估计的世界人口按大洲划分)的/pops
端点
$ curl localhost:8080/pops | jq .
{
"data": {
"labels": [
"Africa",
"Asia",
"Europe",
"Latin America",
"North America"
],
"datasets": [
{
"backgroundColor": [
"#3e95cd",
"#8e5ea2",
"#3cba9f",
"#e8c3b9",
"#c45850"
],
"label": "Population (millions)",
"data": [
2478,
5267,
734,
784,
433
]
}
]
},
"options": {
"plugins": {
"legend": {
"display": false
},
"title": {
"text": "Predicted world population (millions) in 2050",
"display": true
}
}
}
}
示例应用程序还有一些其他图表,所有图表都以不同的格式显示相同的数据。它们都由上面所示的同一端点提供服务
@GetMapping("/pops")
@ResponseBody
public Chart bar() {
return new Chart();
}
在Spring指南和参考文档中,我们经常看到代码块按“类型”(例如Maven与Gradle,或XML与Java)进行分割。它们显示一个选项处于活动状态,其余选项隐藏,如果用户点击另一个选项,则不仅会显示最近的代码片段,还会显示文档中所有与点击匹配的代码片段。例如,如果用户点击“Gradle”,则所有引用“Gradle”的代码片段将同时激活。驱动此功能的Javascript存在多种形式,具体取决于使用它的指南或项目,其中一种形式是NPM包@springio/utils。它不严格是一个ESM模块,但我们仍然可以导入它并看到该功能正常工作。以下是它在index.html
中的样子。
<script type="importmap">
{
"imports": {
...
"@springio/utils": "/npm/@springio/utils"
}
}
</script>
<script type="module">
...
import '@springio/utils';
</script>
然后我们可以添加一个新的标签页,其中包含一些“代码片段”(在本例中只是无用内容)。
<div class="tab-pane fade" id="docs" role="tabpanel">
<div class="container" title="Content">
<div class="content primary"><div class="title">One</div><div class="content">Some content</div></div>
<div class="content secondary"><div class="title">Two</div><div class="content">Secondary</div></div>
<div class="content secondary"><div class="title">Three</div><div class="content">Third option</div></div>
</div>
<div class="container" title="Another">
<div class="content primary"><div class="title">One</div><div class="content">Some more content</div></div>
<div class="content secondary"><div class="title">Two</div><div class="content">Secondary stuff</div></div>
<div class="content secondary"><div class="title">Three</div><div class="content">Third option again</div></div>
</div>
</div>
如果用户选择“One”代码块类型,则显示如下。
驱动此行为的是HTML的结构,其中一个元素标记为“primary”,备选方案标记为“secondary”,然后是实际内容之前的嵌套class="title"
。标题由Javascript提取到按钮中。
Vue是一个轻量级的Javascript库,您可以少量或大量使用它。要开始使用Webjars,我们需要pom.xml
中的依赖项。
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>vue</artifactId>
<version>2.6.14</version>
</dependency>
并将其添加到index.html
中的导入映射中(使用手动资源路径,因为NPM包中的“模块”指向浏览器中无法正常工作的内容)。
<script type="importmap">
{
"imports": {
...
"vue": "/npm/vue/dist/vue.esm.browser.js"
}
}
</script>
然后我们可以编写一个组件并在命名元素中“挂载”它(这是Vue用户指南中的一个示例)。
<script type="module">
import Vue from 'vue';
const EventHandling = {
data() {
return {
message: 'Hello Vue.js!'
}
},
methods: {
reverseMessage() {
this.message = this.message
.split('')
.reverse()
.join('')
}
}
}
new Vue(EventHandling).$mount("#event-handling");
</script>
为了接收动态内容,我们需要一个与#event-handling
匹配的元素,例如:
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container" id="event-handling">
<p>{{ message }}</p>
<button class="btn btn-primary" v-on:click="reverseMessage">Reverse Message</button>
</div>
</div>
因此模板化发生在客户端,它由Vue的v-on
触发的点击事件触发。
如果我们想用Vue替换Hotwired,我们可以从主“message”标签页的内容开始。例如,我们可以用这个替换Stimulus控制器绑定。
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container">
<div id="auth">
{{user}}
</div>
<div id="greeting">{{greeting}}</div>
<input id="name" name="value" type="text" v-model="name" />
<button class="btn btn-primary" v-on:click="greet">Greet</button>
</div>
</div>
然后通过Vue连接user
和greeting
属性。
import Vue from 'vue';
const EventHandling = {
data() {
return {
greeting: '',
name: '',
user: 'Unauthenticated'
}
},
created: function () {
let hello = this;
fetch("/user").then(response => {
response.json().then(data => {
hello.user = `Logged in as: ${data.name}`;
});
});
},
methods: {
greet() {
this.greeting = `Hello, ${this.name}!`;
},
}
}
new Vue(EventHandling).$mount("#message");
created
钩子作为Vue组件生命周期的一部分运行,因此它不一定与Stimulus运行的时间完全相同,但足够接近。
我们也可以用Vue替换图表选择器,然后可以去掉Stimulus,看看效果如何。这是图表标签页(基本上与之前相同,但没有控制器装饰)。
<div class="tab-pane fade" id="chart" role="tabpanel">
<div class="container">
<canvas id="canvas"></canvas>
</div>
<div class="container">
<button class="btn btn-primary" v-on:click="clear">Clear</button>
<button class="btn btn-primary" v-on:click="bar">Bar</button>
</div>
</div>
这是渲染图表的Javascript代码。
<script type="module">
import Vue from 'vue';
import { Chart, BarController, BarElement, LinearScale, CategoryScale, Title, Legend } from 'chart.js';
Chart.register(BarController, BarElement, LinearScale, CategoryScale, Title, Legend);
const ChartHandling = {
methods: {
clear() {
if (this.active) {
this.active.destroy();
}
},
bar() {
let chart = this;
this.clear();
fetch("/pops").then(response => {
response.json().then(data => {
data.type = "bar";
chart.active = new Chart(document.getElementById("canvas"), data);
});
});
}
}
}
new Vue(ChartHandling).$mount("#chart");
</script>
示例代码除了“bar”图表类型外,还包含“pie”和“doughnut”,它们的工作方式相同。
Vue可以使用v-html
属性替换元素的整个内部HTML,因此我们可以开始使用它重新实现Turbo内容。这是新的“test”标签页。
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container" id="frame">
<div id="hi" v-html="html"></div>
<button class="btn btn-primary" v-on:click="hello">Fetch</button>
</div>
</div>
它有一个引用“hello”方法的点击处理程序,以及一个等待接收内容的div。我们可以像这样将按钮附加到“hi”容器。
<script type="module">
import Vue from 'vue';
const HelloHandling = {
data: {
html: ''
},
methods: {
hello() {
const handler = this;
fetch("/test").then(response => {
response.text().then(data => {
handler.html = data;
});
});
},
}
}
new Vue(HelloHandling).$mount("#test");
</script>
为了使其正常工作,我们只需要从服务器端模板中删除<turbo-frame/>
元素(恢复到我们在HTMX示例中所拥有的内容)。
用Vue(或其他库,甚至普通的Javascript)替换我们的Turbo(和HTMX)代码绝对是可能的,但我们可以从示例中看到,它不可避免地会涉及一些样板Javascript。