也许跟大家不太一样,我是这么用 TypeScript 来写前端的

2023-08-14 11:55:16 +08:00
 Hamm

一、当前一些写前端的骚操作

先罗列一下见到过的一些写法吧:)

1. interface(或 Type)一把梭

掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样:

type User = {
    nickname: string
    avatar?: string
    age: number
}

interface User {
    nickname: string
    avatar?: string
    age: number
}

然后其他方法限制下入参类型,搞定,我掌握了 TypeScript 了,工资不得给我涨 3000 ???

这里说明一下, 我司 不允许 直接使用 interface type 来定义非装饰器参数和配置性参数之外其他 任何数据类型

2. 类型体操整花活

要么把属性整成只读了,要么猪狗类型联合了,要么猪尾巴搞丢了,要么牛真的会吹牛逼了。

类型体操确实玩出了很多花活。 昨天说过了:TypeScript 最好玩的就是类型体操, 也恰好是最不应该出现的东西

3. hook 的无限神话

不知道什么时候开始,hook 越来越流行。 听说不会写 hook 的前端程序员,已经算不上高阶程序员了, 不 use 点啥都展示不出牛逼的水平。

4. axios 拦截大法好

随便搜索一下 axios 的文章, 没有 拦截器 这个关键词的文章都算不上 axios 的高端用法了。

二、 我们一些不太一样的前端骚操作

昨天的文章有提到一些关于在前端使用 装饰器 来实现一些基于配置的需求实现, 今天其实想重点聊一聊如何在前端优雅的面向对象。

写过 JavaSpringBootJPA 等代码的后端程序员应该非常熟悉的一些概念:

于是我们开始把后端思维往前端来一个个的转移:)

1. 抽象和面向对象

与后端的交互数据对象、 请求的 API 接口都给抽象到具体的类上去,于是有了:

abstract class AbstractService{
    // 实现一个抽象属性 让子类们实现
    abstract baseUrl!: string
    
    // 再实现一些通用的 如增删改查之类的网络请求
    // save()
    
    // getDetail()
    
    // deleteById()
    
    // select()
    
    // page()
    
    // disabled()
    
    // ......
}
abstract class AbstractBaseEntity<S extends AbstractService> {
    abstract service!: AbstractService

    // 任何数据都是唯一的 ID
    id!: number
    
    // 再来实现一些数据实体的更新和删除方法
    save(){
        await service.save(this.toJson())
        Notify.success("新增成功")
    }
    
    delete(){
        service.deleteById(this.id)
        Notify.success("删除成功")
    }
    
    async validate(scene: EntityScene):Promise<void>{
        return new Promise((resolve,reject)=>{
            // 多场景的话 可以 Switch
            if(...){
                Notify.error("XXX 校验失败")
                reject();
            }
            resove();
        })
    }
    // ......
}
class UserEntity extends AbstractUserEntity<UserService>{
    service = new UserService()
    
    nickname!: string
    age!: number
    avatar?: string
    
    // 用户是否成年人
    isAdult(): boolean{
        return this.age >= 18
    }
    
    async validate(scene: EntityScene): Promise<void> {
        return new Promise((resove,reject)=>{
            if(!this.isAdult()){
                Notify.error("用户未成年, 请确认年龄")
                reject();
            }
            await super.validate(scene)
        })
    }
    
}
<template>
    <el-input v-model="user.nickname"/>
    <el-button @click="onUserSave()">创建用户</el-button>
</template>
<script setup lang="ts">
const user = ref(new UserEntity())
async function onUserSave(){
    await user.validate(EntityScene.SAVE);
    await user.save()
}
</script>

2. 装饰器/切面/反射

装饰器部分的话,昨天的文章有提到一些了,今天主要所说反射和切面部分。

TypeScript 中, 其实装饰器本身就可以理解为一个切面了, 这里与 Java 中还是有很多不同的, 但概念和思维上是基本一致的。

反射 ReflectTypeScript 中比较坑的一个存在, 目前主要是依赖 reflect-metadata 这个第三方库来实现, 将一些元数据存储到 metadata 中, 在需要使用的时候通过反射的方式来获取。 可以参考这篇文章:TypeScript 中的元数据以及 reflect-metadata 实现原理分析

