如何将React应用与Rails API整合起来

185 阅读6分钟

如何将React应用与Rails API整合在一起

Ruby on Rails是一个用于开发服务器端应用程序的流行网络框架。世界各地的大多数应用程序都是用Ruby on Rails构建的。开发人员发现它很有用,因为它提供了开发和维护现代网络应用的各种工具。它得到了经验丰富的程序员和一个活跃的在线社区的支持,并不断改进。

另一方面,React是一个前端的JavaScript库,用于构建客户端的用户界面。由于React支持状态管理、组件架构和虚拟文档对象模型(DOM),因此前端开发变得简单、有序和高效。

当前端开发与服务器端编码分离时,适合当前趋势的强大应用程序得到了保证。

在这篇文章中,我们将利用React和Ruby on Rails来创建一个简单的待办事项列表应用程序。我们将使用React来创建应用程序的前端,从后端检索JSON格式的数据,后端将在Rails上运行。

前提条件

  • 安装[Ruby on Rails]框架
  • 安装了[Node.js]和[npm]包
  • 安装了[Yarn]包管理器
  • 安装了[Heroku CLI]
  • 安装了一个文本编辑器,最好是[VS代码]
  • 安装了网络浏览器,最好是[谷歌浏览器]
  • 精通[React]、[Ruby on Rails]、[JavaScript]、[Heroku]和[Axios]

设置后端 - Rails API

首先,我们将创建后端,并浏览到通过在终端运行以下命令创建的项目目录。

$ rails new tdlist-api --api
$ cd tdlist-api

我们将继续在VS代码编辑器中打开项目目录,并通过添加以下几行来修改文件Gemfile

gem 'rack-cors', :require => 'rack/cors'

注意,我们以后将使用Heroku 来部署我们的应用程序。我们将需要在Gemfile 开发组部分添加sqllite3 ,用于开发和测试,如下所示。

group :development, :test do
  # for development purposes
  gem 'sqlite3'
end

如果需要,我们可以继续将pg gem添加到生产组部分,用于生产目的,以便利用Postgresql ,如下图所示。

group :production do
  # for production purposes
  gem 'pg'
end

我们将需要为我们的应用程序的后台生成我们的模型和控制器。我们将首先通过运行下面的命令生成名为Tdlist 的模型。

$ rails g model Tdlist title:string done:boolean

然后,我们通过执行下面的命令来生成名为Tdlists 的控制器。

$ rails g controller Tdlists index create update destroy

最后,下面的命令将在SQLite 数据库实例中生成一个包含数据的表,名为tdlists

$ rails db:migrate

我们的后端现在已经设置好了。

接下来,在config/routes.rb 文件中,为我们的后端API指定新的路由,如下所示。

Rails.application.routes.draw do
  scope '/api/version1' do
    resources :tdlists
  end
end

我们已经使用resources ,在我们的后端利用POST,PUT,GET, 和DELETE 行动。

到现在为止,我们正在处理控制器。接下来,我们将在下面的代码中利用我们上面定义的动作。

导航到app/controller/tdslist_controller.rb 文件并粘贴以下代码。

class TdslistController < ApplicationController
  def index
    tdlists = Tdlist.order("created_at DESC")
    render json: tdlists
  end

  def create
    tdlist = Tdlist.create(tdlist_param)
    render json: tdlist
  end

  def update
    tdlist = Tdlist.find(params[:id])
    tdlist.update(tdlist_param)
    render json: tdlist
  end

  def destroy
    tdlist = Tdlist.find(params[:id])
    tdlist.destroy
    head :no_content, status: :ok
  end

  private
    def tdlist_param
      params.require(:tdlist).permit(:title, :done)
    end
end

接下来,我们将通过在终端运行下面的命令来启动我们的服务器。

$ rails server

然后,我们导航到链接http://localhost:3000/api/version1/tdlists。由于到目前为止还没有数据,所以将显示一个空白页面。

db/seeds.rb 文件中,填入一些待办事项,并检查我们的API是否如预期那样工作。

Tdlist.create(title: "Schedule meetings: IT, Accounts, HR", done: false)
Tdlist.create(title: "Visit children's home: perform duties", done: false)

然后我们执行下面的命令。

$ rails db:seed
$ rails server

我们可以继续刷新我们的页面,以下JSON数据将显示在浏览器上。

json

我们的后端现在已经完成了。我们将配置我们的前端运行在port 4000 ,后端运行在port 3000 。然后我们将使用Heroku CLI ,在本地开发环境中运行我们的应用程序。

为了实现这一目标,我们将在项目根目录下创建一个名为Procfile.windows 的文件,并粘贴下面几行命令。

api: bundle exec rails server –p 3000
web: yarn --cwd tdlist-app start

上面的命令同时执行React应用程序和Heroku上的Rails服务器。

创建前端--React应用程序

我们将通过运行下面的命令开始全局地创建React应用程序。

$ npm install -g create-react-app
$ create-react-app tdlist-app

在Rails根目录下,将生成一个名为tdlist-app ,包含React文件和组件的新文件夹。然后我们将执行下面的命令来启动React应用程序。

