การสร้างแอปพลิเคชันแบบฟูลสแตกโดยคำนึงถึงความสามารถในการปรับขนาดอาจเป็นเรื่องที่น่ากลัว โดยเฉพาะอย่างยิ่งเมื่อสร้างด้วยเวอร์ชันล่าสุดของ Vue และ Vuex ซึ่งรองรับการพิมพ์แบบเต็มรูปแบบ บทความนี้จะสอนผู้อ่านถึงทุกสิ่งที่จำเป็นต้องรู้เกี่ยวกับการสร้างแอปพลิเคชันฟูลสแตกที่ปรับขนาดได้จากการจัดการสถานะด้วย Vuex 4.0 เพื่อจัดการคำขอ API และการโต้ตอบกับฐานข้อมูลโดยการสำรวจแอปพลิเคชัน CRUD ที่จัดการใบสั่งยาสำหรับปศุสัตว์ที่ไม่แข็งแรง แบ็กเอนด์จะถูกสร้างขึ้นด้วย Rails ซึ่งจะแสดง CRUD API พื้นฐานสำหรับการผสานรวมโดยฟรอนต์เอนด์
บริษัทส่วนใหญ่ใช้สถาปัตยกรรมการพัฒนาแบบ API เนื่องจากช่วยให้ทีมพัฒนาส่วนหน้ามีความยืดหยุ่นมากขึ้นในการเลือกเครื่องมือส่วนหน้าที่เหมาะสมสำหรับการพัฒนา แม้ว่านี่อาจไม่ใช่ตัวเลือกที่ดีที่สุด แต่ก็เป็นตัวเลือกที่น่าเชื่อถือ และหลายบริษัทได้นำสถาปัตยกรรมการพัฒนานี้มาใช้ เนื่องจากได้เพิ่มความยืดหยุ่นให้กับทีมมากขึ้น
ข้อกำหนดเบื้องต้น
ก่อนที่เราจะเริ่มต้น ตรวจสอบให้แน่ใจว่าคุณมีสิ่งต่อไปนี้เพื่อให้ทันกับบทแนะนำนี้:
- ราง V6.x
- Node.js V10.x
- มีความรู้เกี่ยวกับ Ruby on Rails มาก่อน
- ความรู้เกี่ยวกับการทำงานของ TypeScript มาก่อน
- มีความรู้เกี่ยวกับ Vue.js มาก่อน
สิ่งที่เราจะสร้าง
ในบทช่วยสอนนี้ เราจะสร้างแอปพลิเคชัน CRUD แบบฟูลสแตกที่จัดการใบสั่งยาสำหรับการรักษาปศุสัตว์ที่ไม่แข็งแรง ผู้ใช้จะสามารถสร้าง อัปเดต และลบใบสั่งยาสำหรับปศุสัตว์ได้ CRUD APIs จะถูกสร้างขึ้นโดยใช้ Rails โดยเปิดเผย CRUD APIs สำหรับการผสานรวมโดยฟรอนท์เอนด์ ซึ่งจะสร้างด้วย Vue 3 แต่ละสแต็กเหล่านี้จะทำงานบนพอร์ตต่างๆ เป็นแอปพลิเคชันอิสระ
สถาปัตยกรรมพื้นฐานของแอป Full-stack
แอปพลิเคชันฟูลสแตกของเราประกอบด้วยแอปพลิเคชันไคลเอนต์และเซิร์ฟเวอร์ที่ทำงานอย่างอิสระ โดยแต่ละองค์ประกอบของแอปพลิเคชันไคลเอนต์โต้ตอบกับ CRUD API ที่ออกโดยแอปพลิเคชันเซิร์ฟเวอร์ผ่าน Vuex เพื่อการจัดการสถานะแอปพลิเคชันอย่างเหมาะสม แอปพลิเคชันแบ็กเอนด์จัดเก็บข้อมูลใบสั่งยาทั้งหมดในฐานข้อมูล Sqlite 3 ในขณะที่เปิดเผย CRUD API ไปยังแอปพลิเคชันส่วนหน้า
การตั้งค่าบริการแบ็กเอนด์
ในโฟลเดอร์ที่คุณต้องการ ให้สร้างแอป Rails โดยเรียกใช้คำสั่งต่อไปนี้:
rails new vet_clinic_api --api
สิ่งนี้จะบอก Rails ให้สร้างโปรเจ็กต์นี้เป็น API ซึ่งจะเป็นการลบการพึ่งพาส่วนหน้าทั้งหมด (ดูไฟล์)
การกำหนดค่าฐานข้อมูล
เราจะใช้ Sqlite 3 ซึ่งเป็นฐานข้อมูลเริ่มต้นสำหรับแอปพลิเคชัน Rails
rails g scaffold prescriptions vet_prescription:text prescribed_by:text disease:text livestock:text completed:boolean
ด้วยการรันคำสั่งข้างต้น Rails จะสร้างโครงสร้างเริ่มต้นสำหรับการโยกย้าย ทดสอบ โมเดล ตัวควบคุม เส้นทาง:
rails db:migrate
คำสั่งนี้จะเพิ่มตารางของเราลงในฐานข้อมูล
การเริ่มฐานข้อมูล
มาเริ่มต้นฐานข้อมูลของเราด้วยข้อมูลใบสั่งยา เพิ่มข้อมูลโค้ดด้านล่างไปที่ db/migrate/seed.rb
//db/migrate/seed.rb
Prescription.destroy_all
Prescription.create!([{
vet_prescription:"Achyranthes aspera",
prescribed_by:"Dr Chucks",
disease:"Rabbies",
livestock:"goat",
completed:false
},
{
vet_prescription:"Achyranthes aspera",
prescribed_by:"Dr Rex",
disease:"Rabbies",
livestock:"Dog",
completed:false
},
{
vet_prescription:"ethnovet",
prescribed_by:"Dr Chucks",
disease:"Pox",
livestock:"Sheep",
completed:false
}])
p "Created #{Prescription.count} prescriptions"
ไฟล์นี้จะจัดเก็บข้อมูลเริ่มต้นสำหรับการ seed ฐานข้อมูล เพื่อที่ว่าเมื่อแอปเริ่มทำงาน เราจะมีข้อมูลใบสั่งยาที่มีอยู่บางส่วน
เรียกใช้คำสั่งต่อไปนี้เพื่อรันโค้ดใน db/migrate/seed.rb
ซึ่งทำให้ฐานข้อมูลมีข้อมูลใบสั่งยาที่กำหนดไว้ล่วงหน้า:
rails db:seed
ด้วยคำสั่งไม่กี่คำเหล่านี้ เราได้สร้าง CRUD API ที่ใช้งานได้กับ Rails มันง่ายแค่ไหน? (ยิ้ม)
การกำหนดค่า CORS
เนื่องจากเราจะเปิดเผย CRUD API ของเราไปยังส่วนหน้า และทั้งเซิร์ฟเวอร์ส่วนหน้าและส่วนหลังจะทำงานบน PORT ที่แตกต่างกัน เราจึงต้องตั้งค่าการกำหนดค่า CORS บนส่วนหลังของ Rails เพื่อให้สิทธิ์การเข้าถึงการแบ่งปันข้อมูลระหว่างส่วนหน้าและ แบ็กเอนด์
ค้นหา Gemfile
ในรูทโปรเจ็กต์และยกเลิกหมายเหตุบรรทัดของโค้ดต่อไปนี้:
# gem 'rack-cors'
เพิ่มรหัสต่อไปนี้ใน config/environments/initializers/cors.rb
:
//config/environments/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
ข้อมูลโค้ดด้านบนช่วยให้สามารถเข้าถึง Rails CRUD API จากแอปส่วนหน้าที่ทำงานบน PORT ใดก็ได้
รันคำสั่งต่อไปนี้เพื่อเริ่มเซิร์ฟเวอร์ Rails:
rails s
ไปที่ localhost:3000/prescriptions
เพื่อรับการตอบสนอง JSON สำหรับใบสั่งยาทั้งหมด
ลองใช้ Rails หากคุณกำลังทำงานกับลูกค้าที่เปลี่ยนเวิร์กโฟลว์ของโปรเจ็กต์บ่อยๆ ด้วย Rails ทำให้ง่ายต่อการใช้งานคุณสมบัติด้วยคำสั่งและโค้ดไม่กี่บรรทัด นี่เป็นความเห็นส่วนตัวของฉันนะ
แนะนำ Vue
Vue เป็น เฟรมเวิร์กที่ก้าวหน้า สำหรับการสร้างส่วนต่อประสานผู้ใช้ Vue นำเสนอแนวคิด เช่น Virtual DOM ฟังก์ชันการแสดงผล และความสามารถในการแสดงผลฝั่งเซิร์ฟเวอร์ เพื่อเพิ่มประสิทธิภาพการทำงานที่สำคัญสำหรับเว็บแอปพลิเคชัน
Vue 3 มาพร้อมกับคุณสมบัติใหม่และการเปลี่ยนแปลงมากมายสำหรับนักพัฒนา ฟีเจอร์เหล่านี้ออกแบบมาเพื่อปรับปรุงความเสถียรโดยรวมของเฟรมเวิร์ก ตลอดจนความเร็วและความสามารถในการบำรุงรักษา
เราจะใช้ประโยชน์จาก API องค์ประกอบ ซึ่งเป็นหนึ่งในคุณสมบัติที่คาดว่าจะได้รับมากที่สุดของ Vue 3 ซึ่งเป็นวิธีใหม่ในการสร้างองค์ประกอบ Vue ด้วยวิธีการจัดระเบียบและมีประสิทธิภาพมากขึ้นในการเขียนโค้ด และรองรับการตรวจสอบประเภท TypeScript เต็มรูปแบบ
แนะนำ Vuex
Vuex เป็นไลบรารีการจัดการสถานะที่สร้างโดยทีม Vue และใช้สถาปัตยกรรมฟลักซ์เดียวกันกับ Redux ได้รับการออกแบบมาโดยเฉพาะสำหรับ Vue และช่วยให้จัดระเบียบร้านค้าของคุณได้ดียิ่งขึ้น Vuex จะมีความสำคัญหากสถานะแอปพลิเคชัน Vue ของคุณมีความซับซ้อนมากขึ้นเมื่อเติบโตขึ้น Vuex รุ่นล่าสุดที่เสถียร v4.0.0 รองรับ Composition API ที่เปิดตัวใน Vue 3 รวมถึงการอนุมานที่มีประสิทธิภาพมากขึ้นสำหรับ TypeScript
การตั้งค่าแอปพลิเคชันส่วนหน้า
ส่วนหน้าจะถูกตั้งค่าด้วย Vue 3 และ typeScript ในขณะที่ Vuex จะถูกใช้สำหรับการจัดการสถานะของแอปพลิเคชัน
เริ่มต้นด้วยการใช้เครื่องมือ Vue-CLI เพื่อสร้างแอป Vue 3 พร้อมรองรับ typescript
ติดตั้งเครื่องมือ Vue-CLI ทั่วโลกด้วยคำสั่งต่อไปนี้:
npm install --global @vue/cli
สร้างแอป Vue 3 ใหม่พร้อมรองรับ typescript และ Vuex ด้วยคำสั่งด้านล่าง:
vue create vet_clinic_frontend
เลือกตัวเลือกคุณสมบัติเลือกด้วยตนเองแล้วกดปุ่มเว้นวรรคเพื่อเลือกตัวเลือกต่อไปนี้:
- เลือกเวอร์ชันของ Vue
- บาเบล
- TypeScript
- Linter / ฟอร์แมตเตอร์
จากนั้นเลือก Vue 3.x(Preview) เป็นเวอร์ชันสำหรับโปรเจ็กต์
- ป้อนใช่เพื่อใช้ไวยากรณ์คอมโพเนนต์สไตล์คลาส
- ป้อนใช่เพื่อใช้ Babel ควบคู่ไปกับ TypeScript
- เลือกลายที่คุณต้องการ
เมื่อสร้างแอป Vue 3 สำเร็จแล้ว เราจะมีการตั้งค่าโปรเจ็กต์ Vue 3 พร้อม Vuex (V4.x) และรองรับ typescript แบบเต็ม
เราจะเพิ่มความปลอดภัยประเภทให้กับแอปพลิเคชันโดยใช้ typescript ในการเริ่มต้นเซิร์ฟเวอร์การพัฒนา ให้เรียกใช้คำสั่งด้านล่างในเทอร์มินัลของคุณและตรงไปที่ https://localhost:8080
เพื่อดูตัวอย่างโครงการของคุณในเบราว์เซอร์
เราจะใช้เฟรมเวิร์ก CSS ของ Bulma เพื่อจัดรูปแบบแอปพลิเคชันของเรา เรียกใช้คำสั่งต่อไปนี้เพื่อติดตั้ง Bulma CSS:
npm install bulma
หากต้องการนำเข้า Bulma CSS ให้เพิ่มโค้ดต่อไปนี้ใน App.vue
:
//App.vue
<style lang="scss">
@import "~bulma/css/bulma.css";
</style>
การตั้งค่าร้านค้าแอปพลิเคชัน
ร้านค้าแอปพลิเคชันจะได้รับการตั้งค่าด้วย Vuex หากต้องการเปลี่ยนแปลงร้านค้า การดำเนินการจะถูกส่งจากส่วนประกอบซึ่งทำให้เกิดการกลายพันธุ์ ดังนั้นจึงเป็นการอัปเดตร้านค้า
ทำตามขั้นตอนต่อไปนี้เพื่อตั้งค่าร้านค้าแอปพลิเคชัน:
- สร้างวัตถุของรัฐ
- ตั้งค่าการกลายพันธุ์ที่จะเกิดขึ้นในแอปพลิเคชันของเรา
- สร้างการกระทำที่จะส่งผลต่อการกลายพันธุ์ที่ตามมาเหล่านี้
- สร้าง getters สำหรับส่วนประกอบเพื่อคำนวณข้อมูลสถานะโดยตรง
รัฐ
สถานะคือออบเจ็กต์ของร้านค้าที่เก็บข้อมูลระดับแอปพลิเคชันที่ส่วนประกอบทั้งหมดต้องเข้าถึง
สร้าง state.ts
ในไดเร็กทอรีร้านค้าด้วยข้อมูลโค้ดต่อไปนี้:
//src/store/state.ts
export type Prescription = {
id: number;
vet_prescription: string;
prescribed_by: string;
disease: string;
livestock: string;
completed: boolean;
editing: boolean;
};
export type Data = {
vet_prescription: string;
prescribed_by: string;
disease: string;
livestock: string;
};
export type State = {
loading: boolean;
prescriptions: Prescription[];
data: Data | null;
showCreateModal: boolean;
showEditModal: boolean;
showPrescriptionModal: boolean;
editModalPrescriptionId: number | undefined;
showPrescriptionId: number | undefined;
};
export const state: State = {
loading: false,
prescriptions: [],
data: null,
showCreateModal: false,
showEditModal: false,
showPrescriptionModal: false,
editModalPrescriptionId: undefined,
showPrescriptionId: undefined,
};
ที่นี่ เราเพิ่มความปลอดภัยบางประเภทให้กับ Prescription
และ Data
. เรายังส่งออกประเภทเพราะจะใช้ในคำจำกัดความของ getters การกลายพันธุ์และการกระทำ สุดท้าย เราก็เปลี่ยนประเภท State ให้เป็น state
การกลายพันธุ์
การกลายพันธุ์เป็นวิธีการที่ เมื่อถูกกระตุ้น ให้แก้ไขที่จัดเก็บ พวกเขาได้รับสถานะเป็นอาร์กิวเมนต์แรกและเพย์โหลดเป็นอาร์กิวเมนต์ที่สอง ในที่สุดแก้ไขสถานะแอปพลิเคชันด้วยเพย์โหลด ในการสร้างการกลายพันธุ์ เอกสาร Vuex แนะนำให้ใช้ค่าคงที่สำหรับประเภทการกลายพันธุ์
สร้าง mutations.ts
ในไดเร็กทอรีร้านค้าด้วยข้อมูลโค้ดต่อไปนี้:
//src/store/mutations.ts
import { MutationTree } from "vuex";
import { State, Prescription, Data } from "./state";
export enum MutationType {
CreatePrescription = "CREATE_PRESCRIPTION",
SetPrescriptions = "SET_PRESCRIPTIONS",
CompletePrescription = "COMPLETE_PRESCRIPTION",
RemovePrescription = "REMOVE_PRESCRIPTION",
EditPrescription = "EDIT_PRESCRIPTION",
UpdatePrescription = `UPDATE_PRESCRIPTION`,
SetLoading = "SET_LOADING",
SetCreateModal = "SET_CREATE_MODAL",
SetEditModal = "SET_EDIT_MODAL",
SetPrescriptionModal = "SET_PRESCRIPTION_MODAL",
}
ตัวอย่างข้างต้นใช้สัญลักษณ์ enum เพื่อเก็บชื่อที่เป็นไปได้ของการกลายพันธุ์ในแอปของเรา
ต่อไป เราจะประกาศสัญญา (ประเภท) สำหรับแต่ละ MutationType ดังนี้:
//src/store/mutation.ts
export type Mutations = {
[MutationType.CreatePrescription](state: State, prescription: Data): void;
[MutationType.SetPrescriptions](state: State, prescription: Prescription[]): void;
[MutationType.CompletePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;
[MutationType.RemovePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;
[MutationType.EditPrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;
[MutationType.UpdatePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;
[MutationType.SetLoading](state: State, value: boolean): void;
[MutationType.SetCreateModal](state: State, value: boolean): void;
[MutationType.SetEditModal](state: State, value: { showModal: boolean; prescriptionId: number | undefined }): void;
[MutationType.SetPrescriptionModal](state: State, value: { showModal: boolean; prescriptionId: number | undefined }): void;
};
จากนั้น เราจะใช้สัญญาที่ประกาศสำหรับแต่ละ MutationType
ดังต่อไปนี้:
//src/store/mutation.ts
export const mutations: MutationTree<State> & Mutations = {
[MutationType.CreatePrescription](state, prescription) {
state.data == prescription;
},
[MutationType.SetPrescriptions](state, prescriptions) {
state.prescriptions = prescriptions;
},
[MutationType.CompletePrescription](state, newPrescription) {
const prescription = state.prescriptions.findIndex((prescription) => prescription.id === newPrescription.id);
if (prescription === -1) return;
state.prescriptions[prescription] = { ...state.prescriptions[prescription], ...newPrescription };
},
[MutationType.RemovePrescription](state, Prescription) {
const prescription = state.prescriptions.findIndex((prescription) => prescription.id === Prescription.id);
if (prescription === -1) return;
//If prescription exist in the state, remove it
state.prescriptions.splice(prescription, 1);
},
[MutationType.EditPrescription](state, Prescription) {
const prescription = state.prescriptions.findIndex((prescription) => prescription.id === Prescription.id);
if (prescription === -1) return;
//If prescription exist in the state, toggle the editing property
state.prescriptions[prescription] = { ...state.prescriptions[prescription], editing: !state.prescriptions[prescription].editing };
console.log("prescription", state.prescriptions[prescription]);
},
[MutationType.UpdatePrescription](state, Prescription) {
state.prescriptions = state.prescriptions.map((prescription) => {
if (prescription.id === Prescription.id) {
return { ...prescription, ...Prescription };
}
return prescription;
});
},
[MutationType.SetLoading](state, value) {
state.loading = value;
},
[MutationType.SetCreateModal](state, value) {
state.showCreateModal = value;
},
[MutationType.SetEditModal](state, value) {
state.showEditModal = value.showModal;
state.editModalPrescriptionId = value.prescriptionId;
},
[MutationType.SetPrescriptionModal](state, { showModal, prescriptionId }) {
state.showPrescriptionModal = showModal;
state.showPrescriptionId = prescriptionId;
},
};
MutationTree
เป็นประเภททั่วไปที่มาพร้อมกับแพ็คเกจ Vuex เราใช้ข้อมูลนี้ในตัวอย่างด้านบนเพื่อประกาศชนิดของ mutation tree ต้นไม้การกลายพันธุ์และการกลายพันธุ์ทำให้มั่นใจได้ว่าสัญญาได้รับการปฏิบัติอย่างถูกต้อง มิฉะนั้น typescript จะทำให้เกิดข้อผิดพลาด
การกระทำ
การกระทำเป็นวิธีที่ทำให้เกิดการกลายพันธุ์ เมื่อจัดการงานแบบอะซิงโครนัส เช่น การส่งคำขอไปยัง API การดำเนินการจะถูกใช้ก่อนที่จะเรียกการกลายพันธุ์ที่สอดคล้องกับการตอบสนองของ API เป็นเพย์โหลด เราจะได้รับการดำเนินการที่ชัดเจนสำหรับสถานการณ์นี้เมื่อเราสร้างการกระทำของเรา
ก่อนสร้างการกระทำ เราจะติดตั้ง Axios เพื่อจัดการคำขอ Http ทั้งหมดของเราไปยังเซิร์ฟเวอร์ Rails ดังนี้:
npm install axios --save
สร้าง actions.ts
ในไดเร็กทอรีร้านค้าด้วยข้อมูลโค้ดต่อไปนี้:
//src/store/actions.ts
import { ActionContext, ActionTree } from "vuex";
import { Mutations, MutationType } from "./mutations";
import { State, Prescription, Data } from "./state";
import axios from "axios";
const apiUrl = "https://localhost:3000/prescriptions";
export enum ActionTypes {
GetPrescriptions = "GET_PRESCRIPTIONS",
SetCreateModal = "SET_CREATE_MODAL",
SetEditModal = "SET_EDIT_MODAL",
RemovePrescription = "REMOVE_PRESCRIPTION",
CreatePrescription = "CREATE_PRESCRIPTION",
UpdatePrescription = "UPDATE_PRESCRIPTION",
}
ในทำนองเดียวกัน ตัวอย่างข้างต้นใช้สัญลักษณ์ enum เพื่อเก็บชื่อการกระทำที่เป็นไปได้ทั้งหมดในแอปของเรา
ต่อไป เราจะประกาศสัญญา (ประเภท) สำหรับแต่ละประเภทการกระทำดังนี้:
//src/store/actions.ts
type ActionAugments = Omit<ActionContext<State, State>, "commit"> & {
commit<K extends keyof Mutations>(key: K, payload: Parameters<Mutations[K]>[1]): ReturnType<Mutations[K]>;
};
export type Actions = {
[ActionTypes.GetPrescriptions](context: ActionAugments): void;
[ActionTypes.SetCreateModal](context: ActionAugments): void;
[ActionTypes.SetEditModal](context: ActionAugments): void;
[ActionTypes.RemovePrescription](context: ActionAugments, Prescription: { id: number }): void;
[ActionTypes.CreatePrescription](context: ActionAugments, data: Data): void;
[ActionTypes.UpdatePrescription](context: ActionAugments, prescription: Prescription): void;
};
ActionAugments
type จำกัดการคอมมิตทั้งหมดเฉพาะกับการกลายพันธุ์ที่ประกาศไว้เท่านั้น และเพื่อตรวจสอบประเภทของเพย์โหลดด้วย
ต่อไป เราจะใช้สัญญา (ประเภท) ที่ประกาศไว้สำหรับ ActionType แต่ละประเภท เพิ่มโค้ดด้านล่างลงในไฟล์ action.ts:
//src/store/actions.ts
export const actions: ActionTree<State, State> & Actions = {
async [ActionTypes.GetPrescriptions]({ commit }) {
commit(MutationType.SetLoading, true);
const response = await axios.get(apiUrl);
commit(MutationType.SetLoading, false);
commit(MutationType.SetPrescriptions, response.data);
},
async [ActionTypes.SetCreateModal]({ commit }) {
commit(MutationType.SetCreateModal, true);
},
async [ActionTypes.SetEditModal]({ commit }) {
commit(MutationType.SetEditModal, { showModal: true, prescriptionId: 1 });
},
//Optimistic update
async [ActionTypes.RemovePrescription]({ commit }, Prescription) {
if (Prescription != undefined) {
commit(MutationType.RemovePrescription, Prescription);
}
const response = await axios.delete(`${apiUrl}/${Prescription.id}`);
},
async [ActionTypes.CreatePrescription]({ commit, dispatch }, Prescription) {
const response = await axios.post(apiUrl, Prescription);
dispatch(ActionTypes.GetPrescriptions);
},
async [ActionTypes.UpdatePrescription]({ commit, dispatch }, Prescription) {
if (Prescription != undefined) {
commit(MutationType.UpdatePrescription, Prescription);
const response = await axios.patch(`${apiUrl}/${Prescription.id}`, Prescription);
dispatch(ActionTypes.GetPrescriptions);
}
},
};
ที่นี่ เราสร้างตัวแปรการกระทำที่เก็บการกระทำที่ดำเนินการทั้งหมด ในทำนองเดียวกัน ActionTree<State> & Actions
รับรองว่าสัญญา (type Actions
) ดำเนินการอย่างถูกต้อง มิฉะนั้น typescript จะทำให้เกิดข้อผิดพลาด
นอกจากนี้เรายังตั้งค่าการเรียกแบบอะซิงโครนัสไปยังปลายทาง Rails API ใน GetPrescriptions
การกระทำและทริกเกอร์ SetPrescriptions
ประเภทการกลายพันธุ์ที่มีข้อมูลการตอบสนองเป็นเพย์โหลด นอกจากนี้เรายังตั้งค่า SetCreateModal
, SetEditModal
, CreatePrescription
, UpdatePrescription,
และ RemovePrescription
การกระทำ
เก็ตเตอร์
Getters คือเมธอดที่ได้รับสถานะเป็นพารามิเตอร์แรกและส่งคืนข้อมูลที่คำนวณจากสถานะร้านค้า
สร้าง getters.ts
ในไดเร็กทอรีร้านค้าด้วยข้อมูลโค้ดต่อไปนี้:
//src/store/getters.ts
import { GetterTree } from "vuex";
import { State, Prescription } from "./state";
export type Getters = {
completedPrescriptionCount(state: State): number;
totalPrescriptionCount(state: State): number;
getPrescriptionById(state: State): (id: number) => Prescription | undefined;
};
export const getters: GetterTree<State, State> & Getters = {
completedPrescriptionCount(state) {
return state.prescriptions.filter((prescription) => prescription.completed).length;
},
totalPrescriptionCount(state) {
return state.prescriptions.length;
},
getPrescriptionById: (state) => (id: number) => {
return state.prescriptions.find((prescription) => prescription.id === id);
},
};
ข้อมูลโค้ดข้างต้นกำหนดตัวรับต่อไปนี้:
completedPrescriptionCount
– ฟังก์ชั่นที่ได้รับจำนวนใบสั่งยาที่เสร็จสมบูรณ์ทั้งหมดในรัฐของเราtotalPrescriptionCount
– ฟังก์ชั่นที่ได้รับจำนวนใบสั่งยาทั้งหมดในรัฐของเราgetPrescriptionById
– ฟังก์ชันที่ได้รับใบสั่งยาจาก ID
นอกจากนี้เรายังได้เพิ่มความปลอดภัยบางประเภทให้กับ Getters
ร้านค้า
มาโยง state
. ของเรากัน , mutations
, actions
และ getters
ไปยังร้านค้า Vuex ทั่วโลก เราจะอัปเดต store/index.ts
ดังต่อไปนี้:
//src/store/index.ts
import { createStore, Store as VuexStore, CommitOptions, DispatchOptions, createLogger } from "vuex";
import { State, state } from "./state";
import { Mutations, mutations } from "./mutations";
import { Actions, actions } from "./actions";
import { Getters, getters } from "./getters";
export const store = createStore<State>({
plugins: process.env.NODE_ENV === "development" ? [createLogger()] : [],
state,
mutations,
actions,
getters,
});
export function useStore() {
return store as Store;
}
export type Store = Omit<VuexStore<State>, "getters" | "commit" | "dispatch"> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(key: K, payload: P, options?: CommitOptions): ReturnType<Mutations[K]>;
} & {
dispatch<K extends keyof Actions>(key: K, payload?: Parameters<Actions[K]>[1], options?: DispatchOptions): ReturnType<Actions[K]>;
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>;
};
};
state
, mutations
, actions
และ getters
จำเป็นต้องสร้างร้านค้าโดยส่งอ็อบเจ็กต์ที่มีไปยัง createStore
กระบวนการ. ในระหว่างการพัฒนา createLogger
ปลั๊กอินจะบันทึกสถานะ (สถานะก่อนหน้าและสถานะถัดไป) และการกลายพันธุ์ไปยังคอนโซล เพื่อให้สามารถเข้าถึงร้านค้าได้ในทุกองค์ประกอบของแอปพลิเคชันของเรา เราจำเป็นต้องฉีดเข้าไปในแอปพลิเคชันทั้งหมด โชคดีที่เครื่องมือ Vue-CLI ได้นำเข้าร้านค้าทั้งหมดแล้วและส่งต่อภายในอินสแตนซ์ Vue ของแอปพลิเคชัน
การรวม Vuex Store เข้ากับคอมโพเนนต์
ในบทช่วยสอนนี้ API องค์ประกอบของ Vue 3 จะใช้เพื่อสร้างส่วนประกอบทั้งหมดในแอปพลิเคชันส่วนหน้าของเรา
ส่วนประกอบของแอป
ส่วนหน้าของเราควรแสดงรายการข้อมูลใบสั่งยาทันทีที่เปิดตัว เราจะส่ง GetPrescription
การดำเนินการภายใน mounted()
. ของคอมโพเนนต์ เบ็ดวงจรชีวิต ในการเข้าถึงร้านค้าภายในส่วนประกอบ useStore
hook ซึ่งส่งคืนร้านค้าของเรา จะถูกดำเนินการ
//src/App.vue
<script lang="ts">
import { computed, defineComponent, onMounted } from "vue";
import PrescriptionList from "./components/PrescriptionList.vue";
import { useStore } from "./store";
import { ActionTypes } from "./store/actions";
export default defineComponent({
components: { PrescriptionList },
setup() {
const store = useStore();
const loading = computed(() => store.state.loading);
onMounted(() => store.dispatch(ActionTypes.GetPrescriptions));
const completedCount = computed(() => store.getters.completedPrescriptionCount);
const totalCount = computed(() => store.getters.totalPrescriptionCount);
return { loading, completedCount, totalCount };
},
});
</script>
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://bulma.io">
<img src="https://bulma.io/images/bulma-logo.png" width="112" height="28" />
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item"> Home </a>
<a class="navbar-item"> About </a>
</div>
</div>
</nav>
<div class="container mx-auto mt-4">
<h1 class="is-size-3 has-text-centered p-2 has-text-weight-bold is-success">Vet clinic Frontend</h1>
<h3 class="has-text-centered p-2">Manage records of treated livestock in your farm</h3>
<div v-if="loading">
<h3 class="has-text-centered mt-4">Loading...</h3>
</div>
<div v-else>
<p class="has-text-centered mt-2">{{ completedCount }} of {{ totalCount }} treated.</p>
<PrescriptionList />
</div>
</div>
</template>
<style>
@import "~bulma/css/bulma.css";
</style>
ที่นี่ เราได้สร้างคุณสมบัติที่คำนวณได้สามรายการ:
completedCount
ซึ่งเรียกcompletedPrescriptionCount
วิธี getter เพื่อดึงจำนวนการรักษาที่เสร็จสิ้นทั้งหมดtotalCount
ซึ่งเรียกtotalPrescriptionCount
วิธี getter เพื่อรับจำนวนใบสั่งยาทั้งหมดloading
ซึ่งได้รับคุณสมบัติการโหลดของรัฐ
ด้วย API ขององค์ประกอบ Vue 3 วิธีการและคุณสมบัติที่เทมเพลตต้องการจะต้องถูกส่งคืนเพื่อให้สามารถเข้าถึงได้ในเทมเพลต สังเกตว่าเราได้ส่งคืน loading, completedCount, and totalCount
. อย่างไร .
ส่วนประกอบ PrescriptionList
องค์ประกอบนี้จะรับผิดชอบในการดึงรายการใบสั่งยาจากแบ็กเอนด์และส่งข้อมูลใบสั่งยาไปยังส่วนประกอบย่อย
สร้าง PrescriptionList.vue
ภายในโฟลเดอร์ส่วนประกอบด้วยรหัสต่อไปนี้:
//src/components/PrescriptionList.vue
<template>
<table class="table is-hoverable is-striped">
<thead>
<tr>
<th><abbr title="Position">Prescription Id</abbr></th>
<th>Treated</th>
<th>Prescription</th>
<th><abbr title="Won">Prescribed By</abbr></th>
<th><abbr title="Drawn">Disease</abbr></th>
<th><abbr title="Drawn">Livestock</abbr></th>
<th><abbr title="Lost">Actions</abbr></th>
</tr>
</thead>
<tbody v-if="prescriptions">
<tr v-for="prescription in prescriptions" :key="prescription.id">
<PrescriptionListItem v-bind="prescription" />
</tr>
</tbody>
<tfoot>
<CreateModal v-show="showCreateModal"></CreateModal>
<button class="button is-success" @click="setModal">Create Prescription</button>
</tfoot>
</table>
<EditModal v-if="showEditModal" :id="editModalPrescriptionId"></EditModal>
<Prescription v-if="showPrescriptionModal" :id="showPrescriptionId"></Prescription>
</template>
<script>
import CreateModal from "./CreateModal";
import EditModal from "./EditModal";
import Prescription from "./Prescription";
import PrescriptionListItem from "./PrescriptionListItem";
import { defineComponent, computed } from "vue";
import { useStore } from "@/store";
import { MutationType } from "@/store/mutations";
export default defineComponent({
name: "Table",
components: {
CreateModal,
PrescriptionListItem,
Prescription,
EditModal,
},
setup() {
const store = useStore();
const setModal = () => {
store.commit(MutationType.SetCreateModal, true);
};
const showCreateModal = computed(() => store.state.showCreateModal);
const showEditModal = computed(() => store.state.showEditModal);
const editModalPrescriptionId = computed(() => store.state.editModalPrescriptionId);
const showPrescriptionModal = computed(() => store.state.showPrescriptionModal);
const showPrescriptionId = computed(() => store.state.showPrescriptionId);
const prescriptions = computed(() => store.state.prescriptions);
return { showCreateModal, setModal, prescriptions, showEditModal, showPrescriptionModal, editModalPrescriptionId, showPrescriptionId };
},
});
</script>
<style scoped>
table {
width: 100%;
}
.fa {
font-size: 1.2rem;
margin-left: 15px;
}
.fa:hover {
font-size: 1.4rem;
}
</style>
setModal
method เรียกการกลายพันธุ์ที่ตั้งค่า showCreateModal
ให้อยู่ในสถานะเป็น true ส่งผลให้มีการเปิดใช้ modal สำหรับสร้างใบสั่งยา
เราได้สร้างคุณสมบัติที่คำนวณได้ดังต่อไปนี้:
showCreateModal
ซึ่งได้รับshowCreateModal
ทรัพย์สินของรัฐshowEditModal
ซึ่งได้รับshowEditModal
ทรัพย์สินของรัฐshowPrescriptionModal
ซึ่งได้รับshowPrescriptionModal
ทรัพย์สินของรัฐprescription
ซึ่งได้รับรายการใบสั่งยาจากรัฐshowPrescriptionId
ซึ่งได้รับshowPrescriptiond
ทรัพย์สินของรัฐeditPrescriptionId
ซึ่งได้รับeditPrescriptionId
ทรัพย์สินของรัฐ
ส่วนประกอบตามใบสั่งแพทย์
ส่วนประกอบนี้จะได้รับรหัสใบสั่งยาเป็นพร็อพจาก PrescriptionList
ส่วนประกอบ. อุปกรณ์ประกอบฉาก ID จะใช้เพื่อดึงใบสั่งยาด้วย ID ที่เกี่ยวข้องผ่าน getPrescriptionById
วิธี getters และแสดงคุณสมบัติใบสั่งยาในเบราว์เซอร์
สร้าง Prescription.vue ภายในโฟลเดอร์ส่วนประกอบด้วยรหัสต่อไปนี้:
//src/components/Prescription.vue
<template>
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content">
<h1>VIEW PRESCRIPTION</h1>
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-4">Livestock: {{ prescription.livestock }}</p>
<p class="subtitle is-6"><b>Prescribed by:</b> {{ prescription.prescribed_by }}</p>
<p class="subtitle is-6"><b>Disease:</b> {{ prescription.disease }}</p>
</div>
</div>
<div class="content">
<p class="subtitle is-6">Prescription: {{ prescription.vet_prescription }}</p>
</div>
</div>
</div>
</div>
<button class="modal-close is-large" @click="closeModal" aria-label="close"></button>
</div>
</template>
<script lang="ts">
import { computed } from "vue";
import { useStore } from "@/store";
import { MutationType } from "@/store/mutations";
export default {
name: "PrescriptionModal",
props: {
id: { type: Number, required: true },
},
setup(props: any) {
const store = useStore();
const prescription = computed(() => store.getters.getPrescriptionById(Number(props.id)));
const closeModal = () => {
store.commit(MutationType.SetPrescriptionModal, {
showModal: false,
prescriptionId: undefined,
});
};
return { closeModal, prescription };
},
};
</script>
<style scoped>
h1 {
color: #ffffff;
text-align: center;
font-size: 2rem;
margin-bottom: 3rem;
}
</style>
closeModal
เมธอดส่ง SetPrescriptionModal
การกลายพันธุ์ซึ่งตั้งค่า showModal
คุณสมบัติในสถานะเป็นเท็จในขณะที่ prescription
คุณสมบัติที่คำนวณได้เรียก getPrescriptionById
วิธี getter เพื่อรับใบสั่งยาด้วยรหัสของมัน
CreateModal Component
This component is responsible for creating prescriptions.
Create CreateModal.vue inside the components folder with the following code:
//src/components/CreateModal.vue
<template>
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content">
<form @submit.prevent="createPrescription">
<div class="field">
<label class="label">Prescribed By</label>
<div class="control">
<input v-model="prescribedBy" class="input" type="text" placeholder="Enter prescriber's name" />
</div>
</div>
<div class="field">
<label class="label">Prescription</label>
<div class="control">
<textarea v-model="prescription" class="textarea" placeholder="Enter prescription"></textarea>
</div>
</div>
<div class="field">
<label class="label">Disease</label>
<div class="control">
<input v-model="disease" class="input" type="text" placeholder="Enter name of disease" />
</div>
</div>
<div class="field">
<label class="label">Livestock</label>
<div class="control">
<input v-model="livestock" class="input" type="text" placeholder="Enter livestock" />
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
<div class="control" @click="closeModal">
<button class="button is-link is-light">Cancel</button>
</div>
</div>
</form>
</div>
<button class="modal-close is-large" @click="closeModal" aria-label="close"></button>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";
import { useStore } from "@/store";
import { Data } from "@/store/state";
import { MutationType } from "@/store/mutations";
import { ActionTypes } from "@/store/actions";
export default {
name: "CreateModal",
setup() {
const state = reactive({
prescription: "",
prescribedBy: "",
disease: "",
livestock: "",
});
const store = useStore();
const createPrescription = () => {
if (state.prescription === "" || state.prescribedBy === "" || state.disease === "" || state.livestock === "") return;
const prescription: Data = {
vet_prescription: state.prescription,
prescribed_by: state.prescribedBy,
disease: state.disease,
livestock: state.livestock,
};
store.dispatch(ActionTypes.CreatePrescription, prescription);
state.prescription = "";
state.prescribedBy = "";
state.disease = "";
state.livestock = "";
};
const closeModal = () => {
store.commit(MutationType.SetCreateModal, false);
};
return { closeModal, ...toRefs(state), createPrescription };
},
};
</script>
The createPrescription
method dispatches an action that makes a post request to the server, thereby creating a new prescription, while the closeModal
method commits the SetPrescriptionModal
mutation (which sets the showModal property in the state to false).
Working with forms and input element requires two-way data binding, and as such, we used Vue 3’s reactive
method to store values used in the input fields.
Note:When using reactive
, we need to use toRefs
to convert the reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.
EditModal Component
This component is responsible for updating prescriptions. Its logic is similar to the CreatePrescription
component we discussed in the previous section.
Create EditModal.vue inside the components folder with the following code:
//src/components/EditModal.vue
<template>
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content">
<form @submit.prevent="updatePrescription">
<h1>Edit Modal</h1>
<div class="field">
<label class="label">Prescribed By</label>
<div class="control">
<input v-model="prescribedBy" class="input" type="text" placeholder="Enter prescriber's name" />
</div>
</div>
<div class="field">
<label class="label">Prescription</label>
<div class="control">
<textarea v-model="prescription" class="textarea" placeholder="Enter Prescription"></textarea>
</div>
</div>
<div class="field">
<label class="label">Disease</label>
<div class="control">
<input v-model="disease" class="input" type="text" placeholder="Enter name of disease" />
</div>
</div>
<div class="field">
<label class="label">Livestock</label>
<div class="control">
<input v-model="livestock" class="input" type="text" placeholder="Enter livestock" />
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
<div class="control" @click="closeModal">
<button class="button is-link is-light">Cancel</button>
</div>
</div>
</form>
</div>
<button class="modal-close is-large" @click="closeModal" aria-label="close"></button>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, onMounted } from "vue";
import { useStore } from "@/store";
import { Prescription } from "@/store/state";
import { MutationType } from "@/store/mutations";
import { ActionTypes } from "@/store/actions";
export default {
name: "EditModal",
props: {
id: { type: Number, required: true },
},
setup(props: any) {
const state = reactive({
prescription: "",
prescribedBy: "",
disease: "",
livestock: "",
});
const store = useStore();
const setFields = () => {
const prescription = store.getters.getPrescriptionById(Number(props.id));
if (prescription) {
console.log("prescription si kolo", prescription);
state.prescription = prescription.vet_prescription;
state.prescribedBy = prescription.prescribed_by;
state.disease = prescription.disease;
state.livestock = prescription.livestock;
}
};
onMounted(() => {
setFields();
});
const updatePrescription = () => {
if (state.prescription === "" || state.prescribedBy === "" || state.disease === "" || state.livestock === "") return;
const prescription: Prescription = {
id: props.id,
vet_prescription: state.prescription,
prescribed_by: state.prescribedBy,
disease: state.disease,
livestock: state.livestock,
completed: false,
editing: false,
};
store.dispatch(ActionTypes.UpdatePrescription, prescription);
state.prescription = "";
state.prescribedBy = "";
state.disease = "";
state.livestock = "";
};
const closeModal = () => {
store.commit(MutationType.SetEditModal, { showModal: false, prescriptionId: undefined });
};
return { closeModal, ...toRefs(state), updatePrescription };
},
};
</script>
<style scoped>
label {
color: #ffffff;
}
h1 {
color: #ffffff;
text-align: center;
font-size: 2rem;
margin-bottom: 3rem;
}
</style>
The createPrescription
method dispatches an action that makes a put request to the server, thereby updating an existing prescription by its ID, while the closeModal
method commits the SetPrescriptionModal
mutation that sets the showModal
property in the state to false. Calling the setFields
method on the onMounted
lifecycle hook triggers the getPrescriptionById
getters method to fetch a prescription from the store by its ID and then updates the properties in the reactive object with the fetched prescription properties as soon as the component is mounted on the DOM.
Launching the Final Project
In the root directory of your Rails API server, run the following command to start the server:
rails server
Now, you can run the frontend application with the following command:
npm run serve
Finally, your application should be as follows:
บทสรุป
We have built a CRUD API server with Rails and our frontend application on Vue 3 composition API and Vuex, all running on different servers while integrating both stacks to build a fullstack web application. I hope you have learned a great deal from this tutorial. Please reach out in the comment section below if you have any questions or suggestions. Here is the GitHub repo for the full-stack application built in this tutorial.