[React+Nest.js]仿Antd實(shí)現(xiàn)一個(gè)圖片上傳的組件

實(shí)現(xiàn)效果圖

image
image
image
image
image

功能預(yù)覽

  1. 【less】上傳文件的input框樣式的改造
  2. 獲取圖片列表
  3. 點(diǎn)擊上傳圖片 + 轉(zhuǎn)換成base64
  4. 上傳中l(wèi)oading
  5. 上傳失敗
  6. 刪除圖片
  7. 查看大圖
  8. 下載圖片 + Blob URL
  9. useState 與 useEffect的使用
  10. 跨域
  11. 靜態(tài)文件的查看
  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/*"來限制你選擇上傳的文件只能是圖片類型的;
當(dāng)你彈出選擇文件的框之后郑藏,無論是選擇還是取消都會(huì)觸發(fā)onChange事件馁蒂。

原生input上傳文件的樣式很丑呵晚,于是換樣式成了重中之重!

思路:
1.把input框給隱藏掉
2.并且要設(shè)置一個(gè)與最終樣式同樣大小的寬高沫屡,通過子絕父相讓input框覆蓋在最上層饵隙,這樣才能夠命中點(diǎn)擊事件

.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. 點(diǎn)擊上傳圖片 + 轉(zhuǎn)換成base

出現(xiàn)上傳文件的框,無論是選擇還是取消沮脖,都會(huì)觸發(fā)onChange事件金矛,所以要判斷你選擇的targetFile是否存在。

通過FileReder將圖片轉(zhuǎn)成base64勺届,用同步的方式將最終結(jié)果拋出去:

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

獲取到了base64之后再發(fā)送axios請(qǐng)求到后端:

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. 上傳中l(wèi)oading

當(dāng)我們開始上傳圖片的時(shí)候驶俊,會(huì)將uploadLoading設(shè)置成true,上傳成功之后會(huì)將uploadLoading再設(shè)置成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其實(shí)同理, 其次废睦,上傳失敗之后設(shè)置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保存選擇的結(jié)果

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標(biāo)簽的download屬性來實(shí)現(xiàn)下載效果奈应,因?yàn)橄螺d按鈕是Icon,所以點(diǎn)擊download Icon之后觸發(fā)a標(biāo)簽的download

2.使用Blob: 使用URL.createObjectURL()函數(shù)可以創(chuàng)建一個(gè)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.靜態(tài)文件暴露

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請(qǐng)求圖片列表

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末购披,一起剝皮案震驚了整個(gè)濱河市杖挣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刚陡,老刑警劉巖惩妇,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異筐乳,居然都是意外死亡歌殃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蝙云,熙熙樓的掌柜王于貴愁眉苦臉地迎上來氓皱,“玉大人,你說我怎么就攤上這事勃刨〔ú模” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵身隐,是天一觀的道長(zhǎng)廷区。 經(jīng)常有香客問我,道長(zhǎng)贾铝,這世上最難降的妖魔是什么隙轻? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮忌傻,結(jié)果婚禮上大脉,老公的妹妹穿的比我還像新娘。我一直安慰自己水孩,他們只是感情好镰矿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俘种,像睡著了一般秤标。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宙刘,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天苍姜,我揣著相機(jī)與錄音,去河邊找鬼悬包。 笑死衙猪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播垫释,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼丝格,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了棵譬?” 一聲冷哼從身側(cè)響起显蝌,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎订咸,沒想到半個(gè)月后曼尊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脏嚷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年骆撇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片父叙。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艾船,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出高每,到底是詐尸還是另有隱情,我是刑警寧澤践宴,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布鲸匿,位于F島的核電站,受9級(jí)特大地震影響阻肩,放射性物質(zhì)發(fā)生泄漏带欢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一烤惊、第九天 我趴在偏房一處隱蔽的房頂上張望乔煞。 院中可真熱鬧,春花似錦柒室、人聲如沸渡贾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽空骚。三九已至,卻和暖如春擂仍,著一層夾襖步出監(jiān)牢的瞬間囤屹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國打工逢渔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肋坚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像智厌,于是被迫代替她去往敵國和親诲泌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前端無法像原生APP一樣直接操作本地文件峦剔,否則的話打開個(gè)網(wǎng)頁就能把用戶電腦上的文件偷光了档礁,所以需要通過用戶觸發(fā),用...
    孫悟空SUN閱讀 406評(píng)論 0 0
  • 前端無法像原生APP一樣直接操作本地文件吝沫,否則的話打開個(gè)網(wǎng)頁就能把用戶電腦上的文件偷光了呻澜,所以需要通過用戶觸發(fā),用...
    雷波_viho閱讀 823評(píng)論 0 1
  • 本組件基于vuejs框架, 使用ES6基本語法, css預(yù)編譯采用的scss, 圖片裁剪模塊基于cropperjs...
    sufaith_dev閱讀 2,592評(píng)論 0 0
  • 本文轉(zhuǎn)載自博客園博主小火柴的藍(lán)色理想惨险。 Blob Blob是計(jì)算機(jī)界通用術(shù)語之一羹幸,全稱寫作:BLOB(binary...
    小小的開發(fā)人員閱讀 1,555評(píng)論 0 1
  • “斷橋是否下過雪栅受,又想起你的臉,若是無緣再見恭朗,白堤柳簾垂淚好幾遍屏镊。”這是一首歌的聲音痰腮,音樂是掛在樹梢上的流云而芥,一行...
    雁南秋閱讀 2,117評(píng)論 5 5