Thứ năm, 08/08/2019 | 00:00 GMT+7

Cách thiết lập dự án Ruby on Rails với React Frontend

Ruby on Rails là một khung ứng dụng web phía server phổ biến, với hơn 42.000 sao trên GitHub tại thời điểm viết hướng dẫn này. Nó hỗ trợ rất nhiều ứng dụng phổ biến tồn tại trên web ngày nay, như GitHub , Basecamp , SoundCloud , AirbnbTwitch . Với sự nhấn mạnh vào kinh nghiệm của lập trình viên và cộng đồng đam mê đã xây dựng xung quanh nó, Ruby on Rails sẽ cung cấp cho bạn các công cụ bạn cần để xây dựng và duy trì ứng dụng web hiện đại của bạn .

React là một thư viện JavaScript được sử dụng để tạo giao diện user front-end. Được hỗ trợ bởi Facebook, nó là một trong những thư viện front-end phổ biến nhất được sử dụng trên web hiện nay. React cung cấp các tính năng như Mô hình đối tượng tài liệu ảo (DOM) , kiến trúc thành phần và quản lý trạng thái, giúp quá trình phát triển front-end có tổ chức và hiệu quả hơn.

Với giao diện user của web hướng tới các khung công tác tách biệt với mã phía server , việc kết hợp sự sang trọng của Rails với hiệu quả của React sẽ cho phép bạn xây dựng các ứng dụng mạnh mẽ và hiện đại theo xu hướng hiện tại. Bằng cách sử dụng React để hiển thị các thành phần từ bên trong chế độ xem Rails thay vì công cụ mẫu Rails, ứng dụng của bạn sẽ được hưởng lợi từ những tiến bộ mới nhất trong phát triển JavaScript và front-end trong khi vẫn tận dụng tính biểu cảm của Ruby on Rails.

Trong hướng dẫn này, bạn sẽ tạo một ứng dụng Ruby on Rails lưu trữ các công thức nấu ăn yêu thích của bạn, sau đó hiển thị chúng bằng giao diện user React. Khi hoàn tất, bạn có thể tạo, xem và xóa các công thức nấu ăn bằng giao diện React theo kiểu Bootstrap :

Ứng dụng Recipe đã hoàn thành

Nếu bạn muốn xem mã cho ứng dụng này, hãy xem kho lưu trữ đồng hành cho hướng dẫn này trên GitHub Cộng đồng DigitalOcean .

Yêu cầu

Để làm theo hướng dẫn này, bạn cần có những điều sau:

Bước 1 - Tạo một ứng dụng Rails mới

Trong bước này, bạn sẽ xây dựng ứng dụng công thức của bạn trên khung ứng dụng Rails. Đầu tiên, bạn sẽ tạo một ứng dụng Rails mới, ứng dụng này sẽ được cài đặt để hoạt động với React ngay lập tức với ít cấu hình.

Rails cung cấp một số tập lệnh được gọi là trình tạo giúp tạo ra mọi thứ cần thiết để xây dựng một ứng dụng web hiện đại. Để xem danh sách đầy đủ các lệnh này và tác dụng của chúng, hãy chạy lệnh sau trong cửa sổ Terminal :

  • rails -h

Điều này sẽ mang lại một danh sách đầy đủ các tùy chọn, cho phép bạn cài đặt các thông số cho ứng dụng của bạn . Một trong các lệnh được liệt kê là lệnh new , lệnh này sẽ tạo một ứng dụng Rails mới.

Bây giờ, bạn sẽ tạo một ứng dụng Rails mới bằng cách sử dụng trình new . Chạy lệnh sau trong cửa sổ Terminal :

  • rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee

Lệnh trước tạo một ứng dụng Rails mới trong một folder có tên rails_react_recipe , cài đặt các phụ thuộc Ruby và JavaScript cần thiết và cấu hình Webpack. Hãy xem qua các cờ được liên kết với lệnh trình tạo new này:

  • Cờ -d chỉ định công cụ database ưa thích, trong trường hợp này là PostgreSQL.
  • Cờ -T hướng dẫn Rails bỏ qua việc tạo file thử nghiệm, vì bạn sẽ không viết thử nghiệm cho các mục đích của hướng dẫn này. Lệnh này cũng được đề xuất nếu bạn muốn sử dụng một công cụ kiểm tra Ruby khác với công cụ mà Rails cung cấp.
  • --webpack hướng dẫn Rails cấu hình trước cho JavaScript bằng gói webpack , trong trường hợp này là đặc biệt cho một ứng dụng React.
  • --skip-coffee yêu cầu Rails không cài đặt CoffeeScript , điều này không cần thiết cho hướng dẫn này.

Khi lệnh chạy xong, hãy chuyển vào folder rails_react_recipe , là folder root của ứng dụng của bạn:

  • cd rails_react_recipe

Tiếp theo, liệt kê nội dung của folder :

  • ls

Thư mục root này có một số file và folder được tạo tự động tạo nên cấu trúc của ứng dụng Rails, bao gồm file package.json chứa các phần phụ thuộc cho ứng dụng React.

Đến đây bạn đã tạo thành công một ứng dụng Rails mới, bạn đã sẵn sàng kết nối nó với database trong bước tiếp theo.

Bước 2 - Cài đặt database

Trước khi chạy ứng dụng Rails mới, trước tiên bạn phải kết nối nó với database . Trong bước này, bạn sẽ kết nối ứng dụng Rails mới tạo với database PostgreSQL, vì vậy dữ liệu công thức có thể được lưu trữ và tìm nạp khi cần thiết.

Tệp database.yml được tìm thấy trong config/database.yml chứa các chi tiết về database như tên database cho các môi trường phát triển khác nhau. Rails chỉ định tên database cho các môi trường phát triển khác nhau bằng cách thêm dấu gạch dưới ( _ ) theo sau tên môi trường vào tên ứng dụng của bạn. Bạn luôn có thể thay đổi bất kỳ tên database môi trường nào thành bất kỳ tên nào bạn muốn.

Lưu ý: Đến đây, bạn có thể thay đổi config/database.yml để cài đặt role PostgreSQL mà bạn muốn Rails sử dụng để tạo database của bạn . Nếu bạn đã làm theo Yêu cầu Cách sử dụng PostgreSQL với Ứng dụng Ruby on Rails của bạn và tạo một role được bảo mật bằng password , bạn có thể làm theo hướng dẫn trong Bước 4 dành cho macOS hoặc Ubuntu 18.04 .

Như đã nói trước đó, Rails cung cấp rất nhiều lệnh để giúp việc phát triển các ứng dụng web trở nên dễ dàng. Điều này bao gồm các lệnh để làm việc với database , chẳng hạn như create , dropreset . Để tạo database cho ứng dụng của bạn, hãy chạy lệnh sau trong cửa sổ Terminal:

  • rails db:create

Lệnh này tạo một database developmenttest , tạo ra kết quả sau:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

Bây giờ ứng dụng đã được kết nối với database , hãy khởi động ứng dụng bằng cách chạy lệnh sau trong cửa sổ Terminal:

  • rails s --binding=127.0.0.1

Lệnh s hoặc server kích hoạt Puma , là web server được phân phối với Rails theo mặc định và --binding=127.0.0.1 liên kết server với server localhost của bạn.

Khi bạn chạy lệnh này, dấu nhắc lệnh của bạn sẽ không xuất hiện và bạn sẽ thấy kết quả sau:

Output
=> Booting Puma => Rails 5.2.3 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 Use Ctrl-C to stop

Để xem ứng dụng của bạn, hãy mở cửa sổ trình duyệt và chuyển đến http://localhost:3000 . Bạn sẽ thấy trang chào mừng mặc định của Rails:

Trang chào mừng của Rails

Điều này nghĩa là bạn đã cài đặt đúng ứng dụng Rails của bạn .

Để dừng web server bất kỳ lúc nào, hãy nhấn CTRL+C trong cửa sổ Đầu cuối nơi server đang chạy. Hãy tiếp tục và làm điều này ngay bây giờ; bạn sẽ nhận được một tin nhắn tạm biệt từ Puma:

Output
^C- Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting

Dấu nhắc của bạn sau đó sẽ xuất hiện lại.

Bạn đã cài đặt thành công database cho ứng dụng công thức nấu ăn của bạn . Trong bước tiếp theo, bạn sẽ cài đặt tất cả các phụ thuộc JavaScript bổ sung mà bạn cần để kết hợp với giao diện user React của bạn .

Bước 3 - Cài đặt phụ thuộc giao diện user

Trong bước này, bạn sẽ cài đặt các phụ thuộc JavaScript cần thiết trên giao diện user của ứng dụng công thức nấu ăn của bạn. Chúng bao gồm:

Chạy lệnh sau trong cửa sổ Terminal để cài đặt các gói này với trình quản lý gói Yarn:

  • yarn add react-router-dom bootstrap jquery popper.js

