发布于 2026-01-06 0 阅读
0

使用 Python、Flask 和 React 构建一个简单的 CRUD 应用

使用 Python、Flask 和 React 构建一个简单的 CRUD 应用

如今的现代 Web 应用程序通常使用服务器端语言构建,通过 API 提供数据,并使用前端 JavaScript 框架将数据以易于使用的方式呈现给最终用户。Python 是一种动态语言,被众多公司和开发者广泛采用。该语言的核心价值观是软件应该简洁易读,从而提高开发者的工作效率和满意度。您还将使用 Flask 来帮助您快速构建 REST API。React 是 Facebook 开发的一个声明式、高效且灵活的 JavaScript 库,用于构建用户界面。它能够使用称为组件的小型独立代码块创建复杂、交互式且有状态的 UI。

在本教程中,我们将使用 React 构建一个 JavaScript 应用程序作为前端,并构建一个用 Python 编写的持久化 REST API。我们的应用程序将是一个 GitHub 开源书签项目(也称为书签kudo)。

要完成本教程,您需要准备以下几样东西:

首先,你需要创建后端。

使用 Python 创建 REST API

请确保您已安装 Python 3。运行以下命令检查已安装的 Python 版本:

python --version

Enter fullscreen mode Exit fullscreen mode

要安装 Python 3,您可以使用pyenv.

如果您使用的是 macOS,则可以使用 Homebrew 安装:

brew update
brew install pyenv

Enter fullscreen mode Exit fullscreen mode

在 Linux 系统上使用 bash shell 时:

curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash

Enter fullscreen mode Exit fullscreen mode

安装完成后,您可以运行以下命令来安装 Python 3。

pyenv install 3.6.3
pyenv global 3.6.3

Enter fullscreen mode Exit fullscreen mode

您的 REST API 将使用一些第三方代码(库)来帮助您(例如,连接数据库、创建模型模式以及验证传入请求是否已通过身份验证)。Python 有一个强大的工具来管理依赖项,称为 。pipenv要将其安装pipenv到您的计算机上,请按照以下步骤操作:

在 macOS 系统上:

brew install pipenv

Enter fullscreen mode Exit fullscreen mode
pip install --user pipenv

Enter fullscreen mode Exit fullscreen mode

安装完成后pipenv,创建一个目录来存放后端代码:

mkdir kudos_oss && cd kudos_oss

Enter fullscreen mode Exit fullscreen mode

上述命令将创建一个 Python 3 虚拟环境。现在,您可以通过运行以下命令来安装 Flask:

pipenv install flask==1.0.2

Enter fullscreen mode Exit fullscreen mode

Python 3 提供了一些很棒的功能,例如absolute_import…… print_function,你将在本教程中使用到它们。要导入它们,请运行以下命令:

touch __init__.py
touch __main__.py

Enter fullscreen mode Exit fullscreen mode

将以下内容复制并粘贴到__main__.py文件中:

from __future__ import absolute_import, print_function

Enter fullscreen mode Exit fullscreen mode

您的后端需要实现以下用户故事:

  • 作为已认证用户,我想收藏一个 GitHub 开源项目。
  • 作为已认证用户,我想取消收藏一个 GitHub 开源项目。
  • 作为已认证用户,我想列出我之前收藏的所有已添加到书签的 GitHub 开源项目。

标准的 REST API 会公开端点,以便客户端可以访问create、调用updatedelete更新readlist all获取资源。在本节结束时,您的后端应用程序将能够处理以下 HTTP 调用:

# For the authenticated user, fetches all favorited github open source projects
GET /kudos

# Favorite a github open source project for the authenticated user
POST /kudos

# Unfavorite a favorited github open source project
DELETE /kudos/:id

Enter fullscreen mode Exit fullscreen mode

定义Python模型模式

您的 REST API 将有两个核心模式,分别是GithubRepoSchemaKudoSchemaGithubRepoSchema将表示客户端发送的 Github 存储库,而KudoSchema将表示您将在数据库中持久化的数据。

请运行以下命令:

mkdir -p app/kudo
touch app/kudo/schema.py
touch app/kudo/service.py
touch app/kudo/ __init__.py

Enter fullscreen mode Exit fullscreen mode

上述命令将创建一个app目录,该目录下包含一个名为 . 的子目录kudo。然后,第二个命令将创建三个文件:schema.pyservice.py__init__.py

请将以下内容复制并粘贴到schema.py文件中:

from marshmallow import Schema, fields

class GithubRepoSchema(Schema):
  id = fields.Int(required=True)
  repo_name = fields.Str()
  full_name = fields.Str()
  language = fields.Str()
  description = fields.Str()
  repo_url = fields.URL()

