普通视图

发现新文章,点击刷新页面。
昨天以前首页

上传选择、拖拽、剪裁功能

2025年1月24日 17:29

前言

平时我们上传文件、拖拽图片,基本上都是用 antd、elementUI 等库,非常方便,但这不代表我们不需要了解这些东西,实际上图片这些知识点也不是很多,了解总比不知道强

假设我们碰到一个场景,我们的页面效果很简单,就展示一些好看的图片,还支持上传,不需要使用那么庞大的库,客户就要快,此时我们没必要引用那么大的 antd、elementUI 等库,我们直接写一个又快效率又高,那么多号,引入三方库后,还要担心打包文件偏大客户嫌弃问题

下面就介绍上传按钮的自定义、拖拽、剪裁功能

上传选择按钮效果调整

我们先写一个 upload 的样式 css,实际可以使用图片,更适合我们的场景,这里就简写了

//这个样式作为 upload 的样式
.upload {
  width: 200px;
  height: 200px;
  box-shadow: 1px 1px 10 10 #333;
  background: linear-gradient(red, green, blue);
  cursor: pointer;
}

看起来长这样,实际可以用图片代替

image.png

我们将 input 嵌入到 div 中,div 作为样式,input 填充父视图,并隐藏,这样就可以实现点击上传了

<div className="upload">
    <input
        style={{
            width: "100%",
            height: "100%",
            opacity: 0,
        }}
        type="file"
        onChange={(e) => e.target.files)}
    />
</div>

实际上传,还可以通过下面这样,隐藏 input 和 他的时间,通过点击指定的节点,然后响应 input 的 dom(input 的 dom 可以再其他任意地方,也可以点击的时候新创建一个,选择完毕删除),这样也同样实现上传按钮效果的自定义

<div
    className="upload"
    onClick={() => {
        const dom = document.querySelector("input");
        if (!dom) return;
        dom.click();
    }}
>
    <input
        type="file"
        style={{ opacity: 0, pointerEvents: "none" }}
        onChange={(e) => onInputChanged(e.target.files)}
    />
</div>

onChange 回调

onChange 回调中 e.target 就是我们 input 组件,其中的 files 就是我们选择的图片 file 数组,可以使用 file 上传,如果是文件夹,则 file 的 type 则为空,可以通过该参数过滤、错误提示等

const onInputChanged = (fileList: FileList | null) => {
    if (!fileList) return;
    const files = Array.from(fileList).filter((e) => e.type);
    //过滤掉 type 为空的,那不是文件,可能是文件夹,想要文件夹需要一些额外的三方库支持
    console.log(fileList, files);
};

上传拖拽实现

图片拖拽实际上就是走的 onDrop 方法,通过这个方法,可以获取到拖拽进来的文件集合,.dataTransfer.files 可以获取到拖拽进来的文件

但是有一个问题,就是浏览器有一个默认行为,一些浏览器中,拖拽进来的图片默认会开启一个新的窗口打开图片,我们可以通过 preventDefault 方法阻止用户的默认行为,onDragEnteronDragOveronDragLeaveonDrop我们均阻止一下即可,否则仍然可能另起窗口打开图片

psonDragEnter拖拽进入时回调、 onDragOver拖拽进入持续触发、onDragLeave拖拽离开元素触发、onDrop在元素中松手触发,实际上以前我也写过一个 threejs 的浏览器拖拽到 3d 场景的组件库功能,实际上就用到了此类方法衔接拆分

<div
    className="upload"
    onDragEnter={(e) => {
        //拖拽进入出发
        // console.log("onDrag", e);
        e.preventDefault();
    }}
    onDragOver={(e) => {
        //拖拽进入持续触发
        // console.log("onDragOver", e);
        e.preventDefault();
    }}
    onDragLeave={(e) => {
        //拖拽离开元素触发
        // console.log("onDragLeave", e);
        e.preventDefault();
    }}
    onDrop={(e) => {
        //拖拽到div里面
        //e.dataTransfer.files 保存的就是拖拽进来转化的file信息
        console.log("onDrop", e.dataTransfer.files);
        e.preventDefault();
        //拿到拖拽进来的图片,直接回调即可
        onInputChanged(e.dataTransfer.files);
    }}
    onClick={() => {
        const dom = document.querySelector("input");
        if (!dom) return;
        dom.click();
    }}
