资讯专栏INFORMATION COLUMN

基于Node的React图片上传组件实现

cfanr / 2147人阅读

摘要:常用的设置如下下的请求风格下的请求和不太一样,在正式的请求发出之前都会先发一个类型为的请求作为试探,只有当该请求通过以后,正式的请求才能发向服务端。所以服务端路由我们还需要处理这样一个请求注意该请求同样需要设置跨域。

写在前面

红旗不倒,誓把JavaScript进行到底!今天介绍我的开源项目 Royal 里的图片上传组件的前后端实现原理(React + Node),花了一些时间,希望对你有所帮助。

前端实现

遵循React 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接import即可。

import React, { Component } from "react"
import Upload from "../../components/FormControls/Upload/"

//......

render() {
    return (
        
) }

uri 参数是必须传的,是图片上传的后端接口地址,接口怎么写下面会讲到。

渲染页面

组件render部分需要体现三个功能:

图片选取(dialog窗口)

可拖拽功能(拖拽容器)

可预览(预览列表)

上传按钮 (button)

上传完成图片地址和链接 (信息列表)

render函数
render() {
    return (
        
this.handleChange(v)} type="file" size={this.state.size} name="fileSelect" accept="image/*" multiple={this.state.multiple} /> this.handleDragHover(e)} onDragLeave={(e)=>this.handleDragHover(e)} onDrop={(e)=>this.handleDrop(e)} className="upload-drag-area"> 或者将图片拖到此处
{ this._renderPreview(); // 渲染图片预览列表 }
{ this._renderUploadInfos(); // 渲染图片上传信息 }
) }
渲染图片预览列表
_renderPreview() {
    if (this.state.files) {
        return this.state.files.map((item, idx) => {
            return (
                

{item.name}

{this.state.progress[idx]}
) }) } else { return null } }
渲染图片上传信息列表
_renderUploadInfos() {
    if (this.state.uploadHistory) {
        return this.state.uploadHistory.map((item, idx) => {
            return (
                

上传成功,图片地址是: 查看

); }) } else { return null; } }
文件上传

前端要实现图片上传的原理就是通过构建FormData对象,把文件对象append()到该对象,然后挂载在XMLHttpRequest对象上 send() 到服务端。

获取文件对象

获取文件对象需要借助 input 输入框的 change 事件来获取 句柄参数 e

onChange={(e)=>this.handleChange(e)}

然后做以下处理:

e.preventDefault()
let target = event.target
let files = target.files
let count = this.state.multiple ? files.length : 1
for (let i = 0; i < count; i++) {
    files[i].thumb = URL.createObjectURL(files[i])
}
// 转换为真正的数组
files = Array.prototype.slice.call(files, 0)
// 过滤非图片类型的文件
files = files.filter(function (file) {
    return /image/i.test(file.type)
})

这时 files 就是 我们需要的文件对象组成的数组,把它 concat 到原有的 files里。

this.setState({files: this.state.files.concat(files)})

如此,接下来的操作 就可以 通过 this.state.files 取到当前已选中的 图片文件。

利用Promise处理异步上传

文件上传对于浏览器来说是异步的,为了处理 接下来的多图上传,这里引入了 Promise来处理异步操作:

upload(file, idx) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest()
        if (xhr.upload) {
            // 上传中
            xhr.upload.addEventListener("progress", (e) => {
                // 处理上传进度
                this.handleProgress(file, e.loaded, e.total, idx);
            }, false)
            // 文件上传成功或是失败
            xhr.onreadystatechange = (e) => {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                    // 上传成功操作
                    this.handleSuccess(file, xhr.responseText)
                    // 把该文件从上传队列中删除
                    this.handleDeleteFile(file)
                    resolve(xhr.responseText);
                 } else {
                    // 上传出错处理 
                    this.handleFailure(file, xhr.responseText)
                    reject(xhr.responseText);
                 }
            }
        }
        // 开始上传
        xhr.open("POST", this.state.uri, true)
        let form = new FormData()
        form.append("filedata", file)
        xhr.send(form)
    })
}
上传进度计算

利用XMLHttpRequest对象发异步请求的好处是可以 计算请求处理的进度,这是fetch所不具备的。
我们可以为 xhr.upload 对象的 progress 事件添加事件监听:

xhr.upload.addEventListener("progress", (e) => {
    // 处理上传进度
    this.handleProgress(file, e.loaded, e.total, i);
}, false)