Lệnh này sử dụng Yarn để cài đặt các gói được chỉ định và thêm chúng vào file package.json . Để xác minh điều này, hãy xem file package.json nằm trong folder root của dự án:

  • nano package.json

Bạn sẽ thấy các gói đã cài đặt được liệt kê trong khóa dependencies :

~ / rails_react_recipe / package.json
{   "name": "rails_react_recipe",   "private": true,   "dependencies": {     "@babel/preset-react": "^7.0.0",     "@rails/webpacker": "^4.0.7",     "babel-plugin-transform-react-remove-prop-types": "^0.4.24",     "bootstrap": "^4.3.1",     "jquery": "^3.4.1",     "popper.js": "^1.15.0",     "prop-types": "^15.7.2",     "react": "^16.8.6",     "react-dom": "^16.8.6",     "react-router-dom": "^5.0.1"   },   "devDependencies": {     "webpack-dev-server": "^3.7.2"   } } 

Bạn đã cài đặt một số phụ thuộc front-end cho ứng dụng của bạn . Tiếp theo, bạn sẽ cài đặt một trang chủ cho ứng dụng công thức nấu ăn của bạn .

Bước 4 - Cài đặt Trang chủ

Với tất cả các phụ thuộc đã được cài đặt, trong bước này, bạn sẽ tạo một trang chủ cho ứng dụng. Trang chủ sẽ đóng role là trang đích khi user truy cập ứng dụng lần đầu tiên.

Rails tuân theo mô hình kiến trúc Model-View-Controller cho các ứng dụng. Trong mẫu MVC, mục đích của bộ điều khiển là nhận các yêu cầu cụ thể và chuyển chúng đến mô hình hoặc chế độ xem thích hợp. Ngay bây giờ ứng dụng sẽ hiển thị trang chào mừng Rails khi URL root được tải trong trình duyệt. Để thay đổi điều này, bạn sẽ tạo một bộ điều khiển và chế độ xem cho trang chủ và khớp nó với một tuyến đường.

Rails cung cấp trình tạo controller để tạo bộ điều khiển. Bộ tạo controller nhận được tên bộ điều khiển, cùng với một hành động phù hợp. Để biết thêm về điều này, hãy xem tài liệu chính thức của Rails .

Hướng dẫn này sẽ gọi Homepage bộ điều khiển. Chạy lệnh sau trong cửa sổ Terminal để tạo bộ điều khiển Trang chủ với hành động index .

  • rails g controller Homepage index

Ghi chú:
Trên Linux, nếu bạn gặp lỗi FATAL: Listen error: unable to monitor directories for changes. , điều này là do giới hạn hệ thống về số lượng file mà máy của bạn có thể theo dõi các thay đổi. Chạy lệnh sau để sửa nó:

  • echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

Điều này sẽ làm tăng vĩnh viễn số lượng folder mà bạn có thể theo dõi với Listen 524288 . Bạn có thể thay đổi lại điều này bằng cách chạy lệnh tương tự và thay 524288 bằng số bạn muốn.

Chạy lệnh này sẽ tạo ra các file sau:

  • Tệp homepage_controller.rb để nhận tất cả các yêu cầu liên quan đến trang chủ. Tệp này chứa hành động index mà bạn đã chỉ định trong lệnh.
  • Tệp homepage.js để thêm bất kỳ hành vi JavaScript nào liên quan đến bộ điều khiển Homepage .
  • Tệp homepage.scss để thêm các kiểu liên quan đến bộ điều khiển Homepage .
  • Tệp homepage_helper.rb để thêm các phương thức trợ giúp liên quan đến bộ điều khiển Homepage .
  • Tệp index.html.erb là trang xem để hiển thị bất kỳ thứ gì liên quan đến trang chủ.

Ngoài các trang mới này được tạo bằng cách chạy lệnh Rails, Rails cũng cập nhật file định tuyến của bạn có tại config/routes.rb . Nó thêm một tuyến get cho trang chủ của bạn mà bạn sẽ sửa đổi làm tuyến root của bạn .

Một tuyến root trong Rails chỉ định những gì sẽ hiển thị khi user truy cập vào URL root của ứng dụng của bạn. Trong trường hợp này, bạn muốn user xem trang chủ của bạn . Mở file định tuyến có tại config/routes.rb trong trình soạn thảo yêu thích của bạn:

  • nano config/routes.rb

Bên trong file này, thay thế get 'homepage/index' bằng root 'homepage#index' để file trông giống như sau:

~ / rails_react_recipe / config / route.rb
Rails.application.routes.draw do   root 'homepage#index'   # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end 

Sửa đổi này hướng dẫn Rails ánh xạ các yêu cầu tới folder root của ứng dụng tới hành động index của Bộ điều khiển Homepage , từ đó hiển thị bất kỳ thứ gì có trong index.html.erb tại app/views/homepage/index.html.erb trên vào trình duyệt.

Để xác minh điều này đang hoạt động, hãy khởi động ứng dụng của bạn:

  • rails s --binding=127.0.0.1

Mở ứng dụng trong trình duyệt, bạn sẽ thấy một trang đích mới cho ứng dụng của bạn :

Trang chủ ứng dụng

Khi bạn đã xác minh ứng dụng của bạn đang hoạt động, hãy nhấn CTRL+C để dừng server .

Tiếp theo, mở file ~/rails_react_recipe/app/views/homepage/index.html.erb , xóa mã bên trong file , sau đó lưu file dưới dạng trống. Bằng cách này, bạn sẽ đảm bảo nội dung của index.html.erb không can thiệp vào việc hiển thị React của giao diện user của bạn.

Đến đây bạn đã cài đặt trang chủ cho ứng dụng của bạn , bạn có thể chuyển sang phần tiếp theo, nơi bạn sẽ cấu hình giao diện user của ứng dụng để sử dụng React.

Bước 5 - Cấu hình React làm Giao diện user Rails của bạn

Trong bước này, bạn sẽ cấu hình Rails để sử dụng React trên giao diện user của ứng dụng, thay vì công cụ mẫu của nó. Điều này sẽ cho phép bạn tận dụng khả năng hiển thị của React để tạo ra một trang chủ trực quan hấp dẫn hơn.

Rails, với sự trợ giúp của đá quý Webpacker , gói tất cả mã JavaScript của bạn thành các gói . Bạn có thể tìm thấy chúng trong folder gói tại app/javascript/packs . Bạn có thể liên kết các gói này trong dạng xem Rails bằng trình trợ giúp javascript_pack_tag và bạn có thể liên kết các bảng định kiểu được nhập vào các gói bằng trình trợ giúp stylesheet_pack_tag . Để tạo một điểm vào môi trường React của bạn, bạn sẽ thêm một trong các gói này vào bố cục ứng dụng của bạn .

Đầu tiên, đổi tên ~/rails_react_recipe/app/javascript/packs/hello_react.jsx thành ~/rails_react_recipe/app/javascript/packs/Index.jsx .

  • mv ~/rails_react_recipe/app/javascript/packs/hello_react.jsx ~/rails_react_recipe/app/javascript/packs/Index.jsx

Sau khi đổi tên file , hãy mở application.html.erb , file bố cục ứng dụng:

  • nano ~/rails_react_recipe/app/views/layouts/application.html.erb

Thêm các dòng mã được đánh dấu sau vào cuối thẻ head trong file bố cục ứng dụng:

~ / rails_react_recipe / app / views / layouts / application.html.erb
<!DOCTYPE html> <html>   <head>     <title>RailsReactRecipe</title>     <%= csrf_meta_tags %>     <%= csp_meta_tag %>      <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>     <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">     <%= javascript_pack_tag 'Index' %>   </head>    <body>     <%= yield %>   </body> </html> 

Thêm gói JavaScript vào tiêu đề ứng dụng của bạn làm cho tất cả mã JavaScript của bạn có và thực thi mã trong file Index.jsx của bạn trên trang khi nào bạn chạy ứng dụng. Cùng với gói JavaScript, bạn cũng đã thêm thẻ meta viewport để kiểm soát kích thước và tỷ lệ của các trang trên ứng dụng của bạn .

Lưu và thoát khỏi file .

Bây giờ file mục nhập của bạn đã được tải lên trang, hãy tạo một thành phần React cho trang chủ của bạn. Bắt đầu bằng cách tạo folder components trong folder app/javascript :

  • mkdir ~/rails_react_recipe/app/javascript/components

Thư mục components sẽ chứa thành phần cho trang chủ, cùng với các thành phần React khác trong ứng dụng. Trang chủ sẽ chứa một số văn bản và nút kêu gọi hành động để xem tất cả các công thức nấu ăn.

Trong editor , hãy tạo file Home.jsx trong folder components :

  • nano ~/rails_react_recipe/app/javascript/components/Home.jsx

Thêm mã sau vào file :

