使用 Spring Boot 和 React 进行 Bootiful 开发

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(89)   2024-06-13 16:21:42

“我喜欢编写身份验证和授权代码。” ~ 从来没有 Java 开发人员。 厌倦了一遍又一遍地构建相同的登录屏幕? 尝试使用 Okta API 进行托管身份验证、授权和多因素身份验证。

React 在过去几年中获得了很多积极的报道,使其成为对 Java 开发人员有吸引力的前端选择!一旦您了解了它的工作原理,它就会变得很有意义并且开发起来会很有趣。不仅如此,它还非常快!如果您一直在关注我,或者如果您已经阅读了这篇博客,您可能还记得我的使用 Spring Boot 和 Angular 进行 Bootiful Development 教程。今天,我将向您展示如何构建相同的应用程序,但这次使用的是 React。在我们深入探讨之前,让我们更多地谈谈 React 的优点,以及为什么我选择在这篇文章中探索它。

首先,React 不是一个成熟的 Web 框架。它更像是一个用于开发 UI 的工具包,类似于 GWT。如果您想发出 HTTP 请求以从服务器获取数据,React 不会为此提供任何实用程序。但是,它确实有一个庞大 的生态系统,提供了许多库和组件。我所说的巨大是什么意思?这么说吧:根据 npmjs.comAngular 有 17,938 个包。 React 的数量几乎是三倍,达到 42,428!

Angular 是我的好朋友,已经很久了。我不会放弃我的老朋友而采用 React。我只是在结交新朋友。有很多不同背景和观点的朋友对一个人的观点是有好处的!

这篇文章展示了如何将 UI 和 API 构建为单独的应用程序。您将学习如何使用 Spring MVC 创建 REST 端点,配置 Spring Boot 以允许 CORS,以及创建 React 应用程序来显示其数据。此应用程序将显示来自 API 的啤酒列表,然后从 GIPHY 获取与啤酒名称匹配的 GIF。我还将向您展示如何集成 Okta 及其 OpenID Connect (OIDC) 支持以锁定您的 API 并向您的 UI 添加身份验证。

让我们开始吧!

使用 Spring Boot 构建 API

注意:下面关于构建 Spring Boot API 的说明与使用 Spring Boot 和 Angular 进行 Bootiful 开发中的说明相同。为了您的方便,我将它们复制在下面。

要开始使用 Spring Boot,请导航至 start.spring.io。在“搜索依赖项”字段中,选择以下内容:

  • H2:内存数据库
  • JPA:Java 的标准 ORM
  • Rest Repositories:允许您将 JPA 存储库公开为 REST 端点
  • Web:带有 Jackson(用于 JSON)、Hibernate Validator 和嵌入式 Tomcat 的 Spring MVC

如果您更喜欢命令行,可以使用以下命令通过 HTTPie 下载 demo.zip 文件。

http https://start.spring.io/starter.zip \
dependencies==h2,data-jpa,data-rest,web -d

创建一个名为 spring-boot-react-example 的目录,其中包含一个 server 目录。将 demo.zip 的内容展开到 server 目录中。

在您喜欢的 IDE 中打开“服务器”项目并运行 DemoApplication 或使用 ./mvnw spring-boot:run 从命令行启动它。

在其中创建一个com.example.demo.beer 包和一个Beer.java 文件。此类将是保存您的数据的实体。

