Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การสร้างแอปพลิเคชันแบบฟูลสแตกด้วย Vue, Vuex และ Rails

การสร้างแอปพลิเคชันแบบฟูลสแตกโดยคำนึงถึงความสามารถในการปรับขนาดอาจเป็นเรื่องที่น่ากลัว โดยเฉพาะอย่างยิ่งเมื่อสร้างด้วยเวอร์ชันล่าสุดของ Vue และ Vuex ซึ่งรองรับการพิมพ์แบบเต็มรูปแบบ บทความนี้จะสอนผู้อ่านถึงทุกสิ่งที่จำเป็นต้องรู้เกี่ยวกับการสร้างแอปพลิเคชันฟูลสแตกที่ปรับขนาดได้จากการจัดการสถานะด้วย Vuex 4.0 เพื่อจัดการคำขอ API และการโต้ตอบกับฐานข้อมูลโดยการสำรวจแอปพลิเคชัน CRUD ที่จัดการใบสั่งยาสำหรับปศุสัตว์ที่ไม่แข็งแรง แบ็กเอนด์จะถูกสร้างขึ้นด้วย Rails ซึ่งจะแสดง CRUD API พื้นฐานสำหรับการผสานรวมโดยฟรอนต์เอนด์

บริษัทส่วนใหญ่ใช้สถาปัตยกรรมการพัฒนาแบบ API เนื่องจากช่วยให้ทีมพัฒนาส่วนหน้ามีความยืดหยุ่นมากขึ้นในการเลือกเครื่องมือส่วนหน้าที่เหมาะสมสำหรับการพัฒนา แม้ว่านี่อาจไม่ใช่ตัวเลือกที่ดีที่สุด แต่ก็เป็นตัวเลือกที่น่าเชื่อถือ และหลายบริษัทได้นำสถาปัตยกรรมการพัฒนานี้มาใช้ เนื่องจากได้เพิ่มความยืดหยุ่นให้กับทีมมากขึ้น

ข้อกำหนดเบื้องต้น

ก่อนที่เราจะเริ่มต้น ตรวจสอบให้แน่ใจว่าคุณมีสิ่งต่อไปนี้เพื่อให้ทันกับบทแนะนำนี้:

  • ราง V6.x
  • Node.js V10.x
  • มีความรู้เกี่ยวกับ Ruby on Rails มาก่อน
  • ความรู้เกี่ยวกับการทำงานของ TypeScript มาก่อน
  • มีความรู้เกี่ยวกับ Vue.js มาก่อน

สิ่งที่เราจะสร้าง

การสร้างแอปพลิเคชันแบบฟูลสแตกด้วย Vue, Vuex และ Rails

ในบทช่วยสอนนี้ เราจะสร้างแอปพลิเคชัน 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 หากต้องการเปลี่ยนแปลงร้านค้า การดำเนินการจะถูกส่งจากส่วนประกอบซึ่งทำให้เกิดการกลายพันธุ์ ดังนั้นจึงเป็นการอัปเดตร้านค้า

ทำตามขั้นตอนต่อไปนี้เพื่อตั้งค่าร้านค้าแอปพลิเคชัน:

  1. สร้างวัตถุของรัฐ
  2. ตั้งค่าการกลายพันธุ์ที่จะเกิดขึ้นในแอปพลิเคชันของเรา
  3. สร้างการกระทำที่จะส่งผลต่อการกลายพันธุ์ที่ตามมาเหล่านี้
  4. สร้าง 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:

การสร้างแอปพลิเคชันแบบฟูลสแตกด้วย Vue, Vuex และ Rails

บทสรุป

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.