領先一步
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 作為教程?