领先一步
VMware 提供培训和认证,助您加速进步。
了解更多在此简单的 HTML 替换用例中,Vue 并没有带来太多价值,并且对于 SSE 示例来说,它完全没有价值,因此我们将继续使用原生 JavaScript 来实现。这是一个流标签
<div class="tab-pane fade" id="stream" role="tabpanel">
<div class="container">
<div id="load"></div>
</div>
</div>
以及用于填充它的一些 JavaScript
<script type="module">
var events = new EventSource("/stream");
events.onmessage = e => {
document.getElementById("load").innerHTML = e.data;
}
</script>
大多数使用 React 的人可能做的不仅仅是少量逻辑,最终会将所有布局和渲染都放在 JavaScript 中。你不必这样做,并且很容易只使用一点点 React 来体验它。你可以停留在那里,将其用作实用程序库,或者你可以发展到完整的 JavaScript 客户端组件方法。
我们可以在不改变太多的情况下开始尝试。如果您想预先了解,示例代码最终会看起来像 react-webjars 示例。首先是 pom.xml 中的依赖项
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>react</artifactId>
<version>17.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>react-dom</artifactId>
<version>17.0.2</version>
</dependency>
以及 index.html 中的模块映射
<script type="importmap">
{
"imports": {
...
"react": "/npm/react/umd/react.development.js",
"react-dom": "/npm/react-dom/umd/react-dom.development.js"
}
}
</script>
React 目前(至少现在还不是)不打包为 ESM 包,因此没有“module”元数据,我们必须像这样硬编码资源路径。“umd”资源路径是指“通用模块定义”,这是一个较旧的模块化 JavaScript 尝试。它足够接近,只要你眯着眼睛,就可以用类似的方式使用它。
在这些就位后,您可以导入它们定义的函数和对象
<script type="module">
import * as React from 'react';
import * as ReactDOM from 'react-dom';
</script>
因为它们不是真正的 ESM 模块,所以您可以在 HTML <head/> 中的 <script/> 中“全局”级别执行此操作,例如,我们在那里导入 bootstrap。然后,您可以通过创建 React.Component 来定义一些内容。这是一个非常基础的静态示例
<script type="module">
const e = React.createElement;
class RootComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return e(
'h1',
{},
'Hello, world!'
);
}
}
ReactDOM.render(e(RootComponent), document.querySelector('#root'));
</script>
render() 方法返回一个创建新 DOM 元素的函数(一个带有“Hello, world!”内容的 <h1/>)。它由 ReactDOM 附加到 ID 为“root”的元素上,因此我们最好也在“test”标签中添加一个,例如
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container" id="root"></div>
</div>
如果运行它,它应该可以工作,并且在该标签中应该会显示“Hello World”。
大多数 React 应用程序使用通过名为“XJS”的模板语言嵌入到 JavaScript 中的 HTML(它也可以用于其他方式,但实际上是 React 的一部分)。上面的“hello world”示例看起来像这样
<script type="text/babel">
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
ReactDOM.render(
<Hello name="World"/>,
document.getElementById('root')
);
</script>
该组件定义了一个与组件类名匹配的自定义元素 <Hello/>,并且通常以大写字母开头。<Hello/> 片段是一个 XJS 模板,并且组件还有一个 render() 函数,该函数返回一个 XJS 模板。花括号用于插值,props 是一个包含自定义元素所有属性的映射(在本例中为“name”)。最后是那个 <script type="text/babel">,它需要转译 XJS 成浏览器可以理解的实际 JavaScript。在浏览器学会识别此脚本之前,上面的脚本将什么都不做。我们通过导入另一个模块来实现这一点
<script type="importmap">
{
"imports": {
...
"react": "/npm/react/umd/react.development.js",
"react-dom": "/npm/react-dom/umd/react-dom.development.js",
"@babel/standalone": "/npm/@babel/standalone"
}
}
</script>
<script type="module">
...
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import '@babel/standalone';
</script>
React 用户指南建议在大型应用程序中避免使用 @babel/standalone,因为它需要在浏览器中做大量工作,而这些工作可以在构建时一次完成,效率更高。但它非常适合尝试事物,以及用于少量 React 代码的应用程序,例如这个。
现在我们可以将主要的“message”标签迁移到 React。所以让我们修改 Hello 组件并将其附加到另一个元素。消息标签可以简化为一个空元素,准备好接受 React 内容
<div class="tab-pane fade show active" id="message" role="tabpanel">
<div class="container" id="hello"></div>
</div>
我们可以预见需要第二个组件来渲染已认证的用户名,所以让我们从这里开始,将一些代码附加到上面标签中的元素
ReactDOM.render(
<div className="container" id="hello">
<Auth/>
<Hello/>
</div>,
document.getElementById('hello')
);
然后我们可以这样定义 Auth 组件
class Auth extends React.Component {
constructor(props) {
super(props);
this.state = { user: 'Unauthenticated' };
};
componentDidMount() {
let hello = this;
fetch("/user").then(response => {
response.json().then(data => {
hello.setState({user: `Logged in as: ${data.name}`});
});
});
};
render() {
return <div id="auth">{this.state.user}</div>;
}
};
在这种情况下,生命周期回调是 componentDidMount,当组件被激活时,React 会调用它,所以这就是我们放置初始化代码的地方。
另一个组件是将“name”输入传输到问候语的组件
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = { name: '', message: '' };
this.greet = this.greet.bind(this);
this.change = this.change.bind(this);
};
greet() {
this.setState({message: `Hello ${this.state.name}!`})
}
change(event) {
console.log(event)
this.setState({name: event.target.value})
}
render() {
return <div>
<div id="greeting">{this.state.message}</div>
<input id="name" name="value" type="text" value={this.state.name} onChange={this.change}/>
<button className="btn btn-primary" onClick={this.greet}>Greet</button>
</div>;
}
}
render() 方法必须返回单个元素,因此我们必须将内容包装在 <div> 中。值得指出的另一件事是,状态从 HTML 到 JavaScript 的传输不是自动的——React 中没有“双向模型”,您必须向输入添加更改监听器以显式更新状态。此外,我们必须在所有我们想用作监听器的组件方法(在这种情况下是 greet 和 change)上调用 bind()。
要将其余的 Stimulus 内容迁移到 React,我们需要编写一个新的图表选择器。所以我们可以从一个空的“chart”标签开始
<div class="tab-pane fade" id="chart" role="tabpanel" data-controller="chart">
<div class="container">
<canvas id="canvas"></canvas>
</div>
<div class="container" id="chooser"></div>
</div>
并将一个 ReactDOM 元素附加到“chooser”
ReactDOM.render(
<ChartChooser/>,
document.getElementById('chooser')
);
ChartChooser 是封装在组件中的按钮列表
class ChartChooser extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.clear = this.clear.bind(this);
this.bar = this.bar.bind(this);
};
bar() {
let chart = this;
this.clear();
fetch("/pops").then(response => {
response.json().then(data => {
data.type = "bar";
chart.setState({ active: new Chart(document.getElementById("canvas"), data) });
});
});
};
clear() {
if (this.state.active) {
this.state.active.destroy();
}
};
render() {
return <div>
<button className="btn btn-primary" onClick={this.clear}>Clear</button>
<button className="btn btn-primary" onClick={this.bar}>Bar</button>
</div>;
}
}
我们还需要 Vue 示例中的图表模块设置(它在 <script type="text/babel"> 中不起作用)
<script type="module">
import { Chart, BarController, BarElement, LinearScale, CategoryScale, Title, Legend } from 'chart.js';
Chart.register(BarController, BarElement, LinearScale, CategoryScale, Title, Legend);
window.Chart = Chart;
</script>
Chart.js 未打包成您可以导入到 Babel 脚本中的形式。我们将其导入到一个单独的模块中,并且 Chart 必须定义为一个全局变量,以便我们仍然可以在 React 组件中使用它。
要使用 React 渲染“test”标签,我们可以从标签本身开始,再次为空,以接受来自 React 的内容
<div class="tab-pane fade" id="test" role="tabpanel">
<div class="container" id="root"></div>
</div>
并将一个绑定到 React 中的“root”元素
ReactDOM.render(
<Content />,
document.getElementById('root')
);
然后我们可以实现 <Content/> 作为从 /test 端点获取 HTML 的组件
class Content extends React.Component {
constructor(props) {
super(props);
this.state = { html: '' };
this.fetch = this.fetch.bind(this);
};
fetch() {
let hello = this;
fetch("/test").then(response => {
response.text().then(data => {
hello.setState({ html: data });
});
});
}
render() {
return <div>
<div dangerouslySetInnerHTML={{ __html: this.state.html }}></div>
<button className="btn btn-primary" onClick={this.fetch}>Fetch</button>
</div>;
}
}
dangerouslySetInnerHTML 属性是 React 故意命名的,以劝阻人们将其与直接从用户收集的内容一起使用(XSS 问题)。但我们从服务器获取该内容,因此我们可以信任那里的 XSS 保护,并忽略警告。
如果我们使用那个 <Content/> 组件和上面示例中的 SSE 加载器,那么我们可以完全摆脱这个示例中的 Hotwired。
Webjars 很好,但有时您需要更接近 JavaScript。对某些人来说,Webjars 的一个问题是 jar 的大小——Bootstrap jar 将近 2MB,其中大部分在运行时永远不会被使用——而 JavaScript 工具非常注重通过不将整个 NPM 模块打包到您的应用程序中来减小这种开销,并且还通过将资产打包在一起,使它们能够高效下载。Java 工具链也存在一些问题——特别是关于 Sass,正如我们最近在 Petclinic 中发现的那样,缺乏良好的工具。所以也许我们应该看看使用 Node.js 工具链进行构建的选项。
您需要的第一件事是 Node.js。获得它的方法有很多,您可以根据自己的喜好使用任何工具。我们将展示如何使用 Frontend Plugin 来完成。
让我们将插件添加到 turbo 示例中。(如果您想预先了解,最终结果是 nodejs 示例)在 pom.xml 中
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.0</version>
<executions>
<execution>
<id>install-node-and-npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v16.13.1</nodeVersion>
</configuration>
</execution>
<execution>
<id>npm-install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm-build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run-script build</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
...
</plugins>COPY
这里我们有 3 个执行:install-node-and-npm 安装本地 Node.js 和 NPM,npm-install 运行 npm install,npm-build 运行一个脚本来构建 JavaScript,可能还有 CSS。我们需要一个最小的 package.json 来运行所有这些。如果您已安装 npm,您可以运行 npm init 来生成一个新的,或者只需手动创建它
$ cat > package.json
{
"scripts": { "build": "echo Building"}
}
然后我们可以构建
$ ./mvnw generate-resources
您将看到结果是一个新目录
$ ls -d node*
node
当像这样本地安装 npm 时,从命令行快速运行 npm 会很有用。因此,一旦您有了 Node.js,您可以通过在本地创建脚本来使其更轻松
$ cat > npm
#!/bin/sh
cd $(dirname $0)
PATH="$PWD/node/":$PATH
node "node/node_modules/npm/bin/npm-cli.js" "$@"
使其可执行并进行尝试
$ chmod +x npm
$ ./npm install
up to date, audited 1 package in 211ms
found 0 vulnerabilities
现在我们准备构建一些东西了,让我们在 package.json 中设置到目前为止我们在 Webjars 中使用的所有依赖项
{
"name": "js-demo",
"version": "0.0.1",
"dependencies": {
"@hotwired/stimulus": "^3.0.1",
"@hotwired/turbo": "^7.1.0",
"@popperjs/core": "^2.10.1",
"bootstrap": "^5.1.3",
"chart.js": "^3.6.0",
"@springio/utils": "^1.0.5",
"es-module-shims": "^1.3.0"
},
"scripts": {
"build": "echo Building"
}
}
运行 ./npm install(或 ./mvnw generate-resources)会将这些依赖项下载到 node_modules
$ ./npm install
added 7 packages, and audited 8 packages in 8s
2 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
$ ls node_modules/
@hotwired @popperjs @springio bootstrap chart.js es-module-shims
将所有下载和生成的文件添加到您的 .gitignore 是可以的(即 node/、node_modules/ 和 package-lock.json)。
Bootstrap 维护者使用 Rollup 来打包他们的代码,所以这似乎是一个不错的选择。它做得非常出色的一点是“树抖动”,以减少您需要随应用程序一起分发的 JavaScript 量。随时尝试其他工具。要开始使用 Rollup,我们需要在 package.json 中添加一些开发依赖项和一个新的构建脚本
{
...
"devDependencies": {
"rollup": "^2.60.2",
"rollup-plugin-node-resolve": "^2.0.0"
},
"scripts": {
"build": "rollup -c"
}
}
Rollup 有自己的配置文件,所以这里有一个将本地 JavaScript 源打包到应用程序中并在运行时从 /index.js 提供 JavaScript 的配置文件。这是 rollup.config.js
import resolve from 'rollup-plugin-node-resolve';
export default {
input: 'src/main/js/index.js',
output: {
file: 'target/classes/static/index.js',
format: 'esm'
},
plugins: [
resolve({
esm: true,
main: true,
browser: true
})
]
};
所以如果我们把所有的 JavaScript 都移到 src/main/js/index.js,那么在 index.html 中我们就只有一个 <script> 标签,例如在 <body> 的末尾
<script type="module">
import '/index.js';
</script>
我们暂时保留 CSS,以后再处理本地构建。所以,在 index.js 中,我们将所有 <script> 标签的内容混合在一起(或者我们可以将其分成模块并导入它们)
import 'bootstrap';
import '@hotwired/turbo';
import '@springio/utils';
import { Application, Controller } from '@hotwired/stimulus';
import { Chart, BarController, BarElement, PieController, ArcElement, LinearScale, ategoryScale, Title, Legend } from 'chart.js';
Turbo.connectStreamSource(new EventSource("/stream"))
window.Stimulus = Application.start();
Chart.register(BarController, BarElement, PieController, ArcElement, LinearScale, CategoryScale, itle, Legend);
Stimulus.register("hello", class extends Controller {
...
});
Stimulus.register("chart", class extends Controller {
...
});
如果我们构建并运行应用程序,它应该都能正常工作,Rollup 会在 target/classes/static 中创建一个新的 index.js,可执行 JAR 将会拾取它。由于 Rollup 中的“resolve”插件的作用,新的 index.js 包含了运行我们的应用程序所需的所有代码。如果任何依赖项打包为真正的 ESM 包,Rollup 将能够将其未使用的部分摇出来。这至少对 Hotwired Stimulus 有效,而大多数其他依赖项被整体包含,但结果仍然只有 750K(大部分是 Bootstrap)
$ ls -l target/classes/static/index.js
-rw-r--r-- 1 dsyer dsyer 768778 Dec 14 09:34 target/classes/static/index.js
浏览器只需要下载一次,这在服务器是 HTTP 1.1 时是一个优势(HTTP 2 会稍有改变),并且它意味着可执行 JAR 不会因为永远用不到的东西而臃肿。Rollup 还有其他插件选项可以压缩 JavaScript,我们将在下一节中看到其中一些。
到目前为止,我们使用了包含在某些 NPM 库中的纯 CSS。大多数应用程序都需要自己的样式表,并且开发人员更喜欢使用某种形式的模板库和构建时工具来编译成 CSS。最普遍的此类工具(但不是唯一的工具)是 Sass。Bootstrap 使用它,并且确实在其 NPM 包中打包了其源文件,因此您可以根据自己的需求扩展和调整 Bootstrap 样式。
我们可以看看它是如何工作的,通过构建我们应用程序的 CSS,即使我们不做太多自定义。从 NPM 中的一些工具依赖项开始
$ ./npm install --save-dev rollup-plugin-scss rollup-plugin-postcss sass
这导致 package.json 中出现了一些新条目
{
...
"devDependencies": {
"rollup": "^2.60.2",
"rollup-plugin-node-resolve": "^2.0.0",
"rollup-plugin-postcss": "^0.2.0",
"rollup-plugin-scss": "^3.0.0",
"sass": "^1.44.0"
},
...
}
这意味着我们可以更新我们的 rollup.config.js 来使用新工具
import resolve from "rollup-plugin-node-resolve";
import scss from "rollup-plugin-scss";
import postcss from "rollup-plugin-postcss";
export default {
input: "src/main/js/index.js",
output: {
file: "target/classes/static/index.js",
format: "esm",
},
plugins: [
resolve({
esm: true,
main: true,
browser: true,
}),
scss(),
postcss(),
],
};
CSS 处理器在与主输入文件相同的位置查找,因此我们可以创建一个 style.scss 文件在 src/main/js 中并导入 Bootstrap 代码
@import 'bootstrap/scss/bootstrap';
如果我们要实际进行 SCSS 自定义,那么自定义将会跟在后面。然后在 index.js 中,我们添加对它和 Spring utils 库的导入
import './style.scss';
import '@springio/utils/style.css';
...
然后重新构建。这将创建一个新的 index.css 文件(与主输入 JavaScript 文件名相同),然后我们可以将其链接到 index.html 的 <head> 中
<head>
...
<link rel="stylesheet" type="text/css" href="index.css" />
</head>COPY
就是这样。我们有一个 index.js 脚本驱动我们 Turbo 示例的所有 JavaScript 和 CSS,现在我们可以删除 pom.xml 中所有剩余的 Webjars 依赖项。
最后,我们可以将相同的思路应用于 react-webjars 示例,删除 Webjars 并将 JavaScript 和 CSS 提取到单独的源文件中。这样,我们也可以最终摆脱稍微有点问题的 @babel/standalone。我们可以从 react-webjars 示例开始,并添加上面介绍的 Frontend Plugin(或以其他方式获取 Node.js),然后手动或通过 npm CLI 创建一个 package.json。我们将需要 React 依赖项,以及 Babel 的构建时工具。这是我们最终得到的结果
{
"name": "js-demo",
"version": "0.0.1",
"dependencies": {
"@popperjs/core": "^2.10.1",
"@springio/utils": "^1.0.4",
"bootstrap": "^5.1.3",
"chart.js": "^3.6.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"@babel/preset-react": "^7.16.0",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-replace": "^3.0.0",
"postcss": "^8.4.5",
"rollup": "^2.60.2",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-scss": "^3.0.0",
"sass": "^1.44.0",
"styled-jsx": "^4.0.1"
},
"scripts": {
"build": "rollup -c"
}
}
我们需要 commonjs 插件,因为 React 没有打包为 ESM,没有它导入将无法工作。Babel 工具附带一个配置文件 .babelrc,我们使用它来告诉它构建 JSX 和 React 组件
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["styled-jsx/babel"]
}
在这些构建工具就位后,我们可以将 index.html 中的所有 JavaScript 提取出来并将其放入 src/main/resources/static/index.js。这几乎是复制粘贴,但我们需要添加 CSS 导入
import './style.scss';
import '@springio/utils/style.css';
以及来自 React 的导入如下所示
import React from 'react';
import ReactDOM from 'react-dom';
您可以使用 npm run build(或 ./mvnw generate-resources)来构建它,它应该可以工作——所有标签都有一些内容,所有按钮都会生成一些内容。
最后,我们只需要清理 index.html,使其仅导入 index.js 和 index.css,然后 Webjars 项目的所有功能都应该按预期工作。
客户端开发有许多可用的选择,而 Spring Boot 在其中任何一个上都没有太多影响,因此您可以自由选择最适合您的。本文的范围必然有限(我们不可能从各个角度查看所有内容),但希望能够突出一些有趣的可能。我个人在最近用它做了一些小型项目后,发现自己非常喜欢 HTMX,但正如您所知,您的体验可能会有所不同。请在博客上评论或通过 Github 或愤怒的小鸟应用程序发送反馈——听听大家的想法会很有趣。例如,我们是否应该在 spring.io 上将本文作为教程发布?