说明:idx参数是纪录多图上传队列的索引

handleProgress(file, loaded, total, idx) {
    let percent = (loaded / total * 100).toFixed(2) + "%";
    let _progress = this.state.progress;
    _progress[idx] = percent;
    this.setState({ progress: _progress })  // 反馈到DOM里显示
}
拖拽上传

拖拽文件对于HTML5来说其实非常简单,因为它自带的几个事件监听机制可以直接做这类处理。主要用到的是下面三个:

onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}

取消拖拽时的浏览器行为:

handleDragHover(e) {
    e.stopPropagation()
    e.preventDefault()
}

处理拖拽进来的文件:

handleDrop(e) {
    this.setState({progress:[]})
    this.handleDragHover(e)
    // 获取文件列表对象
    let files = e.target.files || e.dataTransfer.files
    let count = this.state.multiple ? files.length : 1
    for (let i = 0; i < count; i++) {
        files[i].thumb = URL.createObjectURL(files[i])
    }
    // 转换为真正的数组 
    files = Array.prototype.slice.call(files, 0)
    // 过滤非图片类型的文件
    files = files.filter(function (file) {
        return /image/i.test(file.type)
    })
    this.setState({files: this.state.files.concat(files)})
}
多图同时上传

支持多图上传我们需要在组件调用处设置属性:

multiple = { true }  // 开启多图上传 
size = { 50 }        // 一次最大上传数量(虽没有上限,为保证服务端正常,建议50以下)

然后我们可以使用 Promise.all() 处理异步操作队列:

handleUpload() {
    let _promises = this.state.files.map((file, idx) => this.upload(file, idx))
    Promise.all(_promises).then( (res) => {
        // 全部上传完成 
        this.handleComplete()
    }).catch( (err) => { console.log(err) })
}

好了,前端工作已经完成,接下来就是Node的工作了。

后端实现

为了方便,后端采用的是express框架来快速搭建Http服务和路由。具体项目见我的github node-image-upload。逻辑虽然简单,但还是有几个可圈可点的地方:

跨域调用

本项目后端采用的是express,我们可以通过 res.header() 设置 请求的 "允许源" 来允许跨域调用:

res.header("Access-Control-Allow-Origin", "*"); 

设置为 * 说明允许任何 访问源,不太安全。建议设置成 你需要的 二级域名,如 jafeney.com

除了 "允许源" ,其他还有 "允许头" 、"允许域"、 "允许方法"、"文本类型" 等。常用的设置如下:

function allowCross(res) {
    res.header("Access-Control-Allow-Origin", "*");   
    res.header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild");
    res.header("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS");
    res.header("X-Powered-By"," 3.2.1")
    res.header("Content-Type", "application/json;charset=utf-8");
}
ES6下的Ajax请求

ES6风格下的Ajax请求和ES5不太一样,在正式的请求发出之前都会先发一个 类型为 OPTIONS的请求 作为试探,只有当该请求通过以后,正式的请求才能发向服务端。

所以服务端路由 我们还需要 处理这样一个 请求:

router.options("*", function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild");
    res.header("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS");
    res.header("X-Powered-By"," 3.2.1")
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
});

注意:该请求同样需要设置跨域。

处理上传

处理上传的图片引人了multiparty模块,用法很简单:

/*使用multiparty处理上传的图片*/
router.post("/upload", function(req, res, next) { 
    // 生成multiparty对象,并配置上传目标路径
    var form = new multiparty.Form({uploadDir: "./public/file/"});
    // 上传完成后处理
    form.parse(req, function(err, fields, files) {
        var filesTmp = JSON.stringify(files, null, 2);
        var relPath = "";
        if (err) {
            // 保存失败 
            console.log("Parse error: " + err);
        } else {
            // 图片保存成功!
            console.log("Parse Files: " + filesTmp);
            // 图片处理
            processImg(files);
        }
    });
});
图片处理

Node处理图片需要引入 gm 模块,它需要用 npm 来安装:

npm install gm --save
BUG说明

注意:node的图形操作gm模块前使用必须 先安装 imagemagickgraphicsmagickLinux (ubuntu)上使用apt-get 安装:

    sudo apt-get install imagemagick
    sudo apt-get install graphicsmagick  --with-webp  // 支持webp格式的图片

MacOS上可以用 Homebrew 直接安装:

    brew install imagemagick
    brew install graphicsmagick --with-webp   // 支持webp格式的图片  