~ / rails_react_recipe / app / javascript / components / Home.jsx
import React from "react"; import { Link } from "react-router-dom";  export default () => (   <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">     <div className="jumbotron jumbotron-fluid bg-transparent">       <div className="container secondary-color">         <h1 className="display-4">Food Recipes</h1>         <p className="lead">           A curated list of recipes for the best homemade meal and delicacies.         </p>         <hr className="my-4" />         <Link           to="/recipes"           className="btn btn-lg custom-button"           role="button"         >           View Recipes         </Link>       </div>     </div>   </div> ); 

Trong đoạn mã này, bạn đã nhập React và cả thành phần Link từ Bộ định tuyến React. Thành phần Link tạo ra một siêu kết nối để chuyển từ trang này sang trang khác. Sau đó, bạn đã tạo và xuất một thành phần chức năng chứa một số ngôn ngữ Đánh dấu cho trang chủ của bạn , được tạo kiểu với các lớp Bootstrap.

Với thành phần Home của bạn tại chỗ, bây giờ bạn sẽ cài đặt định tuyến bằng React Router. Tạo folder định routes trong folder app/javascript :

  • mkdir ~/rails_react_recipe/app/javascript/routes

Thư mục routes sẽ chứa một vài tuyến với các thành phần tương ứng của chúng. Khi nào bất kỳ tuyến đường cụ thể nào được tải, nó sẽ hiển thị thành phần tương ứng của nó cho trình duyệt.

Trong folder định routes , hãy tạo file Index.jsx :

  • nano ~/rails_react_recipe/app/javascript/routes/Index.jsx

Thêm mã sau vào nó:

~ / rails_react_recipe / app / javascript / route / Index.jsx
import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import Home from "../components/Home";  export default (   <Router>     <Switch>       <Route path="/" exact component={Home} />     </Switch>   </Router> ); 

Trong file định tuyến Index.jsx này, bạn đã nhập một số module : module React cho phép ta sử dụng React và các module BrowserRouter , RouteSwitch từ React Router, cùng nhau giúp ta chuyển từ tuyến đường này sang tuyến đường khác. Cuối cùng, bạn đã nhập thành phần Home của bạn , thành phần này sẽ được hiển thị khi nào một yêu cầu trùng với tuyến root ( / ). Khi nào bạn muốn thêm nhiều trang hơn vào ứng dụng của bạn , tất cả những gì bạn cần làm là khai báo một tuyến trong file này và khớp nó với thành phần bạn muốn hiển thị cho trang đó.

Lưu và thoát khỏi file .

Đến đây bạn đã cài đặt thành công định tuyến bằng React Router. Để React nhận biết được các tuyến có sẵn và sử dụng chúng, các tuyến phải có sẵn tại điểm vào ứng dụng. Để làm điều này, bạn sẽ hiển thị các tuyến của bạn trong một thành phần mà React sẽ hiển thị trong file mục nhập của bạn.

Tạo file App.jsx trong folder app/javascript/components :

  • nano ~/rails_react_recipe/app/javascript/components/App.jsx

Thêm mã sau vào file App.jsx :

~ / rails_react_recipe / app / javascript / components / App.jsx
import React from "react"; import Routes from "../routes/Index";  export default props => <>{Routes}</>; 

Trong file App.jsx , bạn đã nhập React và các file định tuyến bạn vừa tạo. Sau đó, bạn đã xuất một thành phần hiển thị các tuyến đường trong các đoạn . Thành phần này sẽ được hiển thị tại điểm đầu vào của ứng dụng, do đó làm cho các tuyến có sẵn khi nào ứng dụng được tải.

Đến đây bạn đã cài đặt App.jsx , đã đến lúc hiển thị nó trong file mục nhập của bạn. Mở file Index.jsx mục nhập:

  • nano ~/rails_react_recipe/app/javascript/packs/Index.jsx

Thay thế mã ở đó bằng mã sau:

~ / rails_react_recipe / app / javascript / pack / Index.jsx
import React from "react"; import { render } from "react-dom"; import 'bootstrap/dist/css/bootstrap.min.css'; import $ from 'jquery'; import Popper from 'popper.js'; import 'bootstrap/dist/js/bootstrap.bundle.min'; import App from "../components/App";  document.addEventListener("DOMContentLoaded", () => {   render(     <App />,     document.body.appendChild(document.createElement("div"))   ); }); 

Trong đoạn mã này, bạn đã nhập React, phương thức kết xuất từ ReactDOM, Bootstrap, jQuery, Popper.js và thành phần App của bạn. Bằng cách sử dụng phương thức kết xuất của ReactDOM, bạn đã kết xuất thành phần App của bạn trong một phần tử div , phần tử này đã được nối vào phần nội dung của trang. Khi nào ứng dụng được tải, React sẽ hiển thị nội dung của thành phần App bên trong phần tử div trên trang.

Lưu và thoát khỏi file .

Cuối cùng, thêm một số kiểu CSS vào trang chủ của bạn.

Mở application.css của bạn trong folder ~/rails_react_recipe/app/assets/stylesheets :

  • nano ~/rails_react_recipe/app/assets/stylesheets/application.css

Tiếp theo, thay thế nội dung của file application.css bằng mã sau:

~ / rails_react_recipe / app / asset / stylesheets / application.css
.bg_primary-color {   background-color: #FFFFFF; } .primary-color {   background-color: #FFFFFF; } .bg_secondary-color {   background-color: #293241; } .secondary-color {   color: #293241; } .custom-button.btn {   background-color: #293241;   color: #FFF;   border: none; } .custom-button.btn:hover {   color: #FFF !important;   border: none; } .hero {   width: 100vw;   height: 50vh; } .hero img {   object-fit: cover;   object-position: top;   height: 100%;   width: 100%; } .overlay {   height: 100%;   width: 100%;   opacity: 0.4; } 

Điều này tạo ra khung cho hình ảnh anh hùng hoặc một biểu ngữ web lớn trên trang đầu của trang web mà bạn sẽ thêm vào sau. Ngoài ra, điều này tạo kiểu cho nút mà user sẽ sử dụng để vào ứng dụng.

Với các kiểu CSS của bạn tại chỗ, hãy lưu và thoát khỏi file . Tiếp theo, khởi động lại web server cho ứng dụng của bạn, sau đó reload ứng dụng trong trình duyệt của bạn. Bạn sẽ thấy một trang chủ hoàn toàn mới:

Kiểu trang chủ

Trong bước này, bạn đã cấu hình ứng dụng của bạn để ứng dụng sử dụng React làm giao diện user . Trong phần tiếp theo, bạn sẽ tạo các mô hình và bộ điều khiển cho phép bạn tạo, đọc, cập nhật và xóa các công thức nấu ăn.

Bước 6 - Tạo bộ điều khiển công thức và mô hình

Đến đây bạn đã cài đặt giao diện user React cho ứng dụng của bạn , trong bước này, bạn sẽ tạo mô hình Recipe và bộ điều khiển. Mô hình công thức sẽ đại diện cho bảng database chứa thông tin về công thức của user trong khi bộ điều khiển sẽ nhận và xử lý các yêu cầu tạo, đọc, cập nhật hoặc xóa công thức nấu ăn. Khi user yêu cầu một công thức, bộ điều khiển công thức sẽ nhận yêu cầu này và chuyển nó đến mô hình công thức, mô hình này sẽ lấy dữ liệu được yêu cầu từ database . Sau đó, mô hình trả về dữ liệu công thức làm phản hồi cho bộ điều khiển. Cuối cùng, thông tin này được hiển thị trên trình duyệt.

Bắt đầu bằng cách tạo mô hình Công thức bằng cách sử dụng lệnh con generate model do Rails cung cấp và bằng cách chỉ định tên của mô hình cùng với các cột và kiểu dữ liệu của nó. Chạy lệnh sau trong cửa sổ Terminal để tạo mô hình Recipe :

  • rails generate model Recipe name:string ingredients:text instruction:text image:string

Lệnh trước hướng dẫn Rails tạo một mô hình Recipe cùng với một cột name của loại string , một ingredients và cột instruction của text loại, và một cột image của loại string . Hướng dẫn này đã đặt tên cho mô hình là Recipe , bởi vì theo quy ước, các mô hình trong Rails sử dụng tên số ít trong khi các bảng database tương ứng của chúng sử dụng tên số nhiều.

Chạy lệnh generate model tạo ra hai file :

  • Một file công recipe.rb chứa tất cả logic liên quan đến mô hình.
  • 20190407161357 _create_recipes.rb (số ở đầu file có thể khác nhau tùy thuộc vào ngày bạn chạy lệnh). Đây là file di chuyển chứa hướng dẫn tạo cấu trúc database .

Tiếp theo, chỉnh sửa file mô hình công thức đảm bảo rằng chỉ dữ liệu hợp lệ mới được lưu vào database . Bạn có thể đạt được điều này bằng cách thêm một số xác nhận database vào mô hình của bạn . Mở mô hình công thức của bạn tại app/models/recipe.rb :

  • nano ~/rails_react_recipe/app/models/recipe.rb

Thêm các dòng mã được đánh dấu sau vào file :

class Recipe < ApplicationRecord   validates :name, presence: true   validates :ingredients, presence: true   validates :instruction, presence: true end 

Trong mã này, bạn đã thêm xác thực mô hình để kiểm tra sự hiện diện của name , ingredients và trường instruction . Nếu không có sự hiện diện của ba trường này, một công thức không hợp lệ và sẽ không được lưu vào database .

Lưu và thoát khỏi file .

Để Rails tạo bảng recipes trong database của bạn, bạn phải chạy một quá trình di chuyển , trong Rails là một cách để áp dụng các thay đổi đối với database của bạn theo chương trình. Để chắc chắn rằng sự di cư làm việc với database bạn đã cài đặt , nó là cần thiết để thay đổi các 20190407161357 _create_recipes.rb file .

Mở file này trong editor :

  • nano ~/rails_react_recipe/db/migrate/20190407161357_create_recipes.rb

Thêm các dòng được đánh dấu sau để file trông giống như sau:

db / migrate / 20190407161357_create_recipes.rb
class CreateRecipes < ActiveRecord::Migration[5.2]   def change     create_table :recipes do |t|       t.string :name, null: false       t.text :ingredients, null: false       t.text :instruction, null: false       t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'       t.timestamps     end   end end 

Tệp di chuyển này chứa một lớp Ruby với một phương thức change và một lệnh để tạo một bảng được gọi là recipes cùng với các cột và kiểu dữ liệu của chúng. Bạn cũng đã cập nhật 20190407161357 _create_recipes.rb với ràng buộc NOT NULL 20190407161357 _create_recipes.rb đối với name , ingredients và cột instruction bằng cách thêm null: false , đảm bảo các cột này có giá trị trước khi thay đổi database . Cuối cùng, bạn đã thêm một URL hình ảnh mặc định cho cột hình ảnh của bạn ; đây có thể là một URL khác nếu bạn muốn sử dụng một hình ảnh khác.

Với những thay đổi này, hãy lưu và thoát khỏi file . Đến đây bạn đã sẵn sàng để chạy quá trình di chuyển và tạo bảng của bạn . Trong cửa sổ Terminal , hãy chạy lệnh sau:

  • rails db:migrate

Ở đây bạn đã sử dụng lệnh di chuyển database , lệnh này thực thi các hướng dẫn trong file di chuyển của bạn. Khi lệnh chạy thành công, bạn sẽ nhận được kết quả tương tự như sau:

Output
== 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

Với mô hình công thức của bạn tại chỗ, hãy tạo bộ điều khiển công thức của bạn và thêm logic để tạo, đọc và xóa công thức. Trong cửa sổ Terminal , hãy chạy lệnh sau:

  • rails generate controller api/v1/Recipes index create show destroy -j=false -y=false --skip-template-engine --no-helper

Trong lệnh này, bạn đã tạo bộ điều khiển Recipes trong folder api/v1 với index , create , showdestroy hành động. Hành động index sẽ xử lý việc tìm nạp tất cả các công thức nấu ăn của bạn, hành động create sẽ chịu trách nhiệm tạo công thức nấu ăn mới, hành động show sẽ tìm nạp một công thức duy nhất và hành động destroy sẽ giữ logic để xóa một công thức.

Bạn cũng đã thông qua một số cờ để làm cho bộ điều khiển nhẹ hơn, bao gồm:

  • -j=false hướng dẫn Rails bỏ qua việc tạo các file JavaScript được liên kết.
  • -y=false hướng dẫn Rails bỏ qua việc tạo các file biểu định kiểu được liên kết.
  • --skip-template-engine , hướng dẫn Rails bỏ qua việc tạo các file dạng xem Rails, vì React đang xử lý các nhu cầu front-end của bạn.
  • --no-helper , hướng dẫn Rails bỏ qua việc tạo file trợ giúp cho bộ điều khiển của bạn.

Chạy lệnh cũng cập nhật file tuyến đường của bạn với tuyến đường cho mỗi hành động trong bộ điều khiển Recipes . Để sử dụng các tuyến này, hãy áp dụng các thay đổi đối với file config/routes.rb của bạn.

Mở file tuyến đường trong editor của bạn:

  • nano ~/rails_react_recipe/config/routes.rb

Khi nó được mở, hãy cập nhật nó để trông giống như mã sau, thay đổi hoặc thêm các dòng được đánh dấu:

~ / rails_react_recipe / config / route.rb
Rails.application.routes.draw do   namespace :api do     namespace :v1 do       get 'recipes/index'       post 'recipes/create'       get '/show/:id', to: 'recipes#show'       delete '/destroy/:id', to: 'recipes#destroy'     end   end   root 'homepage#index'   get '/*path' => 'homepage#index'   # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end 

Trong file tuyến đường này, bạn đã sửa đổi động từ HTTP của tuyến đường createdestroy để nó có thể postdelete dữ liệu. Bạn cũng đã sửa đổi các tuyến cho hành động showdestroy bằng cách thêm tham số :id vào tuyến. :id sẽ giữ số nhận dạng của công thức bạn muốn đọc hoặc xóa.

Bạn cũng đã thêm một tuyến bắt tất cả với get '/*path' sẽ hướng bất kỳ yêu cầu nào khác không trùng với các tuyến hiện có tới hành động index của bộ điều khiển homepage . Bằng cách này, định tuyến trên giao diện user sẽ xử lý các yêu cầu không liên quan đến việc tạo, đọc hoặc xóa công thức nấu ăn.

Lưu và thoát khỏi file .

Để xem danh sách các tuyến có sẵn trong ứng dụng của bạn, hãy chạy lệnh sau trong cửa sổ Terminal :

  • rails routes

Chạy lệnh này sẽ hiển thị danh sách các mẫu URI, động từ và bộ điều khiển hoặc hành động phù hợp cho dự án của bạn.

Tiếp theo, thêm logic để nhận tất cả các công thức cùng một lúc. Rails sử dụng thư viện ActiveRecord để xử lý các việc liên quan đến database như thế này. ActiveRecord kết nối các lớp với các bảng database quan hệ và cung cấp một API phong phú để làm việc với chúng.

Để lấy tất cả các công thức, bạn sẽ sử dụng ActiveRecord để truy vấn bảng công thức và tìm nạp tất cả các công thức tồn tại trong database .

Mở file recipes_controller.rb bằng lệnh sau:

  • nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

Thêm các dòng mã được đánh dấu sau vào bộ điều khiển công thức nấu ăn:

~ / rails_react_recipe / app / controllers / api / v1 / Cooking_controller.rb
class Api::V1::RecipesController < ApplicationController   def index     recipe = Recipe.all.order(created_at: :desc)     render json: recipe   end    def create   end    def show   end    def destroy   end end 

Trong hành động index của bạn, sử dụng all phương pháp được cung cấp bởi ActiveRecord, bạn sẽ có được tất cả các công thức trong database của bạn . Sử dụng phương pháp order , bạn sắp xếp chúng theo thứ tự giảm dần theo ngày tạo của chúng. Bằng cách này, trước tiên bạn có những công thức nấu ăn mới nhất. Cuối cùng, bạn gửi danh sách các công thức nấu ăn của bạn dưới dạng phản hồi JSON với render .

Tiếp theo, thêm logic để tạo công thức nấu ăn mới. Như với việc tìm nạp tất cả các công thức nấu ăn, bạn sẽ dựa vào ActiveRecord để xác thực và lưu các chi tiết công thức được cung cấp. Cập nhật bộ điều khiển công thức của bạn bằng các dòng mã được đánh dấu sau:

~ / rails_react_recipe / app / controllers / api / v1 / Cooking_controller.rb
class Api::V1::RecipesController < ApplicationController   def index     recipe = Recipe.all.order(created_at: :desc)     render json: recipe   end    def create     recipe = Recipe.create!(recipe_params)     if recipe       render json: recipe     else       render json: recipe.errors     end   end    def show   end    def destroy   end    private    def recipe_params     params.permit(:name, :image, :ingredients, :instruction)   end end 

Trong hành động create , bạn sử dụng phương thức create của ActiveRecord để tạo một công thức mới. Phương thức create có khả năng gán tất cả các tham số bộ điều khiển được cung cấp vào mô hình cùng một lúc. Điều này giúp dễ dàng tạo profile , nhưng cũng mở ra khả năng bị sử dụng độc hại. Điều này có thể được ngăn chặn bằng cách sử dụng một tính năng được cung cấp bởi Rails được gọi là tham số mạnh . Bằng cách này, không thể chỉ định các thông số trừ khi chúng được đưa vào danh sách trắng. Trong mã của bạn, bạn đã chuyển một tham số recipe_params cho phương thức create . Công recipe_params là một phương thức private trong đó bạn đưa các tham số bộ điều khiển vào danh sách trắng để ngăn nội dung sai hoặc độc hại xâm nhập vào database của bạn. Trong trường hợp này, bạn đang cho phép name , image , ingredients và tham số instruction để sử dụng hợp lệ phương thức create .

Bộ điều khiển công thức của bạn hiện có thể đọc và tạo công thức nấu ăn. Tất cả những gì còn lại là logic để đọc và xóa một công thức duy nhất. Cập nhật bộ điều khiển công thức nấu ăn của bạn bằng mã sau:

~ / rails_react_recipe / app / controllers / api / v1 / Cooking_controller.rb
class Api::V1::RecipesController < ApplicationController   def index     recipe = Recipe.all.order(created_at: :desc)     render json: recipe   end    def create     recipe = Recipe.create!(recipe_params)     if recipe       render json: recipe     else       render json: recipe.errors     end   end    def show     if recipe       render json: recipe     else       render json: recipe.errors     end   end    def destroy     recipe&.destroy     render json: { message: 'Recipe deleted!' }   end    private    def recipe_params     params.permit(:name, :image, :ingredients, :instruction)   end    def recipe     @recipe ||= Recipe.find(params[:id])   end end 

Trong các dòng mã mới, bạn đã tạo một phương pháp recipe riêng. Phương recipe thức sử dụng phương thức find của ActiveRecord để tìm một công thức có id trùng với id được cung cấp trong params và gán nó cho một biến thể hiện @recipe . Trong show hành động, bạn kiểm tra nếu một công thức sẽ được trả về bởi các recipe phương pháp và gửi nó như một phản ứng JSON, hoặc gửi một lỗi nếu nó không được.

Trong hành động destroy , bạn đã làm điều gì đó tương tự bằng cách sử dụng toán tử chuyển an toàn &. , tránh lỗi nil khi gọi một phương thức. Điều này cho phép bạn xóa công thức chỉ khi nó tồn tại, sau đó gửi tin nhắn dưới dạng phản hồi.

Đến đây bạn đã hoàn tất việc áp dụng các thay đổi này đối với recipes_controller.rb , hãy lưu file và thoát khỏi editor của bạn.

Trong bước này, bạn đã tạo một mô hình và bộ điều khiển cho công thức nấu ăn của bạn . Bạn đã viết tất cả logic cần thiết để làm việc với các công thức nấu ăn trên phần backend . Trong phần tiếp theo, bạn sẽ tạo các thành phần để xem công thức nấu ăn của bạn .

Bước 7 - Xem công thức nấu ăn

Trong phần này, bạn sẽ tạo các thành phần để xem công thức nấu ăn. Trước tiên, bạn sẽ tạo một trang nơi bạn có thể xem tất cả các công thức nấu ăn hiện có, sau đó là một trang khác để xem các công thức nấu ăn riêng lẻ.

Bạn sẽ bắt đầu bằng cách tạo một trang để xem tất cả các công thức nấu ăn. Tuy nhiên, trước khi bạn có thể làm điều này, bạn cần có các công thức nấu ăn, vì database của bạn hiện đang trống. Rails cho ta cơ hội tạo dữ liệu root cho ứng dụng của bạn.

Mở file hạt giống seeds.rb để chỉnh sửa:

  • nano ~/rails_react_recipe/db/seeds.rb

Thay thế nội dung của file hạt giống này bằng mã sau:

~ / rails_react_recipe / db / seed.rb
9.times do |i|   Recipe.create(     name: "Recipe #{i + 1}",     ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',     instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'   ) end 

Trong đoạn mã này, bạn đang sử dụng một vòng lặp để hướng dẫn Rails tạo chín công thức nấu ăn với name , ingredientsinstruction . Lưu và thoát khỏi file .

Để tạo database với dữ liệu này, hãy chạy lệnh sau trong cửa sổ Terminal :

  • rails db:seed

Chạy lệnh này sẽ thêm chín công thức vào database của bạn. Đến đây bạn có thể tìm nạp chúng và hiển thị chúng trên giao diện user .

Thành phần để xem tất cả các công thức sẽ thực hiện một yêu cầu HTTP đối với hành động index trong RecipesController để nhận danh sách tất cả các công thức. Các công thức nấu ăn này sau đó sẽ được hiển thị trong các thẻ trên trang.

Tạo file Recipes.jsx trong folder app/javascript/components :

  • nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

Khi file được mở, hãy nhập các module React và Link vào đó bằng cách thêm các dòng sau:

~ / rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react"; import { Link } from "react-router-dom"; 

Tiếp theo, tạo một lớp Recipes mở rộng lớp React.Component . Thêm đoạn mã được đánh dấu sau để tạo một thành phần React mở rộng React.Component :

~ / rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipes extends React.Component {   constructor(props) {     super(props);     this.state = {       recipes: []     };   }  } export default Recipes; 

Bên trong hàm tạo , ta đang khởi tạo một đối tượng trạng thái chứa trạng thái của công thức nấu ăn của bạn, khi khởi tạo là một mảng trống ( [] ).

Tiếp theo, thêm một phương thức componentDidMount trong lớp Recipe. Phương thức componentDidMount là một phương thức vòng đời React được gọi ngay sau khi một thành phần được mount . Trong phương pháp vòng đời này, bạn sẽ thực hiện một cuộc gọi để tìm nạp tất cả các công thức nấu ăn của bạn . Để làm điều này, hãy thêm các dòng sau:

~ / rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipes extends React.Component {   constructor(props) {     super(props);     this.state = {       recipes: []     };   }    componentDidMount() {       const url = "/api/v1/recipes/index";       fetch(url)         .then(response => {           if (response.ok) {             return response.json();           }           throw new Error("Network response was not ok.");         })         .then(response => this.setState({ recipes: response }))         .catch(() => this.props.history.push("/"));   }  } export default Recipes; 

Trong phương thức componentDidMount , bạn đã thực hiện một cuộc gọi HTTP để tìm nạp tất cả các công thức nấu ăn bằng cách sử dụng API Tìm nạp . Nếu phản hồi thành công, ứng dụng sẽ lưu mảng công thức về trạng thái công thức. Nếu có lỗi, nó sẽ chuyển hướng user đến trang chủ.

Cuối cùng, thêm một phương thức render trong lớp Recipe . Phương thức kết xuất chứa các phần tử React sẽ được đánh giá và hiển thị trên trang trình duyệt khi một thành phần được kết xuất. Trong trường hợp này, render phương pháp sẽ làm cho thẻ của công thức nấu ăn từ trạng thái thành phần. Thêm các dòng được đánh dấu sau vào Recipes.jsx :

~ / rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipes extends React.Component {   constructor(props) {     super(props);     this.state = {       recipes: []     };   }    componentDidMount() {     const url = "/api/v1/recipes/index";     fetch(url)       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.setState({ recipes: response }))       .catch(() => this.props.history.push("/"));   }   render() {     const { recipes } = this.state;     const allRecipes = recipes.map((recipe, index) => (       <div key={index} className="col-md-6 col-lg-4">         <div className="card mb-4">           <img             src={recipe.image}             className="card-img-top"             alt={`${recipe.name} image`}           />           <div className="card-body">             <h5 className="card-title">{recipe.name}</h5>             <Link to={`/recipe/${recipe.id}`} className="btn custom-button">               View Recipe             </Link>           </div>         </div>       </div>     ));     const noRecipe = (       <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">         <h4>           No recipes yet. Why not <Link to="/new_recipe">create one</Link>         </h4>       </div>     );      return (       <>         <section className="jumbotron jumbotron-fluid text-center">           <div className="container py-5">             <h1 className="display-4">Recipes for every occasion</h1>             <p className="lead text-muted">               We’ve pulled together our most popular recipes, our latest               additions, and our editor’s picks, so there’s sure to be something               tempting for you to try.             </p>           </div>         </section>         <div className="py-5">           <main className="container">             <div className="text-right mb-3">               <Link to="/recipe" className="btn custom-button">                 Create New Recipe               </Link>             </div>             <div className="row">               {recipes.length > 0 ? allRecipes : noRecipe}             </div>             <Link to="/" className="btn btn-link">               Home             </Link>           </main>         </div>       </>     );   } } export default Recipes; 

Lưu và thoát khỏi Recipes.jsx .

Đến đây bạn đã tạo một thành phần để hiển thị tất cả các công thức, bước tiếp theo là tạo một lộ trình cho nó. Mở file định tuyến front-end có tại app/javascript/routes/Index.jsx :

  • nano app/javascript/routes/Index.jsx

Thêm các dòng được đánh dấu sau vào file :

~ / rails_react_recipe / app / javascript / route / Index.jsx
import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import Home from "../components/Home"; import Recipes from "../components/Recipes";  export default (   <Router>     <Switch>       <Route path="/" exact component={Home} />       <Route path="/recipes" exact component={Recipes} />     </Switch>   </Router> ); 

Lưu và thoát khỏi file .

Đến đây, bạn nên xác minh mã của bạn đang hoạt động chính xác. Như bạn đã làm trước đây, hãy sử dụng lệnh sau để khởi động server của bạn:

  • rails s --binding=127.0.0.1

Hãy tiếp tục và mở ứng dụng trong trình duyệt của bạn. Bằng cách nhấp vào nút View Recipe trên trang chủ, bạn sẽ thấy một màn hình hiển thị các công thức hạt giống của bạn :

Trang công thức nấu ăn

Sử dụng CTRL+C trong cửa sổ Terminal để dừng server và nhận lại dấu nhắc của bạn.

Đến đây bạn có thể xem tất cả các công thức nấu ăn tồn tại trong ứng dụng của bạn , đã đến lúc tạo thành phần thứ hai để xem các công thức nấu ăn riêng lẻ. Tạo file Recipe.jsx trong folder app/javascript/components :

  • nano app/javascript/components/Recipe.jsx

Như với thành phần Recipes , nhập các module React và Link bằng cách thêm các dòng sau:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom"; 

Tiếp theo, tạo một lớp Recipe mở rộng lớp React.Component bằng cách thêm các dòng mã được đánh dấu:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };      this.addHtmlEntities = this.addHtmlEntities.bind(this);   } }  export default Recipe; 

Giống như với thành phần Recipes , trong hàm tạo, bạn khởi tạo một đối tượng trạng thái chứa trạng thái của một công thức. Bạn cũng ràng buộc một phương thức addHtmlEntities với this để nó có thể được truy cập trong thành phần. Phương thức addHtmlEntities sẽ được sử dụng để thay thế các thực thể ký tự bằng các thực thể HTML trong thành phần.

Để tìm một công thức cụ thể, ứng dụng của bạn cần id của công thức đó. Điều này nghĩa là thành phần Recipe của bạn mong đợi một param id . Bạn có thể truy cập điều này thông qua các props được chuyển vào thành phần.

Tiếp theo, thêm một componentDidMount phương pháp mà bạn sẽ truy cập vào id param từ match then chốt của props đối tượng. Sau khi nhận được id , bạn sẽ thực hiện một yêu cầu HTTP để tìm nạp công thức. Thêm các dòng được đánh dấu sau vào file của bạn:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };      this.addHtmlEntities = this.addHtmlEntities.bind(this);   }    componentDidMount() {     const {       match: {         params: { id }       }     } = this.props;      const url = `/api/v1/show/${id}`;      fetch(url)       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.setState({ recipe: response }))       .catch(() => this.props.history.push("/recipes"));   }  }  export default Recipe; 

