用现代化的方式开发一个图片上传工具
栏目: JavaScript · 发布时间: 5年前
内容简介:写于 2017.04.18对于图片上传,大家一定不陌生。最近工作中遇到了关于图片上传的内容,借此机会认真研究了一番,遂一发不可收拾,最后琢磨了一个东西出来。在开发的过程中有不少的体会,于是打算写一篇文章分享一下心得体会。 本文将会以这个名为项目地址:
写于 2017.04.18
对于图片上传,大家一定不陌生。最近工作中遇到了关于图片上传的内容,借此机会认真研究了一番,遂一发不可收拾,最后琢磨了一个东西出来。在开发的过程中有不少的体会,于是打算写一篇文章分享一下心得体会。 本文将会以这个名为 Dolu
的项目为例子,一步步介绍我是如何进行环境搭建、代码设计以及实际开发的。内容较多,还请耐心读完。
项目地址: github.com/jrainlau/do…
一、环境搭建
本项目使用目前最新的 webpack 2
和 es7
进行开发,所以环境的搭建必不可少。但是由于这个项目比较简单,所以环境的搭建也是非常简单的,只有一个 webpack.config.js
文件:
var path = require('path') var webpack = require('webpack') module.exports = { entry: './src/main.js', // 开发模式用 // entry: './src/dolu.js', // 生产模式用 output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js', // 开发模式用 // filename: 'index.js', // 生产模式用 libraryTarget: 'umd' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules|dist/, use: [ 'babel-loader', 'eslint-loader' ] } ] }, devServer: { historyApiFallback: true, noInfo: true, host: '0.0.0.0' }, performance: { hints: false }, devtool: '#eval-source-map' } if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), new webpack.LoaderOptionsPlugin({ minimize: true }) ]) } 复制代码
考虑到“生产模式”使用的次数不多,所以并没有区分 dev
和 prod
模式,而是手动注释对应的内容进行切换。
定义好入口文件和输出路径后,我使用了 babel-loader
和 eslint-loader
。这两个loader的作用就不多作介绍了,值得注意的是养成使用 eslint
的习惯是极好的,能够有效减少代码的错误,并且能够改掉很多坏习惯。同时在编辑器里(我用VSCODE)中也能够实时进行代码检查,非常方便。
为了使用最新的 es7
,我们也需要在根目录下配置一份 .babelrc
文件:
{ "presets": [ ["latest", { "es2015": { "modules": false } }] ], "plugins": [ ["transform-runtime"] ] } 复制代码
配置好了 webpack.config.js
和 .babelrc
以后,我们打开 package.json
,来看看需要安装的依赖都有哪些:
"devDependencies": { "babel-core": "^6.24.0", "babel-loader": "^6.4.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", "babel-preset-latest": "^6.24.0", "cors": "^2.8.3", "cross-env": "^3.2.4", "eslint": "^3.19.0", "eslint-config-standard": "^10.2.1", "eslint-loader": "^1.7.1", "eslint-plugin-import": "^2.2.0", "eslint-plugin-node": "^4.2.2", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^3.0.1", "multer": "^1.3.0", "webpack": "^2.3.1", "webpack-dev-server": "^2.4.2" } 复制代码
当中的 cors
模块和 multer
模块为我们之后搭建node服务器需要用的,其他都是运行所需。
然后在"scripts"里面写上我们要用到的几条命令:
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", "server": "node ./server/index.js" }, 复制代码
分别对应 开发模式
, 生产模式
, 启动本地后台服务器
。
然后我们在根目录下新建一个 src
目录,一个 index.html
,一个 /src/main.js
。这时候整个项目的目录结构如下:
├── index.html ├── package.json ├── src │ └── main.js ├── webpack.config.js └── .babelrc 复制代码
至此,我们的开发环境已经搭建完毕。
二、功能设计
基本的流程及功能如上图所示,其中的每一步我们都将以模块的方式进行开发。
当然,我们不能满足于这么一点点的功能,我们需要考虑更多的情况更多的可能,扩展一下,也许我们可以这么做:
比如我们在获取图片之后先不进行上传,也许我们还要对转出来的 base64
进行处理或使用,也许我们能够直接上传一堆由第三方提供的 base64
甚至 formdata
。另外我们还需要对上传的方法进行自定义,又或者可以选择多张图片什么的……除此之外,可能还有许许多多的场景,为了开发一个通用的组件,我们需要思考的地方实在有很多很多。
当然,这一次我们的任务比较简单,上面这么多功能已经够我们玩的了,下面我们进入实际的开发。
三、开始coding!
在 /src
目录下新建一个 dolu.js
文件,这将会是我们整个项目的核心。
首先定义一个类:
class Dolulu { constructor (config = {}) {} } 复制代码
然后我们按照上一节脑图的思路,先完成“图片选取”相关的功能。
在这个类里面我们定义一个名为 _pickFile()
的私有方法,这个方法我们不希望被外部调用,只是作为 Dolu
内置的方法。
_pickFile () { const picker = document.querySelector(this.config.picker) picker.addEventListener('change', () => { if (!picker.files.length) { return } const files = [...picker.files] if (files.length > this.config.quantity) { throw new Error('Out of file quantity limit!') } /* * 这时候我们已经拿到了文件数组files,可以马上进行转码 * _transformer()函数是另一个私有方法,用于格式转码 */ this._transformer(files) /* * 加入这一行以实现重复选中同一张图片 */ picker.value = null }) } 复制代码
然后写一个初始化的方法,让 Dolu
实例能够自动开启文件选取功能:
_init () { if (this.config.picker) { return this._pickFile() } } 复制代码
只要在 constructor
里面调用这个方法就可以了。
选择完图片,我们就要对它进行转码了。为了更好地组织我们的代码,我们把这个“图片转成base64”的函数封装成一个模块。在 /src
目录下新建 fileToBase64.js
:
const fileToBase64 = (file) => { const reader = new FileReader() reader.readAsDataURL(file) return new Promise((resolve) => { reader.addEventListener('load', () => { const result = reader.result resolve(result) }) }) } export default fileToBase64 复制代码
代码内容只有15行,其输入为一个图片文件,输出为一串base64编码。返回一个Promise方便接下来我们使用 async/await
语法。
同样的道理,我们新建一个 base64ToBlob.js
文件,以实现输入为base64,输出为formdata的功能:
const base64ToBlob = (base64) => { const byteString = atob(base64.split(',')[1]) const mimeString = base64.split(',')[0].split(':')[1].split(';')[0] const ab = new ArrayBuffer(byteString.length) const ia = new Uint8Array(ab) for (let i = 0, len = byteString.length; i < len; i += 1) { ia[i] = byteString.charCodeAt(i) } let Builder = window.WebKitBlobBuilder || window.MozBlobBuilder let blobUrl if (Builder) { const builder = new Builder() builder.append(ab) blobUrl = builder.getBlob(mimeString) } else { blobUrl = new window.Blob([ab], { type: mimeString }) } const fd = new FormData() fd.append('file', blobUrl) return fd } export default base64ToBlob 复制代码
接下来我们利用这两个模块,构建我们的 _transformer()
方法:
_transformer (files, manually = false) { files.forEach(async (file, index) => { if (isObject(file)) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) { return } const dataUrl = await fileToBase64(file) const formData = await base64ToBlob(dataUrl) if (this.config.autoSend || manually) { this._uploader(formData, index) } } }) 复制代码
可以看到,这个方法会遍历整个files数组,通过筛选保证其文件类型为图片,然后连续转码生成formdata格式数据,作为参数传入 _uploader()
方法中。另外为了方便扩展和使用,同时传入了图片的下标。图片的下标能够方便在上传函数中让用户知道“现在是第几张图片被处理”。
_upload()
函数将会直接调用 Dolu
实例中所定义的上传方法,这个稍后再述。
到这里,我们已经完成了上一节第一张图片的几个“基本功能”了,和外面一捞一大把的教程相差无几。别急,我们马上进入对扩展功能的开发。
四、实现向外输出完整的base64字符串数组
我们重新把目光投向上一节的 _transformer()
函数。这个函数接受一个 数组 ,在内部使用 .forEach()
方法遍历每一个文件,对它进行转码处理。为了向外输出完整的转码后的数组,关键的步骤在于如何确定转码已经完成了。从最简单的想法开始,在 forEach
循环体的外部直接把数组抛出去行不行?比如这样:
_transformer (files, manually = false) { files.forEach(async (file, index) => { if (isObject(file)) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) { return } const dataUrl = await fileToBase64(file) const formData = await base64ToBlob(dataUrl) this.dataUrlArr.push(dataUrl) if (this.config.autoSend || manually) { this._uploader(formData, index) } } }) this.config.getDataUrls(this.dataUrlArr) return this } 复制代码
看起来没有问题,但是在实际的测试中,传入 this.config.getDataUrls
中的 dataUrlArr
首先会是一个空数组,过一会儿才会有数据。为了验证这个结论,我们在 /src
名录下新建一个文件 main.js
,写入如下内容:
import Dolu from './dolu' const dolu = new Dolu({ picker: '#picker', getDataUrls (arr) { console.info(arr) arr.forEach((dataUrl) => { console.log(dataUrl) }) } }) 复制代码
运行一下,发现输出结果如下:
只有一个空数组,而且 forEach()
循环并没有打印出任何东西。这个例子不直观,我们现在把开发者 工具 关掉,然后重新打开,看看会发生什么:
仅仅是重新打开开发者工具,就发现刚才的空数组变成了一个有内容的数组,特别奇怪。
其实原因也很简单,因为 _transformer()
内部的 forEach()
循环,并不能保证图片已经转码完毕,这涉及到浏览器任务队列的知识(此处理解可能有误,欢迎指出),在这里就不展开讨论了。
那么我们只能 等待 图片转码完毕,才调用 this.config.getDataUrls()
方法。要实现这个目的,我们有许多种方法,最简单粗暴的就是利用 setInterval()
进行轮询,当 dataUrlArr.length === files.length
,则立即调用,但是这种做法一点儿也不优雅。我们能不能让函数发送一个 通知 ,当 .push()
方法执行并成功的时候就判断 dataUrlArr.length =?= files.length
,若条件符合则进行相应的处理。
这时候我们可以考虑使用es6新增语法 Proxy
来解决。关于 Proxy
的使用可以查阅我的另外一篇文章 《使用ES6的新特性Proxy来实现一个数据绑定实例》 ,然后我们一起来步入正题吧!
五、使用 Proxy
实现数据绑定
在 /src
目录下的 utils.js
里,我们加入一个新的工具方法:
function proxier (props, callback) { const waitProxy = new Proxy(props, { set (target, property, value) { target[property] = value callback(target, property, value) return true } }) return waitProxy } 复制代码
回到 dolu.js
文件,改写一下 _transformer()
方法:
_transformer (files, manually = false) { const dataUrlArrProxy = proxier(this.dataUrlArr, (target, property, value) => { if (property === 'length') { if (target.length === files.length) { this.config.getDataUrls(this.dataUrlArr) } } }) files.forEach(async (file, index) => { if (isObject(file)) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) { return } const dataUrl = await fileToBase64(file) const formData = await base64ToBlob(dataUrl) dataUrlArrProxy.push(dataUrl) if (this.config.autoSend || manually) { this._uploader(formData, index) } } }) return this } 复制代码
这样,我们每一次转码过后,都会调用 代理数组 dataUrlArrProxy
中的 .push()
方法,这时候代理数组就会自动判断 target.length =?= files.length
然后调用相应的方法。
尝试运行一下,发现结果符合预期。同样的方式,我们可以为 formDataArr
也设置一个代理数组,以实现向外抛出 formdata
数组的目的。
六、服务器搭建
把前端这边的图片选取、图片转码都已经做完了,那么我们是时候搭建一个后台服务器,去测试以 formdata
格式上传图片是否有效了。
进入根目录下的 /server
文件夹,我们新建一个 /imgs
目录以及一个 index.js
文件,内容如下:
const express = require('express') const multer = require('multer') const cors = require('cors') const app = express() app.use(express.static('./public')) app.use(cors()) app.listen(process.env.PORT || 8888) console.log('Node.js Ajax Upload File running at: http://0.0.0.0:8888') app.post('/upload', (req, res) => { const store = multer.diskStorage({ destination: './server/imgs' }) const upload = multer({ storage: store }).any() upload(req, res, function (err) { if (err) { console.log(err) return res.end('Error') } else { console.log(req.body) req.files.forEach(function (item) { console.log(item) }) res.end('File uploaded') } }) }) 复制代码
该服务器将会运行于本地 8888
端口,通过 post
方法发送到 localhost:8888/upload
,然后图片会保存到 server/imgs
目录下。
回到 dolu.js
,我们写一个 _uploader()
方法,该方法会调用 config
里面的自定义设置,调用设置中具体的上传方法:
_uploader (formData, index) { this.config.uploader(formData, index) } 复制代码
在 main.js
中,我们使用 axios
作为上传的工具:
const dolu = new Dolu({ picker: '#picker', autoSend: true, uploader (data, index) { axios({ method: 'post', url: 'http://0.0.0.0:8888/upload', data: data, onUploadProgress: (e) => { const percent = Math.round((e.loaded * 100) / e.total) console.log(percent, index) } }).then((res) => { console.log(res) }).catch((err) => { console.log(err) }) } }) 复制代码
激动人心的时刻来了,我们来测试一下吧!
七、实际运行测试
打开开发者工具当中的 Network
,随便选几张图片进行上传,看看效果如何:
点击去看看发送的是什么东西:
如上图所示,是一个formdata数据。打开 ./server/imgs
目录,我们应该就能看到三个文件了:
上传成功!而且符合我们以“formdata上传的二进制格式”的需求。
以上所述就是小编给大家介绍的《用现代化的方式开发一个图片上传工具》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 现代化网站的渗透测试
- 现代化的PHP-写好注释
- 现代化 Android Pie: 安全与隐私
- Dahlia:一个现代化的 React 框架
- 对现代化网站的渗透测试的思考
- 来!狂撸一款PHP现代化框架 (一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web协议与实践
克里希纳穆尔蒂 (KrishnamurthyBalachander) / 范群波 / 科学出版社 / 2003-7 / 46.0
本书全面论述了传输Web内容的系统和协议,重点讲述了Web中业已成熟和稳定的技术,如TCP/IP协议及DNS技术、HITP/1.0的设计及其与TCP之间的交互;深入阐述了Web高速缓存技术和多媒体流播技术的最新技术动态;分析了Apache Web服务器和Squid代理;还探讨了通信量的分析和测量技术。书中使用了大量示例、技术发展水平报告以及案例分析来阐述Web的工作原理和各个组件之间的交互。本书是......一起来看看 《Web协议与实践》 这本书的介绍吧!