package com.example.demo.beer;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Beer {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    public Beer() {}

    public Beer(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Beer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

添加一个利用 Spring Data 对此实体执行 CRUD 的 BeerRepository 类。

package com.example.demo.beer;

import org.springframework.data.jpa.repository.JpaRepository;

interface BeerRepository extends JpaRepository<Beer, Long> {
}

添加一个使用此存储库并创建一组默认数据的 BeerCommandLineRunner

package com.example.demo.beer;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.stream.Stream;

@Component
public class BeerCommandLineRunner implements CommandLineRunner {

    private final BeerRepository repository;

    public BeerCommandLineRunner(BeerRepository repository) {
        this.repository = repository;
    }

    @Override
    public void run(String... strings) throws Exception {
        // Top beers from https://www.beeradvocate.com/lists/top/
        Stream.of("Kentucky Brunch Brand Stout", "Good Morning", "Very Hazy", "King Julius",
                "Budweiser", "Coors Light", "PBR").forEach(name ->
                repository.save(new Beer(name))
        );
        repository.findAll().forEach(System.out::println);
    }
}

重建您的项目,您应该会在终端中看到打印的啤酒列表。

添加一个 @RepositoryRestResource 注释到 BeerRepository 来暴露它所有的 CRUD作为 REST 端点的操作。

import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
interface BeerRepository extends JpaRepository<Beer, Long> {
}

添加一个 BeerController 类来创建一个端点来过滤掉不太好的啤酒。

package com.example.demo.beer;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
public class BeerController {
    private BeerRepository repository;

    public BeerController(BeerRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/good-beers")
    public Collection<Beer> goodBeers() {
        return repository.findAll().stream()
                .filter(this::isGreat)
                .collect(Collectors.toList());
    }

    private boolean isGreat(Beer beer) {
        return !beer.getName().equals("Budweiser") &&
                !beer.getName().equals("Coors Light") &&
                !beer.getName().equals("PBR");
    }
}

重新构建您的应用程序并导航到 http://localhost:8080/good-beers。您应该会在浏览器中看到好啤酒列表。

使用 HTTPie 时,您还应该在终端窗口中看到相同的结果。

http localhost:8080/good-beers

使用 Create React App 创建项目

如今,创建 API 似乎很容易,这在很大程度上要归功于 Spring Boot。在本节中,我希望向您展示使用 React 创建 UI 也非常简单。如果您按照以下步骤操作,您将创建一个新的 React 应用程序,从 API 获取啤酒名称和图像,并创建组件来显示数据。

要创建 React 项目,请确保您已安装 Node.jsCreate React AppYarn

npm install -g create-react-app@1.4.3

在终端窗口中,cd 进入 spring-boot-react-example 目录的根目录并运行以下命令。此命令将创建一个支持 TypeScript 的新 React 应用程序。

create-react-app client --scripts-version=react-scripts-ts

此过程运行后,您将拥有一个新的 client 目录,其中安装了所有必需的依赖项。要验证一切正常,请进入 client 目录并运行 yarn start。如果一切正常,您应该会在浏览器中看到以下内容。

到目前为止,您已经创建了一个 good-beers API 和一个 React 应用程序,但是您还没有创建 UI 来显示 API 中的啤酒列表。为此,打开 client/src/App.tsx 并添加一个 componentDidMount() 方法。

componentDidMount() {
  this.setState({isLoading: true});

  fetch('http://localhost:8080/good-beers')
    .then(response => response.json())
    .then(data => this.setState({beers: data, isLoading: false}));
}

React 的组件生命周期 将调用 componentDidMount() 方法。上面的代码使用了 fetch,这是 XMLHttpRequest 的现代替代品。根据 caniuse.com,它在大多数浏览器中都受支持

您可以看到它使用响应数据设置了 beers 状态。要初始化此组件的状态,您需要重写构造函数。

constructor(props: any) {
  super(props);

  this.state = {
    beers: [],
    isLoading: false
  };
}

为此,您需要将参数类型添加到类签名中。下面的代码显示了此时您的 App 类的顶部应该是什么样子。

class App extends React.Component<{}, any> {
  constructor(props: any) {
    super(props);

    this.state = {
      beers: [],
      isLoading: false
    };
  }
  // componentDidMount() and render()
}

更改 render() 方法以具有以下 JSX。 JSX 是 Facebook 的类 XML 语法,它通过 JavaScript 呈现 HTML。

render() {
  const {beers, isLoading} = this.state;

  if (isLoading) {
    return <p>Loading...</p>;
  }

  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>Welcome to React</h2>
      </div>
      <div>
        <h2>Beer List</h2>
        {beers.map((beer: any) =>
          <div key={beer.id}>
            {beer.name}
          </div>
        )}
      </div>
    </div>
  );
}

如果您在浏览器中查看 http://localhost:3000,您会看到一条“正在加载...”消息。如果您查看浏览器的控制台,您可能会看到有关 CORS 的问题。

Failed to load http://localhost:8080/good-beers: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access.

要解决此问题,您需要配置 Spring Boot 以允许从 http://localhost:3000 进行跨域访问。

为 Spring Boot 配置 CORS

在服务器项目中,打开 server/src/main/java/com/example/demo/beer/BeerController.java 并添加一个 @CrossOrigin 注释以从客户端 (http://localhost:3000) 启用跨域资源共享 (CORS)。

import org.springframework.web.bind.annotation.CrossOrigin;
...
    @GetMapping("/good-beers")
    @CrossOrigin(origins = "http://localhost:3000")
    public Collection goodBeers() {

进行这些更改后,重新启动服务器,刷新浏览器,您应该能够从 Spring Boot API 中看到啤酒列表。

创建一个 BeerList 组件

为了使此应用程序更易于维护,将啤酒列表的提取和呈现从 App.tsx 移至它自己的 BeerList 组件。创建 src/BeerList.tsx 并用 App.tsx 中的代码填充它。

import * as React from 'react';

class BeerList extends React.Component<{}, any> {
  constructor(props: any) {
    super(props);

    this.state = {
      beers: [],
      isLoading: false
    };
  }

  componentDidMount() {
    this.setState({isLoading: true});

    fetch('http://localhost:8080/good-beers')
      .then(response => response.json())
      .then(data => this.setState({beers: data, isLoading: false}));
  }

  render() {
    const {beers, isLoading} = this.state;

    if (isLoading) {
      return <p>Loading...</p>;
    }

    return (
      <div>
        <h2>Beer List</h2>
        {beers.map((beer: any) =>
          <div key={beer.id}>
            {beer.name}
          </div>
        )}
      </div>
    );
  }
}

export default BeerList;

然后更改 client/src/App.tsx,使其只包含一个 shell 和对 <BeerList/> 的引用。

import * as React from 'react';
import './App.css';
import BeerList from './BeerList';

const logo = require('./logo.svg');

class App extends React.Component<{}, any> {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo"/>
          <h2>Welcome to React</h2>
        </div>
        <BeerList/>
      </div>
    );
  }
}

export default App;

创建 GiphyImage 组件

为了让它看起来更好一点,添加一个 GIPHY 组件来根据啤酒的名称获取图像。创建 client/src/GiphyImage.tsx 并将以下代码放入其中。

import * as React from 'react';

interface GiphyImageProps {
  name: string;
}

class GiphyImage extends React.Component<GiphyImageProps, any> {
  constructor(props: GiphyImageProps) {
    super(props);

    this.state = {
      giphyUrl: '',
      isLoading: false
    };
  }

  componentDidMount() {
    const giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&limit=1&q=';

    fetch(giphyApi + this.props.name)
      .then(response => response.json())
      .then(response => {
        if (response.data.length > 0) {
          this.setState({giphyUrl: response.data[0].images.original.url});
        } else {
          // dancing cat for no images found
          this.setState({giphyUrl: '//media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif'});
        }
        this.setState({isLoading: false});
      });
  }

  render() {
    const {giphyUrl, isLoading} = this.state;

    if (isLoading) {
      return <p>Loading image...</p>;
    }

    return (
      <img src={giphyUrl} alt={this.props.name} width="200"/>
    );
  }
}

export default GiphyImage;

更改 render() 中的 BeerList.tsx 方法以使用此组件。

import GiphyImage from './GiphyImage';
...
render() {
  const {beers, isLoading} = this.state;

  if (isLoading) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <h2>Beer List</h2>
      {beers.map((beer: any) =>
        <div key={beer.id}>
          {beer.name}<br/>
          <GiphyImage name={beer.name}/>
        </div>
      )}
    </div>
  );
}

结果应该类似于以下带有图像的啤酒名称列表。

您刚刚创建了一个使用跨域请求与 Spring Boot API 对话的 React 应用程序。恭喜!

添加 PWA 支持

Create React App 支持开箱即用的渐进式 Web 应用程序 (PWA)。要了解它是如何集成的,请打开 client/README.md 并搜索“Making a Progressive Web App”。

要查看它是如何工作的,请在 yarn build 目录中运行 client。此命令完成后,您将看到如下消息。

The build folder is ready to be deployed.
You may serve it with a static server:

  yarn global add serve
  serve -s build

运行建议的命令,您应该能够打开浏览器查看 http://localhost:5000。您的浏览器可能会在其控制台中显示 CORS 错误,因此再次打开 BeerController.java 并调整其允许的来源以允许端口 5000。

@CrossOrigin(origins = {"http://localhost:3000", "http://localhost:5000"})

重新启动服务器,http://localhost:5000 应该加载啤酒名称和图像。

我在 Chrome 中运行了一个 Lighthouse 审核,发现这个应用此时只得到了 73/100 的分数。

您会在上面的屏幕截图中注意到“Manifest does not have icons at least 512px”。这听起来很容易修复。您可以从此页面 下载一个 512 像素的免费啤酒图标。

注意:这个图标是由 www.flaticon.comFreepik 制作的。它由 CC 3.0 BY 授权。

将下载的 beer.png 复制到 client/public。修改 client/public/manifest.json 以具有此应用程序特定的名称,并添加 512 像素的图标。

{
  "short_name": "Beer",
  "name": "Good Beer",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "beer.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

进行此更改后,我能够为 PWA 获得 82 的 Lighthouse 分数。这份报告中最突出的抱怨是我没有使用 HTTPS。为了查看该应用在使用 HTTPS 时的得分情况,我将其部署到 Pivotal Cloud FoundryHeroku。我很高兴地发现它在两个平台上都获得了?。

要阅读我用来部署所有内容的脚本,请参阅 cloudfoundry.sh< code class="highlighter-rouge" style="font-size: 13px;">heroku.sh 在本文的配套 GitHub 存储库中。非常感谢 @starbuxman@codefinger 帮助创建它们!

使用 Okta 添加身份验证

你可能会想,“这太酷了,很容易理解为什么人们会爱上 React。”尝试过后,您可能会爱上另一个工具:使用 Okta 进行身份验证!为什么是奥克塔?因为您可以免费获得 7,000 名每月活跃用户!值得一试,尤其是当您看到将身份验证添加到 Spring Boot 和使用 Okta 进行 React 是多么容易时。

Okta Spring Boot 启动器

要锁定后端,您可以使用 Okta 的 Spring Boot Starter。要集成此启动器,请将以下依赖项添加到 server/pom.xml

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>0.2.0</version>
</dependency>

您还需要添加一个 <dependencyManagement> 部分来升级 Spring Security 的 OAuth 支持。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
    </dependencies>
</dependencyManagement>

注意: Okta 的 Spring Boot starter 存在一个问题,它无法与 Spring Boot 的 DevTools 一起使用。

现在您需要配置服务器以使用 Okta 进行身份验证。为此,您需要在 Okta 中创建一个 OIDC 应用程序。

在 Okta 中创建 OIDC 应用程序

登录您的 Okta 开发者帐户(如果您没有帐户,请注册)并导航至应用程序 > 添加应用程序。点击Single-Page App,点击Next,然后为该应用命名一个您可以记住的名称。将 localhost:8080 的所有实例更改为 localhost:3000 并单击Done

将客户端 ID 复制到您的 server/src/main/resources/application.properties 文件中。当您在那里时,添加一个与您的 Okta 域匹配的 okta.oauth2.issuer 属性。例如:

okta.oauth2.issuer=https://{yourOktaDomain}.com/oauth2/default
okta.oauth2.clientId={clientId}

注意:{yourOktaDomain} 的值应该类似于dev-123456.oktapreview.com。确保您没有在值中包含 -admin

更新 server/src/main/java/com/okta/developer/demo/DemoApplication.java 以将其启用为资源服务器。

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@EnableResourceServer
@SpringBootApplication

进行这些更改后,您应该能够重新启动服务器并在尝试导航到 http://localhost:8080 时看到访问被拒绝。

Okta 的 React 支持

Okta 的 React SDK 允许您将 OIDC 集成到 React 应用程序中。您可以在 npmjs.com 上找到,了解有关 Okta 的 React SDK 的更多信息。要安装,请运行以下命令:

yarn add @okta/okta-react react-router-dom
yarn add -D @types/react-router-dom

Okta 的 React SDK 依赖于 react-router,因此安装 react-router-dom 的原因。在 client/src/App.tsx 中配置路由是一种常见的做法,因此将其代码替换为下面使用 Okta 设置身份验证的 TypeScript。

import * as React from 'react';
import './App.css';
import Home from './Home';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Security, ImplicitCallback } from '@okta/okta-react';

const config = {
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  redirectUri: window.location.origin + '/implicit/callback',
  clientId: '{clientId}'
};

export interface Auth {
  login(): {};
  logout(): {};
  isAuthenticated(): boolean;
  getAccessToken(): string;
}

class App extends React.Component {

  render() {
    return (
      <Router>
        <Security
          issuer={config.issuer}
          client_id={config.clientId}
          redirect_uri={config.redirectUri}
        >
          <Route path="/" exact={true} component={Home}/>
          <Route path="/implicit/callback" component={ImplicitCallback}/>
        </Security>
      </Router>
    );
  }
}

export default App;

创建 client/src/Home.tsx 以包含 App.tsx 以前包含的应用程序外壳。此类呈现应用程序外壳、登录/注销按钮和 <BeerList/>(如果您已通过身份验证)。

import * as React from 'react';
import './App.css';
import BeerList from './BeerList';
import { withAuth } from '@okta/okta-react';
import { Auth } from './App';

const logo = require('./logo.svg');

interface HomeProps {
  auth: Auth;
}

interface HomeState {
  authenticated: boolean;
}

export default withAuth(class Home extends React.Component<HomeProps, HomeState> {
  constructor(props: HomeProps) {
    super(props);
    this.state = {authenticated: false};
    this.checkAuthentication = this.checkAuthentication.bind(this);
    this.checkAuthentication();
  }

  async checkAuthentication() {
    const isAuthenticated = await this.props.auth.isAuthenticated();
    const {authenticated} = this.state;
    if (isAuthenticated !== authenticated) {
      this.setState({authenticated: isAuthenticated});
    }
  }

  componentDidUpdate() {
    this.checkAuthentication();
  }

  render() {
    const {authenticated} = this.state;
    let body = null;
    if (authenticated) {
      body = (
        <div className="Buttons">
          <button onClick={this.props.auth.logout}>Logout</button>
          <BeerList auth={this.props.auth}/>
        </div>
      );
    } else {
      body = (
        <div className="Buttons">
          <button onClick={this.props.auth.login}>Login</button>
        </div>
      );
    }

    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo"/>
          <h2>Welcome to React</h2>
        </div>
        {body}
      </div>
    );
  }
});