$ yarn --cwd tdlist-app start

运行上面的命令后,将显示默认的React页面。

React app default page

接下来,我们将指定React应用程序在开发模式下,我们的Rails服务器运行在哪个端口上。为了实现这一点,我们将编辑文件tdlist-app/package.json ,添加下面这行命令。

"proxy": "http://localhost:3000"

上面这一行指示React应用程序在开发模式下通过代理与使用3000端口的后端进行通信。为了从应用程序中调用运行在链接http://localhost:3000/api/version1/tdlists上的后端API,我们只需要调用/api/version1/tdlists ,而不是整个链接。

我们还需要用以下几行更新我们的package.json ,以确保我们的React应用程序运行在port 4000 ,而不是默认的port 3000

"start": "set PORT=4000 && react-scripts start"

我们更新后的package.json ,将出现如下图所示。

package.json

现在我们将通过运行下面的命令来调用我们之前创建的Procfile.windows 文件。

$ heroku local -f Procfile.windows

上面的命令运行Rail API和React应用程序。要访问它,我们可以浏览到http://localhost:4000。

创建React组件

在这一部分,我们将建立我们的应用程序将使用的组件。浏览到tdlist-app/src/components 目录,创建一个名为TdlistsContainer.js 的新文件,并粘贴以下代码。

import React, { Component } from "react";

class TdlistsContainer extends Component {
  render() {
    return (
      <div>
        <div className="taskContainer">
          <input
            className="newTask"
            type="text"
            placeholder="Input a New Task and Press Enter"
            maxLength="75"
          />
        </div>
        <div className="wrapItems">
          <ul className="listItems"></ul>
        </div>
      </div>
    );
  }
}

export default TdlistsContainer;

然后在App.js 中导入新的组件。

import React, { Component } from "react";
import "./App.css";
import TdlistsContainer from "./components/TdlistsContainer";

class App extends Component {
  render() {
    return (
      <div className="mainContainer">
        <div className="topHeading">
          <h1>A Simple To-Do List App</h1>
        </div>
        <TdlistsContainer />
      </div>
    );
  }
}

export default App;

然后编辑App.css

.mainContainer {
  width: 35%;
  height: 500px;
  position: relative;
  border-radius: 7px;
  margin: 0 auto;
}

.wrapItems {
  position: absolute;
  bottom: 55px;
  top: 170px;
  right: 0;
  left: 0;
  overflow: auto;
}

.topHeading {
  color: rgb(48, 2, 2);
  font-family: "Times New Roman", Times, serif, sans-serif;
  padding: 7px;
  text-align: center;
}

ul.listItems {
  padding: 0 30px;
}

input.itemCheckbox {
  margin-right: 8px;
  position: relative;
  -webkit-appearance: none;
  float: left;
  border: 1.5px solid #ccc;
  width: 18px;
  height: 18px;
  border-radius: 7px;
  cursor: pointer;
  text-align: center;
  outline: none;
  margin-left: 7px;
  font-weight: bold;
}

li.item {
  font-family: "Times New Roman", Times, serif;
  list-style-type: none;
  font-size: 1.5em;
  border-bottom: 2px solid rgba(80, 2, 90, 0.411);
  padding: 5px 0;
}

li.item:hover .removeItemButton {
  opacity: 2;
  visibility: visible;
}

input.itemCheckbox:checked:after {
  color: green;
  width: 15px;
  content: "\2713";
  font-size: 15px;
  display: block;
  height: 15px;
  left: 0.7px;
  position: absolute;
  bottom: 1.8px;
}

input.itemCheckbox:checked + label.itemDisplay {
  color: #f807a8;
  text-decoration: line-through;
}

input.itemCheckbox + label.itemDisplay {
  color: black;
}

input[placeholder] {
  font-size: 1.2em;
}

.taskContainer {
  padding: 15px;
}

.removeItemButton {
  float: right;
  visibility: hidden;
  color: red;
  background: rgba(0, 0, 0, 0);
  font-size: 25px;
  font-weight: bold;
  line-height: 0;
  border: 1px solid white;
  border-radius: 50%;
  padding: 10px 5px;
  opacity: 0;
  margin-right: 7px;
  cursor: pointer;
}

.taskContainer .newTask {
  padding: 10px;
  width: 100%;
  box-sizing: border-box;
  border-radius: 25px;
}

接下来,我们将刷新前台链接:http://localhost:4000,并显示以下页面。

to-do list

调用API

在这一部分,我们将从我们的后台获取数据。我们将使用Axios 来获取或存储数据。首先,我们将通过运行以下命令来安装Axios ,然后将其导入我们的组件中。

$ cd tdlist-app
$ npm install axios --save

获取待办事项清单项目

我们将编辑我们的组件来初始化状态和组件的行为。然后我们将添加componentDidMount 函数来加载待办事项列表,如图所示。

import React, { Component } from "react";
import axios from "axios";

