领先一步
VMware 提供培训和认证,助你飞速进步。
了解更多在这个简单的 HTML 替换用例中,Vue 并没有真正增加太多价值,对于 SSE 示例也完全没有价值,因此我们将直接使用原生 Javascript 来实现。下面是一个 stream 选项卡
<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 bundle(至少目前还没有),因此没有“module”元数据,我们不得不硬编码资源路径,就像这样。资源路径中的“umd”指的是“Universal Module Definition”,这是一个较早期的 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”的模板语言(它可以用在其他方面,但现在实际上是 React 的一部分)嵌入到 Javascript 中的 HTML。上面的 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
是一个包含自定义元素所有属性的 map(所以这里是“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
组件,并将其附加到另一个元素上。message 选项卡可以简化为一个空的元素,准备好接受 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 中没有“双向绑定模型”,你必须为输入框添加 change 监听器来显式更新状态。此外,我们必须对所有我们想用作监听器的组件方法(本例中的 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 示例中的 chart 模块设置(它在 <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
像这样在本地安装时,有一个快速的方法从命令行运行它非常有用。所以一旦你有了 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
现在我们准备好构建一些东西了,让我们用之前 Webjars 中所有的依赖项来设置 package.json
{
"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 来打包他们的代码,所以这看起来是个不错的选择。它做得非常好的一点是“tree shaking”,可以减少你需要随应用程序一起发布的 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 bundle,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 bundle 中,这样你就可以根据自己的需求扩展和调整 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 处理器查找的位置与主输入文件相同,所以我们只需在 src/main/js
中创建一个 style.scss
并导入 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
,我们用它来告诉 Babel 如何构建 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 作为教程?