Trong componentDidMount phương pháp, sử dụng đối tượng destructuring , bạn sẽ có được id param từ props đối tượng, sau đó sử dụng Fetch API, bạn thực hiện một yêu cầu HTTP để lấy công thức sở hữu id và tiết kiệm nó sang trạng thái thành phần sử dụng setState phương pháp. Nếu công thức không tồn tại, ứng dụng sẽ chuyển hướng user đến trang công thức nấu ăn.

Bây giờ, hãy thêm phương thức addHtmlEntities , phương thức này nhận một chuỗi và thay thế tất cả các dấu ngoặc mở và đóng đã thoát bằng các thực thể HTML của chúng. Điều này sẽ giúp ta chuyển đổi bất kỳ ký tự thoát nào đã được lưu trong hướng dẫn công thức của bạn:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };      this.addHtmlEntities = this.addHtmlEntities.bind(this);   }    componentDidMount() {     const {       match: {         params: { id }       }     } = this.props;      const url = `/api/v1/show/${id}`;      fetch(url)       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.setState({ recipe: response }))       .catch(() => this.props.history.push("/recipes"));   }    addHtmlEntities(str) {     return String(str)       .replace(/&lt;/g, "<")       .replace(/&gt;/g, ">");   } }  export default Recipe; 

Cuối cùng, thêm một render phương pháp mà được các công thức từ nhà nước và làm cho nó trên trang. Để thực hiện việc này, hãy thêm các dòng được đánh dấu sau:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };      this.addHtmlEntities = this.addHtmlEntities.bind(this);   }    componentDidMount() {     const {       match: {         params: { id }       }     } = this.props;      const url = `/api/v1/show/${id}`;      fetch(url)       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.setState({ recipe: response }))       .catch(() => this.props.history.push("/recipes"));   }    addHtmlEntities(str) {     return String(str)       .replace(/&lt;/g, "<")       .replace(/&gt;/g, ">");   }    render() {     const { recipe } = this.state;     let ingredientList = "No ingredients available";      if (recipe.ingredients.length > 0) {       ingredientList = recipe.ingredients         .split(",")         .map((ingredient, index) => (           <li key={index} className="list-group-item">             {ingredient}           </li>         ));     }     const recipeInstruction = this.addHtmlEntities(recipe.instruction);      return (       <div className="">         <div className="hero position-relative d-flex align-items-center justify-content-center">           <img             src={recipe.image}             alt={`${recipe.name} image`}             className="img-fluid position-absolute"           />           <div className="overlay bg-dark position-absolute" />           <h1 className="display-4 position-relative text-white">             {recipe.name}           </h1>         </div>         <div className="container py-5">           <div className="row">             <div className="col-sm-12 col-lg-3">               <ul className="list-group">                 <h5 className="mb-2">Ingredients</h5>                 {ingredientList}               </ul>             </div>             <div className="col-sm-12 col-lg-7">               <h5 className="mb-2">Preparation Instructions</h5>               <div                 dangerouslySetInnerHTML={{                   __html: `${recipeInstruction}`                 }}               />             </div>             <div className="col-sm-12 col-lg-2">               <button type="button" className="btn btn-danger">                 Delete Recipe               </button>             </div>           </div>           <Link to="/recipes" className="btn btn-link">             Back to recipes           </Link>         </div>       </div>     );   }  }  export default Recipe; 

