Computer >> Máy Tính >  >> Lập trình >> Ruby

Xây dựng một ứng dụng Full-Stack với Vue, Vuex và Rails

Việc xây dựng các ứng dụng full-stack có tính đến khả năng mở rộng có thể rất đáng sợ, đặc biệt là khi xây dựng với phiên bản mới nhất của Vue và Vuex, những phiên bản này có hỗ trợ sắp chữ đầy đủ. Bài viết này sẽ dạy cho người đọc mọi thứ họ cần biết về việc xây dựng các ứng dụng full-stack có thể mở rộng từ quản lý nhà nước với Vuex 4.0 để xử lý các yêu cầu API và tương tác cơ sở dữ liệu bằng cách khám phá một ứng dụng CRUD quản lý đơn thuốc điều trị cho vật nuôi không lành mạnh. Phần phụ trợ sẽ được xây dựng bằng Rails, hiển thị các API CRUD cơ bản để tích hợp bởi giao diện người dùng.

Hầu hết các công ty đã áp dụng kiến ​​trúc phát triển dựa trên API vì nó cho phép nhóm phát triển giao diện người dùng linh hoạt hơn để chọn một công cụ giao diện người dùng phù hợp để phát triển. Mặc dù đây có thể không phải là lựa chọn tốt nhất, nhưng đây là một lựa chọn đáng tin cậy và nhiều công ty đã áp dụng kiến ​​trúc phát triển này vì nó tăng thêm tính linh hoạt cho nhóm.

Điều kiện tiên quyết

Trước khi chúng tôi bắt đầu, hãy đảm bảo bạn có những điều sau để theo kịp hướng dẫn này:

  • Đường ray V6.x
  • Node.js V10.x
  • Có kiến ​​thức làm việc trước về Ruby on Rails
  • Có kiến ​​thức làm việc trước về TypeScript
  • Kiến thức làm việc trước đây về Vue.js

Những gì chúng tôi sẽ xây dựng

Xây dựng một ứng dụng Full-Stack với Vue, Vuex và Rails

Trong hướng dẫn này, chúng tôi sẽ xây dựng một ứng dụng CRUD đầy đủ để quản lý việc kê đơn các phương pháp điều trị cho vật nuôi không khỏe mạnh. Người dùng sẽ có thể tạo, cập nhật và xóa các đơn thuốc cho vật nuôi. Các API CRUD sẽ được tạo bằng cách sử dụng Rails, hiển thị các API CRUD để tích hợp bởi giao diện người dùng, sẽ được xây dựng với Vue 3. Mỗi ngăn xếp này sẽ chạy trên các cổng khác nhau như các ứng dụng độc lập.

Kiến trúc cơ bản của ứng dụng Full-stack

Ứng dụng full-stack của chúng tôi bao gồm cả ứng dụng máy khách và máy chủ chạy độc lập, với mỗi thành phần của ứng dụng máy khách tương tác với các API CRUD do ứng dụng máy chủ cấp thông qua Vuex để quản lý đúng trạng thái ứng dụng. Ứng dụng phụ trợ lưu trữ tất cả dữ liệu theo toa trong cơ sở dữ liệu Sqlite 3 trong khi hiển thị các API CRUD cho ứng dụng giao diện người dùng.

Thiết lập Dịch vụ Phụ trợ

Trong thư mục tùy chọn của bạn, hãy tạo ứng dụng Rails bằng cách chạy lệnh sau:

rails new vet_clinic_api --api

Điều này sẽ yêu cầu Rails tạo dự án này dưới dạng API, do đó loại bỏ tất cả các phụ thuộc của giao diện người dùng (tệp xem).

Cấu hình cơ sở dữ liệu

Chúng tôi sẽ sử dụng Sqlite 3, là cơ sở dữ liệu mặc định cho các ứng dụng Rails.

rails g scaffold prescriptions vet_prescription:text prescribed_by:text disease:text livestock:text completed:boolean

Bằng cách chạy lệnh trên, Rails sẽ tạo cấu trúc ban đầu cho quá trình di chuyển, kiểm tra, mô hình, bộ điều khiển, tuyến đường của chúng tôi:

rails db:migrate

Lệnh này sẽ thêm các bảng của chúng ta vào cơ sở dữ liệu.

Đưa vào cơ sở dữ liệu

Hãy bắt đầu cơ sở dữ liệu của chúng tôi với một số dữ liệu theo toa. Thêm đoạn mã bên dưới vào 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"

Tệp này sẽ lưu trữ dữ liệu ban đầu để tạo cơ sở dữ liệu để khi ứng dụng khởi động, chúng tôi sẽ có một số dữ liệu theo đơn hiện có.

Chạy lệnh sau để thực thi mã trong db/migrate/seed.rb , tạo ra cơ sở dữ liệu với một số dữ liệu theo toa được xác định trước:

