[React+Nest.js]仿Antd实现一个图片上传的组件

实现效果图

image
image
image
image
image

功能预览

  1. 【less】上传文件的input框样式的改造
  2. 获取图片列表
  3. 点击上传图片 + 转换成base64
  4. 上传中loading
  5. 上传失败
  6. 删除图片
  7. 查看大图
  8. 下载图片 + Blob URL
  9. useState 与 useEffect的使用
  10. 跨域
  11. 静态文件的查看
  12. 查询/删除/添加文件
  13. uuid
  14. 用同步的方式书写异步的操作(封装 async await promise集合体)
  15. axios

【前端】React

1.【less】上传文件的input框样式的改造

<div className="upload-container item">
    <Icon type="plus" className="icon"/>
    <div className="name">Upload</div>
    <input id="upload-image" 
           type="file" 
           name="image" 
           accept="image/*"
           onChange={()=>handleUploadImage(props)}/>                    
</div>

主要用到的是原生的input框:
type="file"弹出选择上传文件的框;
accept="image/*"来限制你选择上传的文件只能是图片类型的;
当你弹出选择文件的框之后,无论是选择还是取消都会触发onChange事件。

原生input上传文件的样式很丑,于是换样式成了重中之重!

思路:
1.把input框给隐藏掉
2.并且要设置一个与最终样式同样大小的宽高,通过子绝父相让input框覆盖在最上层,这样才能够命中点击事件

.upload-container{
    margin: 5px;
    padding: 3px;
    float: left;
    border-style: dashed;
    background-color: #f5f5f5;
    position: relative; //父相
    flex-direction: column;

    .icon{
      color: #e1e1e1;
      margin-top: 10px;
      font-size: 40px;
      font-weight: bolder;
    }
    .name{
      color: #a3a3a3;
    }
    input{
      width: 100%;
      height: 100%; // 与父样式等宽高
      cursor: pointer;
      position: absolute; // 子绝
      top: 0;
      opacity: 0; // 全透明
    }
  }

2. 用同步的方式书写异步的操作(封装 async await promise集合体)+ 获取图片列表 + useState

const baseUrl = 'http://localhost:8080/image';

const getImageListUrl = `${baseUrl}/list`;

const [list, setList] = useState([]);

const ajax = (url, data={}, type='GET') =>{
    return new Promise((resolve)=>{
        const promise = type === 'GET' ? axios.get(url, {params: data}) : axios.post(url, data)
        promise.then(res=>{
            const data = res.data;
            data.status !== 0 ? message.error(data.msg) : resolve(data);
        }).catch(err => {
            message.error('Network request Error: '+err);
        })
    })
};

const getAndUpdateImageList = async(setList) => {
    const data = await ajax(getImageListUrl);
    setList(data.data);
};

3. 点击上传图片 + 转换成base

出现上传文件的框,无论是选择还是取消,都会触发onChange事件,所以要判断你选择的targetFile是否存在。

通过FileReder将图片转成base64,用同步的方式将最终结果抛出去:

const getBase64 = (file) => {
    const fileReader = new FileReader();
    fileReader.readAsDataURL(file);
    return new Promise((resolve)=>{
        fileReader.onload = (data) => {
            resolve(data.target.result)
        }
    })
};

获取到了base64之后再发送axios请求到后端:

const handleUploadImage= async ({list,action,uploadImage,setUploadLoading, setUploadErrorFileName})=>{
    const input = document.getElementById('upload-image');
    const targetFile = input.files[0];
    if (targetFile){
        setUploadLoading(true);
        const { name } = targetFile;
        const imageBase64 = await getBase64(targetFile);
        await axios.post(action, {imageBase64, name})
            .then(()=>{
                uploadImage(action + "/" + name);
            }).catch(() => {
                setUploadErrorFileName(name);
            });
        setUploadLoading(false);
    }
};

4. 上传中loading

当我们开始上传图片的时候,会将uploadLoading设置成true,上传成功之后会将uploadLoading再设置成false!

const handleUploadImage= async ({list,action,uploadImage,setUploadLoading, setUploadErrorFileName})=>{
    const input = document.getElementById('upload-image');
    const targetFile = input.files[0];
    if (targetFile){
        setUploadLoading(true);
        ...
        setUploadLoading(false);
    }
};
const [uploadLoading, setUploadLoading] = useState(false);

{uploadLoading && renderUploading()}

const renderUploading =()=> (
    <div className="uploading item">
        <Icon className="icon" type='loading' />
        <span>Uploading...</span>
    </div>
);

5. 上传失败

与loading其实同理, 其次,上传失败之后设置uploadErrorFileName来渲染失败的样式。

await axios.post(action, {imageBase64, name})
            .then(()=>{
                ...
            }).catch(() => {
                setUploadErrorFileName(name);
            });
const [uploadErrorFileName, setUploadErrorFileName] = useState(null);

{uploadErrorFileName && renderUploadError(uploadErrorFileName,setUploadErrorFileName)}

const renderUploadError =(uploadErrorFileName, setUploadErrorFileName)=> (
    <div className="uploading item error">
        <Icon className="icon" type='close-circle' />
        <div className="error-message">Error!</div>
        <div className="name">{uploadErrorFileName}</div>
        <div className="config item">
            <Icon className="delete-icon" type="delete" onClick={()=> setUploadErrorFileName(null)}/>
        </div>
    </div>
);

6. 删除图片