Trong phương pháp render này, bạn chia các thành phần được phân tách bằng dấu phẩy thành một mảng và ánh xạ lên nó, tạo danh sách các thành phần. Nếu không có nguyên liệu, ứng dụng sẽ hiển thị thông báo Không có nguyên liệu nào . Nó cũng hiển thị hình ảnh công thức dưới dạng hình ảnh anh hùng, thêm nút xóa công thức bên cạnh hướng dẫn công thức và thêm nút liên kết trở lại trang công thức nấu ăn.

Lưu và thoát khỏi file .

Để xem thành phần Recipe trên một trang, hãy thêm nó vào file tuyến đường của bạn. Mở file tuyến đường của bạn để chỉnh sửa:

  • nano app/javascript/routes/Index.jsx

Bây giờ, hãy thêm các dòng được đánh dấu sau vào file :

~ / rails_react_recipe / app / javascript / route / Index.jsx
import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import Home from "../components/Home"; import Recipes from "../components/Recipes"; import Recipe from "../components/Recipe";  export default (   <Router>     <Switch>       <Route path="/" exact component={Home} />       <Route path="/recipes" exact component={Recipes} />       <Route path="/recipe/:id" exact component={Recipe} />     </Switch>   </Router> ); 

Trong file tuyến đường này, bạn đã nhập thành phần Recipe của bạn và thêm một tuyến đường cho nó. Lộ trình của nó có :id param sẽ được thay thế bằng id của công thức bạn muốn xem.