>
    ...input
</div>

图片剪裁功能

剪裁之前,我们先把拿到的图片显示

const file = files[0];
//读取file
const reader = new FileReader();
reader.onload = (e) => {
    const img = document.querySelector("img1") as HTMLImageElement;;
    //直接给指定节点赋值图片
    img.src = e.target.result
};
reader.readAsDataURL(file);

上面完成显示后,剪裁的话,实际上在图片上放置一个可以移动缩放的框就行了,用于确定裁剪区域,还需要写一些相关事件(懒得写),这里就不多介绍了

确定剪裁区域后(x, y, width, height),下面介绍,将剪裁后的图片绘制问题

图片剪裁实际上就用到了 canvas 功能,功能也很简单,只需要使用 canvas 上下文 drawImage 即可

const img = document.querySelector("img1") as HTMLImageElement;
//我们希望图片剪裁到 120 x 120 分辨率
const canvas = document.createElement('canvas')
canvas.width = 120
canva.height = 120
const ctx = canvas.getContext('2d')
//直接剪裁即可,只需要设置原图剪裁区域,和目标绘制区域即可
ctx?.drawImage(img, x, y, width, height, 0, 0, 120, 120)

ps:如果出现canvas一些区域模糊,实际上是一些视网膜屏(多倍像素屏),可以将原图也按照指定比例(scale、radio)缩放绘制,就不会出现问题了(一般移动设备会出现此类问题)

下面介绍写一下 drawImage,其可以理解为,将原图片,将其按照一定裁剪方式,放到另一个空白的内容上,超出部分剪裁,其一共声明了三个方法

//将原图整个放到另一个空白内容上(cancas),原图距离空白纸左侧 dx,上侧 dy距离,绘制图片,因此会有留白(取决于canvas背景),由于没有设置dw、dh,不会缩放,超出部分不会被绘制
drawImage(image: CanvasImageSource, dx: number, dy: number): void;

//将原图整个放到另一个空白内容上(cancas),设置设置相对空白纸的dx,dy,并且设定目标宽度dw、高度dh(原图会被压缩绘制到canvan指定区域)
drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;