rails db:seed

Với một vài lệnh này, chúng tôi đã tạo một API CRUD chức năng với Rails. Nó dễ dàng như thế nào? (cười)

Cấu hình CORS

Vì chúng tôi sẽ hiển thị các API CRUD của chúng tôi với giao diện người dùng và cả giao diện người dùng và máy chủ phụ trợ sẽ chạy trên một PORT khác, chúng tôi cần thiết lập cấu hình CORS trên phụ trợ Rails để cấp quyền truy cập chia sẻ dữ liệu giữa giao diện người dùng và phụ trợ.

Tìm Gemfile trong thư mục gốc của dự án và bỏ ghi chú dòng mã sau:

# gem 'rack-cors'

Thêm mã sau vào 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

Đoạn mã trên cho phép API Rails CRUD có thể truy cập được từ các ứng dụng giao diện người dùng chạy trên bất kỳ PORT nào.

Chạy lệnh sau để khởi động máy chủ Rails:

rails s

Điều hướng đến localhost:3000/prescriptions để nhận phản hồi JSON cho tất cả các đơn thuốc.

Cân nhắc sử dụng Rails nếu bạn đang làm việc với những khách hàng thường xuyên thay đổi quy trình làm việc trong dự án của họ. Với Rails, thật dễ dàng triển khai các tính năng bằng một vài lệnh và dòng mã. Tuy nhiên, đây là ý kiến ​​cá nhân của tôi.

Giới thiệu Vue

Vue là một khuôn khổ tiến bộ để xây dựng giao diện người dùng. Vue giới thiệu các khái niệm, chẳng hạn như Virtual DOM, các chức năng kết xuất và khả năng kết xuất phía máy chủ, để cung cấp tối ưu hóa hiệu suất đáng kể cho các ứng dụng web.

Vue 3 đi kèm với rất nhiều tính năng và thay đổi mới dành cho các nhà phát triển. Các tính năng này được thiết kế để cải thiện tính ổn định tổng thể của khuôn khổ, cũng như tốc độ và khả năng bảo trì của nó.

Chúng tôi sẽ sử dụng API thành phần, một trong những tính năng được mong đợi nhất của Vue 3. Đây là một cách mới để tạo các thành phần Vue, với cách tiếp cận có tổ chức và hiệu quả hơn để viết mã và hỗ trợ kiểm tra kiểu TypeScript đầy đủ.

Giới thiệu Vuex

Vuex là một thư viện quản lý trạng thái được tạo bởi nhóm Vue và nó dựa trên cùng một kiến ​​trúc thông lượng với Redux. Nó được thiết kế đặc biệt cho Vue và cho phép tổ chức cửa hàng của bạn tốt hơn. Vuex sẽ trở nên quan trọng nếu trạng thái ứng dụng Vue của bạn trở nên phức tạp hơn khi nó phát triển. Bản phát hành ổn định mới nhất của Vuex, v4.0.0, hỗ trợ API thành phần được giới thiệu trong Vue 3, cũng như suy luận mạnh mẽ hơn cho TypeScript.

Thiết lập ứng dụng Frontend

Giao diện người dùng sẽ được thiết lập với Vue 3 và typeScript, trong khi Vuex sẽ được sử dụng để quản lý trạng thái ứng dụng.

Hãy bắt đầu bằng cách sử dụng công cụ Vue-CLI để tạo ứng dụng Vue 3 với hỗ trợ sắp chữ.

Cài đặt công cụ Vue-CLI trên toàn cầu bằng lệnh sau:

npm install --global @vue/cli

Tạo một ứng dụng Vue 3 mới với hỗ trợ typecript và Vuex bằng lệnh bên dưới:

vue create vet_clinic_frontend

Chọn tùy chọn tính năng chọn theo cách thủ công và nhấn phím cách để chọn các tùy chọn sau:

  • Chọn phiên bản Vue
  • Babel
  • TypeScript
  • Linter / Formatter

Tiếp theo, chọn Vue 3.x (Xem trước) làm phiên bản cho dự án.

  • Nhập có để sử dụng cú pháp thành phần kiểu lớp.
  • Nhập có để sử dụng Babel cùng với TypeScript.
  • Chọn bất kỳ linter nào bạn chọn.

Khi ứng dụng Vue 3 được tạo thành công, chúng tôi sẽ thiết lập dự án Vue 3 với Vuex (V4.x) và hỗ trợ typecript đầy đủ.

Chúng tôi sẽ thêm tính năng an toàn kiểu vào ứng dụng bằng cách sử dụng bản ghi. Để khởi động máy chủ phát triển, hãy chạy lệnh bên dưới trong thiết bị đầu cuối của bạn và truy cập https://localhost:8080 để xem trước dự án của bạn trong trình duyệt.

