异步上传图片实现

快速上手

demo 地址

https://github.com/ChenShenhai/koa2-note/tree/master/demo/upload-async

源码理解

demo源码目录

  1. .
  2. ├── index.js # 后端启动文件
  3. ├── node_modules
  4. ├── package.json
  5. ├── static # 静态资源目录
  6. ├── image # 异步上传图片存储目录
  7. └── js
  8. └── index.js # 上传图片前端js操作
  9. ├── util
  10. └── upload.js # 后端处理图片流操作
  11. └── view
  12. └── index.ejs # ejs后端渲染模板

后端代码

入口文件 demo/upload-async/index.js

  1. const Koa = require('koa')
  2. const views = require('koa-views')
  3. const path = require('path')
  4. const convert = require('koa-convert')
  5. const static = require('koa-static')
  6. const { uploadFile } = require('./util/upload')
  7. const app = new Koa()
  8. /**
  9. * 使用第三方中间件 start
  10. */
  11. app.use(views(path.join(__dirname, './view'), {
  12. extension: 'ejs'
  13. }))
  14. // 静态资源目录对于相对入口文件index.js的路径
  15. const staticPath = './static'
  16. // 由于koa-static目前不支持koa2
  17. // 所以只能用koa-convert封装一下
  18. app.use(convert(static(
  19. path.join( __dirname, staticPath)
  20. )))
  21. /**
  22. * 使用第三方中间件 end
  23. */
  24. app.use( async ( ctx ) => {
  25. if ( ctx.method === 'GET' ) {
  26. let title = 'upload pic async'
  27. await ctx.render('index', {
  28. title,
  29. })
  30. } else if ( ctx.url === '/api/picture/upload.json' && ctx.method === 'POST' ) {
  31. // 上传文件请求处理
  32. let result = { success: false }
  33. let serverFilePath = path.join( __dirname, 'static/image' )
  34. // 上传文件事件
  35. result = await uploadFile( ctx, {
  36. fileType: 'album',
  37. path: serverFilePath
  38. })
  39. ctx.body = result
  40. } else {
  41. // 其他请求显示404
  42. ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
  43. }
  44. })
  45. app.listen(3000, () => {
  46. console.log('[demo] upload-pic-async is starting at port 3000')
  47. })