如果您在浏览器中查看您的 React 应用程序,您可能会看到如下错误:

./src/Home.tsx
(4,26): error TS7016: Could not find a declaration file for module '@okta/okta-react'.
'/Users/mraible/spring-boot-react-example/client/node_modules/@okta/okta-react/dist/index.js'
implicitly has an 'any' type.
 Try `npm install @types/@okta/okta-react` if it exists or add a new declaration (.d.ts) file
 containing `declare module '@okta/okta-react';`

使用以下声明创建 client/src/okta.d.ts 以解决此问题。

declare module '@okta/okta-react';

重新启动客户端,您会看到 BeerList 组件需要做一些工作。

./src/Home.tsx
(44,21): error TS2339: Property 'auth' does not exist on type 'IntrinsicAttributes &
IntrinsicClassAttributes<BeerList> & Readonly<{ children?: ReactNode; }> & ...'.

client/src/BeerList.tsx 中,通过创建传递到类签名中的 auth 接口,将 BeerListProps 属性添加到 props。

import { Auth } from './App';

interface BeerListProps {
  auth: Auth;
}

interface BeerListState {
  beers: Array<{}>;
  isLoading: boolean;
}

class BeerList extends React.Component<BeerListProps, BeerListState> {
  ...
}

将以下 CSS 规则添加到 client/src/App.css 以使登录/注销按钮更加明显。