//将原图整个放到另一个空白内容上(cancas),发现多了四个参数sx、sy、sw、sh
//sx、sy、sw、sh相当于对原图区域进行裁剪,将裁剪后的内容,作为图片源绘制到空白纸(canvas)上
//设置设置相对空白内容上(cancas)的dx,dy,并且设定目标宽度、高度(原图会被压缩绘制到canvan指定区域)
drawImage(image: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;

具体参数更细致的如下所示,可以看参考自 MDN,实际上这个地方比较难以理解的主要是下面几个参数

  • image

  • 绘制到上下文的元素。允许任何的画布图像源,例如:HTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmapOffscreenCanvas 或 VideoFrame

  • sx 可选

    • 需要绘制到目标上下文中的,源 image 的子矩形(裁剪)的左上角 X 轴坐标。可以使用 3 参数或 5 参数语法来省略这个参数。
  • sy 可选

    • 需要绘制到目标上下文中的,源 image 的子矩形(裁剪)的左上角 Y 轴坐标。可以使用 3 参数或 5 参数语法来省略这个参数。
  • sWidth 可选

    • 需要绘制到目标上下文中的,源 image 的子矩形(裁剪)的宽度。如果不指定,整个矩形(裁剪)从坐标的 sx 和 sy 开始,到 image 的右下角结束。可以使用 3 参数或 5 参数语法来省略这个参数。使用负值将翻转这个图像。
  • sHeight 可选

    • 需要绘制到目标上下文中的,image的矩形(裁剪)选择框的高度。可以使用 3 参数或 5 参数语法来省略这个参数。使用负值将翻转这个图像。
  • dx

    • 源 image 的左上角在目标画布上 X 轴坐标。
  • dy

    • 源 image 的左上角在目标画布上 Y 轴坐标。
  • dWidth

    • image 在目标画布上绘制的宽度。允许对绘制的图像进行缩放。如果不指定,在绘制时 image 宽度不会缩放。注意,这个参数不包含在 3 参数语法中。
  • dHeight

    • image 在目标画布上绘制的高度。允许对绘制的图像进行缩放。如果不指定,在绘制时 image 高度不会缩放。注意,这个参数不包含在 3 参数语法中。

使用react写一个瀑布流

2025年1月24日 13:08

前言

有时候我们展示一些图片布局的时候,为了能够让用户一个页面能够展示更多的内容,并且还要展示较全的图片内容,但是图片比例是不一样的,有横屏竖屏,并且同时竖屏百分比也不一样,为了能够展示的更优秀,因此有了瀑布流效果

先看一个测试案例效果,我们实现了一个类似的效果,当然要是加上图片就更好看了,这里就不加了

image.png

瀑布流

想要实现瀑布流,至少要知道瀑布流的几个特征,即需要做的一些事情

瀑布流要做的那些事

  • 需要了解布局宽度、单个item的宽度,或者最大宽度,能够保证一瓶放置多少列,也可以写死列数,然后根据列数自动填充(这里就先不支持写死列数了,这个逻辑更简单)
  • 可以通过,最大宽度计算出,最多可以计算出大致多少列,保持列数,适当缩小item宽度即可计算出实际的 item 宽度,当然要参考间隙 gap
  • 计算出一共有多少列后,可以在屏幕上放置指定数量的,竖向排版的布局,提前计算好数据应该在哪一列,然后直接一次渲染所有列即可

实现步骤

  1. 为了更通用,我们编写一个 WaterFallModel,用于保存瀑布流的基础信息,包括总宽度totalWidth(布局不一定能用到,计算用),item 宽度 itemWidth, 间隙 gap,最大item宽度 maxItemWidth(通过该参数配合总宽度可以计算出比较合理的 item 宽度),列数 column 是用于保存计算的 column 结果,每列布局的高度 columnsHeight 用于计算下一个元素应该布局到哪一列
  2. 编一个一个 generateColumns 用于计算出实际的 columns,这个主要是针对于 maxItemWidth,顺便计算出比较合适的 itemWidth,保存下来
  3. 编写一个 waterFallAtIndex 用于计算出下一个元素应该放置在哪一列,再布局的时候,能够保证新元素总是插入到最矮的那一列
  4. 计算好之后,只需要横向同时部署多列纵向布局,将生成的数据填充渲染即可实现瀑布流效果

实现 WaterFallModel 计算模块

我们直线先提取一个用于计算的 WaterFallModel,里面保存有瀑布流 UI布局 的基础信息,还有计算逻辑

//这里我们认为是没有padding,需要外部加上margin或者套一层即可,暂时忽略
export default class WaterFallModel {
    totalWidth: number;
    itemWidth: number = 0;
    maxItemWidth: number;
    gap: number;
    columns: number;
    columnsHeight: number[];

    //计算规则
    //整体布局总宽度是必须要有的
    //如果设置了单个itemWidth宽度,则会根据gap(默认为10),计算页面布局,放置不开则右侧留白
    //如果没有设置itemWith,这里直接建议同时设置 maxItemWidth,则会有限根据最大 ItemWidth 伸开,如果最后一个超出整体宽度,则适当缩小,保证最后一个贴边,不留白
    //有最大这里不要最小了,简化一点逻辑,另外设置最大就足够了(对于图片体验稍好),最小没啥必要
    //gap间距,默认设置为 10,可以设置,间距固定显示
    constructor(totalWidth: number) {
        this.totalWidth = totalWidth;
        this.itemWidth = totalWidth; //设置跟外部一样,默认一列,需要自己手动设置,计算后,此值会有所改变
        this.maxItemWidth = 0;
        this.gap = 10;
        this.columns = 0;
        this.columnsHeight = [];
    }

    generateColumns() {
        if (!this.totalWidth) {
            throw new Error("列数为0");
        }
        if (
            (!this.itemWidth || this.itemWidth <= 0) &&
            !(this.maxItemWidth || this.maxItemWidth <= 0)
        ) {
            throw new Error("列数为0");
        }
        let columns = 0;
        if (this.maxItemWidth) {
            columns =
                Math.ceil((this.totalWidth + this.gap) /
                (this.maxItemWidth + this.gap));
            //根据计算出的列数,计算出实际的 ItemWidth,结果取地板,毕竟像素不存在小数,整体可以少个一个半个像素的,但是越界可能会导致渲染出现问题
            this.itemWidth = Math.floor(
                (this.totalWidth + this.gap) / (columns)
            ) - this.gap;
            console.log("maxItemWidth", columns);
        } else if (this.itemWidth) {
            //查找一共有几列中间有 this.gap,为了最后一个贴边,总体要加上一个gap,取地板即可
            columns =
                Math.floor((this.totalWidth + this.gap) /
                (this.itemWidth + this.gap));
            console.log("itemWidth", columns);
        }
        console.log('columns', columns, 'totalwidth', this.totalWidth, 'itemwidth', this.itemWidth)
        this.columns = columns;
        this.columnsHeight = new Array(this.columns).fill(0);
    }

    //传入宽高比 radio = width / height,给出应当插入的列
    waterFallAtIndex(radio: number) {
        if (!this.columns || this.columns === 1) return 0;
        const height = this.itemWidth / radio;
        let minIdx = 0;
        this.columnsHeight.reduce((pre, cur, idx) => {
            if (cur < pre) {
                minIdx = idx;
                return cur;
            }
            return pre;
        }, this.columnsHeight[0]);
        this.columnsHeight[minIdx] += height;
        return minIdx;
    }
}

加入 ui 实现一个简易的瀑布流

写一个简单的 react 测试案例

const waterfall = useRef<WaterFallModel | null>();
const [waterDataSource, setWaterDataSource] = useState<any[][]>([]);

const generateData = async () => {
    const dom = document.querySelector("#alalala");
    if (!dom) return;
    const rect = dom.getBoundingClientRect();
    const water = (waterfall.current! = new WaterFallModel(rect.width));
    water.maxItemWidth = 200;
    water.generateColumns();
    const datasource: any[][] = new Array(water.columns);
    for (let idx = 0; idx < 100; idx++) {
        //生成100个随机数据,radio随机生成
        sleep(3);
        let radio = (new Date().getTime() % 10) / 8;
        if (radio < 0.5) {
            radio += 0.5;
        }
        const index = water.waterFallAtIndex(radio);
        console.log(index);
        if (!datasource[index]) {
            datasource[index] = [];
        }
        datasource[index].push({
            radio,
            content: `我是第${idx + 1}条数据`,
        });
    }
    console.log("datasource", datasource);
    setWaterDataSource(datasource);
};

const sleep = (interval: number) => {
    return new Promise((resolve) => {
        setTimeout(resolve, interval);
    });
};

<div
    id="alalala"
    style={{
        width: "100%",
        height: "100%",
        display: "flex",
    }}
>
    {waterDataSource.map((list, index) => (
        <div
            key={index}
            style={{
                width: waterfall.current?.itemWidth,
                marginLeft: index > 0 ? waterfall.current?.gap : 0,
                display: "flex",
                flexDirection: "column",
            }}
        >
            {list.map((item, idx) => (
                <div
                    key={idx}
                    style={{
                        width: "100%",
                        marginTop:
                            idx > 0 ? waterfall.current?.gap : 0,
                        height:
                            waterfall.current!.itemWidth /
                            item.radio,
                        display: "flex",
                        justifyContent: "center",
                        alignItems: "center",
                        backgroundColor: "red",
                        color: "white",
                    }}
                >
                    {item.content}
                </div>
            ))}
        </div>
    ))}
</div>

最后

瀑布流就做到这里了,就这点逻辑也花费一个小时时间,可以根据需要,将 瀑布流的 UI + 计算数据model,组合成一个通用组件,应用到项目里,这里就不多做了,仅仅作为一个思路,毕竟封装成一个组件也不难,就是稍微多花点时间

最后,祝大家新年快乐,发发发 🤣🤣🤣🤣🤣🤣

小程序上传、查看附件(图片)

2025年1月24日 13:08

前言

微信小程序开发中一般会用到上传功能,而上传又分为从相册选择上传从微信会话中选择上传,相册上传比较常见,会话则是微信小程序特有的

因为微信的跨平台性和限制,通用的选择附件功能最好是直接从微信会话中选择比较好,其很通用,支持更多附件类型

上传附件

上传附件,一般是从相册选择、从微信会话选择,两个是分开的,一般涉及到非图片的都是使用从微信会话选择,也有两者都使用的

下面就以 wx.chooseMessageFile 为例,选择上传附件功能

const chooseUploadFile = ({
    count,
    size
} = {}) => {
    if (!count) count = 1
    if (!size) size = 2 * 1024 * 1024
    return new Promise((resolve, reject) => {
        wx.chooseMessageFile({
            count,
            success: (res) => {
                //这里让选一个
                const file = res.tempFiles[0]
                if (file.size > size) {
                    wx.showToast({
                        title: '大小超出限制',
                        icon: 'none'
                    })
                    reject(res)
                    return
                }
                //wx.uploadFile请求自己封装一下即可
                uploadFile(file).then((res) => {
                    resolve(res)
                }).then(err => {
                    reject(err)
                })
            },
            fail: (err) => {
                reject(err)
            }
        })
    })
}

//上传自己看着封装,有直接上传的,有预上传 + 上传
wx.uploadFile({
    url: ...,
    filePath: file.path,
    name: 'file',
    formData: {...},
    success: function (res) {},
    fail: function (error) {}
})

查看附件

这个跟普通移动端开发一不一样,可能移动端一个 url,无论是图片、网页、office都能显示,而小程序 webview 限制不较大就不行了,仅仅支持图片,仅仅支持图片的话还不如直接使用图片查看

因此查看附件将图片、office 等类型分离

  • 通过校验扩展名类型来实现,因此上传时扩展名的要求比较重要了,当然也可以直接使用 mineType,这个就更细了,有些可能都不保存或者不正确哈,最好扩展名,扩展名差的太远就不管了
  • 图片类型,一般都是 jpg|png|bmp|gif|svg|webp|svg 等,支持一些常用的,使用 wx.previewImage 可以打开(只要扩展名都是图片类型,就算是 jpg 改成 png 也没事,实际是根据图片二进制的前面字节识别)
  • 视频类型,一般都是mp4、avi、wmv、mkv、flv等,使用 wx.previewMedia 可以访问,同时也支持图片访问
  • 常见其他文件类型(office),常见的doc、docx、xls、xlsx、ppt、pptx、pdf、wps、txt等,可以先使用 wx.downloadFile 下载到本地,然后使用 wx.openDocument 打开
if (/jpe{0,1}g|png|bmp|gif|svg|webp/.test(ext)) {
    //假设只支持这些图片类型
    wx.previewImage({
      urls: [url],
      success: (res) => {
        console.log(res)
      },
      fail: (err) => {
        console.log(err)
      }
    })
    return
} else if (/^(docx?|xlsx?|pptx?|pdf|wps|txt)$/.test(ext)) {
    //假设只支持这些 office 类型
    wx.showLoading({
      title: '加载中...'
    })
    //先使用 downloadFile 下载url 对应内容,然后使用 openDocument 打开
    wx.downloadFile({
      url: url,
      success: (res) => {
        wx.hideLoading()
        console.log(res)
        wx.openDocument({
          filePath: res.tempFilePath,
          success: (res) => {
            console.log(res)
          }, fail(err) {
            console.log(err)
            wx.showToast({
              title: '打开失败',
              icon: 'error'
            })
          }
        })
      }, fail: (err) => {
        wx.hideLoading()
        console.log(err)
        x.showToast({
          title: '下载失败',
          icon: 'error'
        })
      }
    })
} else {
    wx.showToast({
      title: '暂不支持打开此文件类型',
      icon: 'error'
    })
  }
}