const deleteImage = async ({name}, setList)=>{
    await ajax(deleteImageUrl,{name});
    getAndUpdateImageList(setList);
};

7. 查看大图

用到了Antd的上传图片一样,用到的Modal框去显示大图, 用previewSrc保存选择的结果

const [previewSrc, setPreviewSrc] = useState(null);

 <Modal
    width={800} 
    className="preview-modal"
    visible={previewSrc !== null}
    title={null}
    footer={null}
    onCancel={()=>setPreviewSrc(null)} >
         <img src={previewSrc} alt=""/>
</Modal>

8. 下载图片 + blob

思路:
1.用a标签的download属性来实现下载效果,因为下载按钮是Icon,所以点击download Icon之后触发a标签的download

2.使用Blob: 使用URL.createObjectURL()函数可以创建一个Blob URL

<Icon type="download" className="icon" onClick={()=>downloadImage(item, onDownload)}/>
<a id="download-image"  download={item.name}/>
const downloadImage=(item, onDownload)=>{
    const target = document.getElementById('download-image');
    const blob = new Blob([item.src]);
    target.href = URL.createObjectURL(blob);
    target.click();
    onDownload(item);
};

【后端】Nest

1.跨域

app.enableCors();

2.静态文件暴露

const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useStaticAssets(join(__dirname, '..', 'data/images'), {
    prefix: '/images/',
  });

3.写文件

  1. 解析base64,生成buffer
  2. fs.writeFile(buffer)
@Post('/image/add')
  getImage(@Req() req, @Res() res): void {
    const {imageBase64, name} = req.body;
    const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, '');
    const dataBuffer = new Buffer(base64Data, 'base64');
    fs.writeFile(`data/images/${name}`, dataBuffer, (err) => {
      if (err) {
        res.send(err);
      } else {
        res.send({status: 0 });
      }
    });
  }

4.读文件

fs.readdirSync(文件名)

@Get('/image/list')
  getProductList(@Res() res): void {
    const data = fs.readdirSync('data/images');
    const url = 'http://localhost:8080/images/';
    res.send({
      data: data.map(item => ({
        name: item,
        id: uuid(),
        src: url + item,
      })),
      status: 0,
    });
  }

5.删文件

fs.unlinkSync(文件名)

  @Get('/image/delete')
  deleteImage(@Query() query, @Res() res): void {
    const files = fs.readdirSync('data/images');
    const target = files.filter(item => item === query.name);
    if (target) {
      fs.unlinkSync('data/images/' + target);
      res.send({status: 0 });
    }
    res.send({status: 1 });
  }

案例

用useEffect代替了didMount请求图片列表

const [list, setList] = useState([]);

useEffect(() => { 
    getAndUpdateImageList(setList);
},[]);


import React,{useState, useEffect} from 'react';
import UploadImage from "./component/upload-image/upload-image";
import axios from "axios";
import {message} from "antd";

const baseUrl = 'http://localhost:8080/image';

const getImageListUrl = `${baseUrl}/list`;
const addImageUrl = `${baseUrl}/add`;
const deleteImageUrl = `${baseUrl}/delete`;

const ajax = (url, data={}, type='GET') =>{
    return new Promise((resolve)=>{
        const promise = type === 'GET' ? axios.get(url, {params: data}) : axios.post(url, data)
        promise.then(res=>{
            const data = res.data;
            data.status !== 0 ? message.error(data.msg) : resolve(data);
        }).catch(err => {
            message.error('Network request Error: '+err);
        })
    })
};

const getAndUpdateImageList = async(setList) => {
    const data = await ajax(getImageListUrl);
    setList(data.data);
};

const deleteImage = async ({name}, setList)=>{
    await ajax(deleteImageUrl,{name});
    getAndUpdateImageList(setList);
};

const uploadImage = (item, setList) => {
    getAndUpdateImageList(setList);
};

const downloadImage = (item) => {
    console.log('downloadImage', item)
};

function App() {
    const [list, setList] = useState([]);

    useEffect(() => {
        getAndUpdateImageList(setList);
    },[]);

    return (
      <div className="App">
        <UploadImage
            action={addImageUrl}
            list={list}
            onUpload={(item)=>uploadImage(item, setList)}
            onDelete={(item)=>deleteImage(item, setList)}
            onDownload={(item)=>downloadImage(item)}
        />
      </div>
  );
}

export default App;

源码

https://github.com/shenleStm/Upload-Image

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • 前端无法像原生APP一样直接操作本地文件,否则的话打开个网页就能把用户电脑上的文件偷光了,所以需要通过用户触发,用...
    孙悟空SUN阅读 367评论 0 0
  • 前端无法像原生APP一样直接操作本地文件,否则的话打开个网页就能把用户电脑上的文件偷光了,所以需要通过用户触发,用...
    雷波_viho阅读 815评论 0 1
  • 本组件基于vuejs框架, 使用ES6基本语法, css预编译采用的scss, 图片裁剪模块基于cropperjs...
    sufaith_dev阅读 2,573评论 0 0
  • 本文转载自博客园博主小火柴的蓝色理想。 Blob Blob是计算机界通用术语之一,全称写作:BLOB(binary...
    小小的开发人员阅读 1,536评论 0 1
  • “断桥是否下过雪,又想起你的脸,若是无缘再见,白堤柳帘垂泪好几遍。”这是一首歌的声音,音乐是挂在树梢上的流云,一行...
    雁南秋阅读 2,084评论 5 5