预设尺寸

有些时候除了原图,我们可能需要把原图等比例缩小作为预览图或者缩略图。这个异步操作还是用Promise来实现:

function reSizeImage(paths, dstPath, size) {
    return new Promise(function(resolve, reject) {
        gm(dstPath)
        .noProfile()
        .resizeExact(size)
        .write("." + paths[1] + "@" + size + "00." + paths[2], function (err) {
            if (!err) {
                console.log("resize as " + size + " ok!")
                resolve()
            }
            else {
                reject(err)
            };
        });
    });
}
重命名图片

为了方便排序和管理图片,我们按照 "年月日 + 时间戳 + 尺寸" 来命名图片:

var _dateSymbol = new Date().toLocaleDateString().split("-").join("");
var _timeSymbol = new Date().getTime().toString();

至于图片尺寸 使用 gmsize() 方法来获取,处理方式如下:

gm(uploadedPath).size(function(err, size) {
    var dstPath = "./public/file/" + _dateSymbol + _timeSymbol 
        + "_" + size.width + "x" + size.height + "." 
        + _img.originalFilename.split(".")[1];
    var _port = process.env.PORT || "9999";
        relPath = "http://" + req.hostname + ( _port!==80 ? ":" + _port : "" ) 
        + "/file/" + _dateSymbol + _timeSymbol + "_" + size.width + "x" 
        +  size.height + "." + _img.originalFilename.split(".")[1];
    // 重命名
    fs.rename(uploadedPath, dstPath, function(err) {
        if (err) {
            reject(err)
        } else {
            console.log("rename ok!");
        }
    });
});
总结

对于大前端的工作,理解图片上传的前后端原理仅仅是浅层的。我们的口号是 "把JavaScript进行到底!",现在无论是 ReactNative的移动端开发,还是NodeJS的后端开发,前端工程师可以做的工作早已不仅仅是局限于web页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 全栈工程师 更为贴切吧。

当然,全栈 两个字的分量很重,不积跬步,无以至千里,功力低下的我还需要不断修炼和实践!

参考

张鑫旭 《基于HTML5的可预览多图片Ajax上传》

@欢迎关注我的 github 和 个人博客 -Jafeney

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80057.html

相关文章

  • 一些基于React、Vue、Node.js、MongoDB技术栈实践项目

    摘要:利用中间件实现异步请求,实现两个用户角色实时通信。目前还未深入了解的一些概念。往后会写更多的前后台联通的项目。删除分组会连同组内的所有图片一起删除。算是对自己上次用写后台的一个强化,项目文章在这里。后来一直没动,前些日子才把后续的完善。 欢迎访问我的个人网站:http://www.neroht.com/ 刚学vue和react时,利用业余时间写的关于这两个框架的训练,都相对简单,有的...

    tangr206 评论0 收藏0
  • 第三方库

    摘要:微信支付,支付宝支付,银联支付三大支付总结支付宝植入总结支付宝的植基于和百度地图的组件库基于百度地图封装的组件库,使用这个库最好需要先了解和百度地图。 Commento - 多说 & Disqus 开源替代品 Commento - 多说 & Disqus 开源替代品 anime.js 简单入门教程 强大轻量的动画库 anime.js 入门教程 来自B站的开源的MagicaSakura源...

    seanHai 评论0 收藏0
  • 第三方库

    摘要:微信支付,支付宝支付,银联支付三大支付总结支付宝植入总结支付宝的植基于和百度地图的组件库基于百度地图封装的组件库,使用这个库最好需要先了解和百度地图。 Commento - 多说 & Disqus 开源替代品 Commento - 多说 & Disqus 开源替代品 anime.js 简单入门教程 强大轻量的动画库 anime.js 入门教程 来自B站的开源的MagicaSakura源...

    gityuan 评论0 收藏0
  • GitHub 值得收藏前端项目[每月更新...]

    摘要:也是一款优秀的响应式框架站点所使用的一套框架为微信服务量身设计的一套框架一组很小的,响应式的组件,你可以在网页的项目上到处使用一个可定制的文件,使浏览器呈现的所有元素,更一致和符合现代标准。 GitHub 值得收藏的前端项目 整理与收集的一些比较优秀github项目,方便自己阅读,顺便分享出来,大家一起学习,本篇文章会持续更新,版权归原作者所有。欢迎github star与fork 预...

    maxmin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<