clipboard 剪贴板

2025年1月24日 13:06

前言

平时我们也会时不时用到剪贴板的功能,使用上很简单,尤其是文本操作的,但是有时候也会阻止复制、粘贴文件的操作,并且还在一些环境下存在无法使用的问题,这里面就简单介绍一下

clipboard 剪贴板

直接写入到 clipboard

一般是调用按钮写入固定内容

//写入粘贴板
navigator.clipboard.writeText("我是联盟第一辅助")

//实际上 button 也有一个 data-clipboard-target 属性
//只需要传入指定节点的 id选择器名称,就可以复制
<button data-clipboard-target="#demoInput">点我复制</button>

读取 clipboard 文本

//读取粘贴板文本,可能会失败,那就是没授权
navigator.clipboard.readText().then((text) => {
    console.log(text);
});

监听复制 copy

我们可以直接监听 copy 事件,并阻止,也可以更新粘贴内容

document.addEventListener("copy", (e) => {
    //阻止用户在当前页面复制
    e.preventDefault();
    //当然除了阻止也可以更新剪贴板内容
    navigator.clipboard.writeText("版权归我虽有,违法必究")
});

粘贴 paste

有时我们会监听粘贴时间,遇到剪裁粘贴过来的文件,我们可以直接就拿到文件信息上传,返回 url 到编辑器了,编辑器显示本地或者 url 的图片内容,也可以直接粘贴文本,混砸的这边就不多说了