Sử dụng lệnh rails s để khởi động lại server của bạn, sau đó truy cập http://localhost:3000 trong trình duyệt của bạn. Nhấp vào nút Xem công thức nấu ăn để chuyển đến trang công thức nấu ăn. Trên trang công thức nấu ăn, xem bất kỳ công thức nào bằng cách nhấp vào nút Xem công thức nấu ăn . Bạn sẽ được chào đón với một trang được điền dữ liệu từ database của bạn:

Trang công thức đơn

Trong phần này, bạn đã thêm chín công thức vào database của bạn và tạo các thành phần để xem các công thức này, cả riêng lẻ và dưới dạng một bộ sưu tập. Trong phần tiếp theo, bạn sẽ thêm một thành phần để tạo công thức nấu ăn.

Bước 8 - Tạo công thức nấu ăn

Bước tiếp theo để có một ứng dụng công thức thực phẩm có thể sử dụng được là khả năng tạo công thức nấu ăn mới. Trong bước này, bạn sẽ tạo một thành phần để tạo công thức nấu ăn. Thành phần này sẽ chứa một biểu mẫu để thu thập chi tiết công thức được yêu cầu từ user và sẽ thực hiện yêu cầu hành động create trong bộ điều khiển Recipe để lưu dữ liệu công thức.