class KudoSchema(GithubRepoSchema):
  user_id = fields.Email(required=True)

Enter fullscreen mode Exit fullscreen mode

您可能已经注意到,这些模式继承自marshmallow 库Schema中的一个包。Marshmallow 是一个与 ORM/ODM/框架无关的库,用于将复杂数据类型(例如对象)序列化/反序列化为原生 Python 数据类型,反之亦然。

marshmallow运行以下命令安装库:

pipenv install marshmallow==2.16.3

Enter fullscreen mode Exit fullscreen mode

使用 MongoDB 实现 Python REST API 持久化

太棒了!你现在已经创建好了第一个文件。这些模式用于表示传入的请求数据以及你的应用程序在 MongoDB 中持久化的数据。为了连接数据库并执行查询,你将使用 MongoDB 官方创建和维护的名为pymongo的库。

pymongo运行以下命令安装库:

pipenv install pymongo==3.7.2

Enter fullscreen mode Exit fullscreen mode

你可以使用本地机器上已安装的 MongoDB,也可以使用 Docker 启动一个 MongoDB 容器。本教程假设你已经安装了 Docker 和 docker-compose。

docker-compose我们会为您管理 MongoDB 容器。

创造docker-compose.yml

touch docker-compose.yml

Enter fullscreen mode Exit fullscreen mode

将以下内容粘贴到其中:

version: '3'
services:
  mongo:
    image: mongo
    restart: always
    ports:
     - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: mongo_user
      MONGO_INITDB_ROOT_PASSWORD: mongo_secret

Enter fullscreen mode Exit fullscreen mode

现在您只需执行以下操作即可启动 MongoDB 容器:

docker-compose up

Enter fullscreen mode Exit fullscreen mode

MongoDB 启动并运行后,就可以开始编写MongoRepository类了。最好让类只承担单一职责,这样后端应用程序中唯一需要显式处理 MongoDB 的地方就是该类MongoRepository

首先创建一个目录,用于存放所有与持久化相关的文件,例如:repository

mkdir -p app/repository

Enter fullscreen mode Exit fullscreen mode

然后,创建用于存放 MongoRepository 类的文件:

touch app/repository/mongo.py
touch app/repository/ __init__.py

Enter fullscreen mode Exit fullscreen mode

pymongo正确安装并启动 MongoDB 后,将以下内容粘贴到app/repository/mongo.py文件中。

import os
from pymongo import MongoClient

COLLECTION_NAME = 'kudos'

class MongoRepository(object):
 def __init__ (self):
   mongo_url = os.environ.get('MONGO_URL')
   self.db = MongoClient(mongo_url).kudos

 def find_all(self, selector):
   return self.db.kudos.find(selector)

 def find(self, selector):
   return self.db.kudos.find_one(selector)

 def create(self, kudo):
   return self.db.kudos.insert_one(kudo)

 def update(self, selector, kudo):
   return self.db.kudos.replace_one(selector, kudo).modified_count

 def delete(self, selector):
   return self.db.kudos.delete_one(selector).deleted_count

Enter fullscreen mode Exit fullscreen mode

如您所见,该类非常简单,它在初始化时创建一个数据库连接,然后将其保存到一个实例变量中MongoRepository供后续方法使用:find_all、、、、。请注意,所有方法都显式地使用了 pymongo API。findcreateupdatedelete

您可能已经注意到,该类MongoRepository会读取一个环境变量MONGO_URL。要导出该环境变量,请运行:

export MONGO_URL=mongodb://mongo_user:mongo_secret@0.0.0.0:27017/

Enter fullscreen mode Exit fullscreen mode

考虑到将来可能需要使用其他数据库,将应用程序与 MongoDB 解耦是一个好主意。为了简单起见,我们将创建一个抽象类来表示一个数据库Repository;整个应用程序都应该使用这个类。

将以下内容粘贴到app/repository/ __init__.py文件中:

class Repository(object):
 def __init__ (self, adapter=None):
   self.client = adapter()

 def find_all(self, selector):
   return self.client.find_all(selector)

 def find(self, selector):
   return self.client.find(selector)

 def create(self, kudo):
   return self.client.create(kudo)

 def update(self, selector, kudo):
   return self.client.update(selector, kudo)

 def delete(self, selector):
   return self.client.delete(selector)

Enter fullscreen mode Exit fullscreen mode

你可能还记得,你正在开发的用户故事是:已认证用户应该能够创建、删除和列出所有收藏的 GitHub 开源项目。为了实现这一点,这些MongoRepository方法将非常有用。