粘贴普通文本

这类直接粘贴文本到指定 dom

document.addEventListener("paste", (e) => {
    //e.clipboard.files 这就是粘贴板上的文件信息,文件可以直接上传也可以显示
    const dom = document.querySelector("#paste-div");
    if (!dom) return 
    navigator.clipboard.readText().then((text) => {
        dom.innerHTML = text;
    });
});

复制文件,读取文件

我们可以直接获取文件内容,然后创建图片节点显示到页面上,也可以直接上传,都是没问题的

document.addEventListener("paste", (e) => {
    const files = e.clipboardData?.files;
    if (!files || files.length < 1) return;
    //我们假设只取第一张图片
    //读取后,我们直接创建一张图片到页面上吧
    const reader = new FileReader();
    reader.onload = (e) => {
        const img = document.createElement("img");
        img.src =  e.target.result; //result就是我们的图片内容
        const root = document.querySelector("#root");
        root.appendChild(img);
    };
    reader.readAsDataURL(files[0]);
});

clipboard 兼容性与 execCommand

clipboard 好用是好用,但是兼容性不好,如果没有授权、一些低版本浏览器、http浏览器环境下,其返回的是 undefined,那么我们在此环境下就要使用 execCommand 了, execCommand 需要配合 input、textarea使用,相对比较麻烦,且即将丢弃,但作为兼容性代码还是可以使用的