Chúng tôi sẽ sử dụng khung CSS Bulma để tạo kiểu cho ứng dụng của chúng tôi. Chạy lệnh sau để cài đặt Bulma CSS:

npm install bulma

Để nhập Bulma CSS, hãy thêm mã sau vào App.vue :

//App.vue
<style lang="scss">
@import "~bulma/css/bulma.css";
</style>

Thiết lập Cửa hàng Ứng dụng

Kho ứng dụng sẽ được thiết lập với Vuex. Để thực hiện các thay đổi đối với cửa hàng, các hành động sẽ được thực hiện từ thành phần, điều này sẽ kích hoạt các đột biến, do đó cập nhật cửa hàng.

Thực hiện các bước sau để thiết lập kho ứng dụng:

  1. Tạo các đối tượng trạng thái.
  2. Thiết lập các đột biến sẽ xảy ra trong ứng dụng của chúng tôi.
  3. Tạo các hành động sẽ dẫn đến những đột biến tiếp theo này.
  4. Tạo getters cho các thành phần để tính toán trực tiếp dữ liệu trạng thái.

Bang

Trạng thái là một đối tượng lưu trữ chứa dữ liệu cấp ứng dụng cần được truy cập bởi tất cả các thành phần.

Tạo một state.ts tệp trong thư mục cửa hàng với đoạn mã sau:

//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,
};

Ở đây, chúng tôi thêm một số loại an toàn vào PrescriptionData . Chúng tôi cũng xuất các loại vì chúng sẽ được sử dụng trong các định nghĩa về getters, đột biến và hành động. Cuối cùng, chúng tôi chuyển kiểu State sang trạng thái.

Đột biến

Đột biến là các phương thức, khi được kích hoạt, sẽ sửa đổi cửa hàng. Họ nhận trạng thái làm đối số đầu tiên và tải trọng là đối số thứ hai, cuối cùng sửa đổi trạng thái ứng dụng với tải trọng. Để tạo đột biến, tài liệu Vuex khuyên bạn nên sử dụng hằng số cho các loại đột biến.

Tạo mutations.ts tệp trong thư mục cửa hàng với đoạn mã sau:

//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",
}

Đoạn mã trên sử dụng biểu tượng enum để chứa tất cả các tên có thể có của các đột biến trong ứng dụng của chúng tôi.

Tiếp theo, chúng ta sẽ khai báo một hợp đồng (loại) cho mỗi MutationType, như sau:

//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;
};

Sau đó, chúng tôi sẽ triển khai các hợp đồng được khai báo cho mỗi MutationType , như sau:

//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 là một loại chung được vận chuyển cùng với gói Vuex. Chúng tôi đã sử dụng nó trong đoạn mã trên để khai báo một loại cây đột biến. Cây đột biến và các đột biến đảm bảo rằng hợp đồng được thực hiện một cách chính xác; nếu không, Typecript sẽ gây ra lỗi.

Hành động

Hành động là phương pháp kích hoạt đột biến. Khi xử lý các tác vụ không đồng bộ, chẳng hạn như yêu cầu API, các hành động được sử dụng trước khi gọi các đột biến tương ứng với phản hồi API dưới dạng tải trọng. Chúng tôi sẽ triển khai rõ ràng cho tình huống này khi chúng tôi tạo ra các hành động của mình.

Trước khi tạo các hành động, chúng tôi sẽ cài đặt Axios để xử lý tất cả yêu cầu Http của chúng tôi tới máy chủ Rails, như sau:

npm install axios --save

Tạo một actions.ts tệp trong thư mục cửa hàng với đoạn mã sau:

//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",
}

Tương tự, đoạn mã trên sử dụng biểu tượng enum để chứa tất cả các tên có thể có của các hành động trong ứng dụng của chúng tôi.

Tiếp theo, chúng ta sẽ khai báo một hợp đồng (loại) cho mỗi ActionType, như sau:

//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 loại chỉ hạn chế tất cả các cam kết đối với các đột biến đã khai báo của chúng và cũng để kiểm tra loại tải trọng.

Tiếp theo, chúng tôi sẽ triển khai hợp đồng (các loại) được khai báo cho mỗi ActionType. Thêm mã bên dưới vào tệp 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);
    }
  },
};

Ở đây, chúng tôi đã tạo một biến hành động lưu trữ tất cả các hành động đã triển khai. Tương tự, ActionTree<State> & Actions đảm bảo rằng hợp đồng (type Actions ) được thực hiện một cách chính xác; nếu không, Typecript sẽ gây ra lỗi.

Chúng tôi cũng thiết lập lệnh gọi không đồng bộ của mình tới điểm cuối API Rails trong GetPrescriptions hành động và kích hoạt SetPrescriptions loại đột biến với dữ liệu phản hồi là tải trọng. Chúng tôi cũng thiết lập SetCreateModal , SetEditModal , CreatePrescription , UpdatePrescription,RemovePrescription hành động.