在实际使用中, 我们早前用的是 class-transformer 这个库, 之前我对这个库的评价应该是非常高的: “如果没有 class-transformer 这个库,TypeScript 狗都不写。”

确实很棒的一个库,但是在后来,我们写了个通用的内部框架, 为了适配 微信小程序端 以及 uniapp 端, 再加上有一些特殊的业务功能以及 class-transfromer 的写法和命名方式我个人不太喜欢的种种原因, 我们放弃了这个库, 但我们仿照了它的思想重新实现了一个内部使用的库,做了一些功能的阉割和新特性的添加。

核心功能的一些说明

3. 再次强调面向对象

为了整个前端项目的工程化、结构化、高度抽象化,这里不得不再次强调面向对象的设计:)

4. 严格但又有趣的 tsdoc

我们先来看一些注释的截图吧:)

一些详细的注释、弃用的方法、选填的参数、传入参数后可能影响或依赖的其他参数,在注释里写好玩的 emoji或者图片,甚至是 直接在注释里写调用 demo, 让调用方可以很轻松愉快的对接调用, 玩归玩, 确实对整体项目的质量有很大的帮助。

三、 写在最后

中午跟同事吃饭聊了聊现在国内大前端的一个状态, 当时聊到一个关键词 舒适区, 还有前端整个技术栈过于灵活的一些优缺点, 几个大老爷们都发出了一些感慨, 如果前端能够更标准化一些, 像 Java 一样, 说不定前端还能上升几个高度。

That's all, 今天水的文章到此结束。

代码已经在 Github 开源: https://github.com/HammCn/AirPower4T

1949 次点击
所在节点    前端开发
7 条回复
manasheep
2023-08-14 11:59:18 +08:00
直接用 Blazor ,C#写前端
walpurgis
2023-08-14 12:18:16 +08:00
angular 欢迎你
这些操作在 OOP 代码中很常见,不算骚,没有流行开的原因只是因为 React/Vue 都搞 hooks ,推荐代码风格都往 FP 走了,这套东西没法用
Hamm
2023-08-14 12:37:14 +08:00
@walpurgis 就是从 ag 中抄过来的,公司现在用 Vue ,抄过来塞进去了。
Pencillll
2023-08-14 13:09:29 +08:00
写文章时至少放一些有效的代码吧……比如这里 `abstract baseUrl!: string` 会报错,还有在非 async 函数里用 await 这种错误
实体类和注解这个确实很像后端了,前端没有 orm 的加持要搞这种感觉会带来不少麻烦(序列化、组合之类),而且 前端 oop 也有些坑,比如父类是个同步方法,子类想要改成异步的就头疼了

> 我司 不允许 直接使用 interface type 来定义非装饰器参数和配置性参数之外其他 任何数据类型。
这个我比较感兴趣,能展开讲讲吗,为啥不允许用 interface type
Hamm
2023-08-14 14:43:36 +08:00
@Pencillll 你说得对,文中的代码都是在文章编辑器中撸出来的,没有在代码编辑器中验证。
目前生产上的代码和开发中的代码,写着目前还没有很多头疼的事情发生。

不允许的原因,可能是根本的原因就是下面这张图
https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e9097f4a344408d8b3fa0191f64e121~tplv-k3u1fbpfcp-watermark.image

前端直接使用 interface 和 type 声明,等于把一些瞎搞的属性名称硬编码了,无法使用装饰器这些方案来方便的转换。
之前就写了很多转换方法,但已经为时已晚,得自己去各种出口入口的地方加上转换方法来应对后端的瞎搞。
Hamm
2023-08-14 14:45:19 +08:00
@Pencillll 如果对接的 api 接口改动不频繁,或者是实现很标准,我觉得应该不至于现在这个鬼样子。
相反,因为后端的不规范,现在才出了这套方案。
当然,比起 orm 来说,差得东西还很多,但对于这种后端瞎搞的场景,已经基本支持了。
Pencillll
2023-08-14 15:32:29 +08:00
@Hamm 理解,我有时候也会遇到这种困境,用注解进行转换看起来挺不错的,以后尝试一下

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/965078

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX