Xây dựng ứng dụng full-stack với khả năng mở rộng trong tâm trí có thể đáng sợ, đặc biệt khi dùng Vue và Vuex mới nhất, có hỗ trợ TypeScript đầy đủ. Bài viết này dạy mọi thứ cần biết về xây dựng ứng dụng full-stack scalable từ state management với Vuex 4.0 xử lý API request và tương tác database bằng cách khám phá ứng dụng CRUD quản lý prescription điều trị gia súc không khỏe. Backend dùng Rails, expose CRUD API cơ bản cho frontend tích hợp.
Hầu hết công ty áp dụng kiến trúc phát triển dựa trên API vì cho team frontend linh hoạt chọn công cụ phù hợp. Mặc dù không phải lựa chọn tốt nhất, nó đã là lựa chọn tin cậy, nhiều công ty áp dụng vì thêm linh hoạt cho team.
Điều kiện tiên quyết
Trước khi bắt đầu, đảm bảo bạn có:
- Rails V6.x
- Node.js V10.x
- Kiến thức làm việc trước với Ruby on Rails
- Kiến thức làm việc trước với TypeScript
- Kiến thức làm việc trước với Vue.js
Chúng ta sẽ xây dựng gì

Trong tutorial, xây dựng ứng dụng full-stack CRUD quản lý prescription điều trị gia súc không khỏe. User tạo, cập nhật, xóa prescription cho gia súc. CRUD APIs dùng Rails, expose API cho frontend tích hợp, frontend dùng Vue 3. Mỗi stack chạy port khác nhau như ứng dụng độc lập.
Kiến trúc cơ bản Full-stack App
Ứng dụng full-stack gồm client và server chạy độc lập, mỗi component client tương tác với CRUD APIs từ server qua Vuex để quản lý state ứng dụng đúng đắn. Backend lưu toàn bộ data prescription trong database Sqlite 3 đồng thời expose CRUD APIs cho frontend.
Setup Backend Service
Trong thư mục tùy ý, tạo Rails app bằng lệnh:
rails new vet_clinic_api --api
Lệnh này bảo Rails tạo project dạng API, loại bỏ mọi dependency frontend (view files).
Cấu hình Database
Chúng ta dùng Sqlite 3, database mặc định Rails.
rails g scaffold prescriptions vet_prescription:text prescribed_by:text disease:text livestock:text completed:boolean
Chạy lệnh trên, Rails scaffold cấu trúc ban đầu cho migration, test, model, controller, route:
rails db:migrate
Lệnh này thêm table vào database.
Seed Database
Seed database với data prescription. Thêm code 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"
File này lưu data khởi tạo để seed database khi app start, có sẵn data prescription.
Chạy lệnh sau thực thi code trong db/migrate/seed.rb, seed database với data định sẵn:
rails db:seed
Với vài lệnh, tạo functional CRUD API với Rails. Dễ dàng như thế nào? (mỉm cười)
Cấu hình CORS
Vì expose CRUD APIs cho frontend, cả frontend và backend chạy PORT khác, cần setup CORS trên Rails backend cho phép chia sẻ data giữa frontend và backend.
Tìm Gemfile ở root project, uncomment dòng:
# gem 'rack-cors'
Thêm code 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
Snippet cho phép Rails CRUD API truy cập từ frontend app chạy trên bất kỳ PORT.
Chạy lệnh sau start Rails server:
rails s
Truy cập localhost:3000/prescriptions nhận JSON response tất cả prescriptions.
Cân nhắc dùng Rails nếu làm việc với client thay đổi workflow dự án thường xuyên. Với Rails, dễ implement feature với vài lệnh và dòng code. Đây ý kiến cá nhân.
Giới thiệu Vue
Vue là progressive framework xây dựng user interface. Vue giới thiệu khái niệm Virtual DOM, render functions, server-side rendering capabilities, cung cấp tối ưu hiệu năng đáng kể cho web app.
Vue 3 mang nhiều tính năng và thay đổi mới cho dev. Các tính năng thiết kế cải thiện stability, speed, maintainability.
Chúng ta dùng Composition API, một tính năng mong đợi nhất Vue 3. Cách mới tạo Vue component, tiếp cận organized và efficient hơn viết code, hỗ trợ type-checking TypeScript đầy đủ.
Giới thiệu Vuex
Vuex là state management library do team Vue tạo, dựa trên flux architecture như Redux. Được thiết kế riêng cho Vue và cho phép tổ chức store tốt hơn. Vuex trở nên quan trọng nếu state ứng dụng Vue phức tạp khi phát triển. Phiên bản stable Vuex v4.0.0 hỗ trợ Composition API Vue 3, cũng như type inference TypeScript mạnh mẽ hơn.
Setup Frontend Application
Frontend setup với Vue 3 và TypeScript, Vuex quản lý state ứng dụng.
Bắt đầu dùng Vue-CLI tool tạo Vue 3 app với TypeScript và Vuex support.
Cài Vue-CLI global:
npm install --global @vue/cli
Tạo Vue 3 app mới với TypeScript và Vuex:
vue create vet_clinic_frontend
Chọn manually select feature, nhấn space chọn:
- Choose Vue version
- Babel
- TypeScript
- Linter / Formatter
Tiếp, chọn Vue 3.x(Preview) làm version dự án.
- Enter yes dùng class-style component syntax.
- Enter yes dùng Babel cùng TypeScript.
- Chọn linter tùy ý.
Khi Vue 3 app generated thành công, có project Vue 3 setup với Vuex (V4.x) và TypeScript đầy đủ.
Thêm type safety dùng TypeScript. Chạy dev server:
npm run serve
Vào https://localhost:8080 preview project trình duyệt.
Dùng Bulma CSS framework style app. Cài Bulma CSS:
npm install bulma
Import Bulma CSS, thêm code vào App.vue:
//App.vue <style lang="scss"> @import "~bulma/css/bulma.css"; </style>
Setup Application Store
Application store setup với Vuex. Để thay đổi store, actions dispatch từ component, trigger mutations, cập nhật store.
Các bước setup store:
- Tạo state objects.
- Setup mutations xảy ra trong app.
- Tạo actions commit mutations.
- Tạo getters cho component compute state data trực tiếp.
State
State là object store giữ data cấp ứng dụng cần truy cập bởi tất cả component.
Tạo file state.ts trong thư mục store với code:
//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, thêm type safety cho Prescription và Data. Export types vì dùng trong định nghĩa getters, mutations, actions. Cuối cùng cast State type cho state.
Mutations
Mutations là phương thức khi trigger, sửa đổi store. Nhận state làm đối số đầu, payload thứ hai, cuối cùng sửa state với payload. Tạo mutations, Vuex docs khuyên dùng constants cho mutation types.
Tạo file mutations.ts trong store với code:
//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", }
Snippet dùng enum symbol giữ tất cả tên mutation có thể trong app.
Tiếp, khai báo contract (type) cho mỗi 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; };
Sau đó, implement contract cho mỗi 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 là generic type shipped với Vuex package. Dùng trong snippet khai báo type mutation tree. Mutation tree và mutations đảm bảo contract implement đúng; nếu không TypeScript báo lỗi.
Actions
Actions là phương thức trigger mutations. Khi xử lý async task như request API, actions dùng trước gọi mutations tương ứng với API response làm payload. Chúng ta sẽ thấy implement rõ ràng khi tạo actions.
Trước tạo actions, cài Axios xử lý Http request đến Rails server:
npm install axios --save
Tạo file actions.ts trong store với code:
//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ự, snippet dùng enum symbol giữ tên actions.
Tiếp, khai báo contract (type) cho mỗi ActionType:
//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 restrict commits chỉ đến mutations khai báo và check payload type.
Tiếp, implement contract (types) cho mỗi ActionType. Thêm code vào actions.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, tạo variable actions lưu tất cả actions implement. Tương tự, ActionTree<State> & Actions đảm bảo contract (type Actions) implement đúng; nếu không TypeScript báo lỗi.
Chúng ta cũng setup async call đến Rails API endpoint trong action GetPrescriptions và trigger mutation SetPrescriptions với response data làm payload. Cũng setup actions SetCreateModal, SetEditModal, CreatePrescription, UpdatePrescription, và RemovePrescription.
Getters
Getters là phương thức nhận state làm tham số đầu và trả về thông tin computed từ store state.
Tạo file getters.ts trong store với code:
//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); }, };
Code định nghĩa getters sau:
completedPrescriptionCount– Function lấy tổng prescription completed trong state.totalPrescriptionCount– Function lấy tổng prescription trong state.getPrescriptionById– Function lấy prescription bằng ID.
Cũng thêm type safety cho Getters.
Store
Kết nối state, mutations, actions, getters vào global Vuex store. Update 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 required tạo store bằng cách pass object chứa chúng vào createStore. Trong development, plugin createLogger log state (previous và next state) và mutations ra console. Để store accessible trong tất cả component, inject vào toàn app. May mắn Vue-CLI đã import store và pass trong Vue instance của app.
Tích hợp Vuex Store vào Component
Trong tutorial, Vue 3 Composition API dùng tạo tất cả component frontend.
App Component
Frontend nên render list prescription data ngay khi launch. Dispatch action GetPrescription trong lifecycle hook mounted() của component. Để truy cập store trong component, hook useStore trả về store, sẽ 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>
Ở đây, tạo ba computed properties:
completedCount, gọi gettercompletedPrescriptionCountlấy tổng treatment completed.totalCount, gọi gettertotalPrescriptionCountlấy tổng prescriptions.loading, lấy loading property của state.
Với Vue 3 Composition API, methods và properties template cần phải return để accessible trong template. Chú ý return loading, completedCount, and totalCount.
PrescriptionList Component
Component này lấy list prescriptions từ backend và pass data prescription cho child component.
Tạo PrescriptionList.vue trong components folder với code:
//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>
Phương thức setModal invoke mutation set showCreateModal trong state thành true, launch modal tạo prescription.
Tạo computed properties sau:
showCreateModal, lấy propertyshowCreateModalcủa state.showEditModal, lấy propertyshowEditModalcủa state.showPrescriptionModal, lấy propertyshowPrescriptionModalcủa state.prescription, lấy list prescription từ state.showPrescriptionId, lấy propertyshowPrescriptiondcủa state.editPrescriptionId, lấy propertyeditPrescriptionIdcủa state.
Prescription Component
Component nhận prescription ID như prop từ PrescriptionList. ID props dùng fetch prescription với ID tương ứng qua getter getPrescriptionById và render properties prescription trình duyệt.
Tạo Prescription.vue trong components folder:
//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> <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>
Phương thức closeModal commit mutation SetPrescriptionModal, set property showModal trong state thành false, trong khi computed property prescription gọi getter getPrescriptionById lấy prescription bằng Id.
CreateModal Component
Component này tạo prescription.
Tạo CreateModal.vue trong components folder:
//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>
Phương thức createPrescription dispatch action post request đến server, tạo prescription mới, trong khi closeModal commit mutation SetPrescriptionModal (set showModal trong state thành false).
Làm việc với form và input element yêu cầu two-way data binding, nên dùng reactive của Vue 3 lưu giá trị dùng trong input fields.
Lưu ý: Khi dùng reactive, cần toRefs convert reactive object thành plain object, mỗi property trên object kết quả là ref trỏ đến property tương ứng trong object gốc.
EditModal Component
Component này update prescription. Logic tương tự CreatePrescription component phần trước.
Tạo EditModal.vue trong components folder:
//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>
Phương thức createPrescription dispatch action put request đến server, update prescription hiện có bằng ID, trong khi closeModal commit mutation SetPrescriptionModal set property showModal trong state thành false. Gọi setFields trên lifecycle hook onMounted trigger getter getPrescriptionById fetch prescription từ store bằng ID và update properties trong reactive object với properties prescription fetched ngay khi component mount trên DOM.
Khởi chạy Final Project
Trong root directory Rails API server, chạy lệnh start server:
rails server
Giờ chạy frontend app:
npm run serve
Cuối cùng, ứng dụng nên như sau:

Kết luận
Chúng ta đã xây dựng CRUD API server với Rails và frontend app trên Vue 3 Composition API và Vuex, tất cả chạy trên server khác nhau đồng thời tích hợp cả hai stack xây dựng fullstack web application. Hy vọng bạn học được nhiều từ tutorial. Hãy comment bên dưới nếu có câu hỏi hoặc góp ý. Đây là GitHub repo cho full-stack application xây dựng trong tutorial.