您很快就要实现 REST API 的端点。首先,您需要创建一个服务类,该类能够将传入的请求负载转换为我们KudoSchema在 `<object>` 中定义的表示形式。传入的请求负载(由 `<object>` 表示)与您在数据库中持久化的对象(由 `<object>` 表示)app/kudo/schema.py之间的区别在于:前者有一个 ` <object>` ,它决定了对象的所有者。GithubSchemaKudoSchemauser_Id

将以下内容复制到app/kudo/service.py文件中:

from ..repository import Repository
from ..repository.mongo import MongoRepository
from .schema import KudoSchema

class Service(object):
 def __init__ (self, user_id, repo_client=Repository(adapter=MongoRepository)):
   self.repo_client = repo_client
   self.user_id = user_id

   if not user_id:
     raise Exception("user id not provided")

 def find_all_kudos(self):
   kudos = self.repo_client.find_all({'user_id': self.user_id})
   return [self.dump(kudo) for kudo in kudos]

 def find_kudo(self, repo_id):
   kudo = self.repo_client.find({'user_id': self.user_id, 'repo_id': repo_id})
   return self.dump(kudo)

 def create_kudo_for(self, githubRepo):
   self.repo_client.create(self.prepare_kudo(githubRepo))
   return self.dump(githubRepo.data)

 def update_kudo_with(self, repo_id, githubRepo):
   records_affected = self.repo_client.update({'user_id': self.user_id, 'repo_id': repo_id}, self.prepare_kudo(githubRepo))
   return records_affected > 0

 def delete_kudo_for(self, repo_id):
   records_affected = self.repo_client.delete({'user_id': self.user_id, 'repo_id': repo_id})
   return records_affected > 0

 def dump(self, data):
   return KudoSchema(exclude=['_id']).dump(data).data

 def prepare_kudo(self, githubRepo):
   data = githubRepo.data
   data['user_id'] = self.user_id
   return data

Enter fullscreen mode Exit fullscreen mode

请注意,您的构造函数接收 `The`和 `The`__init__作为参数,它们将在该服务的所有操作中使用。这就是使用类来表示存储库的优势所在。就服务而言,它并不关心 `The`是将数据持久化到 MongoDB、PostgreSQL,还是通过网络将数据发送到第三方服务 API,它只需要知道 `The` 是一个已配置适配器的实例,该适配器实现了诸如`get`、` get` 和 `get` 之类的方法user_idrepo_clientrepo_clientrepo_clientRepositorycreatedeletefind_all

定义您的 REST API 中间件

至此,后端开发已完成 70%。现在可以着手实现 HTTP 端点和 JWT 中间件,它们将保护您的 REST API 免受未经身份验证的请求攻击。

您可以先创建一个目录,用于存放与 HTTP 相关的文件。

mkdir -p app/http/api

Enter fullscreen mode Exit fullscreen mode

在这个目录下,您将看到两个文件,endpoints.py分别是 和middlewares.py。要创建它们,请运行以下命令:

touch app/http/api/ __init__.py
touch app/http/api/endpoints.py
touch app/http/api/middlewares.py

Enter fullscreen mode Exit fullscreen mode

发送到您的 REST API 的请求均采用 JWT 身份验证,这意味着您需要确保每个请求都包含有效的JSON Web Token。JWTpyjwt将自动处理验证。要安装它,请运行以下命令

pipenv install pyjwt==1.7.1

Enter fullscreen mode Exit fullscreen mode

现在您已经了解了 JWT 中间件的作用,接下来需要编写它。请将以下内容粘贴到middlewares.py文件中。

from functools import wraps
from flask import request, g, abort
from jwt import decode, exceptions
import json

def login_required(f):
   @wraps(f)
   def wrap(*args, **kwargs):
       authorization = request.headers.get("authorization", None)
       if not authorization:
           return json.dumps({'error': 'no authorization token provied'}), 403, {'Content-type': 'application/json'}

       try:
           token = authorization.split(' ')[1]
           resp = decode(token, None, verify=False, algorithms=['HS256'])
           g.user = resp['sub']
       except exceptions.DecodeError as identifier:
           return json.dumps({'error': 'invalid authorization token'}), 403, {'Content-type': 'application/json'}

       return f(*args, **kwargs)

   return wrap

Enter fullscreen mode Exit fullscreen mode

Flask 提供了一个名为 `context` 的模块g,它是一个在请求生命周期中共享的全局上下文。该中间件会检查请求是否有效。如果有效,中间件会提取已认证用户的详细信息并将其持久化到全局上下文中。

定义您的 REST API 端点

现在 HTTP 处理程序应该很容易了,因为你已经完成了重要的部分,现在只需要把所有东西组合在一起。

由于您的最终目标是创建一个可在 Web 浏览器上运行的 JavaScript 应用程序,因此您需要确保 Web 浏览器在执行预检请求时能够正常工作,您可以点击此处了解更多信息。为了在您的 REST API 中实现 CORS,您需要安装flask_cors……

