บริษัทที่สร้างฝั่งฟรอนต์เอนด์ทั้งหมดของแอปพลิเคชันมักเลือกเฟรมเวิร์กเดียวกัน เช่น Rails เพื่อสร้างแบ็กเอนด์ เป็นหลายปีแล้ว นี่เป็นตัวเลือกที่ดีที่สุดและน่าเชื่อถือที่สุด
วันนี้ ไลบรารีและเฟรมเวิร์กจำนวนมาก ในจักรวาลฟรอนต์เอนด์ที่พัฒนาอย่างต่อเนื่อง ช่วยให้นักพัฒนาสามารถเลือกแพลตฟอร์มที่แตกต่างกันสำหรับแบ็กเอนด์และฟรอนต์เอนด์ และรวมพวกมันได้อย่างง่ายดาย
React กลายเป็นไททานของฟรอนต์เอนด์ Pangea หากคุณทำงานกับ Ruby on Rails โอกาสคือคุณเคยต้องสลับหน้า Rails เริ่มต้นเป็นโค้ด React (หรือเฟรมเวิร์กฟรอนต์อื่น) บางครั้ง คุณอาจแค่ชอบฟีเจอร์ของ Rails + React และอยากรวมพลังของทั้งสองเทคโนโลยีเข้าในแอปเดียว
และ นั่นโอเค! บทความนี้มุ่งสำรวจตัวอย่างจริง: แอปพลิเคชัน CRUD ที่จัดการสินค้าคงคลังเบียร์ จะถูกสร้างด้วย Rails ก่อน แล้วฟรอนต์เอนด์ใหม่ที่ทำด้วย React จะถูกรวมเข้ามาต่อไป
แอปพลิเคชัน Beer CRUD
ในไม่กี่ขั้นตอน คุณจะเข้าใจชิ้นส่วนหลักของ React ว่า Rails นำ React ไปใช้อย่างไร และคุณสามารถเริ่มรวม Rails และ React เข้าในโปรเจกต์อนาคตของคุณได้อย่างไร
การตั้งค่า (Setup)
เป็นข้อกำหนดเบื้องต้น คุณต้องมีสภาพแวดล้อมที่ตั้งค่าพร้อมกับ Ruby (และ Rails), Node.js และ Yarn
คุณสามารถเลือก npm เพื่อจัดการแพ็กเกจฟรอนต์ได้ แต่เราจะใช้ Yarn เพื่อความเรียบง่าย
ในโฟลเดอร์ที่คุณชอบ ให้รันคำสั่งต่อไปนี้:
rails new crud-rails-react นี่จะเริ่มโปรเจกต์ Rails ของเรา จากนั้นเปิดโปรเจกต์ที่สร้างขึ้นใน IDE ของคุณและไปที่ Gemfile โดยตรง
เราต้องแก้บั๊กที่คำสั่งนี้สร้างสำหรับ gem SQLite ดังนั้น ตรวจสอบให้แน่ใจว่าคุณค้นหา gem sqlite3 และเปลี่ยนเป็น:
gem 'sqlite3', '~> 1.3.10' สิ่งนี้จะป้องกันข้อผิดพลาดบางอย่างที่เกี่ยวข้องกับการจัดการเวอร์ชันฐานข้อมูล เนื่องจากเราจะใช้ SQLite เป็นฐานข้อมูลเริ่มต้นสำหรับตัวอย่าง CRUD
อย่ากังวล ในฐานข้อมูลอื่นๆ ปัญหานี้จะไม่เกิดขึ้น
การตั้งค่าฐานข้อมูล (Database Config)
ผมชอบสร้างสิ่งต่างๆ จากฐานข้อมูลขึ้นไปสู่เลเยอร์บนสุด ดังนั้น มาเริ่มสร้างโมเดลฐานข้อมูลกัน
เราจะต้องการเพียงหนึ่งโมเดล ดังนั้น ไม่มีคำสั่งฟีเจอร์ไหนดีกว่า Rails scaffolding สำหรับงานนี้:
rails g scaffold Beer brand:string style:string country:string quantity:integer & rake db:migrate โมเดลนี้ค่อนข้างพื้นฐาน ดังนั้นรู้สึกสบายๆ เพิ่มแอตทริบิวต์และชนิดข้อมูลอื่นๆ ได้ตามที่คุณต้องการ
ภายในโฟลเดอร์ db/migrate มีไฟล์ใหม่ที่มีชื่อลงท้ายด้วย "_create_beers.rb" นี่คือเรคอร์ดที่ Rails สร้างให้เราเพื่อแทนเบียร์
โมเดล จะถูกสร้างภายใต้โฟลเดอร์ app/models ทิ้งไว้เหมือนเดิม และเพิ่มโค้ดต่อไปนี้ลงในไฟล์ db/seeds.rb:
Beer.create(brand: 'Double Stout', style: 'Stout', country: 'England', quantity: 54) Beer.create(brand: 'Spaten', style: 'Helles', country: 'Germany', quantity: 3) Beer.create(brand: 'Newcastle', style: 'Brown ale', country: 'UK', quantity: 12) ไฟล์นี้จะเก็บโหลดข้อมูลเริ่มต้นสำหรับฐานข้อมูลเมื่อแอปเริ่มต้น เพื่อปล่อยพวกมันเข้าไปในฐานข้อมูล ให้รันคำสั่งต่อไปนี้:
rake db:seed เสร็จแล้ว! ตอนนี้คุณมี เบียร์บนโต๊ะ แล้ว
การตั้งค่า Webpacker (Webpacker Setup)
Webpacker เป็นหนึ่งใน static module bundler ที่ใช้กันอย่างแพร่หลายที่สุดสำหรับแอปพลิเคชัน JavaScript ด้วยเหตุนี้ จึงเป็นคู่คู่ที่สมบูรณ์แบบในการนำความสามารถของ React เข้าไปในแอปพลิเคชันที่มีอยู่แล้ว
Rails ยังให้เรา Webpacker bundler ที่ปรับให้เหมาะสมกับการจัดการแอปพลิเคชันที่คล้าย JavaScript ภายใน Rails
เพื่อติดตั้ง ให้เพิ่มบรรทัดที่สองลงใน Gemfile ของคุณ ดังนี้:
gem 'webpacker', '~> 4.3.x' ยอดเยี่ยม! นี่คือ gem เพียงตัวเดียวที่เราต้องเพิ่มสำหรับการพัฒนาแอปทั้งหมด นี่เป็นไปได้เพราะเรามอบหน้าที่ฟรอนต์ให้ Yarn ซึ่งจะตั้งค่าต่อไปในบทความนี้
ตอนนี้ ถึงเวลาติดตั้งอัปเดตโดยการออกคำสั่งต่อไปนี้:
bundle install bundle exec rake webpacker:install bundle exec rake webpacker:install:react คำสั่งแรกเป็นที่รู้จักดีสำหรับนักพัฒนา Rails ส่วนใหญ่ เรากำลังติดตั้ง dependencies ทั้งหมด รวมถึง Webpacker เอง
เมื่อ Webpacker ติดตั้งเสร็จ เราสามารถเลียนแบบคำสั่งของมันผ่าน Rake เพื่อติดตั้ง dependencies ของ Webpacker รวมถึงของ React
นี่เป็นขั้นตอนสำคัญมาก เพราะนี่คือที่ที่ Webpacker จะแน่ใจว่า dependencies JavaScript ทั้งหมดของคุณตั้งค่าอย่างเหมาะสมสำหรับสภาพแวดล้อม Rails แล้ว อย่าข้ามขั้นตอนนี้หรือพยายามรันพวกมันโดยตรงเหมือนที่คุณทำปกติกับ npm หรือ Yarn นะครับ?
เมื่อคำสั่งเสร็จสิ้น โฟลเดอร์และไฟล์สองสามอย่าง (อย่าง node_modules และ package.json) จะถูกสร้างขึ้นด้วย
การตั้งค่าฟรอนต์เอนด์ (Front-end Setup)
การตั้งค่าทั้งหมดที่เราทำมาแล้วเพียงพอที่จะทำให้แอปพลิเคชัน CRUD ทำงานกับ Rails เท่านั้น หากคุณเริ่มเซิร์ฟเวอร์ Rails ผ่านคำสั่ง rails s นี่จะเป็นผลลัพธ์:
Rails auto-generated beer CRUD
อย่างไรก็ตาม เราต้องการ CRUD ของตัวเองกับ React
ขั้นแรก คุณต้องแน่ใจว่า dependencies ฟรอนต์เอนด์ที่จำเป็นทั้งหมดถูกตั้งค่าผ่าน Yarn:
- React
- React Router เพื่อจัดการการนำทางในแอป React
- Ant Design สำหรับ React components สำเร็จรูป
Ant Design (รู้จักว่า antd) เป็นไลบรารีโอเพนซอร์สที่อุดมสมบูรณ์สำหรับแอปพลิเคชันระดับองค์กร มันให้ชุด React components ที่ปรับแต่งได้สูงมากซึ่งทำให้การพัฒนาเว็บแอปง่ายขึ้นมาก
เพื่อติดตั้งทุกอย่าง ให้รันคำสั่งต่อไปนี้:
yarn add antd react-router-dom เราไม่ต้องเพิ่มไลบรารี react อย่างชัดเจน เพราะ react-router-dom จะทำให้มัน
ในจุดนี้ เมื่อคุณเปิดไฟล์ package.json เนื้อหาที่สร้างอัตโนมัติจะเป็นดังนี้:
{ "dependencies": { "@babel/preset-react": "^7.12.1", "@rails/webpacker": "4.3.0", "antd": "^4.7.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "prop-types": "^15.7.2", "react": "^17.0.0", "react-dom": "^17.0.0", "react-router-dom": "^5.2.0" }, "devDependencies": { "webpack-dev-server": "^3.11.0" } } นี่คือการตั้งค่าพื้นฐานที่สุดสำหรับคอมโบ Rails-React ดังนั้น เราพร้อมที่จะไปต่อกับโค้ด
การปรับด้าน Rails (Adapting the Rails Side)
มีการกระทำสำคัญบางอย่างที่ต้องจัดการฝั่ง Rails ก่อนไปต่อฟรอนต์
ขั้นแรก เราต้องกำหนดคอนโทรลเลอร์ที่จะจัดการการ redirect หน้า เนื่องจากเราสร้าง single-page application (SPA) จึงต้องการคอนโทรลเลอร์เพียงตัวเดียว ซึ่งถูกสร้างแล้ว: BeersController
เปิดมันภายใต้โฟลเดอร์ app/controllers และเปลี่ยนเนื้อหาเป็นดังนี้:
class BeersController < ApplicationController def index end end อย่ากังวล โค้ดที่เราลบทิ้งจะถูกวางในคอนโทรลเลอร์ถัดไปที่จะสร้าง
ฟังก์ชันเดียวของคอนโทรลเลอร์นี้คือการให้ route โดยตรงจาก Rails ไป React ซึ่งเป็นเหตุผลที่เราตั้งค่าเฉพาะเมธอด index
เพื่อเชื่อมต่อโดยตรงกับมัน ให้เปิดไฟล์ routes.rb ภายใต้โฟลเดอร์ config และเปลี่ยนเนื้อหาเป็นดังนี้:
Rails.application.routes.draw do root 'beers#index' # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end สังเกต config root ใหม่ ใช่ เรากำลังแมป endpoint root ไปยังเมธอด index ของ beers
คุณยังต้องว่างเปล่าไฟล์ index.html.erb ภายในโฟลเดอร์ app/views/beers เนื่องจากเราไม่ต้องการให้เนื้อหาเว็บ Rails ใดๆ ถูกเรนเดอร์ นี่คือทริกที่เราสามารถใช้เพื่อบังคับให้ Rails แสดงโค้ด React เท่านั้น
Beer API
ตอนนี้ มาไปสร้าง Beer API โครงสร้าง API จะเหมือนกับ BeersController เกือบทุกประการ แต่มีการเปลี่ยนแปลงเล็กน้อย เพื่อสร้างมัน ให้รันคำสั่งต่อไปนี้:
rails generate controller api/v1/Beers ให้แน่ใจว่าสร้างมันโดยใช้ระบบเวอร์ชันนิ่ง (versioning) นี่จะช่วยให้คุณพัฒนา API ของคุณในอนาคตและแยกมันจากคอนโทรลเลอร์ทั่วไปได้
ตอนนี้ เปิดไฟล์ app/controllers/api/v1/beers_controller.rb และแทนที่โค้ดด้วยสิ่งต่อไปนี้:
class Api::V1::BeersController < ApplicationController before_action :set_beer, only: [:show, :edit, :update, :destroy] # GET /beers # GET /beers.json def index @beers = Beer.all.order(brand: :asc) render json: @beers end # GET /beers/1 # GET /beers/1.json def show if @beer render json: @beer else render json: @beer.errors end end # GET /beers/new def new @beer = Beer.new end # GET /beers/1/edit def edit end # POST /beers # POST /beers.json def create @beer = Beer.new(beer_params) if @beer.save render json: @beer else render json: @beer.errors end end # PATCH/PUT /beers/1 # PATCH/PUT /beers/1.json def update end # DELETE /beers/1 # DELETE /beers/1.json def destroy @beer.destroy render json: { notice: 'Beer was successfully removed.' } end private # Use callbacks to share common setup or constraints between actions. def set_beer @beer = Beer.find(params[:id]) end # Only allow a list of trusted parameters through. def beer_params params.permit(:brand, :style, :country, :quantity) end end ส่วนใหญ่ของ operations ถูกนำกลับมาใช้ใหม่จากคอนโทรลเลอร์ก่อนหน้า
สนิปเพต before_action จะดูแลการกู้คืนออบเจ็กต์ beer ที่เหมาะสมตามพารามิเตอร์ id ภายใน requests เฉพาะ operations ที่วางในอาร์เรย์หลัง :only clause เท่านั้นที่จะต้องการฟีเจอร์ auto-recover นี้
ส่วนที่เหลือของเมธอดเทียบเท่ากับแต่ละ operation ของ CRUD แค่จำได้ว่าให้คืนค่า JSON เสมอเป็นการตอบสนองต่อ requests ของคุณ เพราะนี่คือรูปแบบที่เราจะใช้ภายใน React components ของเรา
สุดท้าย คุณจะต้องปรับ config/routes.rb อีกครั้งเพื่อรวม routes ที่สร้างใหม่ ดังนั้น ตรวจสอบให้แน่ใจว่าเปลี่ยนเนื้อหาไฟล์เป็นดังนี้:
Rails.application.routes.draw do namespace :api do namespace :v1 do get 'beers/index' post 'beers/create' delete 'beers/:id', to: 'beers#destroy' end end root 'beers#index' # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html end Route destroy สาธิตวิธีแมปไปยังเมธอด destroy หากไม่ได้ตั้งค่าอย่างชัดเจนใน path
React Components (คอมโพเนนต์ React)
React ทำงานผ่านคอมโพเนนต์ ซึ่งทำหน้าที่เหมือนบล็อกก่อสร้างของเว็บแอปพลิเคชัน แต่ละตัวทำหน้าที่หนึ่งหรือหลายอย่างที่มีความหมายเป็นบริบท
สั้นๆ คอมโพเนนต์ประกอบด้วย JavaScript class หรือ function ที่สามารถรับ properties เป็นพารามิเตอร์ ประมวลผล business logic ภายใน และในที่สุด คืนค่า UI component ที่ทำงานได้แทนส่วนหนึ่งของหน้าจอ
มาดูโค้ดสนิปเพตที่ดึงมาจากตัวอย่างต่อไปที่เราจะสร้าง:
<Layout> <Header /> <Content>...</Content> <Footer>Honeybadger ©2020.</Footer> </Layout> ใช่ React components ใช้ภาษามาร์กอัพแบบกำหนดเองชื่อ JSX (JavaScript XML) ที่คล้าย HTML มาก ทุกแท็ก HTML พร้อมใช้งานภายในไฟล์ JSX ด้วย คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ JSX ได้ที่นี่
ตัวอย่างข้างบนสาธิตว่า antd จัดการกับคอมโพเนนต์โครงสร้าง layout อย่างไร คอมโพเนนต์ประกอบด้วยคอมโพเนนต์อื่นๆ และซ้อนกัน ทำให้เป็นหนึ่งเดียว บางตัวรับ properties (optional หรือไม่) และบางตัวสามารถมีเนื้อหาภายในได้
คอมโพเนนต์สามารถเป็น class-based หรือ function-based
Class Components (คอมโพเนนต์แบบคลาส)
Class-based components ถูกสร้างเป็น JavaScript classes ตามปกติ เหมือนตัวอย่างด้านล่าง:
class Beers extends React.Component {} พวกมันสืบทอดจากคลาส React.Component มี lifecycle และให้เมธอดการใช้งานเพื่อรันโค้ดก่อนการเริ่มต้น การเรนเดอร์ และขั้นตอนการทำลาย
อย่างไรก็ตาม เมธอดที่สำคัญที่สุด (และจำเป็น) คือ render() ซึ่งถูกเรียกทุกครั้งที่คอมโพเนนต์ถูกอัปเดต
Functional Components (คอมโพเนนต์แบบฟังก์ชัน)
Functional components ใช้ arrow function ของ ES6 และลดความซับซ้อนของ React components ด้าน syntax และ complexity
คอมโพเนนต์ Beer เดียวกันข้างบนจะถูกแทนในรูปฟังก์ชันดังนี้:
const Beers = () => <div>My Beers</div>; ง่ายกว่ามากใช่ไหม?
การตั้งค่า React (Setting Up React)
เราได้ว่างหน้า index ของ Rails แล้ว ตอนนี้ เราจะบอก Rails ว่ามันต้องทำให้ React เป็นฟรอนต์เอนด์เริ่มต้น
เพื่อบรรลุเป้าหมายนี้ คุณต้องเพิ่มบรรทัดโค้ดต่อไปนี้ลงในแท็ก <head> ของไฟล์ app/views/layouts/application.html.erb ของคุณ:
<%= javascript_pack_tag 'index' %> นี่จะเพิ่ม JavaScript pack เข้าไปใน header ของแอปพลิเคชัน ซึ่งจะทำให้ไฟล์ JavaScript ทั้งหมด รวมถึง React ทำงานภายในหน้า index
เราต้องแน่ใจว่าไฟล์ index.jsx มีชื่อเดียวกัน เพราะมันถูกชี้โดย import pack
เพื่อวัตถุประสงค์นี้ ให้เปลี่ยนชื่อไฟล์ app/javascript/packs/hello_react.jsx ที่สร้างอัตโนมัติเป็น index.jsx
จากนั้น แทนที่โค้ดด้วยสิ่งต่อไปนี้:
import React from "react"; import { render } from "react-dom"; import App from "../components/App"; document.addEventListener("DOMContentLoaded", () => { render(<App />, document.body.appendChild(document.createElement("div"))); }); อย่าสับสนไฟล์นี้กับไฟล์แอปพลิเคชัน React เนื่องจากมันเพียงแค่ไฟล์ที่จะโหลดลำดับชั้นแอป React ทั้งหมดเข้า DOM ผ่านฟังก์ชัน render ของ ReactDOM
ปกติ แอป React ทุกตัวเริ่มจากไฟล์ index.js ที่โหลดทุกอย่างที่จำเป็น รวมถึง React เอง
แท็ก App แมปคอมโพเนนต์บนสุดของลำดับชั้นของเรา ดังนั้น ให้สร้างมันเป็น index.jsx ภายใต้โฟลเดอร์ javascript/components (สร้างโฟลเดอร์ด้วยตนเองหากยังไม่มี) และวางโค้ดต่อไปนี้:
import React from "react"; import Routes from "../routes/index"; import "antd/dist/antd.css"; export default () => <>{Routes}</>; ทางเลือกอื่น คุณสามารถ import ไฟล์ CSS ของ antd ภายใน index.jsx ทั้งสองวิธีจะทำงานได้
รายการ routes วางอยู่ภายใต้โฟลเดอร์ routes พวกมันถูกดึงมาจากไลบรารี React Router ซึ่งทำงานหนักส่วนใหญ่ให้เรา นี่คือเนื้อหาของมัน:
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> ); แต่ละ route ของคุณต้องแมปภายในแท็ก Route ที่แตกต่างกัน พารามิเตอร์ path ต้องตรงกับ URI ของแต่ละ endpoint route ในขณะที่พารามิเตอร์ component ระบุคอมโพเนนต์ที่ React Router ควร redirect request ไป
จำไว้ว่าเราจะมี route เดียวสำหรับ SPA ของเรา คุณสามารถเพิ่ม path อื่นๆ ได้ที่นี่ หากคุณต้องการแมป /beers สำหรับรายการเบียร์ ตัวอย่างเช่น แต่เราจะทำให้เรียบง่าย
สังเกตด้วยว่าเรากำลัง import คอมโพเนนต์ Home ที่นี่ ซึ่งยังไม่มีอยู่ ดังนั้น มาสร้างมันเป็น Home.jsx ภายใต้โฟลเดอร์ components จากนั้น เพิ่มโค้ดต่อไปนี้:
import { Layout } from "antd"; import React from "react"; import Beers from "./Beers"; import Header from "./Header"; const { Content, Footer } = Layout; export default () => ( <Layout className="layout"> <Header /> <Content style={{ padding: "0 50px" }}> <div className="site-layout-content" style={{ margin: "100px auto" }}> <h1>Beer Catalog</h1> <Beers /> </div> </Content> <Footer style={{ textAlign: "center" }}>Honeybadger ©2020.</Footer> </Layout> ); เมื่อพูดถึง React components ผมชอบสร้างจากบนลงล่าง วิธีนี้คุณสามารถมองภาพรวมของคอมโพเนนต์ทั้งหมดที่จำเป็นเพื่อให้แอปทำงานเป็นหนึ่งเดียว
คอมโพเนนต์ Home พฤติกรรมเหมือน assembler มันจัดหาทุกชิ้นส่วนคอมโพเนนต์ของแอป เช่น Layout, Header, Content, และ Footer
สำคัญที่จะแยกแยะอย่างถูกต้องว่าแต่ละชิ้นส่วนมาจากไหน Antd ให้ชุดคอมโพเนนต์สำเร็จรูปมากมาย เช่น Layout, Content และ Footer เพื่อประกอบหน้าจอของคุณ
พวกมันเน้นโครงสร้างของส่วนหน้าหลัก แต่บางตัวก็ให้ built-in CSS styles ซึ่งจะทำให้เราได้ลุคที่ดีขึ้น
คอมโพเนนต์ Header (The Header Component)
ไฟล์คอมโพเนนต์ Header.jsx ซึ่งควรสร้างภายในโฟลเดอร์ javascript/components จะเก็บเนื้อหาของ header รวมถึงเมนู antd แบบง่ายและ div พร้อมโลโก้ Honeybadger ตามที่แสดงด้านล่าง:
Antd menu items
ด้านล่าง คุณสามารถหาโค้ดที่จะวางใน Header.jsx:
import React from "react"; import { Layout, Menu } from "antd"; const { Header } = Layout; export default () => ( <Header> <div className="logo" /> <Menu theme="dark" mode="horizontal" defaultSelectedKeys={["1"]}> <Menu.Item key="1">Home</Menu.Item> <Menu.Item key="2">Our Services</Menu.Item> <Menu.Item key="3">Contact</Menu.Item> </Menu> </Header> ); คอมโพเนนต์ Menu ของ Antd ใช้งานง่ายแต่กว้างขวางในแง่ของตัวเลือกการปรับแต่งที่พร้อมใช้งาน ช่วยให้เราสร้าง navigation drawers, drop-downs, groups และ subgroups ได้
สังเกตว่าเรากำลังให้ defaultSelectedKeys อาร์เรย์ที่บอกเมนูว่ารายการไหน active
เมนูของเราจะไม่นำทางไปที่ไหน มันจะครอบครองหน้าจอเพื่อ fulfill look-and-feel เท่านั้น ดังนั้น มาไปที่คอมโพเนนต์ Beers กัน
คอมโพเนนต์ Beers (The Beers Component)
คอมโพเนนต์นี้เน้นที่การแสดงรายการเบียร์ และ actions ที่พร้อมใช้งานภายในตาราง เช่น การลบ การแบ่งหน้า (pagination) และการโหลดตารางใหม่
การแสดงภาพของ React actions & components
ดูภาพด้านบน เราแยกคอมโพเนนต์และ actions ลงระดับต่ำกว่า เพื่อให้คุณเข้าใจได้ดีว่าจะทำอะไรที่นี่
React State (สถานะ React)
React components ถูกสร้างด้วยออบเจ็กต์ state ออบเจ็กต์นี้ทำหน้าที่เป็น store ที่แนบมากับคอมโพเนนต์ที่กำหนดโดยตรง แต่ละคอมโพเนนต์มีออบเจ็กต์ state ของตัวเอง และทุกครั้งที่คุณเปลี่ยน state คอมโพเนนต์จะถูก re-rendered ใหม่
การกระทำแรกของคอมโพเนนต์ Beers คือการแสดงรายการบนตาราง เพื่อวัตถุประสงค์นี้ เราจะต้องเก็บรายการนี้ในอาร์เรย์:
state = { beers: [], }; รายการเบียร์ (The Beers Listing)
เพื่อให้อาร์เรย์นี้มีข้อมูล เราจะต้องดึงรายการจาก API controller ที่เราสร้างก่อนหน้านี้ ดูฟังก์ชันที่จะดึงมัน:
loadBeers = () => { const url = "api/v1/beers/index"; fetch(url) .then((data) => { if (data.ok) { return data.json(); } throw new Error("Network error."); }) .then((data) => { data.forEach((beer) => { const newEl = { key: beer.id, id: beer.id, brand: beer.brand, style: beer.style, country: beer.country, quantity: beer.quantity, }; this.setState((prevState) => ({ beers: [...prevState.beers, newEl], })); }); }) .catch((err) => message.error("Error: " + err)); }; เพื่อความเรียบง่าย เราจะใช้ Fetch API ที่พร้อมใช้งานในเบราว์เซอร์สมัยใหม่ทุกครั้งที่เราต้องการขอข้อมูลจาก API
ฟังก์ชันข้างบนใช้ขั้นตอนไม่กี่ขั้นตอนในการดึงอาร์เรย์ของเบียร์จาก API:
- มันขอ endpoint /index แบบ asynchronous และ
thenตรวจสอบว่าสถานะ response เท่ากับ OK - หากเป็นเช่นนั้น เราคืนค่าข้อมูลเป็น JSON ไม่เช่นนั้น ให้โยน
Error Thenเรา iterate ผ่านอาร์เรย์ผลลัพธ์เพื่อประกอบออบเจ็กต์ beer ของเราเองและเพิ่มเข้าไปในอาร์เรย์beersของ state- หากมีอะไรผิดพลาดระหว่างกระบวนการ บล็อก
catchจะจับ exception และแสดงเป็น message alert
ใช่ไหม? นี่คือขั้นตอนเดียวกันที่เราจะทำสำหรับ requests อื่นๆ ทั้งหมด
แต antd แสดงข้อมูลในตารางอย่างไร? คำถามดี! มาดูโค้ดต่อไปนี้:
columns = [ { title: "Brand", dataIndex: "brand", key: "brand", }, ...{ title: "", key: "action", render: (_text, record) => ( <Popconfirm title="Are you sure to delete this beer?" onConfirm={() => this.deleteBeer(record.id)} okText="Yes" cancelText="No"> <a href="#" type="danger"> Delete{" "} </a> </Popconfirm> ), }, ]; ผมลดความซับซ้อนลงนิดหน่อยเพื่อความเข้าใจที่ดีขึ้น นี่คืออาร์เรย์ที่แทนโครงร่างของตารางของเรา นี่คือวิธีที่ตาราง antd ทำงาน พวกมันต้องการรับข้อมูลเมตาของโครงสร้างตาราง (แถวและคอลัมน์) ของคุณเป็นอาร์เรย์
แต่ละคอลัมน์คือออบเจ็กต์ภายในอาร์เรย์ และลำดับสำคัญที่นี่ แอตทริบิวต์ title รับชื่อคอลัมน์ ในขณะที่ชื่อ dataIndex คือวิธีที่มันจะรู้จักภายใน React components และ key คือตัวระบุที่ไม่ซ้ำกัน
สำหรับคอลัมน์ส่วนใหญ่ การตั้งค่าเหมือนกัน ยกเว้นคอลัมน์ actions ที่นั่น เราต้องระบุลิงก์ของ action ที่จะทริกเกอร์เมื่อผู้ใช้ต้องการลบรายการ สังเกตว่าเราใช้คอมโพเนนต์ Popconfirm ของ antd
มันเป็นคอมโพเนนต์ที่ดีมากซึ่งช่วยงานในการแจ้งผู้ใช้ยืนยันการกระทำก่อนที่จะเกิดขึ้น ภาพด้านล่างสาธิตลักษณะ:
กล่องโต้ตอบยืนยันก่อนลบ
การกระทำลบ (Delete Action)
เพื่อลบรายการ เราจะต้องดำเนินการหลัก 2 อย่าง: การเรียก delete บน API และการโหลดตารางใหม่
ฟังก์ชัน delete คล้ายกับ fetch แรกที่เราทำ:
deleteBeer = (id) => { const url = `api/v1/beers/${id}`; fetch(url, { method: "delete", }) .then((data) => { if (data.ok) { this.reloadBeers(); return data.json(); } throw new Error("Network error."); }) .catch((err) => message.error("Error: " + err)); }; เห็นไหม? สิ่งใหม่เพียงอย่างเดียวที่นี่คือ HTTP method ที่ส่งเป็นพารามิเตอร์ที่สองของเมธอด fetch เพิ่มเติม ภายใน clause then เราเรียกฟังก์ชัน reloadBeers ซึ่งจะ re-fetch เบียร์ทั้งหมดจากแบ็กเอนด์อีกครั้ง
เนื้อหาของฟังก์ชันนี้ค่อนข้างเป็นดังนี้:
reloadBeers = () => { this.setState({ beers: [] }); this.loadBeers(); }; เรากำลัง reset อาร์เรย์ beers ของ state และเรียกฟังก์ชัน load อีกครั้ง
คอมโพเนนต์สุดท้าย (The Final Component)
สุดท้าย เราต้องประกอบคอมโพเนนต์โดยการเรียกแท็ก antd อย่างชัดเจน มาดูว่ามันไปพร้อมกับโค้ดคอมโพเนนต์สุดท้ายอย่างไร:
import { Table, message, Popconfirm } from "antd"; import React from "react"; import AddBeerModal from "./AddBeerModal"; class Beers extends React.Component { columns = [ { title: "Brand", dataIndex: "brand", key: "brand", }, { title: "Style", dataIndex: "style", key: "style", }, { title: "Country", dataIndex: "country", key: "country", }, { title: "Quantity", dataIndex: "quantity", key: "quantity", }, { title: "", key: "action", render: (_text, record) => ( <Popconfirm title="Are you sure to delete this beer?" onConfirm={() => this.deleteBeer(record.id)} okText="Yes" cancelText="No"> <a href="#" type="danger"> Delete{" "} </a> </Popconfirm> ), }, ]; state = { beers: [], }; componentDidMount() { this.loadBeers(); } loadBeers = () => { const url = "api/v1/beers/index"; fetch(url) .then((data) => { if (data.ok) { return data.json(); } throw new Error("Network error."); }) .then((data) => { data.forEach((beer) => { const newEl = { key: beer.id, id: beer.id, brand: beer.brand, style: beer.style, country: beer.country, quantity: beer.quantity, }; this.setState((prevState) => ({ beers: [...prevState.beers, newEl], })); }); }) .catch((err) => message.error("Error: " + err)); }; reloadBeers = () => { this.setState({ beers: [] }); this.loadBeers(); }; deleteBeer = (id) => { const url = `api/v1/beers/${id}`; fetch(url, { method: "delete", }) .then((data) => { if (data.ok) { this.reloadBeers(); return data.json(); } throw new Error("Network error."); }) .catch((err) => message.error("Error: " + err)); }; render() { return ( <> <Table className="table-striped-rows" dataSource={this.state.beers} columns={this.columns} pagination={{ pageSize: 5 }} /> <AddBeerModal reloadBeers={this.reloadBeers} /> </> ); } } export default Beers; ตอนนี้ คุณเห็นทุกอย่างรวมกัน ฟังก์ชัน render จะแสดงสองแท็กที่เรา import ที่นั่น: คอมโพเนนต์ Table ของ antd และ AddBeerModal (modal form ที่เราจะสร้างในไม่ช้า)
คอมโพเนนต์ table อุดมสมบูรณ์ในลักษณะที่มันอนุญาตให้เราแบ่งหน้าผ่านผลลัพธ์โดยอัตโนมัติด้วยการตั้งค่าออบเจ็กต์ pagination คุณสมบัติเดียวที่เราเพิ่มที่นี่คือขนาดของแต่ละหน้า (5 ผลลัพธ์ต่อหน้า)
แอตทริบิวต์ dataSource รับรายการเบียร์ที่เราสร้างจากแบ็กเอนด์ และแอตทริบิวต์ columns รับข้อมูลเมตาที่เราสร้างไว้แล้ว
คอมโพเนนต์ AddBeerModal (The AddBeerModal Component)
ด้านล่างตาราง คุณสามารถเห็นปุ่มเพิ่มเบียร์ใหม่ เมื่อเราคลิกที่ปุ่มนี้ มันจะเปิด modal พร้อมฟอร์มเพื่อลงทะเบียนเบียร์ใหม่ในแคตตาล็อกของเรา ตามที่คุณเห็นด้านล่าง:
เพิ่มเบียร์ใหม่ในแคตตาล็อก
นี่เป็นวิธีที่ยอดเยี่ยมในการสำรวจว่า antd จัดการฟอร์มอย่างไร
ขั้นแรก มาแยก actions ที่เราจะมีในคอมโพเนนต์นี้ โปรดสังเกตว่าคอมโพเนนต์เองประกอบด้วยสองตัว: Button และ Modal
นี่หมายความว่าเราจะต้องแมป operations ที่เกี่ยวข้องกับทั้งสอง:
showModalและhandleCancelจัดการการเปิดและปิด modalonFinishถูกทริกเกอร์เมื่อเราส่งฟอร์ม
พวกมันจะเล่นกับ state ของคอมโพเนนต์ ซึ่งจะเก็บแค่ modal toggle (คือ ว่ามัน visible หรือไม่):
state = { visible: false, }; เพื่อแสดงหรือซ่อน modal เราแค่ต้องสลับ Boolean นี้:
this.setState({ visible: true, }); เพื่อเรียก Beer API และลงทะเบียนเบียร์ใหม่ เราจะต้องใช้ Fetch API อีกครั้ง:
onFinish = (values) => { const url = "api/v1/beers/"; fetch(url, { method: "post", headers: { "Content-Type": "application/json", }, body: JSON.stringify(values), }) .then((data) => { if (data.ok) { this.handleCancel(); return data.json(); } throw new Error("Network error."); }) .then(() => { this.props.reloadBeers(); }) .catch((err) => console.error("Error: " + err)); }; นี่คือครั้งแรกที่เราเรียก request ที่เราส่งข้อมูลไปที่เซิร์ฟเวอร์ ในกรณีนี้ เราจะต้องบอก API อย่างชัดเจนว่า type ของข้อมูลที่กำลังส่งไปคืออะไร นั่นคือเหตุผลที่แอตทริบิวต์ headers ต้องถูกแจ้ง
หากทุกอย่างไปได้ดี เราแค่ปิด modal และโหลดรายการของตารางใหม่
ตอนนี้ มาเห็นทุกอย่างรวมกัน พร้อมกับ component render:
import { Button, Form, Input, Modal, Select } from "antd"; import React from "react"; const { Option } = Select; class AddBeerModal extends React.Component { formRef = React.createRef(); state = { visible: false, }; onFinish = (values) => { const url = "api/v1/beers/"; fetch(url, { method: "post", headers: { "Content-Type": "application/json", }, body: JSON.stringify(values), }) .then((data) => { if (data.ok) { this.handleCancel(); return data.json(); } throw new Error("Network error."); }) .then(() => { this.props.reloadBeers(); }) .catch((err) => console.error("Error: " + err)); }; showModal = () => { this.setState({ visible: true, }); }; handleCancel = () => { this.setState({ visible: false, }); }; render() { return ( <> <Button type="primary" onClick={this.showModal}> Create New + </Button> <Modal title="Add New Beer ..." visible={this.state.visible} onCancel={this.handleCancel} footer={null}> <Form ref={this.formRef} layout="vertical" onFinish={this.onFinish}> <Form.Item name="brand" label="Brand" rules={[{ required: true, message: "Please input your beer brand!" }]}> <Input placeholder="Input your beer brand" /> </Form.Item> <Form.Item name="style" label="Style" rules={[{ required: true, message: "Please input your beer style!" }]}> <Input placeholder="Input your beer style" /> </Form.Item> <Form.Item name="country" label="Country" rules={[ { required: true, message: "Please input the country of the beer!", }, ]} > <Select showSearch placeholder="Select your beer country" optionFilterProp="children" style={{ width: "100%" }}> <Option value="Finland">Finland</Option> <Option value="Germany">Germany</Option> <Option value="Netherlands">Netherlands</Option> <Option value="UK">UK</Option> <Option value="USA">USA</Option> <Option value="Other">Other</Option> </Select> </Form.Item> <Form.Item name="quantity" label="Quantity" rules={[{ required: true, message: "Please input the quantity!" }]}> <Input type="number" placeholder="How many beers you desire?" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit"> Submit </Button> </Form.Item> </Form> </Modal> </> ); } } export default AddBeerModal; Antd อนุญาตให้เราระบุ rules ของแต่ละ form item แยกกัน หากฟิลด์จำเป็น เพียงแค่ระบุโดยการให้แอตทริบิวต์ rules คุณสามารถปรับแต่งข้อความที่มันจะแสดงได้ หากผู้ใช้ส่งฟอร์มโดยไม่กรอกอย่างเหมาะสม:
การตรวจสอบความถูกต้องของอินพุตฟอร์ม
ดูที่คอมโพเนนต์ Select ซึ่งแปล combo box ดูว่าการสร้างคอมโพเนนต์ซับซ้อนง่ายแค่ไหนด้วยการให้แอตทริบิวต์ที่ถูกต้อง ตัวอย่างเช่น หากคุณต้องการทำให้ select ของคุณค้นหาได้ เพียงแค่วางคุณสมบัติ showSearch ไว้ที่นั่น และเสร็จแล้ว:
การกรองผลลัพธ์ภายใน Select
Antd จะกรองตัวเลือก select อัตโนมัติตามอินพุตของคุณ
การจัดสไตล์ (Styling)
บางครั้ง คุณจะต้องให้ CSS styling บางอย่างแก่คอมโพเนนต์ที่ไม่ได้ให้ default (อย่างตารางของ antd) หรือปรับแต่งตัวที่มาพร้อม built-in
เพื่อทำเช่นนี้ คุณสามารถสร้างไฟล์ CSS จำนวนมากได้ และจัดระเบียบในโครงสร้างที่คุณชอบ Rails สร้างไฟล์ application.css ไว้แล้ว ภายใต้โฟลเดอร์ app/assets/stylesheets เปิดมันและเพิ่มเนื้อหาต่อไปนี้:
.site-layout-content { background: #fff; padding: 24px; min-height: 380px; } .logo { width: 200px; min-height: 31px; margin: 16px 24px 16px 0; float: left; background-image: url(https://www.honeybadger.io/images/navbar_logo.svg?1602785015); background-repeat: no-repeat; } .table-striped-rows th, .table-striped-rows td { border-bottom: 1px solid #dedddd !important; } .table-striped-rows tr:nth-child(2n) td { background-color: #fbfbfb; } .table-striped-rows thead { background-color: #f1f1f1; } นี่คือกฎ CSS เพื่อทำให้ตารางของเราเป็น striped ตัวอย่างเช่น รู้สึกสบายๆ เพิ่มสไตล์เพิ่มเติมมากเท่าที่คุณต้องการที่นี่
การทดสอบ (Testing)
ก่อนจะไปทดสอบ เราต้องปิดการตรวจสอบ CSRF token ที่ Rails กำหนดค่าให้แอปของเราอัตโนมัติ เพื่อทำเช่นนี้ ไปที่ไฟล์ app/controllers/application_controller.rb และเปลี่ยนเป็น:
class ApplicationController < ActionController::Base protect_from_forgery with: :null_session end ด้วยวิธีนี้ เราหลีกเลี่ยงการต้อง validate tokens ทุกครั้งที่เราทำ request
ยอดเยี่ยม! ตอนนี้ เริ่มเซิร์ฟเวอร์ของคุณผ่านคำสั่ง rails s เข้าถึง https://localhost:3000/ และเล่นกับ CRUD
สรุป (Conclusion)
เป็นงานบ้าน ผมแนะนำให้คุณลอง implement ฟังก์ชัน update ของ CRUD คุณสามารถปรับเมธอด edit ที่ API controller เพื่อรับข้อมูลเบียร์ที่อัปเดตและดำเนินการอัปเดตฐานข้อมูล สำหรับ view modal อีกตัวจะเหมาะสมมากสำหรับจัดการฟอร์ม edit
คุณยังสามารถหา source code สำหรับบทเรียนนี้ได้ที่นี่ ขอให้การเรียนรู้ดี!