用代碼聊聊我們跟目前主流前端編程不一樣的地方
作者:Hamm
https://juejin.cn/post/7272372568623710264
寫在前面
也許跟大部分的前端開發(fā)者不同,我們使用了 Vue3 和 TypeScript, 但我們也許是又回到了 老古董 的編程方式中, 也許是習(xí)慣了 面向?qū)ο?OOP) ,又或者是跑了一圈, 相比現(xiàn)在的 JavaScript、Python、PHP、Go、C# 等,依然還是鐘愛 Java, 我們不否認(rèn)現(xiàn)在的主流在 函數(shù)式編程(FP) 上, 也越來越少的開發(fā)者喜歡在前端也這么抽象的使用 面向?qū)ο缶幊?/strong>, 這篇文章只表達(dá)我們自己的喜好, 不強(qiáng)加任何觀點(diǎn)。
很多人問為什么我們?cè)赩ue上這么搞,這里總結(jié)幾個(gè)原因:招聘的成本、重慶這個(gè)N線互聯(lián)網(wǎng)城市的現(xiàn)狀、公司的決策 等等。
我們?nèi)绾问褂妹嫦驅(qū)ο?/h1>
我們?cè)谇岸艘惨肓舜罅康拿嫦驅(qū)ο蟮挠白樱艘恍?數(shù)據(jù)交互實(shí)體、 相似API的封裝 等:)
什么 Service / Entity 是不是像極了 Java 寫后端時(shí)候的樣子?
-
數(shù)據(jù)實(shí)體的封裝
數(shù)據(jù)實(shí)體作為前后端交互的主要數(shù)據(jù)對(duì)象,承載著前端和后端的數(shù)據(jù)交互、組件之間的數(shù)據(jù)交互等重要步驟。
如后端編程一樣,我們將先按照指定的規(guī)范對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行約束,這里我們沒有使用目前大家都喜歡的一些方式進(jìn)行封裝:如 interface, type 等,而是使用了 class 來進(jìn)行封裝。這里我們只談?wù)勥@么封裝的目的:
-
固定字段規(guī)范的數(shù)據(jù)實(shí)體
所有的數(shù)據(jù)庫交互實(shí)體,都會(huì)包含ID等字段,所以我們先定義個(gè) BaseEntity 來進(jìn)行數(shù)據(jù)約束:
class BaseEntity {
id!: number
createTime!: number
}
class UserEntity extends BaseEntity {
// 無需再編寫 基類中 聲明的公共字段
name!: string
}
如上的方式,我們可以少寫很多公共的字段,而在一些公共的組件中需要傳遞數(shù)據(jù)時(shí)候, 我們可以限制類型為 BaseEntity 的子類即可,這樣組件內(nèi)就能取出傳入數(shù)據(jù)的公共字段,比如可以直接取出 ID, 也可以自動(dòng)的對(duì)創(chuàng)建時(shí)間進(jìn)行友好的格式化顯示等。
當(dāng)然,這里也可以使用 interface 來定義,同樣的實(shí)現(xiàn)繼承來避免寫相同的字段, 我們?yōu)槭裁匆廊皇褂胏lass, 請(qǐng)繼續(xù)閱讀
-
不固定規(guī)范的數(shù)據(jù)實(shí)體
難免碰到一些后端開發(fā)者不太喜歡使用 相同的公共字段,就像下面的一些數(shù)據(jù)交互方式:
{
"user_id": 123,
"user_name": "admin"
}
json復(fù)制代碼{
"role_id": 122,
"role_name": "管理員"
}
如上習(xí)慣的數(shù)據(jù)交互方式,就不太適合使用 interface 繼承來處理了, 但是配合裝飾器,我們依然使用 class 的繼承來實(shí)現(xiàn)這個(gè)需求:
我們還是聲明了 BaseEntity 和 UserEntity,但是我們加上了一些裝飾器, 來配置一些關(guān)于數(shù)據(jù)轉(zhuǎn)換方面的信息:)
class BaseEntity {
id!: number
createTime!: number
}
// 表示所有的用戶字段 都需要用 user_ 開頭
@Prefix("user_")
class UserEntity extends BaseEntity {
name!: string
idcard!: string
}
// 表示所有的角色字段 都需要用 role_ 開頭
@Prefix("role_")
class UserEntity extends BaseEntity {
name!: string
}
這樣,我們就完成了一些配置,但可能這還不夠,比如某一些字段 確實(shí)沒有前綴,或者我們根本不想使用后端不規(guī)范的命名,比如后端給電話起了個(gè)簡(jiǎn)寫的 pnum 當(dāng)手機(jī)號(hào)?
@Prefix("user_")
class UserEntity extends BaseEntity {
name!: string
// 身份證號(hào)這個(gè)字段不需要前綴 就是 idcard
@IgnorePrefix()
idcard!: string
@IgnorePrefix()
@Alias("pnum") //使用別名將后端的屬性名稱替代掉
phone!: string
}
好的,于是我們開心的完成了關(guān)于字段的名稱問題的一些配置, 但我們還需要一些處理的方法。于是我們聲明一個(gè) BaseModel 的類作為超類,讓 BaseEntity 去繼承它,這樣所有的實(shí)體都擁有了這些轉(zhuǎn)換方法,如果你是個(gè)不帶 ID 的普通數(shù)據(jù)模型也可以直接繼承 BaseModel 。
// 讀取裝飾器的一些配置,提供一些轉(zhuǎn)換的方法
class BaseModel {
// 具體的轉(zhuǎn)換方法實(shí)現(xiàn)可以查看文末提供的開源項(xiàng)目代碼
toJson() {
// 當(dāng)前對(duì)象轉(zhuǎn)為普通的JSON對(duì)象的方法
}
fromJson(json: Record<string, any>) {
// 將后端給過來的JSON轉(zhuǎn)為我們需要的類對(duì)象
}
}
class BaseEntity extends BaseModel {
// 不再重復(fù)寫了
}
那么接下來我們就可以完成一些數(shù)據(jù)轉(zhuǎn)換,然后實(shí)現(xiàn)不管后端的字段名如何,都能輕松的應(yīng)對(duì):
const json = {} // 從后端拿回來的JSON
const user = new UserEntity().fromJson(json) // 當(dāng)然,還可以直接提供一些靜態(tài)方法:
// 如 const user = UserEntity.fromJson(json) 、 const userList = UserEntity.fromJsonArray(jsonArray)
console.log(user.id) // 直接取我們自己聲明的 id 而不是跟著后端走的 user_id
怎么樣,是不是很開心,我管你怎么改,我字段名可以不被你牽著鼻子走, 即使后端接口把 user_id 改成了 userid ,我也不需要在我的代碼中一個(gè)個(gè)的搜索跟著改:我只需要將 UserEntity 配置的裝飾器改為 @Prefix("user"),如果對(duì)方需要改成 userId, 我還可以再寫個(gè)裝飾器, @Hump(),然后在 BaseModel 中轉(zhuǎn)換的時(shí)候判斷是否標(biāo)記這個(gè)駝峰裝飾器, 來選擇是否需要將字段名自動(dòng)駝峰處理。
:) 是不是很有意思?
-
更變態(tài)的數(shù)據(jù)轉(zhuǎn)換需求
如上所說,我們可以自動(dòng)來處理一些字段名稱的處理,我們也能來做一些字段屬性類型的處理:
-
布爾、數(shù)字、字符串的轉(zhuǎn)換
-
如果沒有值,需要給默認(rèn)值
-
如果是數(shù)組或者掛載的其他對(duì)象,如用戶身上帶了角色
-
是枚舉值,需要枚舉字典等
-
等等等等...
-
相似API的封裝
在日常開發(fā)中,我們通常會(huì)遇到相同結(jié)構(gòu)和請(qǐng)求方式的接口,有相同的接口命名方式,相同的參數(shù)和返回值等:)
一般來說,接口的請(qǐng)求地址可能不太一樣,我們可以聲明一個(gè)抽象類,要求子類中自行傳入這個(gè)地址:
于是我們嘗試使用一個(gè) AbstractBaseService 類來進(jìn)行一些基于面向?qū)ο罄^承的處理:)
abstract class AbstractBaseService {
abstract apiUrl: string
add() {
request(this.apiUrl + "/add")
}
delete() {
} // 刪除
// 等等等等
}
那么我們其他的子類就可以直接繼承這個(gè) Service 同時(shí)實(shí)現(xiàn)一下 apiUrl 這個(gè)屬性 (Java: 直接抽象屬性???)
class UserService extends AbstractBaseService {
apiUrl = "user"
}
UserService 就擁有了所有父類中的增刪改查方法,是不是很爽?當(dāng)然,這里再加上泛型,把數(shù)據(jù)類型也約束上:
abstract class AbstractBaseService<E extends BaseEntity> {
abstract apiUrl: string
add(entity: E) {
request(this.apiUrl + "/add", entity.toJson())
}
delete(entity: E) {
request(this.apiUrl + "/delete", entity.toJson())
}
}
// 子類傳入對(duì)應(yīng)的泛型約束
class UserService extends AbstractBaseService<UserEntity> {
apiUrl = "user"
}
那么,這里的封裝不僅實(shí)現(xiàn)了父類方法的復(fù)用,連接口請(qǐng)求把類型都卡死了:
const user = new UserEntity()
user.id = 1
new UserService().add(user) // 正常不報(bào)錯(cuò)
const role = new RoleEntity()
role.id = 1
new UserService().add(role) // 滾犢子 類型不匹配
這樣就完成了公共部分的封裝,而且還加上了一些類型約束,如果再把這些通用的操作以及動(dòng)態(tài)綁定的數(shù)據(jù)統(tǒng)一抽到一個(gè) hook 中, 那豈不是美滋滋?像這樣:)
// ClassConstructor是我們封裝的包裝類
export function useAdd<E extends BaseEntity>(ServiceClass: ClassConstructor<E>, EntityClass: ClassConstructor<E>) {
const formData = ref(new EntityClass())
const service = ref(new ServiceClass())
const isLoading = ref(false)
const onAdd = () => {
isLoading.value = true
try{
service.add(formData.value);
}catch (e){
alert("添加失敗")
}finally {
isLoading.value = false
}
}
return {
formData, onAdd
}
}
調(diào)用的視圖可就更簡(jiǎn)單了:)
<template>
<form>
<input type="text" v-model="formData.name"/>
<button @click="onAdd"></button>
</form>
</template>
<script setup lang="ts">
const {formData,onAdd} = useAdd(UserService,UserEntity)
</script>
這么寫起來,是不是爽了很多呢?
本文總結(jié)
本文代碼可能沒有經(jīng)過驗(yàn)證,都是寫文章的時(shí)候順帶在 markdown 中直接手?jǐn)]的,如有錯(cuò)誤請(qǐng)?jiān)u論區(qū)指出。
這里又回到了之前文章說的話題上了,我們也不僅僅是只使用了面向?qū)ο螅覀円彩褂昧撕瘮?shù)式的一些hook。
沒有必要在二者之間做出選擇,成年人,為什么不能都要呢?
前端大學(xué) 公眾號(hào) 祝 您:2023 年暴富!萬事如意!
分享前端干貨,點(diǎn)贊就是最大的支持,
比心??