ps:吐槽一下,低版本和没权限也就忍了,http 也不行这就说不过去了,就算是 https 在浏览器环境安全仍然堪忧,多此一举呀,还不如直接一口气封装好一个兼容性方法😂

document.execCommand("copy") //复制;
document.execCommand("paste") //粘贴
document.execCommand("cut") //剪切

下面就以一个 copy 为例,写一个兼容性代码

function copyText(text: string) {
    //有 clipboard 我们就是用,没有就不适用
    if (navigator.clipboard) {
        navigator.clipboard.writeText(text);
    } else {
        const input = document.createElement("input");
        input.setAttribute("value", text);
        document.body.appendChild(input);
        input.select();
        document.execCommand("copy");
        document.body.removeChild(input);
    }
}

柯里化与类型标注

2025年1月24日 13:06

柯里化

柯里化也是一个比较常用的功能,但是直接写柯里化的功能的很少,一般直接使用,当然也有没使用的,个人也是偶然间才听到这个名词,它其实并没有那么神奇,这里直接介绍一下

其可以将我们一个多参函数的调用分批次传参执行,执行到满足参数条件后,直接出结果,因此在某些场景非常好用(当然面向对象的功能更写多了,感觉就是链式的面向过程版),可以这么理解

下面通过案例更好的理解柯里化,不仅仅完成他的实现,还完成他的 ts 类型标注(都上 ts 了,实际上更推荐面向对象方式,可以使用链式写法,这里也给出类型标注,有些场景确实好用哈)

