取得領先
VMware 提供培訓和認證,以加速您的進度。
了解更多在這個簡單的 HTML 替換用例中,Vue 並沒有真正增加很多價值,並且對於 SSE 範例來說根本沒有價值,所以我們將繼續使用 Vanilla 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 組合包(無論如何),因此沒有「模組」元數據,我們必須像這樣硬編碼資源路徑。 資源路徑中的「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 應用程式都使用嵌入在 Javascript 中的 HTML,透過一種名為「XJS」的樣板語言(它可以透過其他方式使用,但實際上現在是 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 範本,並且該元件還有一個傳回 XJS 範本的 render()
函數。 大括號用於插值,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
元件並將其附加到不同的元素。 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 中沒有「雙向模型」,你必須將變更監聽器添加到輸入以明確更新狀態。 此外,我們必須在我們想要用作監聽器的所有元件方法上呼叫 bind()
(在本例中為 greet
和 change
)。
要將其餘的 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>;
}
}
React 故意命名 dangerouslySetInnerHTML
屬性,以阻止人們將其與直接從使用者那裡收集的內容一起使用(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
現在我們準備好建置一些東西了,讓我們使用到目前為止在 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
提供 Javsacript 的範例。 這是 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 處理器會尋找與主輸入檔案相同的位置,因此我們可以在 src/main/js
中建立一個 style.scss
並匯入 Bootstrap 程式碼
@import 'bootstrap/scss/bootstrap';
如果我們真的這樣做,SCSS 中的自訂項目會遵循它。 然後在 index.js
中,我們為此新增匯入,以及 Spring 實用程式函式庫
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
範例開始,並如上所述新增前端插件(或者以其他方式取得 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 上的教學課程發布?