前言
本文是前端工作坊的教學文件,介紹如何使用 Vue 3 和 Express 實作內容管理系統,並搭配 Firebase 實現持久化和認證。
開啟專案
開啟前端專案。
串接 API
修改列表頁面
修改 CustomerListView.vue 檔,將假資料移除,改為呼叫後端 API 取得真實資料。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 
 | <script setup>import { reactive } from 'vue';
 import { useRouter } from 'vue-router';
 
 const router = useRouter();
 
 const state = reactive({
 customers: [],
 });
 
 const createCustomer = () => {
 router.push({ name: 'customer-create' });
 };
 
 const updateCustomer = (id) => {
 router.push({ name: 'customer-edit', params: { id } });
 };
 
 const deleteCustomer = (id) => {
 const index = customers.value.findIndex(customer => customer.id === id);
 customers.value.splice(index, 1);
 };
 
 (async () => {
 try {
 const response = await fetch('http://localhost:3000/api/customers', {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 const data = await response.json();
 state.customers = data;
 } catch (err) {
 console.error(err);
 }
 })();
 </script>
 
 <template>
 <div class="d-flex justify-content-between align-items-end mb-3">
 <div class="fs-2">
 Customers
 </div>
 <div>
 <button
 type="button"
 class="btn btn-primary btn-sm"
 @click="createCustomer"
 >
 Create
 </button>
 </div>
 </div>
 <table class="table table-striped table-bordered align-middle">
 <thead>
 <tr>
 <th>
 ID
 </th>
 <th>
 Name
 </th>
 <th>
 Actions
 </th>
 </tr>
 </thead>
 <tbody>
 <tr
 v-for="customer in state.customers"
 :key="customer.id"
 >
 <td>
 {{ customer.id }}
 </td>
 <td>
 {{ customer.name }}
 </td>
 <td>
 <button
 type="button"
 class="btn btn-warning btn-sm me-3"
 @click="updateCustomer(customer.id)"
 >
 Edit
 </button>
 <button
 type="button"
 class="btn btn-danger btn-sm"
 @click="deleteCustomer(customer.id)"
 >
 Delete
 </button>
 </td>
 </tr>
 </tbody>
 </table>
 </template>
 
 | 
修改刪除功能
修改 CustomerListView.vue 檔,改寫 deleteCustomer 方法,改為呼叫後端 API 刪除真實資料。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | const deleteCustomer = async (id) => {try {
 await fetch(`http://localhost:3000/api/customers/${id}`, {
 method: 'DELETE',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 state.customers = state.customers.filter((customer) => customer.id !== id);
 } catch (err) {
 console.error(err);
 }
 };
 
 | 
修改表單元件
修改 CustomerForm.vue 檔,定義一個 formData 雙向綁定,並暴露給父層元件使用,然後將表單的欄位綁定到 formData 身上。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 
 | <script setup>import { ref } from 'vue';
 
 
 const formData = defineModel('formData', {
 type: Object,
 default: () => ({}),
 });
 
 const form = ref();
 
 const validateField = (e) => {
 const { target } = e;
 target.classList.toggle('is-valid', target.checkValidity());
 target.classList.toggle('is-invalid', !target.checkValidity());
 };
 
 const validateForm = () => {
 form.value.classList.add('was-validated');
 return form.value.checkValidity();
 };
 
 
 defineExpose({
 validateForm,
 formData,
 });
 </script>
 
 <template>
 <div class="card">
 <div class="card-body">
 <form
 ref="form"
 >
 <div class="mb-3">
 <label
 for="name"
 class="form-label"
 >
 Name
 </label>
 <input
 id="name"
 v-model="formData.name"
 type="text"
 class="form-control"
 required
 @input="validateField"
 >
 <div class="invalid-feedback">
 Please provide a valid name.
 </div>
 </div>
 </form>
 </div>
 </div>
 </template>
 
 | 
實作新增表單
修改 CustomerCreateView.vue 檔,重寫 createCustomer 方法,改為呼叫後端 API 新增真實資料。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | const createCustomer = async () => {if (!form.value.validateForm()) return;
 
 try {
 await fetch('http://localhost:3000/api/customers', {
 method: 'POST',
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify(form.value.formData),
 });
 router.push({ name: 'customer-list' });
 } catch (err) {
 console.error(err);
 }
 };
 
 | 
實作修改表單
修改 CustomerEditView.vue 檔,重寫 updateCustomer 方法,改為呼叫後端 API 修改真實資料。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 
 | <script setup>import CustomerForm from '@/components/CustomerForm.vue';
 import { reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 const route = useRoute();
 const router = useRouter();
 
 const form = ref();
 
 const state = reactive({
 customer: {},
 });
 
 const updateCustomer = async () => {
 if (!form.value.validateForm()) return;
 
 try {
 await fetch(`http://localhost:3000/api/customers/${route.params.id}`, {
 method: 'PUT',
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify(form.value.formData),
 });
 router.push({ name: 'customer-list' });
 } catch (err) {
 console.error(err);
 }
 };
 
 (async () => {
 try {
 const response = await fetch(`http://localhost:3000/api/customers/${route.params.id}`, {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 const data = await response.json();
 state.customer = data;
 } catch (err) {
 console.error(err);
 }
 })();
 </script>
 
 <template>
 <div class="d-flex justify-content-between align-items-end mb-3">
 <div class="fs-2">
 Edit Customer
 </div>
 <div>
 <button
 type="button"
 class="btn btn-danger btn-sm me-3"
 @click="router.push({ name: 'customer-list' })"
 >
 Cancel
 </button>
 <button
 type="button"
 class="btn btn-success btn-sm"
 @click="updateCustomer"
 >
 Save
 </button>
 </div>
 </div>
 <CustomerForm
 ref="form"
 v-model:form-data="state.customer"
 />
 </template>
 
 | 
提交修改。
| 12
 3
 
 | git add .git commit -m "Use api instead of fake data"
 git push
 
 | 
管理 API
建立模組
在 src 資料夾,建立 api 資料夾。
在 src/api 資料夾,建立 customer.js 檔,將呼叫後端 API 的方法集中在此模組進行管理。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 
 | const list = async () => {const response = await fetch('http://localhost:3000/api/customers', {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 return await response.json();
 };
 
 const create = async (data) => {
 const response = await fetch('http://localhost:3000/api/customers', {
 method: 'POST',
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify(data),
 });
 return await response.json();
 };
 
 const get = async (id) => {
 const response = await fetch(`http://localhost:3000/api/customers/${id}`, {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 return await response.json();
 };
 
 const update = async (id, data) => {
 const response = await fetch(`http://localhost:3000/api/customers/${id}`, {
 method: 'PUT',
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify(data),
 });
 return await response.json();
 };
 
 const destroy = async (id) => {
 await fetch(`http://localhost:3000/api/customers/${id}`, {
 method: 'DELETE',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 };
 
 export {
 create,
 destroy,
 get,
 list,
 update,
 };
 
 | 
在 src/api 資料夾,建立 index.js 檔,匯出 customer 模組。
| 1
 | export * as customer from './customer';
 | 
重構列表頁面
修改 CustomerListView.vue 檔,改成使用 customer 模組呼叫 API。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 
 | <script setup>import { customer } from '@/api';
 import { reactive } from 'vue';
 import { useRouter } from 'vue-router';
 
 const router = useRouter();
 
 const state = reactive({
 customers: [],
 });
 
 const createCustomer = () => {
 router.push({ name: 'customer-create' });
 };
 
 const updateCustomer = (id) => {
 router.push({ name: 'customer-edit', params: { id } });
 };
 
 const deleteCustomer = async (id) => {
 try {
 await customer.destroy(id);
 state.customers = state.customers.filter((customer) => customer.id !== id);
 } catch (err) {
 console.error(err);
 }
 };
 
 (async () => {
 try {
 state.customers = await customer.list();
 } catch (err) {
 console.error(err);
 }
 })();
 </script>
 
 <template>
 
 </template>
 
 | 
重構新增表單
修改 CustomerCreateView.vue 檔,改成使用 customer 模組呼叫 API。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | <script setup>import { customer } from '@/api';
 import CustomerForm from '@/components/CustomerForm.vue';
 import { ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 const router = useRouter();
 
 const form = ref();
 
 const createCustomer = async () => {
 if (!form.value.validateForm()) return;
 
 try {
 await customer.create(form.value.formData);
 router.push({ name: 'customer-list' });
 } catch (err) {
 console.error(err);
 }
 };
 </script>
 
 <template>
 
 </template>
 
 | 
重構修改表單
修改 CustomerEditView.vue 檔,改成使用 customer 模組呼叫 API。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 
 | <script setup>import { customer } from '@/api';
 import CustomerForm from '@/components/CustomerForm.vue';
 import { reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 const route = useRoute();
 const router = useRouter();
 
 const form = ref();
 
 const state = reactive({
 customer: {},
 });
 
 const updateCustomer = async () => {
 if (!form.value.validateForm()) return;
 
 try {
 await customer.update(route.params.id, form.value.formData);
 router.push({ name: 'customer-list' });
 } catch (err) {
 console.error(err);
 }
 };
 
 (async () => {
 try {
 state.customer = await customer.get(route.params.id);
 } catch (err) {
 console.error(err);
 }
 })();
 </script>
 
 <template>
 
 </template>
 
 | 
提交修改。
| 12
 3
 
 | git add .git commit -m "Add customer api module"
 git push
 
 | 
建立環境變數
Ref: https://vitejs.dev/guide/env-and-mode
建立 .env.local 檔,這個檔案會被版本控制忽略。以 VITE_ 開頭的環境變數,會被 Vite 自動載入。以下新增 VITE_API_URL 環境變數,未來如果需要根據環境而有不同的 API URL,就可以靈活修改。
| 1
 | VITE_API_URL=http://localhost:3000
 | 
建立 .env.example 檔,這個檔案會被版本控制紀錄。這是環境變數的模板。
修改 src/api/customer.js 檔,使用 VITE_API_URL 環境變數,取代原先寫死的 API URL。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 
 | const { VITE_API_URL } = import.meta.env;
 const list = async () => {
 const response = await fetch(`${VITE_API_URL}/api/customers`, {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 return await response.json();
 };
 
 const create = async (data) => {
 const response = await fetch(`${VITE_API_URL}/api/customers`, {
 method: 'POST',
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify(data),
 });
 return await response.json();
 };
 
 const get = async (id) => {
 const response = await fetch(`${VITE_API_URL}/api/customers/${id}`, {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 return await response.json();
 };
 
 const update = async (id, data) => {
 const response = await fetch(`${VITE_API_URL}/api/customers/${id}`, {
 method: 'PUT',
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify(data),
 });
 return await response.json();
 };
 
 const destroy = async (id) => {
 await fetch(`${VITE_API_URL}/api/customers/${id}`, {
 method: 'DELETE',
 headers: {
 'Content-Type': 'application/json',
 },
 });
 };
 
 
 
 | 
提交修改。
| 12
 3
 
 | git add .git commit -m "Use env for api url"
 git push
 
 | 
使用 Axios 套件
Ref: https://github.com/axios/axios
安裝依賴套件。Axios 提供更強大的 HTTP 請求功能,使得處理請求更加方便和靈活。
修改 src/api/customer.js 檔,使用 Axios 進行 HTTP 請求,而不是瀏覽器原生的 fetch 方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | import axios from 'axios';
 const { VITE_API_URL } = import.meta.env;
 
 const client = axios.create({
 baseURL: VITE_API_URL,
 });
 
 const list = async () => {
 const response = await client.get('/api/customers');
 return response.data;
 };
 
 const create = async (data) => {
 const response = await client.post('/api/customers', data);
 return response.data;
 };
 
 const get = async (id) => {
 const response = await client.get(`/api/customers/${id}`);
 return response.data;
 };
 
 const update = async (id, data) => {
 const response = await client.put(`/api/customers/${id}`, data);
 return response.data;
 };
 
 const destroy = async (id) => {
 await client.delete(`/api/customers/${id}`);
 };
 
 
 
 | 
提交修改。
| 12
 3
 
 | git add .git commit -m "Use axios instead of fetch"
 git push
 
 | 
建立資料模型
新增 src/models/Customer.js 檔,為客戶建立一個 Customer 類別。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | class Customer {constructor({
 id,
 name,
 }) {
 this.id = id;
 this.name = name;
 }
 }
 
 export default Customer;
 
 | 
新增 src/models/index.js 檔。
| 12
 3
 4
 5
 
 | import Customer from './Customer';
 export {
 Customer,
 };
 
 | 
修改 src/api/customer.js 檔,將後端返回的資料轉換為 Customer 實例。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 
 | import { Customer } from '@/models';import axios from 'axios';
 
 const { VITE_API_URL } = import.meta.env;
 
 const client = axios.create({
 baseURL: VITE_API_URL,
 });
 
 
 
 
 const list = async () => {
 const response = await client.get('/api/customers');
 return response.data.map((customer) => new Customer(customer));
 };
 
 
 
 
 const create = async (data) => {
 const response = await client.post('/api/customers', data);
 return new Customer(response.data);
 };
 
 
 
 
 const get = async (id) => {
 const response = await client.get(`/api/customers/${id}`);
 return new Customer(response.data);
 };
 
 
 
 
 const update = async (id, data) => {
 const response = await client.put(`/api/customers/${id}`, data);
 return new Customer(response.data);
 };
 
 const destroy = async (id) => {
 await client.delete(`/api/customers/${id}`);
 };
 
 export {
 create,
 destroy,
 get,
 list,
 update,
 };
 
 | 
修改 src/views/CustomerListView.vue 檔,新增 JSDoc 註解以明確定義資料的類型,讓編輯器能夠產生提示的功能。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | 
 const state = reactive({
 
 
 
 customers: [],
 });
 
 
 
 | 
修改 src/components/CustomerForm.vue 檔。
| 12
 3
 4
 5
 6
 7
 
 | 
 
 const formData = defineModel('formData', {
 type: Object,
 default: () => ({}),
 });
 
 | 
提交修改。
| 12
 3
 
 | git add .git commit -m "Add data models"
 git push
 
 | 
程式碼