简单案例

function sum(a, b) {
    return a + b
}
let res = sum(1, 2)
console.log(res)


function curry(a) {  
    return function (b) {
        return a + b
    }
}
res = curry(1)(2)
console.log(res);

通用型案例

function curry(fn) {
    return function subCurry(...args) {
        //传递累计参数数量 跟要求的 fn 的参数数量对比(函数的 length 就是参数数量哈)
        if (args.length >= fn.length) {
            //参数足够,多余的函数调用会自动过滤掉
            return fn.apply(this, args);
        } else {
            //返回一个子调用的柯里化函数
            return function (...nextArgs) {
                return subCurry.apply(this, [...args, ...nextArgs]);
            };
        }
    };
}

function sum(a, b, c, d, e) {
    return a + b + c + d + e;
}
let res = sum(1, 2, 3, 4, 5);
console.log("multi", res);

//使用通用柯里化
const currySum = curry(sum);
res = currySum(1)(2)(3)(4)(5); //currySum(1, 2)(3)(4, 5) 也可以
console.log("curryEx", res);

ts类型标注

我们直接对柯里化类型化,我们直接写一个类型

type CurryedType<P, R> = P extends []
    ? () => R //当fn没有参数时
    : P extends [infer A]
    ? (x: A) => R //当fn只有一个参数,或者是传递的最后一个参数时
    : P extends [infer A, ...infer Rest]
    ? (x: A) => CurryedType<Rest, R> //当 fn 传递过程中,仍然还有有参数是
    : never;

declare function curry<P extends any[], R>(
    fn: (...args: P) => R
): CurryedType<P, R>;


function sum(a: number, b: number, c: number, d: number, e: number) {
    return a + b + c + d + e;
}

//可以测试空函数、单参数函数
const currySum = curry(sum);
currySum(1)(2)(3)(4)(5);

类型标注就这样了,仔细使用会发现,这样形成的参数是一次一次展开的,一次只能传递一个参数,如果想要支持一次传递若干个参数,怎么类型标注呢(难度再次飙升,个人是不想写了🤣)

有人可能直接想在 ts 文件写柯里化代码,那就更头疼了哈,里面嵌套也得上(个人不推荐)

个人感觉,都上 ts了,更应该用上面向对象的链式或者其他写法了吧,柯里化实际上并没有面向对象好读😂

我们写一个链式(这不比柯里化好读、好用么,也没那么复杂的类型标注,也更适合ts🤣,这可能也是很多开发者并不追求柯里化的效果吧,并没有吹得那么神奇,个人也是偶然才看到的柯里化的概念😂)

class SumClass {
    count: number = 0;

    add(...args: number[]) {
        args.forEach((val) => (this.count += val));
        return this;
    }

    sum(): number {
        return this.count;
    }
}

const sumObj = new SumClass();
const sum = sumObj.add(1).add(2).add(3, 4).add(5).sum();
❌
❌