Tạo file NewRecipe.jsx trong folder app/javascript/components :

  • nano app/javascript/components/NewRecipe.jsx

Trong file mới, hãy nhập các module React và Liên kết bạn đã sử dụng cho đến nay trong các thành phần khác:

~ / rails_react_recipe / app / javascript / components / NewRecipe.jsx
import React from "react"; import { Link } from "react-router-dom"; 

Tiếp theo, tạo một NewRecipe lớp mà kéo dài React.Component lớp. Thêm đoạn mã được đánh dấu sau để tạo một thành phần React mở rộng react.Component .

~ / rails_react_recipe / app / javascript / components / NewRecipe.jsx
import React from "react"; import { Link } from "react-router-dom";  class NewRecipe extends React.Component {   constructor(props) {     super(props);     this.state = {       name: "",       ingredients: "",       instruction: ""     };      this.onChange = this.onChange.bind(this);     this.onSubmit = this.onSubmit.bind(this);     this.stripHtmlEntities = this.stripHtmlEntities.bind(this);   } }  export default NewRecipe; 

Trong phương NewRecipe khởi tạo của thành phần NewRecipe , bạn đã khởi tạo đối tượng trạng thái của bạn với các trường name , ingredientsinstruction . Đây là những trường bạn cần để tạo một công thức hợp lệ. Bạn cũng có ba phương pháp; onChange , onSubmitstripHtmlEntities mà bạn đã ràng buộc với this . Các phương thức này sẽ xử lý việc cập nhật trạng thái, gửi biểu mẫu và chuyển đổi các ký tự đặc biệt (như < ) thành các giá trị thoát / mã hóa của chúng (như &lt; ), tương ứng.

Tiếp theo, tạo phương thức stripHtmlEntities bằng cách thêm các dòng được đánh dấu vào thành phần NewRecipe :

~ / rails_react_recipe / app / javascript / components / NewRecipe.jsx
class NewRecipe extends React.Component {   constructor(props) {     super(props);     this.state = {       name: "",       ingredients: "",       instruction: ""     };      this.onChange = this.onChange.bind(this);     this.onSubmit = this.onSubmit.bind(this);     this.stripHtmlEntities = this.stripHtmlEntities.bind(this);   }    stripHtmlEntities(str) {     return String(str)       .replace(/</g, "&lt;")       .replace(/>/g, "&gt;");   }  }  export default NewRecipe; 

Trong phương thức stripHtmlEntities , bạn đang thay thế các ký tự <> bằng giá trị thoát của chúng. Bằng cách này, bạn sẽ không lưu trữ HTML thô trong database của bạn .

Tiếp theo, thêm các phương thức onChangeonSubmit vào thành phần NewRecipe để xử lý việc chỉnh sửa và gửi biểu mẫu:

~ / rails_react_recipe / app / javascript / components / NewRecipe.jsx
class NewRecipe extends React.Component {   constructor(props) {     super(props);     this.state = {       name: "",       ingredients: "",       instruction: ""     };      this.onChange = this.onChange.bind(this);     this.onSubmit = this.onSubmit.bind(this);     this.stripHtmlEntities = this.stripHtmlEntities.bind(this);   }    stripHtmlEntities(str) {     return String(str)       .replace(/</g, "&lt;")       .replace(/>/g, "&gt;");   }    onChange(event) {     this.setState({ [event.target.name]: event.target.value });   }    onSubmit(event) {     event.preventDefault();     const url = "/api/v1/recipes/create";     const { name, ingredients, instruction } = this.state;      if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)       return;      const body = {       name,       ingredients,       instruction: instruction.replace(/\n/g, "<br> <br>")     };      const token = document.querySelector('meta[name="csrf-token"]').content;     fetch(url, {       method: "POST",       headers: {         "X-CSRF-Token": token,         "Content-Type": "application/json"       },       body: JSON.stringify(body)     })       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.props.history.push(`/recipe/${response.id}`))       .catch(error => console.log(error.message));   }  }  export default NewRecipe; 

Trong phương thức onChange , bạn đã sử dụng tên thuộc tính được tính toán của ES6 để đặt giá trị của mọi đầu vào của user thành khóa tương ứng trong trạng thái của bạn. Trong phương thức onSubmit , bạn đã kiểm tra rằng không có đầu vào bắt buộc nào trống. Sau đó, bạn xây dựng một đối tượng có chứa các tham số được yêu cầu bởi bộ điều khiển công thức để tạo một công thức mới. Sử dụng biểu thức chính quy , bạn thay thế mọi ký tự dòng mới trong hướng dẫn bằng thẻ ngắt, vì vậy bạn có thể giữ lại định dạng văn bản do user nhập.

Để bảo vệ khỏi các cuộc tấn công truy vấn yêu cầu chéo (CSRF) , Rails đính kèm mã thông báo bảo mật CSRF vào trang HTML . Mã thông báo này được yêu cầu khi nào yêu cầu không phải GET được thực hiện. Với hằng số token trong mã trước đó, ứng dụng của bạn xác minh mã thông báo trên server và ném một ngoại lệ nếu mã thông báo bảo mật không trùng với những gì được mong đợi. Trong phương thức onSubmit , ứng dụng truy xuất mã thông báo CSRF được nhúng trong trang HTML của bạn bằng Rails và thực hiện một yêu cầu HTTP với một chuỗi JSON. Nếu công thức được tạo thành công, ứng dụng sẽ chuyển hướng user đến trang công thức nơi họ có thể xem công thức mới tạo của bạn .

Cuối cùng, thêm một render phương pháp mà làm cho một hình thức cho user nhập vào các chi tiết cho công thức user muốn tạo ra:

~ / rails_react_recipe / app / javascript / components / NewRecipe.jsx
class NewRecipe extends React.Component {   constructor(props) {     super(props);     this.state = {       name: "",       ingredients: "",       instruction: ""     };      this.onChange = this.onChange.bind(this);     this.onSubmit = this.onSubmit.bind(this);     this.stripHtmlEntities = this.stripHtmlEntities.bind(this);   }    stripHtmlEntities(str) {     return String(str)       .replace(/</g, "&lt;")       .replace(/>/g, "&gt;");   }    onChange(event) {     this.setState({ [event.target.name]: event.target.value });   }    onSubmit(event) {     event.preventDefault();     const url = "/api/v1/recipes/create";     const { name, ingredients, instruction } = this.state;      if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)       return;      const body = {       name,       ingredients,       instruction: instruction.replace(/\n/g, "<br> <br>")     };      const token = document.querySelector('meta[name="csrf-token"]').content;     fetch(url, {       method: "POST",       headers: {         "X-CSRF-Token": token,         "Content-Type": "application/json"       },       body: JSON.stringify(body)     })       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.props.history.push(`/recipe/${response.id}`))       .catch(error => console.log(error.message));   }    render() {     return (       <div className="container mt-5">         <div className="row">           <div className="col-sm-12 col-lg-6 offset-lg-3">             <h1 className="font-weight-normal mb-5">               Add a new recipe to our awesome recipe collection.             </h1>             <form onSubmit={this.onSubmit}>               <div className="form-group">                 <label htmlFor="recipeName">Recipe name</label>                 <input                   type="text"                   name="name"                   id="recipeName"                   className="form-control"                   required                   onChange={this.onChange}                 />               </div>               <div className="form-group">                 <label htmlFor="recipeIngredients">Ingredients</label>                 <input                   type="text"                   name="ingredients"                   id="recipeIngredients"                   className="form-control"                   required                   onChange={this.onChange}                 />                 <small id="ingredientsHelp" className="form-text text-muted">                   Separate each ingredient with a comma.                 </small>               </div>               <label htmlFor="instruction">Preparation Instructions</label>               <textarea                 className="form-control"                 id="instruction"                 name="instruction"                 rows="5"                 required                 onChange={this.onChange}               />               <button type="submit" className="btn custom-button mt-3">                 Create Recipe               </button>               <Link to="/recipes" className="btn btn-link mt-3">                 Back to recipes               </Link>             </form>           </div>         </div>       </div>     );   }  }  export default NewRecipe; 

Trong phương thức kết xuất, bạn có một biểu mẫu chứa ba trường đầu vào; một cho tên công recipeName , công recipeIngredients , người nhận và instruction . Mỗi trường đầu vào có một trình xử lý sự kiện onChange gọi phương thức onChange . Ngoài ra, có một trình xử lý sự kiện onSubmit trên nút gửi gọi phương thức onSubmit sau đó gửi dữ liệu biểu mẫu.

Lưu và thoát khỏi file .

Để truy cập thành phần này trong trình duyệt, hãy cập nhật file tuyến của bạn với tuyến của nó:

  • nano app/javascript/routes/Index.jsx

Cập nhật file tuyến đường của bạn để bao gồm các dòng được đánh dấu sau:

