※ 認識MVC架構:
- Model (模型):
- Data Model: 負責資料庫的交互與資料的映射,通常用來定義資料結構和進行資料庫操作。
- Business Model: 處理商業邏輯,將原始資料進行處理和轉換(在這次的設計中,這部分由 Controller 處理,省略business model的開發。)。
- Controller (控制器):
- 從 Model 取得資料並進行處理,根據商業邏輯整理好後傳給 View。
- 就是負責調控和協調。
- View (視圖):
- 負責將資料呈現給使用者,通常是前端的畫面渲染。View 不包含任何邏輯處理,專門做畫面的渲染。
※ MVC架構的優點:
可以將資料做分層化的處理,也就是單一職責原則。

※ 建立Model :model --> base.ts
建立Model 的主要功能有:
- 執行 CRUD(建立、讀取、更新、刪除)操作。

※ 定義接口(IBase) :
export interface IBase<T> {
findAll(): Promise<T[] | null>;
findOne(id: any): Promise<T | null>;
create(date: Omit<T, 'id'>): Promise<T | null>;
update(id: any, date: Partial<Omit<T, 'id'>>): Promise<T | null>;
delete(id: any): Promise<void>;
}
程式碼解說:
export interface就是把一個物件結構的描述從一個模組拿出來,讓其他模組也能使用這個結構定義。IBase前綴 "I"明確地表示這是一個接口,所以IBase表示這是一個基礎接口。- 泛型 T: 使用泛型
T使這個接口可以適用於各種類型的資料,不限於特定資料結構。 - Promise: 使用 Promise 以便在非同步操作(如資料庫查詢)完成後處理結果。
- Omit 和 Partial: 使用
Omit和Partial來靈活地定義資料結構,避免在創建和更新時強制要求提供所有欄位。 findAll(): Promise<T[] | null>:它會取得資料庫中所有的記錄。findOne(id: any): Promise<T | null>:根據 ID 查找特定的資料。create(data: Omit<T, 'id'>): Promise<T | null>:接受一個不包含id的資料物件(Omit<T, 'id'>),用於在資料庫中建立新記錄。update(id: any, data: Partial<Omit<T, 'id'>>): Promise<T | null>:
Partial<Omit<T, 'id'>>),用於根據 ID 更新現有記錄。delete(id: any): Promise<void>:
根據所給的 ID 刪除資料記錄,void表示操作完成且無返回值,於刪除資料庫中的記錄。
※ 建立database transaction,資料庫交易 :
export interface IBase<T> {
findAll(trx?: Knex.Transaction): Promise<T[] | null>;
findOne(id: any, trx?: Knex.Transaction): Promise<T | null>;
create(data: Omit<T, 'id'>, trx?: Knex.Transaction): Promise<T | null>;
update(id: any, data: Partial<Omit<T, 'id'>>, trx?: Knex.Transaction): Promise<T | null>;
delete(id: any, trx?: Knex.Transaction): Promise<void>;
}
程式碼解說:
- trx?:表示這個參數是可選的,不傳遞也沒關係。它是 Knex.js 用於資料庫交易(transaction)的物件。
- Omit:目的是排除某個屬性。在創建資料時,由系統自動生成或管理的屬性,有ID、時間戳(
created_at和updated_at)、唯一標識碼(UUID)、系統內部狀態。 - findAll(trx?: Knex.Transaction): Promise<T[] | null>; 查找所有記錄,返回一個包含所有記錄的 Promise,類型為陣列(T[]),或是空值(null)。
- findOne(id: any, trx?: Knex.Transaction): Promise<T | null>; 根據 ID 查找單一記錄,返回一個包含該記錄的 Promise,類型為 T 或是空值(null)。
- create(data: Omit<T, 'id'>, trx?: Knex.Transaction): Promise<T | null>; 創建一個新的記錄,傳入的資料不包含 ID(Omit<T, 'id'>),返回一個包含創建結果的 Promise,類型為 T 或是空值(null)。
- update(id: any, data: Partial<Omit<T, 'id'>>, trx?: Knex.Transaction): Promise<T | null>: 說的是,更新指定 ID 的記錄,傳入需要修改的屬性,返回一個包含更新結果的 Promise,類型為 T 或是空值(null)。
- delete(id: any, trx?: Knex.Transaction): Promise<void>; 根據 ID 刪除記錄,返回一個 Promise,完成後不返回數據。
T表示單一的一個資料元素,而T[]則表示一個包含多個資料元素的陣列。也就是說,查詢所有資料用T[]表示資料陣列,查詢單一資料或創建新資料用T表示單個資料。void是 TypeScript 和 JavaScript 中用來表示「沒有返回值」的函數。
※ 定義抽象類別(Base):
export abstract class Base<T> implements IBase<T> {
constructor({ knexSql, tableName }: { knexSql: Knex, tableName?: string }) {
}
}
程式碼解說:
- export abstract class Base<T> implements IBase<T>:
- 定義了一個泛型抽象類別 Base這個框架,來實現 IBase<T> 這個介面。這表示 Base 類別必須包含 IBase 介面定義的方法,但因為它是抽象類別,所以不能直接實例化。
constructor方法:
- constructor({ knexSql, tableName }: { knexSql: Knex, tableName?: string }) 允許在創建 Base 類實例時傳入一個包含 knexSql 和可選的 tableName 的物件。
- 從 App 傳入參數:
- 在 App 中,可以根據需要傳入這些參數,當創建 Base 或其子類的實例時,提供所需的資料庫連接和表名。
- 如果 tableName 未傳入,則可以在子類或其他地方決定其具體值。
export abstract class Base<T> implements IBase<T> {
//定義屬性:
protected knexSql: Knex;
protected tableName: string = '';
protected schema = {};
//在建構函數中初始化和設定這些屬性,以確保類的實例具有正確的初始狀態。
constructor({ knexSql, tableName }: { knexSql: Knex, tableName?: string }) {
//設定值:
this.knexSql = knexSql;
if (tableName) this.tableName = tableName;
}
程式碼解說:。
- protected knexSql: Knex;
- 定義了一個受保護的屬性 knexSql,類型為 Knex。這表示這個屬性只能在 Base 類別及其子類中使用。目的是增強程式碼的封裝性和安全性。用來存儲資料庫連接實例。
- protected tableName: string = '';
- 定義了一個受保護的屬性 tableName,類型為 string,並預設值為空字串。
- protected schema = {};
- 定義了一個受保護的屬性 schema,預設為空物件。這可能用來存儲資料庫表格的結構定義。
- this.knexSql = knexSql;:將傳入的 knexSql 參數賦值給 this.knexSql,將資料庫連接實例保存到該類的屬性中。
- if (tableName) this.tableName = tableName;:是否提供了 tableName 參數,如果提供,則將其賦值給 this.tableName。
※ 將資料庫中的資料轉換為應用程式中的資料格式:
認識lodash:
Lodash 是一個很實用的function集合包,包含了所有的函數和功能。目的是為了簡化重複性的代碼,減少開發時間和錯誤。
Lodash 中常用的功能:
- 陣列處理:_.map, _.filter, _.reduce 等函數,讓你可以方便地操作陣列。
- 物件處理:_.assign, _.cloneDeep, _.merge 等函數,讓物件的操作變得簡單。
- 數據查詢:_.find, _.findIndex, _.includes 等函數,幫助你快速查找數據。
- 函數處理:_.debounce, _.throttle, _.once 等函數,控制函數的調用次數。
- 字串處理:_.camelCase, _.kebabCase, _.startCase 等函數,方便格式化字串。
安裝lodash:
npm install lodash認識@types/lodash:
這個是 TypeScript 的型別定義包,用於 Lodash 函式庫。當你在用 TypeScript 開發時,它提供 Lodash 函數的型別定義,這樣編譯器在使用 Lodash 時得到自動完成功能和型別檢查。
安裝@types/lodash:
npm install --save-dev @types/lodash
private DBData2DataObject = (data) => {
const transform = mapValues(data, (value, key) => {
if (['updatedAt', 'createdAt'].includes(key)) return new Date(value);
// check if a string is json
if (typeof value === "object") return JSON.stringify(value);
return value;
})
}
程式碼解說:
- private DBData2DataObject = (data) => { ... }:這是一個箭頭函數,它接收一個 data 參數。這個函數被定義為 private,表示它只能在類別內部使用。
- const transform = mapValues(data, (value, key) => { ... }):這一行使用 mapValues 函數來遍歷 data 中的每個鍵值對。mapValues 是一個類似於 Array.prototype.map 的函數,但它是用來處理物件的。對於 data 中的每個鍵值對,都會執行提供的箭頭函數 (value, key) => { ... }。
- if (['updatedAt', 'createdAt'].includes(key)) return new Date(value);:這一行檢查當前鍵(key)是否為 updatedAt 或 createdAt,如果是,則將對應的值轉換為 Date 對象並返回。
- if (typeof value === "object") return JSON.stringify(value);:這一行檢查當前值(value)是否為物件,如果是,則將其轉換為 JSON 字符串並返回。
- return value;:如果值不是物件,則直接返回該值。
- 最後,transform 變數將包含轉換後的資料。
這段程式碼的目的是將輸入資料中的 updatedAt 和 createdAt 字段轉換為 Date 對象,並將所有物件型別的值轉換為 JSON 字符串。
※ 判斷輸入的字串是否為有效的 JSON 格式:utils --> index.ts
export const isJson = (value: string) => {
try {
return Boolean(JSON.parse(value));
} catch (e) {
return false;
}
}
程式碼解說:
- 函數定義:
export const isJson = (value: string) => {}
- export:表示這個函數可以被其他模組引用。
- const isJson:定義了一個常數函數,名稱為 isJson。
- (value: string):函數接受一個字串類型的參數 value。
try區塊:
try {
return Boolean(JSON.parse(value));
}
- JSON.parse(value):嘗試將字串解析為 JSON。
- Boolean():將解析結果轉換為布林值。如果解析成功,返回 true。
catch區塊:
catch (e) {
return false;
}
- 如果解析失敗,捕捉錯誤並返回 false。
※ 將資料庫中的資料轉換為應用程式中的資料格式:
private DBData2DataObject = (data: any) => {
const transform = mapValues(data, (value, key) => {
if (['updatedAt', 'createdAt'].includes(key)) return new Date(value);
if (isJson(value)) return JSON.parse(value);
return value;
});
return mapKeys(transform, (_value, key) => camelCase(key));
}
程式碼解說:
- 私有方法:
- private DataObject2DBData = (data: any) => { ... }:這是一個私有的箭頭函數,僅供類的內部使用。
- 轉換數據(mapValues):
- 這行代碼會遍歷 data 中的每個屬性,並根據屬性名和屬性值進行處理。
- 如果屬性名是 updatedAt 或 createdAt,就把這個屬性的值轉換成日期(Date)物件。
- 如果屬性值是 JSON 字符串,就把這個字符串解析成 JavaScript 物件。
- 對於其他情況,保持屬性值不變。
- 鍵名轉換(mapKeys):
- 這行代碼會遍歷轉換後的每個屬性,將屬性名從蛇形命名法(如 created_at)轉換成駝峰命名法(如 createdAt),使其符合 JavaScript 的命名慣例。
※ 導入isJson函數:

※ data參數增加any:
private DBData2DataObject = (data: any) => {}

※ 將應用程式中的資料物件轉換成適合存入資料庫的格式:
private DataObject2DBData = (data: any) => {
const transform = mapValues(data, (value, key) => {
if (['updatedAt', 'createdAt'].includes(key)) return new Date(value);
if (isJson(value)) return JSON.parse(value);
return value;
});
return mapKeys(transform, (_value, key) => snakeCase(key));
}
程式碼解說:
- 將日期字段轉換為
Date物件。 - 將 JSON 字符串解析為 JavaScript 物件。
- 將鍵名轉換為蛇形命名法。
※ CRUD的function:
1. 查詢所有記錄:
public findAll = async (trx?: Knex.Transaction) => {
//select col1, col2, ...from tableName
let sqlBuilder = this.knexSql(this.tableName).select(this.schema);
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
const result = await sqlBuilder;
if (isEmpty(result)) return null;
return result.map(this.DBData2DataObject) as T[];
}
程式碼解說:
- 方法定義:
public findAll = async (trx?: Knex.Transaction) => {}
- public:表示這個方法是公開的,可以被外部訪問。
- findAll:方法名稱。
- async:表示這個方法是非同步的,會返回一個 Promise。
- trx?: Knex.Transaction:可選的參數 trx,類型為 Knex 的交易物件。
- 建立 SQL 查詢:
let sqlBuilder = this.knexSql(this.tableName).select(this.schema);
- 使用 Knex.js 建立一個 SQL 查詢建構物件(sqlBuilder),從 this.tableName 表中選擇 this.schema 中定義的欄位。
- 交易處理:
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
如果有提供 trx 交易物件,則將查詢納入該交易,以確保資料的一致性和原子性。
- 執行查詢:
const result = await sqlBuilder;
- 執行查詢並等待結果。
- 結果處理:
if (isEmpty(result)) return null;
return result.map(this.DBData2DataObject) as T[];
- 將資料庫裡面的資料拉出來,如果結果為空,返回 null。
- 如果有結果,會將結果轉換成資料物件,並以 T[] 型別返回。
- 使用as T[]的原因:明確地指定返回的結果是一個具有型別 T 的數組。

※ 查詢單筆記錄:
public findOne = async (id: any, trx?: Knex.Transaction) => {
let sqlBuilder = this.knexSql(this.tableName).select(this.schema).where(id);
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
const result = await sqlBuilder;
if (isEmpty(result)) return null;
return this.DBData2DataObject(result[0]) as T;
}
程式碼解說:
1. 定義方法:
public findOne = async (id: any, trx?: Knex.Transaction) => { ... }
- 這是一個公開的異步箭頭函數 findOne,接收兩個參數:id 和可選的交易 trx。
- public 表示這個方法可以從類的外部訪問。
- 建構 SQL 查詢:
let sqlBuilder = this.knexSql(this.tableName).select(this.schema).where(id);
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
- 使用
knexSql方法來構建 SQL 查詢,選擇schema中的字段,並根據id進行查詢。 - 如果提供了交易
trx,則使用transacting方法將查詢包含在交易中。
3. 執行查詢:
const result = await sqlBuilder;
執行查詢並等待結果,將查詢結果存儲在 result 變量中。
4. 檢查結果是否為空:
if (isEmpty(result)) return null;
- 使用
isEmpty函數檢查結果是否為空。 - 如果結果為空,返回
null。
5. 轉換並返回結果:
return this.DBData2DataObject(result[0]) as T;
- 如果結果不為空,將結果中的第一條記錄轉換為資料物件,這是通過
DBData2DataObject方法來完成的。 - 將轉換後的結果以型別
T返回,這意味著返回的物件具有型別T。
※ 建立單筆記錄:
public create = async (data: Omit<T, 'id'>, trx?: Knex.Transaction) => {
let sqlBuilder = this.knexSql(this.tableName).insert(this.DataObject2DBData(data));
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
const result = await sqlBuilder;
if (isEmpty(result)) return null;
const id = result[0];
return await this.findOne(id, trx);
}
程式碼解說:
- 方法定義:
public create = async (data: Omit<T, 'id'>, trx?: Knex.Transaction) => {
- data: Omit<T, 'id'>:傳入的資料不包含 id 屬性,類型為 T(除了 id)。
- 建立 SQL 插入查詢:
let sqlBuilder = this.knexSql(this.tableName).insert(this.DataObject2DBData(data));
- 使用 Knex.js 建立一個插入查詢,將資料插入到 this.tableName 表中,資料經過 this.DataObject2DBData(data) 處理。
- 結果處理:
if (isEmpty(result)) return null;
const id = result[0];
return await this.findOne(id, trx);
- 如果有結果,提取插入記錄的 id,並使用 findOne 方法根據 id 查詢並返回新創建的記錄。
※ 更新單筆記錄:
public update = async (id: any, data: Partial<Omit<T, 'id'>>, trx?: Knex.Transaction) => {
let sqlBuilder = this.knexSql(this.tableName).update(this.DataObject2DBData(data).where({ id }));
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
await sqlBuilder;
return await this.findOne(id, trx);}程式碼解說:
1. 建構更新查詢:let sqlBuilder = this.knexSql(this.tableName).update(this.DataObject2DBData(data)).where({ id });
- 使用
knexSql方法來構建更新查詢,指定更新的表this.tableName。 - 使用
this.DataObject2DBData(data)方法將data轉換為適合資料庫的格式。 - 使用
where方法指定更新條件,即id。
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
如果提供了交易trx,則使用transacting方法將查詢包含在交易中。3. 執行更新查詢:await sqlBuilder;
執行更新查詢,等待更新操作完成。4. 查詢並返回更新後的記錄:return await this.findOne(id, trx);
使用findOne方法根據id查詢並返回更新後的記錄,並傳遞可選的交易trx。※ 刪除單筆記錄:
public delete = async (id: any, trx?: Knex.Transaction) => {
程式碼解說:
let sqlBuilder = this.knexSql(this.tableName).where({ id }).del();
if (trx) sqlBuilder = sqlBuilder.transacting(trx);
await sqlBuilder;
return
}return;
- 方法完成後不返回任何數據,因為這個方法的主要目的是刪除指定記錄。
※ 建立產品檔案 :Model -->product.ts

※ 定義Production 的 Schema:
export interface Product {
//符合 TypeScript 的語法和類型系統
id: number;
name: string;
amount: number;
description: string;
pre_order: number;
price: number;
}
程式碼解說:
- id: number;:產品的唯一標識符,類型為數字。
- name: string;:產品名稱,類型為字串。
- amount: number;:產品數量,類型為數字。
- description: string;:產品描述,類型為字串。
- pre_order: number;:預購數量,類型為數字。
- price: number;:產品價格,類型為數字。
※ 定義Production 的 型別:
export interface IProductModel extends IBase<Product> {}
程式碼解說:
- export interface IProductModel extends IBase<Product> {}:
- export:表示這個介面可以被其他模組引用。
- interface IProductModel:定義了一個介面 IProductModel。
- extends IBase<Product>:表示 IProductModel 繼承自 IBase<Product>。
- IBase<Product>:這表示 IProductModel 繼承了 IBase 介面的所有方法和屬性,而這些方法和屬性的泛型類型為 Product。這意味著 IProductModel 會擁有 IBase 定義的所有 CRUD 操作(如 findAll, findOne, create, update, delete),但這些操作針對的是 Product 類型的資料。
簡單來說,IProductModel 介面是 IBase 介面的具體化,專門用於操作 Product 類型的資料。這樣定義能讓代碼更具模組化和可重用性。
※ 實作class:
- 類別定義:
export class ProductModel extends Base<Product> implements IProductModel {}

程式碼解說:
- export class ProductModel:定義並導出一個名為 ProductModel 的類別。
- extends Base<Product>:表示這個類別繼承自 Base<Product> 類別。
- implements IProductModel:表示這個類別實現了 IProductModel 介面。
- tableName 屬性:
tableName = 'products';
程式碼解說:
- 定義資料表的名稱為 products。
schema屬性:
schema = {
id: 'id',
name: 'name',
amount: 'amount',
description: 'description',
pre_order: 'pre_order',
price: 'price'
};
程式碼解說:
- 定義資料表的結構,把資料物件的屬性對應到資料庫的欄位,其中value是資料庫裡面的欄位名稱。
- 建構函數:
constructor({ knexSql, tableName }: { knexSql: Knex; tableName?: string }) {
super({ knexSql, tableName });
}
- 接受一個包含 knexSql 和可選 tableName 的物件作為參數,並呼叫父類別的建構函數(
super),將這些參數傳遞給父類別進行初始化。
5.將資料表的名稱設定為 products:
tableName = "products";
- 靜態方法
createModel:專門用來生成模型(model)
static createModel = ({
knexSql,
tableName,
}: {
knexSql: Knex;
tableName?: string;
}) => {
return new ProductModel({ knexSql, tableName })
}
程式碼解說:
- static:表示這是一個靜態方法,可以直接使用 ProductModel.createModel() 來調用。
- createModel:方法名稱。
- 參數:接受一個包含 knexSql 和可選 tableName 的物件。
- 返回:創建並返回一個 ProductModel 的新實例,也就無需每次手動傳入參數來初始化它。
※ 建立一個管理者的資料夾,負責建立不同的管理 manager:

※ 在manager資料夾中,建立modelManager.ts:

※ 設定資料庫模型的管理:
- 匯入模組:
import { IProductModel, ProductModel } from "@/model/products";
import { Knex } from "knex";
2.使用 models 進行資料庫操作:
export const modelManager = ({knexSql}: {knexSql: Knex}) => {
const productModel = ProductModel.createModel({knexSql});
return {productModel}
}
程式碼解說:
- 從
modelManagerFile文件中導入modelManager,並用配置好的knex物件初始化它,然後可以使用models進行各種資料庫操作。 - ProductModel.createModel:這是一個靜態方法,用於創建 ProductModel 的實例。
- 參數 { knexSql }:將 knexSql 物件傳遞給 createModel 方法,這通常是資料庫連接的配置。
3.定義一個模型相關的環境資訊:
export interface ModelContext {
productModel: IProductModel;
}
程式碼解說:
- 接口定義:接口用於描述物件的結構和類型。
- 導出:
export關鍵字允許這個接口在其他文件中被導入使用。 productModel必須符合IProductModel介面所定義的結構和型別。
※ 調用 modelManager 函數:src --> app.ts

在app.ts新增程式碼:
import { ModelContext, modelManager } from './manager/modelManager';//新增
class App {
private modelCtx: ModelContext;//新增
constructor() {
this.modelCtx = modelManager({ knexSql: this.knexSql });//新增
}
- 匯入模組:
import { ModelContext, modelManager } from './manager/modelManager';
程式碼解說:
目的是從指定的路徑(./manager/modelManager)匯入兩個模組:ModelContext 和 modelManager。
- ModelContext:這是一個介面,用來描述資料模型上下文的結構和類型。
- modelManager:這是一個函數,用來初始化和管理資料模型,並返回一個包含所有資料模型實例的物件。
2. 類別定義:
class App {
private modelCtx: ModelContext;
程式碼解說:
目的是在 App 類別中定義一個私有屬性 modelCtx,它的類型為 ModelContext。主要用途:
- 強制型別:透過將 modelCtx 的類型設定為 ModelContext,可以確保 modelCtx 符合特定的結構和類型要求,這有助於在編譯時期捕捉潛在的錯誤。
- 資料模型管理:modelCtx 用來存儲資料模型的上下文,這樣可以方便地在應用程式的其他地方訪問和操作這些資料模型。
- 封裝性:private 關鍵字表示 modelCtx 屬性只能在 App 類別內部訪問,這增加了程式碼的封裝性和安全性,防止外部未經授權的訪問和修改。
- "Ctx" 是 "Context" 的縮寫。通常用來表示一個上下文或環境,用來存儲和共享關鍵資料和設定。
3.建構函數:
constructor() {
this.modelCtx = modelManager({ knexSql: this.knexSql });
}
程式碼解說:
目的是在 App 類別的建構函數中初始化資料模型上下文。
- 呼叫 modelManager 函數,並將 knexSql 資料庫連接實例傳遞給它。
- modelManager 函數會根據傳入的 knexSql 初始化資料模型,並返回包含這些模型的上下文物件。
- 這個上下文物件被儲存在 this.modelCtx 屬性中。
※ 指定 modelManager 函數的返回值類型:manager --> modelManager.ts
export const modelManager = ({ knexSql }: { knexSql: Knex }): ModelContext => {
}
程式碼解說:
ModelContext 是為了指定 modelManager 函數的返回值類型。




