pipenv install flask_cors==3.0.7

Enter fullscreen mode Exit fullscreen mode

接下来,实现你的接口。请将上面的内容粘贴到app/http/api/endpoints.py文件中。

from .middlewares import login_required
from flask import Flask, json, g, request
from app.kudo.service import Service as Kudo
from app.kudo.schema import GithubRepoSchema
from flask_cors import CORS

app = Flask( __name__ )
CORS(app)

@app.route("/kudos", methods=["GET"])
@login_required
def index():
 return json_response(Kudo(g.user).find_all_kudos())

@app.route("/kudos", methods=["POST"])
@login_required
def create():
   github_repo = GithubRepoSchema().load(json.loads(request.data))

   if github_repo.errors:
     return json_response({'error': github_repo.errors}, 422)

   kudo = Kudo(g.user).create_kudo_for(github_repo)
   return json_response(kudo)

@app.route("/kudo/<int:repo_id>", methods=["GET"])
@login_required
def show(repo_id):
 kudo = Kudo(g.user).find_kudo(repo_id)

 if kudo:
   return json_response(kudo)
 else:
   return json_response({'error': 'kudo not found'}, 404)

@app.route("/kudo/<int:repo_id>", methods=["PUT"])
@login_required
def update(repo_id):
   github_repo = GithubRepoSchema().load(json.loads(request.data))

   if github_repo.errors:
     return json_response({'error': github_repo.errors}, 422)

   kudo_service = Kudo(g.user)
   if kudo_service.update_kudo_with(repo_id, github_repo):
     return json_response(github_repo.data)
   else:
     return json_response({'error': 'kudo not found'}, 404)


@app.route("/kudo/<int:repo_id>", methods=["DELETE"])
@login_required
def delete(repo_id):
 kudo_service = Kudo(g.user)
 if kudo_service.delete_kudo_for(repo_id):
   return json_response({})
 else:
   return json_response({'error': 'kudo not found'}, 404)

def json_response(payload, status=200):
 return (json.dumps(payload), status, {'content-type': 'application/json'})

Enter fullscreen mode Exit fullscreen mode

太棒了!一切就绪!现在你应该可以使用以下命令运行你的 REST API:

FLASK_APP=$PWD/app/http/api/endpoints.py FLASK_ENV=development pipenv run python -m flask run --port 4433

Enter fullscreen mode Exit fullscreen mode

创建 React 客户端应用程序

要创建您的 React 客户端应用程序,您将使用 Facebook 的这款出色create-react-app工具来绕过所有 webpack 的麻烦。

安装create-react-app很简单。本教程将使用[此处应填写具体工具名称] yarn。请确保您已安装[此处应填写具体工具名称],或者使用您偏好的依赖管理器。

要安装create-react-app,请运行以下命令:

yarn global add create-react-app

Enter fullscreen mode Exit fullscreen mode

你需要一个目录来放置你的 React 应用程序,请在文件夹web内创建该目录pkg/http

mkdir -p app/http/web

Enter fullscreen mode Exit fullscreen mode

现在,创建一个 React 应用程序:

cd app/http/web
create-react-app app

Enter fullscreen mode Exit fullscreen mode

create-react-app生成样板应用程序可能需要几分钟时间。请转到刚刚创建的app目录并运行npm start

默认情况下,生成的 React 应用create-react-app会监听 3000 端口。让我们将其更改为监听 8080 端口。

修改start文件中的命令app/http/web/app/package.json,使其使用正确的端口。

启动脚本

然后运行 ​​React 应用。

cd app
npm start

Enter fullscreen mode Exit fullscreen mode

运行此命令npm start将启动一个监听 8080 端口的 Web 服务器。http://localhost:8080/在浏览器中打开该服务器。浏览器应该会加载 React 并渲染由该命令自动创建的 App.js 组件create-react-app

React 应用首次运行

你现在的目标是使用Material Design创建一个简洁美观的用户界面。幸运的是,React 社区创建了https://material-ui.com/,它基本上是将 Material Design 的概念转化为 React 组件。

运行以下命令安装 Material Design 所需的组件。

yarn add @material-ui/core
yarn add @material-ui/icons

Enter fullscreen mode Exit fullscreen mode

太好了,现在你已经有了诸如 Grid、Card、Icon、AppBar 等众多组件,可以导入并使用了。你很快就会用到它们。接下来我们来谈谈受保护的路由。

使用 Okta 为您的 React 应用添加身份验证

编写安全的用户身份验证和构建登录页面很容易出错,甚至可能导致新项目的失败。Okta让能够轻松快速地实现所有用户管理功能。立即注册一个免费的开发者帐户,并在 Okta 中创建一个 OpenID Connect 应用程序,即可开始使用。