class TdlistsContainer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      tdlists: [],
    };
  }

  loadTdlists() {
    axios
      .get("/api/version1/tdlists")
      .then((res) => {
        this.setState({ tdlists: res.data });
      })
      .catch((error) => console.log(error));
  }

  componentDidMount() {
    this.loadTdlists();
  }

  render() {
    return (
      <div>
        <div className="taskContainer">
          <input
            className="newTask"
            type="text"
            placeholder="Input a New Task and Press Enter"
            maxLength="75"
            onKeyPress={this.createTodo}
          />
        </div>
        <div className="wrapItems">
          <ul className="listItems">
            {this.state.tdlists.map((tdlist) => {
              return (
                <li className="item" tdlist={tdlist} key={tdlist.id}>
                  <input className="itemCheckbox" type="checkbox" />
                  <label className="itemDisplay">{tdlist.title}</label>
                  <span className="removeItemButton">x</span>
                </li>
              );
            })}
          </ul>
        </div>
      </div>
    );
  }
}

export default TdlistsContainer;

现在,我们需要重新启动我们的Heroku local ,并刷新页面,下面的结果将被显示出来。

to-do list updated

添加一个新的待办事项列表项目

为了添加一个新的待办事项列表项,我们将调用POST/api/version1/tdlists 。我们需要一个新的函数,使我们能够添加新的待办事项,然后我们继续更新状态。我们将利用immutability-helper来更新状态。我们将运行下面的命令来安装这个包,然后我们把它导入我们的组件中。

$ npm install immutability-helper --save
import update from "immutability-helper";

然后,我们将更新文本框的属性,使其有一个onKeyPress 事件,如下所示。

<input
  className="newTask"
  type="text"
  placeholder="Input a New Task and Press Enter"
  maxLength="75"
  onKeyPress={this.newTdlist}
/>

然后,我们创建一个newTdlist 函数,如下所示。

newTdlist = (e) => {
  if (e.key === "Enter" && !(e.target.value === "")) {
    axios
      .post("/api/version1/tdlists", { tdlist: { title: e.target.value } })
      .then((res) => {
        const tdlists = update(this.state.tdlists, {
          $splice: [[0, 0, res.data]],
        });

        this.setState({
          tdlists: tdlists,
          inputValue: "",
        });
      })
      .catch((error) => console.log(error));
  }
};

在上面的代码片段中,我们通过使用$splice 函数,在tdlists 数组的顶部添加了一个新的待办事项。然后我们可以继续添加一个新的待办事项来测试该应用程序。

我们可以注意到,在添加一个新的待办事项后,文本框的值保持不变。为了清除文本框,我们添加以下代码。

this.state = {
  tdlists: [],
  inputValue: "",
};

为了用我们在上面的代码片断中定义的新参数更新状态,我们粘贴下面的代码。

this.setState({
  tdlists: tdlists,
  inputValue: "",
});

我们将继续添加一个新的函数,每当文本框的值发生变化时就调用这个函数。

handleChange = (e) => {
  this.setState({ inputValue: e.target.value });
};

最后,我们将编辑文本框,使其具有新的属性。

<input
  className="newTask"
  type="text"
  placeholder="Input a New Task and Press Enter"
  maxLength="75"
  onKeyPress={this.newTdlist}
  value={this.state.inputValue}
  onChange={this.handleChange}
/>

更新待办事项列表项目

为了将待办事项列表标记为已完成,我们将修改复选框元素,并创建一个新的函数来更新状态,如下所示。

<input
  className="itemCheckbox"
  type="checkbox"
  checked={tdlist.done}
  onChange={(e) => this.modifyTdlist(e, tdlist.id)}
/>
modifyTdlist = (e, id) => {
  axios
    .put(`/api/version1/tdlists/${id}`, { tdlist: { done: e.target.checked } })
    .then((res) => {
      const tdlistIndex = this.state.tdlists.findIndex(
        (x) => x.id === res.data.id
      );
      const tdlists = update(this.state.tdlists, {
        [tdlistIndex]: { $set: res.data },
      });
      this.setState({
        tdlists: tdlists,
      });
    })
    .catch((error) => console.log(error));
};

删除待办事项清单项目

要删除待办事项,我们将更新span 元素,如下图所示。

<span
  className="removeItemButton"
  onClick={(e) => this.removeTdlist(tdlist.id)}
>
  x
</span>

然后我们将创建一个removeTdlist 函数。

removeTdlist = (id) => {
  axios
    .delete(`/api/version1/tdlists/${id}`)
    .then((res) => {
      const tdlistIndex = this.state.tdlists.findIndex((x) => x.id === id);
      const tdlists = update(this.state.tdlists, {
        $splice: [[tdlistIndex, 1]],
      });
      this.setState({
        tdlists: tdlists,
      });
    })
    .catch((error) => console.log(error));
};

该应用程序现在已经准备好进行测试了。

收尾工作

我们已经成功地实现了一个React应用程序并创建了组件,TdlistsContainer 。我们已经使用React状态处理了我们的应用程序的行为。

另一方面,通过Rails,我们建立了一个后端,处理JSON数据,正如我们处理我们的前端一样。我们使用主容器来渲染用户界面,而不是使用组件来使事情变得简单。这让我们有更多的时间来关注其他重要的概念和数据流。