.Buttons {
  margin-top: 10px;
}

.Buttons button {
  font-size: 1em;
}

您的浏览器 shoa,检查以下重新。

单击按钮登录时,输入用于创建 Okta Developer 帐户的电子邮件和密码。当它将您重定向回您的应用程序时,您可能会在浏览器的控制台中看到“正在加载...”和 CORS 错误。

发生此错误是因为 Spring 的 @CrossOrigin 不能很好地与 Spring Security 配合使用。为了解决这个问题,在simpleCorsFilter 的body 中添加一个DemoApplication.java bean。

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Arrays;
import java.util.Collections;

@EnableResourceServer
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean simpleCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:5000"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

要使其全部在客户端上运行,请修改 componentDidMount() 中的 client/src/BeerList.tsx 方法以设置授权标头。

async componentDidMount() {
  this.setState({isLoading: true});

  try {
    const response = await fetch('http://localhost:8080/good-beers', {
      headers: {
        Authorization: 'Bearer ' + await this.props.auth.getAccessToken()
    }
  });
    const data = await response.json();
    this.setState({beers: data, isLoading: false});
  } catch (err) {
    this.setState({error: err});
  }
}

您还需要在 error 接口中添加 BeerListState

interface BeerListState {
  beers: Array<{}>;
  isLoading: boolean;
  error: string;
}