Okta注册

登录后,点击“添加应用程序”创建新应用程序。

添加应用程序

选择单页应用平台选项。

选择水疗应用程序

默认应用程序设置应与图中所示相同。

Okta 水疗设置

太好了!OIDC 应用部署完成后,您现在可以继续推进,保护那些需要身份验证的路由。

创建你的 React 路由

React Router是最常用的 URL 路由库,用于将 URL 路由到 React 组件。React Router 提供了一系列组件,可以帮助用户在应用程序中进行导航。

你的 React 应用将有两个路由:

/根路由不需要用户登录,它实际上是应用程序的首页。用户应该能够访问此页面进行登录。您将使用Okta React SDK将 react-router 与 Okta 的 OpenID Connect API 集成。

/homeHome 路由将渲染应用程序中大部分 React 组件。它应该实现以下用户故事。

已认证用户应能够通过 GitHub API 搜索其偏好的开源项目。已认证用户应能够收藏其喜欢的开源项目。已认证用户应能够在不同的标签页中查看其先前收藏的开源项目和搜索结果。

安装方法:react-router运行以下命令:

yarn add react-router-dom

Enter fullscreen mode Exit fullscreen mode

要安装 Okta React SDK,请运行以下命令:

yarn add @okta/okta-react

Enter fullscreen mode Exit fullscreen mode

现在,请开始创建您的主组件:

mkdir -p src/Main

Enter fullscreen mode Exit fullscreen mode

然后,在主目录中创建一个名为index.js:的文件

touch src/Main/index.js

Enter fullscreen mode Exit fullscreen mode

然后将以下内容粘贴到刚刚创建的文件中:

import React, { Component } from 'react';
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom'
import { Security, ImplicitCallback, SecureRoute } from '@okta/okta-react';

import Login from '../Login'
import Home from '../Home'

class Main extends Component {
 render() {
   return (
     <Router>
       <Security
         issuer={yourOktaDomain}
         client_id={yourClientId}
         redirect_uri={'http://localhost:8080/implicit/callback'}
         scope={['openid', 'profile', 'email']}>

         <Switch>
           <Route exact path="/" component={Login} />
           <Route path="/implicit/callback" component={ImplicitCallback} />
           <SecureRoute path="/home" component={Home} />
         </Switch>
       </Security>
     </Router>
   );
 }
}

export default Main;

Enter fullscreen mode Exit fullscreen mode

暂时不用担心 ` Homeand`Login组件,你很快就会用到它们。现在先专注于 ` Security, SecureRoute` 和 `and`ImplicitCallback组件。

为了使 React 中的路由正常工作,你需要将整个应用程序包裹在一个路由器组件中。同样地,为了允许在应用程序的任何位置访问身份验证,你需要将应用程序包裹在SecurityOkta 提供的组件中。Okta 也需要访问路由器,因此该Security组件应该嵌套在路由器内部。

对于需要身份验证的路由,您将使用 Okta 组件进行定义SecureRoute。如果未经身份验证的用户尝试访问该路由/home,他/她将被重定向到/根路由。

ImplicitCallback组件是 Okta 完成登录过程后用户将被重定向到的路由/URI 目标位置。

请继续修改src/index.js以挂载您的主组件。

import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from 'react-router-dom'
import { createBrowserHistory } from 'history'

import Main from './Main';

const history = createBrowserHistory();

ReactDOM.render((
  <Router history={history}>
    <Main history={history} />
  </Router>
), document.getElementById('root'))

Enter fullscreen mode Exit fullscreen mode

现在您可以创建登录组件了。如前所述,所有用户(不仅限于已认证用户)都可以访问此组件。登录组件的主要目标是验证用户身份。

在目录下app,你会找到一个名为src`source` 的目录。接下来,创建一个名为 `Login` 的目录。

mkdir -p src/Login

Enter fullscreen mode Exit fullscreen mode

然后,在登录目录中创建一个名为index.js.

touch src/Login/index.js

Enter fullscreen mode Exit fullscreen mode

然后将以下内容粘贴到文件中:

import React from 'react'
import Button from '@material-ui/core/Button';
import { Redirect } from 'react-router-dom'
import { withAuth } from '@okta/okta-react';

class Login extends React.Component {
 constructor(props) {
   super(props);
   this.state = { authenticated: null };
   this.checkAuthentication = this.checkAuthentication.bind(this);
   this.login = this.login.bind(this);
 }

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

 async componentDidMount() {
   this.checkAuthentication()
 }

 async login(e) {
   this.props.auth.login('/home');
 }