Getters

Getters là các phương thức nhận trạng thái làm tham số đầu tiên của nó và trả về thông tin đã tính toán từ trạng thái cửa hàng.

Tạo getters.ts tệp trong thư mục cửa hàng với đoạn mã sau:

//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);
  },
};

Đoạn mã trên xác định các getters sau:

  • completedPrescriptionCount - Một chức năng nhận tổng số đơn thuốc đã hoàn thành ở trạng thái của chúng tôi.
  • totalPrescriptionCount - Một chức năng nhận tổng số đơn thuốc trong tiểu bang của chúng tôi.
  • getPrescriptionById - Một chức năng nhận đơn thuốc theo ID của nó.

Chúng tôi cũng đã thêm một số loại an toàn cho Getters.

Cửa hàng

Hãy chuyển state của chúng tôi , mutations , actionsgetters đến cửa hàng Vuex toàn cầu. Chúng tôi sẽ cập nhật store/index.ts , như sau:

//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 , actionsgetters bắt buộc phải tạo một cửa hàng bằng cách chuyển một đối tượng chứa chúng vào createStore phương pháp. Trong quá trình phát triển, createLogger plugin ghi lại trạng thái (trạng thái trước đó và trạng thái tiếp theo) và các đột biến đối với bảng điều khiển. Để có thể truy cập cửa hàng trong tất cả các thành phần của ứng dụng của chúng tôi; chúng ta cần đưa nó vào toàn bộ ứng dụng. May mắn thay, công cụ Vue-CLI đã nhập toàn bộ cửa hàng và chuyển nó vào phiên bản Vue của ứng dụng.

Tích hợp Vuex Store vào Thành phần

Trong hướng dẫn này, API thành phần Vue 3 sẽ được sử dụng để tạo tất cả các thành phần trong ứng dụng giao diện người dùng của chúng tôi.

Thành phần ứng dụng

Giao diện người dùng của chúng tôi sẽ hiển thị danh sách dữ liệu theo toa ngay khi nó khởi chạy. Chúng tôi sẽ gửi GetPrescription hành động trong mounted() của thành phần móc vòng đời. Để truy cập cửa hàng trong một thành phần, hãy useStore hook, trả về cửa hàng của chúng tôi, sẽ được thực thi.

//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>

Tại đây, chúng tôi đã tạo ba thuộc tính được tính toán:

  • completedCount , gọi là completedPrescriptionCount phương thức getter để truy xuất tổng số lần điều trị đã hoàn thành.
  • totalCount , gọi là totalPrescriptionCount phương thức getter để truy xuất tổng số đơn thuốc.
  • loading , lấy thuộc tính tải của trạng thái.

Với API cấu thành Vue 3, các phương thức và thuộc tính theo yêu cầu của mẫu phải được trả về để có thể truy cập được trong mẫu. Lưu ý cách chúng tôi đã trả lại loading, completedCount, and totalCount .

Thành phần PrescriptionList

Thành phần này sẽ chịu trách nhiệm truy xuất danh sách các đơn thuốc từ phần phụ trợ và chuyển dữ liệu đơn thuốc cho thành phần con của nó.

Tạo PrescriptionList.vue bên trong thư mục thành phần với mã sau:

//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 phương thức gọi ra đột biến đặt showCreateModal ở trạng thái thành true, dẫn đến việc khởi chạy phương thức tạo đơn thuốc.

Chúng tôi đã tạo các thuộc tính được tính toán sau:

  • showCreateModal , lấy showCreateModal tài sản của nhà nước.
  • showEditModal , lấy showEditModal tài sản của nhà nước.
  • showPrescriptionModal , lấy showPrescriptionModal tài sản của nhà nước.
  • prescription , nơi nhận danh sách các đơn thuốc từ tiểu bang.
  • showPrescriptionId , lấy showPrescriptiond tài sản của nhà nước.
  • editPrescriptionId , lấy editPrescriptionId tài sản của nhà nước.

Thành phần thuốc theo toa

Thành phần này sẽ nhận được ID theo toa làm chỗ dựa từ PrescriptionList thành phần. Đạo cụ ID sẽ được sử dụng để tìm nạp đơn thuốc có ID tương ứng qua getPrescriptionById phương thức getters và hiển thị các thuộc tính theo toa trong trình duyệt.

Tạo Prescription.vue bên trong thư mục thành phần với mã sau:

//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 phương thức cam kết SetPrescriptionModal đột biến, đặt showModal thuộc tính ở trạng thái thành false, trong khi prescription computed property calls the getPrescriptionById getter method to retrieve a prescription by its Id.

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:

Xây dựng một ứng dụng Full-Stack với Vue, Vuex và Rails

Kết luận

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.