更改构造函数,使其将 error 初始化为空字符串。

this.state = {
  beers: [],
  isLoading: false,
  error: ''
};

然后更改 render() 方法以在发生错误时显示错误。

render() {
  const {beers, isLoading, error} = this.state;

  if (isLoading) {
    return <p>Loading ...</p>;
  }

  if (error.length > 0) {
    return <p>Error: {error}</p>;
  }

  return (...)
}

现在您应该能够以经过身份验证的用户身份查看啤酒列表。

如果有效,恭喜!

清理那些 TypeScript 警告

您可能会注意到浏览器的控制台报告了一些 TypeScript 警告。

./src/BeerList.tsx
[16, 22]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise
type, the empty type ('{}'), or suppress this occurrence.
[52, 27]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise
type, the empty type ('{}'), or suppress this occurrence.
./src/GiphyImage.tsx
[7, 59]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise
type, the empty type ('{}'), or suppress this occurrence.

要解决第一个问题,请更改 client/src/BeerList.tsx 使其构造函数如下所示:

constructor(props: BeerListProps) {
  ...
}

第二期,在Beer中创建一个client/src/BeerList.tsx接口。将它放在顶部的其他接口旁边。

interface Beer {
  id: number;
  name: string;
}

然后将 {beers.map((beer: any) => 更改为 {beers.map((beer: Beer) =>

第三个问题可以通过在 GiphyImageState 中创建一个新的 client/src/GiphyImage.tsx 接口来定义状态属性来解决。

interface GiphyImageState {
  giphyUrl: string;
  isLoading: boolean;
}

class GiphyImage extends React.Component<GiphyImageProps, GiphyImageState> {
  ...
}

进行这些更改后,您应该摆脱 TypeScript 警告。

了解有关 Spring Boot 和 React 的更多信息

要了解有关 React、Spring Boot 或 Okta 的更多信息,请查看以下资源:

  • Eric Vicenti 的 React Workshop 简介 – 强烈推荐学习 React!
  • 我在比利时 Devoxx 与 Deepu K SasidharanAngular vs React Smackdown 谈话
  • 如何在 React 中获取数据 by Robin Wieruch
  • 在 15 分钟内构建带有用户身份验证的 React 应用程序
  • 构建带有身份验证的 Preact 应用
  • 使用 Okta 的 React SDK 创建自定义登录表单

您可以在 GitHub 上找到与本文相关的源代码。主要示例(无身份验证)在 master 分支中,而 Okta 集成在 okta 分支中。要签出本地计算机上的 Okta 分支,请运行以下命令。

git clone git@github.com:oktadeveloper/spring-boot-react-example.git
git checkout okta

如果您发现任何问题,请在下面添加评论,我会尽力提供帮助。如果您喜欢本教程,我很乐意让您在 Twitter 上关注我。要获得更多此类文章的通知,请关注 @oktadev

“我喜欢编写身份验证和授权代码。” ~ 从来没有 Java 开发人员。 厌倦了一遍又一遍地构建相同的登录屏幕? 尝试使用 Okta API 进行托管身份验证、授权和多因素身份验证。

标签2: Java教程
地址:https://www.cundage.com/article/jcg-bootiful-development-spring-boot-react.html

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...