~ / rails_react_recipe / app / javascript / route / Index.jsx
import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import Home from "../components/Home"; import Recipes from "../components/Recipes"; import Recipe from "../components/Recipe"; import NewRecipe from "../components/NewRecipe";  export default (   <Router>     <Switch>       <Route path="/" exact component={Home} />       <Route path="/recipes" exact component={Recipes} />       <Route path="/recipe/:id" exact component={Recipe} />       <Route path="/recipe" exact component={NewRecipe} />     </Switch>   </Router> ); 

Với tuyến đường tại chỗ, hãy lưu và thoát file của bạn. Khởi động lại server phát triển của bạn và truy cập http://localhost:3000 trong trình duyệt của bạn. Điều hướng đến trang công thức nấu ăn và nhấp vào nút Tạo công thức mới . Bạn sẽ tìm thấy một trang có biểu mẫu để thêm công thức nấu ăn vào database của bạn :

Tạo trang công thức nấu ăn

Nhập các chi tiết công thức cần thiết và nhấp vào nút Tạo Công thức ; bạn sẽ thấy công thức mới tạo trên trang.

Trong bước này, bạn đã đưa ứng dụng công thức nấu ăn của bạn vào cuộc sống bằng cách thêm khả năng tạo công thức nấu ăn. Trong bước tiếp theo, bạn sẽ thêm chức năng xóa công thức nấu ăn.

Bước 9 - Xóa công thức nấu ăn

Trong phần này, bạn sẽ sửa đổi thành phần Công thức của bạn để có thể xóa công thức.

Khi bạn nhấp vào nút xóa trên trang công thức, ứng dụng sẽ gửi yêu cầu xóa công thức khỏi database . Để thực hiện việc này, hãy mở file Recipe.jsx của bạn:

  • nano app/javascript/components/Recipe.jsx

Trong phương thức khởi tạo của thành phần Recipe , liên kết this với phương thức deleteRecipe :

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };     this.addHtmlEntities = this.addHtmlEntities.bind(this);     this.deleteRecipe = this.deleteRecipe.bind(this);   } ... 

Bây giờ thêm phương thức deleteRecipe vào thành phần Recipe:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };      this.addHtmlEntities = this.addHtmlEntities.bind(this);     this.deleteRecipe = this.deleteRecipe.bind(this);   }    componentDidMount() {     const {       match: {         params: { id }       }     } = this.props;     const url = `/api/v1/show/${id}`;     fetch(url)       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.setState({ recipe: response }))       .catch(() => this.props.history.push("/recipes"));   }    addHtmlEntities(str) {     return String(str)       .replace(/&lt;/g, "<")       .replace(/&gt;/g, ">");   }    deleteRecipe() {     const {       match: {         params: { id }       }     } = this.props;     const url = `/api/v1/destroy/${id}`;     const token = document.querySelector('meta[name="csrf-token"]').content;      fetch(url, {       method: "DELETE",       headers: {         "X-CSRF-Token": token,         "Content-Type": "application/json"       }     })       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(() => this.props.history.push("/recipes"))       .catch(error => console.log(error.message));   }    render() {     const { recipe } = this.state;     let ingredientList = "No ingredients available"; ...  

Trong phương thức deleteRecipe , bạn sẽ xóa id của công thức, sau đó tạo url của bạn và lấy mã thông báo CSRF. Tiếp theo, bạn thực hiện yêu cầu DELETE tới bộ điều khiển Recipes để xóa công thức. Nếu công thức được xóa thành công, ứng dụng sẽ chuyển hướng user đến trang công thức nấu ăn.

Để chạy mã trong phương thức deleteRecipe khi nào nút xóa được nhấp, hãy chuyển mã đó làm trình xử lý sự kiện nhấp vào nút. Thêm sự kiện onClick vào nút xóa trong phương thức render :

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
... return (   <div className="">     <div className="hero position-relative d-flex align-items-center justify-content-center">       <img         src={recipe.image}         alt={`${recipe.name} image`}         className="img-fluid position-absolute"       />       <div className="overlay bg-dark position-absolute" />       <h1 className="display-4 position-relative text-white">         {recipe.name}       </h1>     </div>     <div className="container py-5">       <div className="row">         <div className="col-sm-12 col-lg-3">           <ul className="list-group">             <h5 className="mb-2">Ingredients</h5>             {ingredientList}           </ul>         </div>         <div className="col-sm-12 col-lg-7">           <h5 className="mb-2">Preparation Instructions</h5>           <div             dangerouslySetInnerHTML={{               __html: `${recipeInstruction}`             }}           />         </div>         <div className="col-sm-12 col-lg-2">           <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>             Delete Recipe           </button>         </div>       </div>       <Link to="/recipes" className="btn btn-link">         Back to recipes       </Link>     </div>   </div> ); ... 

Tại thời điểm này trong hướng dẫn, file Recipe.jsx hoàn chỉnh của bạn sẽ trông giống như sau:

~ / rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react"; import { Link } from "react-router-dom";  class Recipe extends React.Component {   constructor(props) {     super(props);     this.state = { recipe: { ingredients: "" } };      this.addHtmlEntities = this.addHtmlEntities.bind(this);     this.deleteRecipe = this.deleteRecipe.bind(this);   }    addHtmlEntities(str) {     return String(str)       .replace(/&lt;/g, "<")       .replace(/&gt;/g, ">");   }    componentDidMount() {     const {       match: {         params: { id }       }     } = this.props;     const url = `/api/v1/show/${id}`;     fetch(url)       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(response => this.setState({ recipe: response }))       .catch(() => this.props.history.push("/recipes"));   }    deleteRecipe() {     const {       match: {         params: { id }       }     } = this.props;     const url = `/api/v1/destroy/${id}`;     const token = document.querySelector('meta[name="csrf-token"]').content;     fetch(url, {       method: "DELETE",       headers: {         "X-CSRF-Token": token,         "Content-Type": "application/json"       }     })       .then(response => {         if (response.ok) {           return response.json();         }         throw new Error("Network response was not ok.");       })       .then(() => this.props.history.push("/recipes"))       .catch(error => console.log(error.message));   }    render() {     const { recipe } = this.state;     let ingredientList = "No ingredients available";     if (recipe.ingredients.length > 0) {       ingredientList = recipe.ingredients         .split(",")         .map((ingredient, index) => (           <li key={index} className="list-group-item">             {ingredient}           </li>         ));     }      const recipeInstruction = this.addHtmlEntities(recipe.instruction);      return (       <div className="">         <div className="hero position-relative d-flex align-items-center justify-content-center">           <img             src={recipe.image}             alt={`${recipe.name} image`}             className="img-fluid position-absolute"           />           <div className="overlay bg-dark position-absolute" />           <h1 className="display-4 position-relative text-white">             {recipe.name}           </h1>         </div>         <div className="container py-5">           <div className="row">             <div className="col-sm-12 col-lg-3">               <ul className="list-group">                 <h5 className="mb-2">Ingredients</h5>                 {ingredientList}               </ul>             </div>             <div className="col-sm-12 col-lg-7">               <h5 className="mb-2">Preparation Instructions</h5>               <div                 dangerouslySetInnerHTML={{                   __html: `${recipeInstruction}`                 }}               />             </div>             <div className="col-sm-12 col-lg-2">               <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>                 Delete Recipe               </button>             </div>           </div>           <Link to="/recipes" className="btn btn-link">             Back to recipes           </Link>         </div>       </div>     );   } }  export default Recipe; 

Lưu và thoát khỏi file .

Khởi động lại server ứng dụng và chuyển đến trang chủ. Nhấp vào nút Xem Công thức nấu ăn để xem tất cả các công thức nấu ăn hiện có, xem bất kỳ công thức riêng lẻ nào và nhấp vào nút Xóa Công thức nấu ăn trên trang để xóa bài viết. Bạn sẽ được chuyển hướng đến trang công thức nấu ăn và công thức đã xóa sẽ không còn nữa.

Với nút xóa đang hoạt động, bây giờ bạn có một ứng dụng công thức nấu ăn đầy đủ chức năng!

Kết luận

Trong hướng dẫn này, bạn đã tạo một ứng dụng công thức nấu ăn với Ruby on Rails và giao diện user React, sử dụng PostgreSQL làm database và Bootstrap để tạo kiểu. Nếu bạn muốn xem qua nhiều nội dung Ruby on Rails hơn, hãy xem phần Bảo mật Truyền thông của ta trong hướng dẫn Ứng dụng Rails ba tầng Sử dụng SSH Tunnels hoặc xem loạt bài Cách viết mã trong Ruby của ta để làm mới các kỹ năng Ruby của bạn. Để tìm hiểu sâu hơn về React, hãy thử bài viết Cách hiển thị dữ liệu từ API DigitalOcean với React của ta .


Tags:

Các tin liên quan