 render() {
   if (this.state.authenticated) {
     return <Redirect to='/home' />
   } else {
     return (
       <div style={{height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
         <Button variant="contained" color="primary" onClick={this.login}>Login with Okta</Button>
       </div>
     )
   }
 }
}

export default withAuth(Login);

Enter fullscreen mode Exit fullscreen mode

要让登录页面正常工作,您需要为 Home 组件创建一个占位符。

请创建一个名为Home

mkdir -p src/Home

Enter fullscreen mode Exit fullscreen mode

然后,在该目录下创建一个名为index.js:的文件

touch src/Home/index.js

Enter fullscreen mode Exit fullscreen mode

然后将以下内容粘贴到其中:

import React from 'react'

const home = (props) => {
  return (
    <div>Home</div>
  )
};

export default home;

Enter fullscreen mode Exit fullscreen mode

现在尝试运行npm starthttp://localhost:8080在浏览器中打开。您应该会看到以下页面。

登录按钮

在登录组件中,您使用 Okta React SDK 来检查用户是否已登录。如果用户已登录,则应将其重定向到相应的/home路由;否则,用户可点击Login With Okta重定向到 Okta,进行身份验证,然后进入首页。

登录页面

目前首页是空白的,但最终你希望首页看起来像这样:

首页

Home 组件由 Material Design 组件(如TabAppBarButton和 )Icon以及一些您需要创建的自定义组件组成。

您的应用需要列出所有已收藏的开源项目以及搜索结果。如上图所示,“首页”组件使用标签页将已收藏的开源项目与搜索结果分开显示。第一个标签页列出用户收藏的所有开源项目,第二个标签页则列出搜索结果。

您可以创建一个组件来表示“赞”和“搜索结果”列表中的开源项目,这就是 React 组件的魅力所在:它们非常灵活且可重用。

请创建一个名为GithubRepo

mkdir -p src/GithubRepo

Enter fullscreen mode Exit fullscreen mode

然后,在该目录下创建一个名为index.js:的文件

touch src/GithubRepo/index.js

Enter fullscreen mode Exit fullscreen mode

然后将以下内容粘贴到其中:

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import FavoriteIcon from '@material-ui/icons/Favorite';

const styles = theme => ({
  card: {
    maxWidth: 400,
  },
  media: {
    height: 0,
    paddingTop: '56.25%', // 16:9
  },
  actions: {
    display: 'flex',
  }
});

class GithubRepo extends React.Component {
  handleClick = (event) => {
    this.props.onKudo(this.props.repo)
  }

  render() {
    const { classes } = this.props;

    return (
      <Card className={classes.card}>
        <CardHeader
          title={this.props.repo.full_name}
        />
        <CardContent>
          <Typography component="p" style={{minHeight: '90px', overflow: 'scroll'}}>
            {this.props.repo.description}
          </Typography>
        </CardContent>
        <CardActions className={classes.actions} disableActionSpacing>
          <IconButton aria-label="Add to favorites" onClick={this.handleClick}>
            <FavoriteIcon color={this.props.isKudo ? "secondary" : "primary"} />
          </IconButton>
        </CardActions>
      </Card>
    );
  }
}

export default withStyles(styles)(GithubRepo);

Enter fullscreen mode Exit fullscreen mode

GithubRepo是一个非常简单的组件,它接收两个参数props:一个repo对象,其中包含对 Github 存储库的引用和一个isKudo布尔标志,该标志指示该存储库是否repo已被添加到书签。

你需要的下一个组件是SearchBar。它将承担两个职责:注销用户和在每次按下Enter搜索文本字段中的键时调用 React。

创建一个名为SearchBar

mkdir -p src/SearchBar

Enter fullscreen mode Exit fullscreen mode

然后,在该目录中创建一个名为index.js

touch src/SearchBar/index.js

Enter fullscreen mode Exit fullscreen mode

粘贴以下内容:

import React from 'react';
import PropTypes from 'prop-types';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import InputBase from '@material-ui/core/InputBase';
import Button from '@material-ui/core/Button';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import SearchIcon from '@material-ui/icons/Search';
import { withAuth } from '@okta/okta-react';

const styles = theme => ({
  root: {
    width: '100%',
  },
  MuiAppBar: {
    alignItems: 'center'
  },
  grow: {
    flexGrow: 1,
  },
  title: {
    display: 'none',
    [theme.breakpoints.up('sm')]: {
      display: 'block',
    },
  },
  search: {
    position: 'relative',
    borderRadius: theme.shape.borderRadius,
    backgroundColor: fade(theme.palette.common.white, 0.15),
    '&:hover': {
      backgroundColor: fade(theme.palette.common.white, 0.25),
    },
    marginRight: theme.spacing.unit * 2,
    marginLeft: 0,
    width: '100%',
    [theme.breakpoints.up('sm')]: {
      marginLeft: theme.spacing.unit * 3,
      width: 'auto',
    },
  },
  searchIcon: {
    width: theme.spacing.unit * 9,
    height: '100%',
    position: 'absolute',
    pointerEvents: 'none',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  inputRoot: {
    color: 'inherit',
    width: '100%',
  },
  inputInput: {
    paddingTop: theme.spacing.unit,
    paddingRight: theme.spacing.unit,
    paddingBottom: theme.spacing.unit,
    paddingLeft: theme.spacing.unit * 10,
    transition: theme.transitions.create('width'),
    width: '100%',
    [theme.breakpoints.up('md')]: {
      width: 400,
    },
  },
  toolbar: {
    alignItems: 'center'
  }
});

class SearchBar extends React.Component {
  constructor(props) {
    super(props);
    this.logout = this.logout.bind(this);
  }

  async logout(e) {
    e.preventDefault();
    this.props.auth.logout('/');
  }

  render() {
    const { classes } = this.props;

    return (
      <div className={classes.root}>
        <AppBar position="static" style={{alignItems: 'center'}}>
          <Toolbar>
            <div className={classes.search}>
              <div className={classes.searchIcon}>
                <SearchIcon />
              </div>
              <InputBase
                placeholder="Search for your OOS project on Github + Press Enter"
                onKeyPress={this.props.onSearch}
                classes={{
                  root: classes.inputRoot,
                  input: classes.inputInput,
                }}
              />
            </div>
            <div className={classes.grow} />
            <Button onClick={this.logout} color="inherit">Logout</Button>
          </Toolbar>
        </AppBar>
      </div>
    );
  }
}

SearchBar.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(withAuth(SearchBar));

Enter fullscreen mode Exit fullscreen mode

SearchBar组件接收一个prop名为onSearch`is` 的函数,该函数应keyPress在搜索文本输入中触发的每个事件中调用。

SearchBar使用了withAuthOkta React SDK 提供的辅助函数,该函数会将auth对象注入到props组件中。该auth对象有一个名为 `delete` 的方法logout,可以清除会话中所有与用户相关的数据。这正是您注销用户所需要的。

现在该开始编写Home组件了。该组件的一个依赖项是react-swipeable-views库,该库会在用户切换标签页时添加漂亮的动画效果。

要安装 react-swipeable-views,请运行以下命令:

yarn add react-swipeable-views

Enter fullscreen mode Exit fullscreen mode

您还需要向 Python REST API 和 GitHub REST API 发送 HTTP 请求。GitHub HTTP 客户端需要提供一个方法或函数来向以下 URL 发送请求:https://api.github.com/search/repositories?q=USER-QUERY。您将使用q查询字符串来传递用户想要查询的 GitHub 代码库的关键词。

githubClient.js创建一个名为“ .”的文件

touch src/githubClient.js

Enter fullscreen mode Exit fullscreen mode

请将以下内容粘贴到其中:

export default {
 getJSONRepos(query) {
   return fetch('https://api.github.com/search/repositories?q=' + query).then(response => response.json());
 }
}

Enter fullscreen mode Exit fullscreen mode

现在,你需要创建一个 HTTP 客户端,用于向你在本教程第一部分中实现的 Python REST API 发出 HTTP 请求。由于所有发送到 Python REST API 的请求都需要用户进行身份验证,因此你需要Authorization使用accessTokenOkta 提供的 HTTP 标头进行设置。

请创建一个名为“.”的文件apiClient.js

touch src/apiClient.js

Enter fullscreen mode Exit fullscreen mode

安装后axios即可帮助您向 Flask API 执行 HTTP 调用。

yarn add axios

Enter fullscreen mode Exit fullscreen mode

然后,粘贴以下内容:

import axios from 'axios';

const BASE_URI = 'http://localhost:4433';

const client = axios.create({
 baseURL: BASE_URI,
 json: true
});

class APIClient {
 constructor(accessToken) {
   this.accessToken = accessToken;
 }

 createKudo(repo) {
   return this.perform('post', '/kudos', repo);
 }

 deleteKudo(repo) {
   return this.perform('delete', `/kudos/${repo.id}`);
 }

 getKudos() {
   return this.perform('get', '/kudos');
 }

 async perform (method, resource, data) {
   return client({
     method,
     url: resource,
     data,
     headers: {
       Authorization: `Bearer ${this.accessToken}`
     }
   }).then(resp => {
     return resp.data ? resp.data : [];
   })
 }
}

export default APIClient;

Enter fullscreen mode Exit fullscreen mode

太棒了!您APIClient的方法perform是将用户的令牌添加accessTokenAuthorization每个请求的 HTTP 标头中,这意味着它会对每个请求进行身份验证。当服务器收到这些 HTTP 请求时,您的 Okta 中间件将能够验证令牌并从中提取用户详细信息。

通常情况下,你可能会创建单独的组件来获取用户的书签和搜索 GitHub 仓库。为了简单起见,你会将它们全部放在同一个组件中HomeComponent

将以下内容粘贴到src/Home/index.js文件中。

import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import SwipeableViews from 'react-swipeable-views';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Grid from '@material-ui/core/Grid';
import { withAuth } from '@okta/okta-react';

import GithubRepo from "../GithubRepo"
import SearchBar from "../SearchBar"

import githubClient from '../githubClient'
import APIClient from '../apiClient'

const styles = theme => ({
 root: {
   flexGrow: 1,
   marginTop: 30
 },
 paper: {
   padding: theme.spacing.unit * 2,
   textAlign: 'center',
   color: theme.palette.text.secondary,
 },
});

class Home extends React.Component {
 state = {
   value: 0,
   repos: [],
   kudos: []
 };

 async componentDidMount() {
   const accessToken = await this.props.auth.getAccessToken()
   this.apiClient = new APIClient(accessToken);
   this.apiClient.getKudos().then((data) =>
     this.setState({...this.state, kudos: data})
   );
 }

 handleTabChange = (event, value) => {
   this.setState({ value });
 };

 handleTabChangeIndex = index => {
   this.setState({ value: index });
 };

 resetRepos = repos => this.setState({ ...this.state, repos })

 isKudo = repo => this.state.kudos.find(r => r.id == repo.id)
  onKudo = (repo) => {
   this.updateBackend(repo);
 }

 updateBackend = (repo) => {
   if (this.isKudo(repo)) {
     this.apiClient.deleteKudo(repo);
   } else {
     this.apiClient.createKudo(repo);
   }
   this.updateState(repo);
 }

 updateState = (repo) => {
   if (this.isKudo(repo)) {
     this.setState({
       ...this.state,
       kudos: this.state.kudos.filter( r => r.id !== repo.id )
     })
   } else {
     this.setState({
       ...this.state,
       kudos: [repo, ...this.state.kudos]
     })
   }
 }

 onSearch = (event) => {
   const target = event.target;
   if (!target.value || target.length < 3) { return }
   if (event.which !== 13) { return }

   githubClient
     .getJSONRepos(target.value)
     .then((response) => {
       target.blur();
       this.setState({ ...this.state, value: 1 });
       this.resetRepos(response.items);
     })
 }
  renderRepos = (repos) => {
   if (!repos) { return [] }
   return repos.map((repo) => {
     return (
       <Grid item xs={12} md={3} key={repo.id}>
         <GithubRepo onKudo={this.onKudo} isKudo={this.isKudo(repo)} repo={repo} />
       </Grid>
     );
   })
 }

 render() {
   return (
     <div className={styles.root}>
       <SearchBar auth={this.props.auth} onSearch={this.onSearch} />
        <Tabs
         value={this.state.value}
         onChange={this.handleTabChange}
         indicatorColor="primary"
         textColor="primary"
         fullWidth
       >
         <Tab label="Kudos" />
         <Tab label="Search" />
       </Tabs>

       <SwipeableViews
         axis={'x-reverse'}
         index={this.state.value}
         onChangeIndex={this.handleTabChangeIndex}
       >
         <Grid container spacing={16} style={{padding: '20px 0'}}>
           { this.renderRepos(this.state.kudos) }
         </Grid>
         <Grid container spacing={16} style={{padding: '20px 0'}}>
           { this.renderRepos(this.state.repos) }
         </Grid>
       </SwipeableViews>
     </div>
   );
 }
}

export default withStyles(styles)(withAuth(Home));

Enter fullscreen mode Exit fullscreen mode

现在运行程序npm start并在浏览器中打开http://localhost:8080。您应该可以登录、搜索 GitHub 代码库、收藏代码库并在您的 Kudos 列表中看到它!

最终运行应用程序

了解更多关于 Python、Flask 和 React 的信息

正如我们所见,React 是一个功能强大且易于使用的 JavaScript 库,拥有惊人的普及率和社区增长。在本教程中,您学习了如何使用 React、Python 和 Flask 构建一个功能齐全且安全的 JavaScript 应用。要了解更多关于 React 和其他技术的信息,请查看 @oktadev 团队提供的其他优秀资源:

如有任何疑问,欢迎在下方留言。别忘了关注我们:在Twitter上关注我们,在Facebook上点赞,在LinkedIn上查看我们,并订阅我们的YouTube频道

文章来源:https://dev.to/oktadev/build-a-simple-crud-app-with-python-flask-and-react-30k5