后端上传图片流写操作
入口文件 demo/upload-async/util/upload.js

  1. const inspect = require('util').inspect
  2. const path = require('path')
  3. const os = require('os')
  4. const fs = require('fs')
  5. const Busboy = require('busboy')
  6. /**
  7. * 同步创建文件目录
  8. * @param {string} dirname 目录绝对地址
  9. * @return {boolean} 创建目录结果
  10. */
  11. function mkdirsSync( dirname ) {
  12. if (fs.existsSync( dirname )) {
  13. return true
  14. } else {
  15. if (mkdirsSync( path.dirname(dirname)) ) {
  16. fs.mkdirSync( dirname )
  17. return true
  18. }
  19. }
  20. }
  21. /**
  22. * 获取上传文件的后缀名
  23. * @param {string} fileName 获取上传文件的后缀名
  24. * @return {string} 文件后缀名
  25. */
  26. function getSuffixName( fileName ) {
  27. let nameList = fileName.split('.')
  28. return nameList[nameList.length - 1]
  29. }
  30. /**
  31. * 上传文件
  32. * @param {object} ctx koa上下文
  33. * @param {object} options 文件上传参数 fileType文件类型, path文件存放路径
  34. * @return {promise}
  35. */
  36. function uploadFile( ctx, options) {
  37. let req = ctx.req
  38. let res = ctx.res
  39. let busboy = new Busboy({headers: req.headers})
  40. // 获取类型
  41. let fileType = options.fileType || 'common'
  42. let filePath = path.join( options.path, fileType)
  43. let mkdirResult = mkdirsSync( filePath )
  44. return new Promise((resolve, reject) => {
  45. console.log('文件上传中...')
  46. let result = {
  47. success: false,
  48. message: '',
  49. data: null
  50. }
  51. // 解析请求文件事件
  52. busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
  53. let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
  54. let _uploadFilePath = path.join( filePath, fileName )
  55. let saveTo = path.join(_uploadFilePath)
  56. // 文件保存到制定路径
  57. file.pipe(fs.createWriteStream(saveTo))
  58. // 文件写入事件结束
  59. file.on('end', function() {
  60. result.success = true
  61. result.message = '文件上传成功'
  62. result.data = {
  63. pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`
  64. }
  65. console.log('文件上传成功!')
  66. resolve(result)
  67. })
  68. })
  69. // 解析结束事件
  70. busboy.on('finish', function( ) {
  71. console.log('文件上结束')
  72. resolve(result)
  73. })
  74. // 解析错误事件
  75. busboy.on('error', function(err) {
  76. console.log('文件上出错')
  77. reject(result)
  78. })
  79. req.pipe(busboy)
  80. })
  81. }
  82. module.exports = {
  83. uploadFile
  84. }

前端代码

  1. <button class="btn" id="J_UploadPictureBtn">上传图片</button>
  2. <hr/>
  3. <p>上传进度<span id="J_UploadProgress">0</span>%</p>
  4. <p>上传结果图片</p>
  5. <div id="J_PicturePreview" class="preview-picture">
  6. </div>
  7. <script src="/js/index.js"></script>

上传操作代码

  1. (function(){
  2. let btn = document.getElementById('J_UploadPictureBtn')
  3. let progressElem = document.getElementById('J_UploadProgress')
  4. let previewElem = document.getElementById('J_PicturePreview')
  5. btn.addEventListener('click', function(){
  6. uploadAction({
  7. success: function( result ) {
  8. console.log( result )
  9. if ( result && result.success && result.data && result.data.pictureUrl ) {
  10. previewElem.innerHTML = '<img src="'+ result.data.pictureUrl +'" style="max-width: 100%">'
  11. }
  12. },
  13. progress: function( data ) {
  14. if ( data && data * 1 > 0 ) {
  15. progressElem.innerText = data
  16. }
  17. }
  18. })
  19. })
  20. /**
  21. * 类型判断
  22. * @type {Object}
  23. */
  24. let UtilType = {
  25. isPrototype: function( data ) {
  26. return Object.prototype.toString.call(data).toLowerCase();
  27. },
  28. isJSON: function( data ) {
  29. return this.isPrototype( data ) === '[object object]';
  30. },
  31. isFunction: function( data ) {
  32. return this.isPrototype( data ) === '[object function]';
  33. }
  34. }
  35. /**
  36. * form表单上传请求事件
  37. * @param {object} options 请求参数
  38. */
  39. function requestEvent( options ) {
  40. try {
  41. let formData = options.formData
  42. let xhr = new XMLHttpRequest()
  43. xhr.onreadystatechange = function() {
  44. if ( xhr.readyState === 4 && xhr.status === 200 ) {
  45. options.success(JSON.parse(xhr.responseText))
  46. }
  47. }
  48. xhr.upload.onprogress = function(evt) {
  49. let loaded = evt.loaded
  50. let tot = evt.total
  51. let per = Math.floor(100 * loaded / tot)
  52. options.progress(per)
  53. }
  54. xhr.open('post', '/api/picture/upload.json')
  55. xhr.send(formData)
  56. } catch ( err ) {
  57. options.fail(err)
  58. }
  59. }
  60. /**
  61. * 上传事件
  62. * @param {object} options 上传参数
  63. */
  64. function uploadEvent ( options ){
  65. let file
  66. let formData = new FormData()
  67. let input = document.createElement('input')
  68. input.setAttribute('type', 'file')
  69. input.setAttribute('name', 'files')
  70. input.click()
  71. input.onchange = function () {
  72. file = input.files[0]
  73. formData.append('files', file)
  74. requestEvent({
  75. formData,
  76. success: options.success,
  77. fail: options.fail,
  78. progress: options.progress
  79. })
  80. }
  81. }
  82. /**
  83. * 上传操作
  84. * @param {object} options 上传参数
  85. */
  86. function uploadAction( options ) {
  87. if ( !UtilType.isJSON( options ) ) {
  88. console.log( 'upload options is null' )
  89. return
  90. }
  91. let _options = {}
  92. _options.success = UtilType.isFunction(options.success) ? options.success : function() {}
  93. _options.fail = UtilType.isFunction(options.fail) ? options.fail : function() {}
  94. _options.progress = UtilType.isFunction(options.progress) ? options.progress : function() {}
  95. uploadEvent(_options)
  96. }
  97. })()

运行效果

images/upload-async-result