├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── copy.js ├── dist ├── LICENSE.txt ├── package.json ├── qiniu4js.es.js ├── qiniu4js.es.js.map ├── qiniu4js.js ├── qiniu4js.js.map ├── qiniu4js.min.js ├── qiniu4js.min.js.map └── types │ ├── Main.d.ts │ ├── upload │ ├── TokenFunc.d.ts │ ├── Uploader.d.ts │ ├── UploaderBuilder.d.ts │ ├── hook │ │ ├── SimpleUploadListener.d.ts │ │ └── UploadListener.d.ts │ ├── interceptor │ │ ├── SimpleUploadInterceptor.d.ts │ │ └── UploadInterceptor.d.ts │ ├── pattren │ │ ├── ChunkUploadPattern.d.ts │ │ ├── DirectUploadPattern.d.ts │ │ └── IUploadPattern.d.ts │ ├── task │ │ ├── BaseTask.d.ts │ │ ├── ChunkTask.d.ts │ │ └── DirectTask.d.ts │ ├── url │ │ └── Domain.d.ts │ └── uuid │ │ └── UUID.d.ts │ └── util │ ├── Log.d.ts │ ├── Polyfill.d.ts │ └── isWechat.d.ts ├── index.html ├── package-lock.json ├── package.json ├── src ├── Main.ts ├── upload │ ├── TokenFunc.ts │ ├── Uploader.ts │ ├── UploaderBuilder.ts │ ├── hook │ │ ├── SimpleUploadListener.ts │ │ └── UploadListener.ts │ ├── interceptor │ │ ├── SimpleUploadInterceptor.ts │ │ └── UploadInterceptor.ts │ ├── pattren │ │ ├── ChunkUploadPattern.ts │ │ ├── DirectUploadPattern.ts │ │ └── IUploadPattern.ts │ ├── task │ │ ├── BaseTask.ts │ │ ├── ChunkTask.ts │ │ └── DirectTask.ts │ ├── url │ │ └── Domain.ts │ └── uuid │ │ └── UUID.ts └── util │ ├── Log.ts │ ├── Polyfill.ts │ └── isWechat.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.ejs text eol=lf 12 | *.js text eol=lf 13 | *.md text eol=lf 14 | *.txt text eol=lf 15 | *.json text eol=lf 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | .idea 4 | node_modules 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2016 lsxiao. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qiniu4js 2 | TypeScript编写,不依赖任何三方库。 3 | 4 | ## 已完成 5 | 6 | - [x] 文件直传 7 | - [x] 分块上传 8 | - [x] 多文件上传 9 | - [x] token共享 10 | - [x] 自动重传 11 | - [x] 任务拦截器 12 | - [x] 文件过滤 13 | - [x] 多实例(可以创建多个上传实例,互不影响) 14 | - [x] 图片缩放 15 | - [x] 图片质量压缩 16 | 17 | ## 待完成 18 | - [ ] 完善文档 19 | - [ ] 自定义变量 20 | - [ ] 使用七牛API进行图片处理 21 | 22 | ## 安装 23 | 24 | ```bash 25 | sudo npm install qiniu4js --save 26 | ``` 27 | 28 | ## 导入 29 | 30 | ### 浏览器 31 | ```html 32 | 33 | 36 | ``` 37 | ### es6 38 | ```javascript 39 | import {UploaderBuilder,Uploader} from 'qiniu4js'; 40 | let uploader = new UploaderBuilder().build(); 41 | ``` 42 | 43 | ### CommonJS 44 | ```javascript 45 | var Qiniu = require('qiniu4js'); 46 | var uploader = new Qiniu.UploaderBuilder().build(); 47 | ``` 48 | 49 | ## 使用方法 50 | 51 | 52 | ```javascript 53 | //es6 54 | //构建uploader实例 55 | let uploader = new UploaderBuilder() 56 | .debug(false)//开启debug,默认false 57 | .button('uploadButton')//指定上传按钮 58 | .domain({http: "http://img.yourdomain.com", https: "https://img.yourdomain.com"})//默认为{http: "http://upload.qiniu.com", https: "https://up.qbox.me"} 59 | .scheme("https")//默认从 window.location.protocol 获取,可以通过指定域名为 "http://img.yourdomain.com" 来忽略域名选择。 60 | .retry(0)//设置重传次数,默认0,不重传 61 | .compress(0.5)//默认为1,范围0-1 62 | .scale([200,0])第一个参数是宽度,第二个是高度,[200,0],限定高度,宽度等比缩放.[0,100]限定宽度,高度等比缩放.[200,100]固定长宽 63 | .size(1024*1024)//分片大小,最多为4MB,单位为字节,默认1MB 64 | .chunk(true)//是否分块上传,默认true,当chunk=true并且文件大于4MB才会进行分块上传 65 | .auto(true)//选中文件后立即上传,默认true 66 | .multiple(true)//是否支持多文件选中,默认true 67 | .accept(['.gif','.png','video/*'])//过滤文件,默认无,详细配置见http://www.w3schools.com/tags/att_input_accept.asp 68 | 69 | // 在一次上传队列中,是否分享token,如果为false每上传一个文件都需要请求一次token,默认true。 70 | // 71 | // 如果saveKey中有需要在客户端解析的变量,则忽略该值。 72 | .tokenShare(true) 73 | 74 | // 设置token获取函数,token获取完成后,必须调用`setToken(token);`不然上传任务不会执行。 75 | // 76 | // 覆盖tokenUrl的设置。 77 | .tokenFunc(function (setToken,task) { 78 | setTimeout(function () { 79 | setToken("token"); 80 | }, 1000); 81 | }) 82 | 83 | // 设置token获取URL:客户端向该地址发送HTTP GET请求, 若成功,服务器端返回{"uptoken": 'i-am-token'}。 84 | // 85 | // 覆盖tokenFunc的设置。 86 | .tokenUrl('/qiniu/upload-token') 87 | 88 | // 设置token获取过程是否使用了saveKey,默认false。 89 | // 90 | // 若为true,则listener中的onTaskGetKey不会被调用。 91 | .saveKey(true) 92 | 93 | // 设置tokenUrl请求中的saveKey参数和七牛上传策略中的saveKey字段。 94 | // 95 | // 客户端解析变量(七牛不支持在saveKey中使用这些变量): 96 | // * $(uuid) 97 | // * $(imageInfo.width) $(imageInfo.height) 98 | // 99 | // 如参数中有需要在客户端解析的变量,则忽略tokenShare的设置。 100 | // 101 | // 若设置了,则listener中的onTaskGetKey不会被调用。 102 | // 103 | // 关于saveKey,见https://developer.qiniu.com/kodo/manual/vars 104 | .saveKey('dir1/dir2/$(uuid)_$(imageInfo.width)x$(imageInfo.height)$(ext)') 105 | 106 | //任务拦截器 107 | .interceptor({ 108 | //拦截任务,返回true,任务将会从任务队列中剔除,不会被上传 109 | onIntercept: function (task) { 110 | return task.file.size > 1024 * 1024; 111 | }, 112 | //中断任务,返回true,任务队列将会在这里中断,不会执行上传操作。 113 | onInterrupt: function (task) { 114 | if (this.onIntercept(task)) { 115 | alert("请上传小于1m的文件"); 116 | return true; 117 | } 118 | else { 119 | return false; 120 | } 121 | } 122 | } 123 | //你可以添加多个任务拦截器 124 | .interceptor({...}) 125 | .listener({ 126 | onReady(tasks) { 127 | //该回调函数在图片处理前执行,也就是说task.file中的图片都是没有处理过的 128 | //选择上传文件确定后,该生命周期函数会被回调。 129 | 130 | },onStart(tasks){ 131 | //所有内部图片任务处理后执行 132 | //开始上传 133 | 134 | },onTaskGetKey(task){ 135 | //为每一个上传的文件指定key,如果不指定则由七牛服务器自行处理 136 | return "test.png"; 137 | 138 | },onTaskProgress: function (task) { 139 | //每一个任务的上传进度,通过`task.progress`获取 140 | console.log(task.progress); 141 | 142 | },onTaskSuccess(task){ 143 | //一个任务上传成功后回调 144 | console.log(task.result.key);//文件的key 145 | console.log(task.result.hash);//文件hash 146 | },onTaskFail(task) { 147 | //一个任务在经历重传后依然失败后回调此函数 148 | 149 | },onTaskRetry(task) { 150 | //开始重传 151 | 152 | },onFinish(tasks){ 153 | //所有任务结束后回调,注意,结束不等于都成功,该函数会在所有HTTP上传请求响应后回调(包括重传请求)。 154 | 155 | }} 156 | }).build(); 157 | 158 | 159 | //如果auto设置为false,则需要手动启动上传。 160 | //uploader.start(); 161 | 162 | 163 | //由于安全原因,display:none的file input,无法通过代码调用click方法,必须通过如下处理,让用户来实现click,从而打开文件选择窗口: 164 | 165 | //你可以自行监听HTML元素的click事件,在回调函数内部打开文件选择窗口。你也可以使用jQuery监听,下面使用的是原生的JavaScript的方式。 166 | button = document.getElementById('button'); 167 | button.addEventListener("click", function () { 168 | uploader.chooseFile(); 169 | } 170 | ``` 171 | 172 | 173 | ## 版本说明 174 | - 1.0.10 (2017-3-4) 175 | - fix https://github.com/lsxiao/qiniu4js/issues/21 176 | - fix https://github.com/lsxiao/qiniu4js/issues/18 177 | - 1.0.8 (2017-2-7) 178 | - button方法,可以指定上传按钮,传入id 179 | 180 | - 1.0.7 (2016-11-28) 181 | - scheme方法,可选上传协议。 182 | 183 | - 1.0.6 (2016-11-25) 184 | - 修复微信x5内核不支持Object.assign的问题,添加Object.assign polyfill. 185 | - 微信x5 对input.accept属性做了限制,如果要上传图片请使用accept(['image/*']) 186 | - Canvas.toBlob polyfill 187 | - String.endsWith polyfill 188 | 189 | - 1.0.3 (2016-11-22) 190 | - fix typescript编译出错导致无法被uglify。 191 | 192 | - 1.0.1 (2016-11-18) 193 | - 图片缩放后,无法分块上传的问题。 194 | 195 | - 1.0.0 (2016-11-9) 196 | - scale没有默认值的问题,正式版本发布。 197 | 198 | - 0.0.11 (2016-11-9) 199 | - 图片质量压缩和宽高缩放,修复progress在分块上传的时候相同值可能会多次重复出现的问题。 200 | 201 | - 0.0.10 (2016-10-21) 202 | - 修复JSON parse失败导致的task.result为false的问题。 203 | 204 | - 0.0.9 (2016-10-21) 205 | - 分块上传,自定义上传域名 206 | 207 | - 0.0.8 (2016-10-19) 208 | - fix bug,当没有选中任何文件的时候,会触发上传函数。 209 | 210 | - 0.0.7 (2016-10-19) 211 | - 更换上传域名为upload.qiniu.com 212 | 213 | - 0.0.6 (2016-10-19) 214 | - 绕过缓存,避免每次都上传同样的文件。 215 | 216 | - 0.0.5 (2016-10-19) 217 | - tokenFunc(setToken,task)增加task参数 218 | 219 | - 0.0.4 (2016-10-19) 220 | - 修复了一个关于accept选项的bug。 221 | 222 | - 0.0.3 (2016-10-19) 223 | - 任务拦截器实现。 224 | 225 | - 0.0.2 (2016-10-19) 226 | - 基本功能的实现。 227 | 228 | 229 | 230 | ## 维护人 231 | 知乎 : [@面条](https://www.zhihu.com/people/lsxiao) 232 | 233 | Github : [@lsxiao](https://github.com/lsxiao) 234 | 235 | 236 | ## 开源许可 237 | MIT 238 | -------------------------------------------------------------------------------- /copy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const pkg = require('./package.json') 4 | 5 | delete pkg.private 6 | delete pkg.devDependencies 7 | delete pkg.scripts 8 | delete pkg.eslintConfig 9 | delete pkg.babel 10 | fs.writeFileSync('dist/package.json', JSON.stringify(pkg, null, ' '), 'utf-8') 11 | fs.writeFileSync('dist/LICENSE.txt', fs.readFileSync('LICENSE.txt', 'utf-8'), 'utf-8') -------------------------------------------------------------------------------- /dist/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2016 lsxiao. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qiniu4js", 3 | "version": "1.0.13", 4 | "description": "Maybe is the best js sdk for qiniu", 5 | "homepage": "https://www.gihub.com/lsxiao/qiniu4js", 6 | "repository": "lsxiao/qiniu4js", 7 | "author": "lsxiao ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "qiniu", 11 | "js", 12 | "sdk" 13 | ], 14 | "types": "./types/Main.d.ts", 15 | "main": "qiniu4js.js", 16 | "jsnext:main": "qiniu4js.es.js", 17 | "bugs": { 18 | "url": "https://github.com/lsxiao/qiniu4js/issues" 19 | } 20 | } -------------------------------------------------------------------------------- /dist/qiniu4js.es.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"qiniu4js.es.js","sources":["webpack:///webpack/universalModuleDefinition","webpack:///webpack/bootstrap 5d31703e51cd4702f6cb","webpack:///./src/util/Log.ts","webpack:///./src/upload/Uploader.ts","webpack:///./src/upload/UploaderBuilder.ts","webpack:///./src/upload/task/BaseTask.ts","webpack:///./src/Main.ts","webpack:///./src/upload/hook/SimpleUploadListener.ts","webpack:///./src/upload/interceptor/SimpleUploadInterceptor.ts","webpack:///./src/upload/pattren/ChunkUploadPattern.ts","webpack:///./src/upload/pattren/DirectUploadPattern.ts","webpack:///./src/upload/task/ChunkTask.ts","webpack:///./src/upload/task/DirectTask.ts","webpack:///./src/upload/uuid/UUID.ts","webpack:///./src/util/Polyfill.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"Qiniu\"] = factory();\n\telse\n\t\troot[\"Qiniu\"] = factory();\n})(this, function() {\nreturn \n\n\n// WEBPACK FOOTER //\n// webpack/universalModuleDefinition"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 4);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 5d31703e51cd4702f6cb","class Log {\n static get enable() {\n return this._enable;\n }\n static set enable(value) {\n this._enable = value;\n }\n static d(object) {\n if (!Log._enable) {\n return;\n }\n console.debug(object);\n }\n\n static l(object) {\n if (!Log._enable) {\n return;\n }\n console.log(object);\n }\n static e(object) {\n if (!Log._enable) {\n return;\n }\n console.error(object);\n }\n static w(object) {\n if (!Log._enable) {\n return;\n }\n console.warn(object);\n }\n static i(object) {\n if (!Log._enable) {\n return;\n }\n console.info(object);\n }\n}\nLog._enable = false;\nexport default Log;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/util/Log.ts\n// module id = 0\n// module chunks = 0","import DirectTask from \"./task/DirectTask\";\nimport { ChunkTask } from \"./task/ChunkTask\";\nimport UUID from \"./uuid/UUID\";\nimport UploaderBuilder from \"./UploaderBuilder\";\nimport log from \"../util/Log\";\nimport SimpleUploadListener from \"./hook/SimpleUploadListener\";\nimport DirectUploadPattern from \"./pattren/DirectUploadPattern\";\nimport ChunkUploadPattern from \"./pattren/ChunkUploadPattern\";\nimport \"../util/Polyfill\";\nclass Uploader {\n constructor(builder) {\n this.FILE_INPUT_EL_ID = 'qiniu4js-input';\n this._taskQueue = []; //任务队列\n this._tasking = false; //任务执行中\n this._scale = []; //缩放大小,限定高度等比缩放[h:200,w:0],限定宽度等比缩放[h:0,w:100],限定长宽[h:200,w:100]\n this._saveKey = false;\n /**\n * 处理文件\n */\n this.handleFiles = () => {\n //如果没有选中文件就返回\n if (this.fileInput.files.length == 0) {\n return;\n }\n //生成task\n this.generateTask();\n //是否中断任务\n let isInterrupt = false;\n let interceptedTasks = [];\n //任务拦截器过滤\n for (let task of this.taskQueue) {\n for (let interceptor of this.interceptors) {\n //拦截生效\n if (interceptor.onIntercept(task, this.taskQueue)) {\n interceptedTasks.push(task);\n log.d(\"任务拦截器拦截了任务:\");\n log.d(task);\n }\n //打断生效\n if (interceptor.onInterrupt(task, this.taskQueue)) {\n //将打断标志位设为true\n isInterrupt = true;\n break;\n }\n }\n }\n if (isInterrupt) {\n log.w(\"任务拦截器中断了任务队列\");\n return;\n }\n //从任务队列中去除任务\n for (let task of interceptedTasks) {\n let index = this.taskQueue.indexOf(task);\n if (index != -1) {\n this.taskQueue.splice(index, 1);\n }\n }\n //回调函数函数\n this.listener.onReady(this.taskQueue);\n //处理图片\n this.handleImages().then(() => {\n //自动上传\n if (this.auto) {\n log.d(\"开始自动上传\");\n this.start();\n }\n });\n };\n this.resolveUUID = s => {\n let re = /\\$\\(uuid\\)/;\n if (re.test(s)) {\n return s.replace(re, UUID.uuid());\n }\n return s;\n };\n this.resolveImageInfo = (blob, s) => {\n let widthRe = /\\$\\(imageInfo\\.width\\)/;\n let heightRe = /\\$\\(imageInfo\\.height\\)/;\n if (!widthRe.test(s) && !heightRe.test(s)) {\n return Promise.resolve(s);\n }\n return new Promise(resolve => {\n let img = new Image();\n img.src = URL.createObjectURL(blob);\n img.onload = () => {\n s = s.replace(widthRe, img.width.toString());\n s = s.replace(heightRe, img.height.toString());\n resolve(s);\n };\n });\n };\n this.onSaveKeyResolved = saveKey => {\n this._tokenShare = this._tokenShare && this._saveKey == saveKey;\n return saveKey;\n };\n this._retry = builder.getRetry;\n this._size = builder.getSize;\n this._chunk = builder.getChunk;\n this._auto = builder.getAuto;\n this._multiple = builder.getMultiple;\n this._accept = builder.getAccept;\n this._button = builder.getButton;\n this._buttonEventName = builder.getButtonEventName;\n this._compress = builder.getCompress;\n this._scale = builder.getScale;\n this._saveKey = builder.getSaveKey;\n this._tokenFunc = builder.getTokenFunc;\n this._tokenShare = builder.getTokenShare;\n this._listener = Object.assign(new SimpleUploadListener(), builder.getListener);\n this._interceptors = builder.getInterceptors;\n this._domain = builder.getDomain;\n this._fileInputId = `${this.FILE_INPUT_EL_ID}_${new Date().getTime()}`;\n log.enable = builder.getIsDebug;\n this.validateOptions();\n this.init();\n }\n /**\n * 初始化操作\n */\n init() {\n this.initFileInputEl();\n }\n /**\n * 初始化file input element\n */\n initFileInputEl() {\n //查询已经存在的file input\n let exist = document.getElementById(this._fileInputId);\n //创建input元素\n this._fileInput = exist ? exist : document.createElement('input');\n this.fileInput.type = 'file'; //type file\n this.fileInput.id = this._fileInputId; //id 方便后面查找\n this.fileInput.style.display = 'none'; //隐藏file input\n //多文件\n if (this.multiple) {\n //多文件\n this.fileInput.multiple = true;\n }\n //文件类型\n if (this.accept && this.accept.length != 0) {\n let acceptValue = '';\n for (let value of this.accept) {\n acceptValue += value;\n acceptValue += ',';\n }\n if (acceptValue.endsWith(',')) {\n acceptValue = acceptValue.substring(0, acceptValue.length - 1);\n }\n this.fileInput.accept = acceptValue;\n log.d(`accept类型 ${acceptValue}`);\n }\n //将input元素添加到body子节点的末尾\n document.body.appendChild(this.fileInput);\n //选择文件监听器\n this.fileInput.addEventListener('change', this.handleFiles, false);\n if (this._button != undefined) {\n let button = document.getElementById(this._button);\n button.addEventListener(this._buttonEventName, this.chooseFile.bind(this));\n }\n }\n /**\n * 上传完成或者失败后,对本次上传任务进行清扫\n */\n resetUploader() {\n log.d(\"开始重置 uploader\");\n this.taskQueue.length = 0;\n log.d(\"任务队列已清空\");\n this._token = null;\n log.d(\"token已清空\");\n log.d(\"uploader 重置完毕\");\n }\n /**\n * 是否是分块任务\n * @param task\n * @returns {boolean}\n */\n static isChunkTask(task) {\n return task.constructor.name === ChunkTask.name && task instanceof ChunkTask;\n }\n /**\n * 是否是直传任务\n * @param task\n * @returns {boolean}\n */\n static isDirectTask(task) {\n return task.constructor.name === DirectTask.name && task instanceof DirectTask;\n }\n /**\n * 生成task\n */\n generateTask() {\n this.resetUploader();\n let files = this.fileInput.files;\n //遍历files 创建上传任务\n for (let i = 0; i < this.fileInput.files.length; i++) {\n let file = files[i];\n let task;\n //只有在开启分块上传,并且文件大小大于4mb的时候才进行分块上传\n if (this.chunk && file.size > UploaderBuilder.BLOCK_SIZE) {\n task = new ChunkTask(file, UploaderBuilder.BLOCK_SIZE, this.size);\n } else {\n task = new DirectTask(file);\n }\n if (this._saveKey == false) {\n task.key = this.listener.onTaskGetKey(task);\n }\n this.taskQueue.push(task);\n }\n }\n /**\n * 处理图片-缩放-质量压缩\n */\n handleImages() {\n let promises = [];\n if (this.compress != 1 || this.scale[0] != 0 || this.scale[1] != 0) {\n for (let task of this.taskQueue) {\n if (!task.file.type.match('image.*')) {\n continue;\n }\n log.d(`${task.file.name} 处理前的图片大小:${task.file.size / 1024} kb`);\n let canvas = document.createElement('canvas');\n let img = new Image();\n let ctx = canvas.getContext('2d');\n img.src = URL.createObjectURL(task.file);\n let _this = this;\n promises.push(new Promise(resolve => img.onload = () => {\n let imgW = img.width;\n let imgH = img.height;\n let scaleW = _this.scale[0];\n let scaleH = _this.scale[1];\n if (scaleW == 0 && scaleH > 0) {\n canvas.width = imgW / imgH * scaleH;\n canvas.height = scaleH;\n } else if (scaleH == 0 && scaleW > 0) {\n canvas.width = scaleW;\n canvas.height = imgH / imgW * scaleW;\n } else if (scaleW > 0 && scaleH > 0) {\n canvas.width = scaleW;\n canvas.height = scaleH;\n } else {\n canvas.width = img.width;\n canvas.height = img.height;\n }\n //这里的长宽是绘制到画布上的图片的长宽\n ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n console.log(canvas);\n console.log(canvas.toBlob);\n //0.95是最接近原图大小,如果质量为1的话会导致比原图大几倍。\n canvas.toBlob(blob => {\n resolve(blob);\n log.d(`${task.file.name} 处理后的图片大小:${blob.size / 1024} kb`);\n }, \"image/jpeg\", _this.compress * 0.95);\n }).then(blob => {\n blob.name = task.file.name;\n task.file = blob;\n if (Uploader.isChunkTask(task)) {\n task.spliceFile2Block();\n }\n }));\n }\n }\n return Promise.all(promises);\n }\n /**\n * 检验选项合法性\n */\n validateOptions() {\n log.d(\"开始检查构建参数合法性\");\n if (!this._tokenFunc) {\n throw new Error('你必须提供一个获取Token的回调函数');\n }\n if (!this.scale || !(this.scale instanceof Array) || this.scale.length != 2 || this.scale[0] < 0 || this.scale[1] < 0) {\n throw new Error('scale必须是长度为2的number类型的数组,scale[0]为宽度,scale[1]为长度,必须大于等于0');\n }\n log.d(\"构建参数检查完毕\");\n }\n /**\n * 开始上传\n */\n start() {\n log.d(`上传任务遍历开始`);\n if (this.fileInput.files.length == 0) {\n throw new Error('没有选中的文件,无法开始上传');\n }\n if (this.tasking) {\n throw new Error('任务执行中,请不要重复上传');\n }\n this.listener.onStart(this.taskQueue);\n //遍历任务队列\n for (let task of this.taskQueue) {\n log.d(`上传文件名:${task.file.name}`);\n log.d(`上传文件大小:${task.file.size}字节,${task.file.size / 1024} kb,${task.file.size / 1024 / 1024} mb`);\n //根据任务的类型调用不同的上传模式进行上传\n if (Uploader.isDirectTask(task)) {\n log.d('该上传任务为直传任务');\n //直传\n new DirectUploadPattern(this).upload(task);\n } else if (Uploader.isChunkTask(task)) {\n log.d('该上传任务为分片任务');\n //分块上传\n new ChunkUploadPattern(this).upload(task);\n } else {\n throw new Error('非法的task类型');\n }\n }\n }\n /**\n * 所有任务是否完成\n * @returns {boolean}\n */\n isTaskQueueFinish() {\n for (let task of this.taskQueue) {\n if (!task.isFinish) {\n return false;\n }\n }\n return true;\n }\n /**\n * 选择文件\n */\n chooseFile() {\n this.fileInput.click();\n }\n getToken(task) {\n if (this._tokenShare && this._token != undefined) {\n return Promise.resolve(this._token);\n }\n log.d(`开始获取上传token`);\n return Promise.resolve(this._tokenFunc(this, task)).then(token => {\n log.d(`上传token获取成功: ${token}`);\n this._token = token;\n return token;\n });\n }\n requestTaskToken(task, url) {\n return this.resolveSaveKey(task).then(saveKey => {\n return this.requestToken(url, saveKey);\n });\n }\n requestToken(url, saveKey) {\n return new Promise((resolve, reject) => {\n if (typeof saveKey == \"string\") {\n url += (/\\?/.test(url) ? \"&\" : \"?\") + \"saveKey=\" + encodeURIComponent(saveKey);\n }\n url += (/\\?/.test(url) ? \"&\" : \"?\") + new Date().getTime();\n let xhr = new XMLHttpRequest();\n xhr.open('GET', url, true);\n xhr.onreadystatechange = () => {\n if (xhr.readyState != XMLHttpRequest.DONE) {\n return;\n }\n if (xhr.status == 200) {\n resolve(xhr.response.uptoken);\n return;\n }\n reject(xhr.response);\n };\n xhr.onabort = () => {\n reject('aborted');\n };\n xhr.responseType = 'json';\n xhr.send();\n });\n }\n resolveSaveKey(task) {\n let saveKey = this._saveKey;\n if (typeof saveKey != \"string\") {\n return Promise.resolve(undefined);\n }\n return Promise.resolve(saveKey).then(this.resolveUUID).then(saveKey => this.resolveImageInfo(task.file, saveKey)).then(this.onSaveKeyResolved);\n }\n get retry() {\n return this._retry;\n }\n get size() {\n return this._size;\n }\n get auto() {\n return this._auto;\n }\n get multiple() {\n return this._multiple;\n }\n get accept() {\n return this._accept;\n }\n get compress() {\n return this._compress;\n }\n get scale() {\n return this._scale;\n }\n get listener() {\n return this._listener;\n }\n get fileInput() {\n return this._fileInput;\n }\n get chunk() {\n return this._chunk;\n }\n get taskQueue() {\n return this._taskQueue;\n }\n get tasking() {\n return this._tasking;\n }\n set tasking(value) {\n this._tasking = value;\n }\n get interceptors() {\n return this._interceptors;\n }\n get domain() {\n return this._domain;\n }\n}\nexport default Uploader;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/Uploader.ts\n// module id = 1\n// module chunks = 0","import Uploader from \"./Uploader\";\nimport SimpleUploadInterceptor from \"./interceptor/SimpleUploadInterceptor\";\n/**\n * UploaderBuilder\n *\n */\nclass UploaderBuilder {\n constructor() {\n this._retry = 0; //最大重试次数\n this._domain = UploaderBuilder.UPLOAD_DOMAIN; //上传域名\n this._scheme = null; //上传域名的 scheme\n this._size = 1024 * 1024; //分片大小,单位字节,上限4m,不能为0\n this._chunk = true; //分块上传\n this._auto = true; //自动上传,每次选择文件后\n this._multiple = true; //是否支持多文件\n this._accept = []; //接受的文件类型\n this._compress = 1; //图片压缩质量\n this._scale = [0, 0]; //缩放大小,限定高度等比[h:200,w:0],限定宽度等比[h:0,w:100],限定长宽[h:200,w:100]\n this._saveKey = false;\n this._tokenShare = true; //分享token,如果为false,每一次HTTP请求都需要新获取Token\n this._interceptors = []; //任务拦截器\n this._isDebug = false; //\n }\n /**\n * 设置上传的域名,默认是 {http: 'http://upload.qiniu.com', https: 'https://up.qbox.me'}\n * @param domain\n * @returns {UploaderBuilder}\n */\n domain(domain) {\n this._domain = domain;\n return this;\n }\n /**\n * 设置上传域名的协议类型,默认从 window.location.protocol 读取\n * @param scheme\n * @returns {UploaderBuilder}\n */\n scheme(scheme) {\n this._scheme = scheme;\n return this;\n }\n /**\n * 添加一个拦截器\n * @param interceptor\n * @returns {UploaderBuilder}\n */\n interceptor(interceptor) {\n this._interceptors.push(Object.assign(new SimpleUploadInterceptor(), interceptor));\n return this;\n }\n /**\n * 上传失败后的重传尝试次数\n * @param retry 默认0次,不尝试次重传\n * @returns {UploaderBuilder}\n */\n retry(retry) {\n this._retry = retry;\n return this;\n }\n /**\n * 设置分片大小\n * @param size 分块大小,单位字节,默认4*1024*1024字节(4mb)\n * @returns {UploaderBuilder}\n */\n size(size) {\n this._size = Math.min(Math.max(size, 1), UploaderBuilder.MAX_CHUNK_SIZE);\n return this;\n }\n /**\n * 选择文件后,是否自动上传\n * @param auto 默认true\n * @returns {UploaderBuilder}\n */\n auto(auto) {\n this._auto = auto;\n return this;\n }\n /**\n * 是否支持多文件选择\n * @param multiple 默认true\n * @returns {UploaderBuilder}\n */\n multiple(multiple) {\n this._multiple = multiple;\n return this;\n }\n /**\n * 接受上传的文件类型\n * @param accept 数组形式例如:['.png','video/*']\n *\n * 详细配置见http://www.w3schools.com/tags/att_input_accept.asp\n *\n * @returns {UploaderBuilder}\n */\n accept(accept) {\n this._accept = accept;\n return this;\n }\n /**\n * 设置上传按钮\n * @param button 上传按钮ID\n * @param eventName 上传按钮的监听事件名称,默认为 \"click\" 。\n * @returns {UploaderBuilder}\n */\n button(button, eventName = \"click\") {\n this._button = button;\n this._buttonEventName = eventName;\n return this;\n }\n /**\n * 图片质量压缩,只在上传的文件是图片的时候有效\n * @param compress 0-1,默认1,不压缩\n * @returns {UploaderBuilder}\n */\n compress(compress) {\n this._compress = Math.max(Math.min(compress, 1), 0);\n return this;\n }\n /**\n * 图片缩放\n * @returns {UploaderBuilder}\n * @param scale\n */\n scale(scale) {\n this._scale = scale;\n return this;\n }\n /**\n * 设置 saveKey\n * @param saveKey\n * @returns {UploaderBuilder}\n */\n saveKey(saveKey) {\n this._saveKey = saveKey;\n return this;\n }\n /**\n * 获取Token的地址\n * @param tokenUrl\n * @returns {UploaderBuilder}\n */\n tokenUrl(tokenUrl) {\n this._tokenFunc = (uploader, task) => {\n return uploader.requestTaskToken(task, tokenUrl);\n };\n return this;\n }\n /**\n * 获取Token的函数\n * @param tokenFunc\n * @returns {UploaderBuilder}\n */\n tokenFunc(tokenFunc) {\n this._tokenFunc = (uploader, task) => {\n return new Promise(resolve => {\n tokenFunc(resolve, task);\n });\n };\n return this;\n }\n /**\n * 上传生命周期钩子\n * @param listener\n * @returns {UploaderBuilder}\n */\n listener(listener) {\n this._listener = listener;\n return this;\n }\n /**\n * 是否分享token,如果为false每上传一个文件都需要请求一次Token。\n * @param tokenShare\n * @returns {UploaderBuilder}\n */\n tokenShare(tokenShare) {\n this._tokenShare = tokenShare;\n return this;\n }\n /**\n * 是否分块上传\n * @param chunk 默认false\n * @returns {UploaderBuilder}\n */\n chunk(chunk) {\n this._chunk = chunk;\n return this;\n }\n /**\n * 是否开启debug模式\n * @param debug 默认false\n * @returns {UploaderBuilder}\n */\n debug(debug) {\n this._isDebug = debug;\n return this;\n }\n get getRetry() {\n return this._retry;\n }\n get getSize() {\n return this._size;\n }\n get getAuto() {\n return this._auto;\n }\n get getMultiple() {\n return this._multiple;\n }\n get getAccept() {\n return this._accept;\n }\n get getButton() {\n return this._button;\n }\n get getButtonEventName() {\n return this._buttonEventName;\n }\n get getCompress() {\n return this._compress;\n }\n get getScale() {\n return this._scale;\n }\n get getListener() {\n return this._listener;\n }\n get getSaveKey() {\n return this._saveKey;\n }\n get getTokenFunc() {\n return this._tokenFunc;\n }\n get getTokenShare() {\n return this._tokenShare;\n }\n get getChunk() {\n return this._chunk;\n }\n get getIsDebug() {\n return this._isDebug;\n }\n get getInterceptors() {\n return this._interceptors;\n }\n get getDomain() {\n let domain = this._domain;\n if (!domain) {\n domain = UploaderBuilder.UPLOAD_DOMAIN;\n }\n if (typeof domain != \"string\") {\n let scheme = this._scheme;\n if (typeof scheme != \"string\") {\n let protocol = window.location.protocol;\n scheme = protocol.substring(0, protocol.length - 1);\n }\n domain = domain[scheme];\n }\n return domain.endsWith('/') ? domain.substring(0, domain.length - 1) : domain;\n }\n build() {\n return new Uploader(this);\n }\n}\nUploaderBuilder.MAX_CHUNK_SIZE = 4 * 1024 * 1024; //分片最大值\nUploaderBuilder.BLOCK_SIZE = UploaderBuilder.MAX_CHUNK_SIZE; //分块大小,只有大于这个数才需要分块\nUploaderBuilder.UPLOAD_DOMAIN = { http: 'http://upload.qiniu.com', https: 'https://up.qbox.me' };\nexport default UploaderBuilder;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/UploaderBuilder.ts\n// module id = 2\n// module chunks = 0","/**\n * 上传任务\n */\nclass BaseTask {\n constructor(file) {\n this._retry = 0; //已重试次数\n this._progress = 0; //任务进度,最大100\n this._isSuccess = false; //是否上传成功\n this._isFinish = false; //是否结束\n this._file = file;\n this._createDate = new Date();\n }\n get file() {\n return this._file;\n }\n set file(file) {\n this._file = file;\n }\n get retry() {\n return this._retry;\n }\n set retry(value) {\n this._retry = value;\n }\n get createDate() {\n return this._createDate;\n }\n set createDate(value) {\n this._createDate = value;\n }\n get startDate() {\n return this._startDate;\n }\n set startDate(value) {\n this._startDate = value;\n }\n get endDate() {\n return this._endDate;\n }\n set endDate(value) {\n this._endDate = value;\n }\n get isSuccess() {\n return this._isSuccess;\n }\n set isSuccess(value) {\n this._isSuccess = value;\n }\n get progress() {\n return this._progress;\n }\n set progress(value) {\n this._progress = Math.min(Math.max(0, value), 100);\n }\n get result() {\n return this._result;\n }\n set result(value) {\n this._result = value;\n }\n get error() {\n return this._error;\n }\n set error(value) {\n this._error = value;\n }\n get key() {\n return this._key;\n }\n set key(value) {\n this._key = value;\n }\n get isFinish() {\n return this._isFinish;\n }\n set isFinish(value) {\n this._isFinish = value;\n }\n}\nexport default BaseTask;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/task/BaseTask.ts\n// module id = 3\n// module chunks = 0","import Uploader from \"./upload/Uploader\";\nimport UploaderBuilder from \"./upload/UploaderBuilder\";\nexport { Uploader, UploaderBuilder };\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/Main.ts\n// module id = 4\n// module chunks = 0","class SimpleUploadListener {\n onReady(taskQueue) {}\n onStart(taskQueue) {}\n onTaskProgress(task) {}\n onTaskGetKey(task) {\n return null;\n }\n onTaskFail(task) {}\n onTaskSuccess(task) {}\n onTaskRetry(task) {}\n onFinish(taskQueue) {}\n}\nexport default SimpleUploadListener;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/hook/SimpleUploadListener.ts\n// module id = 5\n// module chunks = 0","class SimpleUploadInterceptor {\n onIntercept(task) {\n return false;\n }\n onInterrupt(task) {\n return false;\n }\n}\nexport default SimpleUploadInterceptor;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/interceptor/SimpleUploadInterceptor.ts\n// module id = 6\n// module chunks = 0","import log from \"../../util/Log\";\n/**\n * 分块上传\n */\nclass ChunkUploadPattern {\n constructor(uploader) {\n this.uploader = uploader;\n }\n init(uploader) {\n this.uploader = uploader;\n }\n upload(task) {\n this.task = task;\n this.uploader.getToken(task).then(token => {\n task.startDate = new Date();\n this.uploadBlock(token);\n });\n }\n uploadBlock(token) {\n log.d(`准备开始上传块`);\n let chain = Promise.resolve();\n log.d(`共${this.task.blocks.length}块等待上传`);\n log.d(`共${this.task.totalChunkCount}分片等待上传`);\n this.task.blocks.forEach((block, blockIndex) => {\n block.chunks.forEach((chunk, chunkIndex) => {\n chain = chain.then(() => {\n log.d(`开始上传第${blockIndex + 1}块,第${chunkIndex + 1}片`);\n return this.uploadChunk(chunk, token);\n });\n });\n });\n chain.then(() => {\n return this.concatChunks(token);\n }).then(() => {\n //所有任务都结束了\n if (this.uploader.isTaskQueueFinish()) {\n log.d(`上传任务队列已结束`);\n //更改任务执行中标志\n this.uploader.tasking = false;\n //监听器调用\n this.uploader.listener.onFinish(this.uploader.taskQueue);\n }\n }).catch(response => {\n log.w(`${this.task.file.name}分块上传失败`);\n this.task.error = response;\n this.task.isSuccess = false;\n this.task.isFinish = true;\n this.task.endDate = new Date();\n this.uploader.listener.onTaskFail(this.task);\n });\n }\n uploadChunk(chunk, token) {\n return new Promise((resolve, reject) => {\n let isFirstChunkInBlock = chunk.block.chunks.indexOf(chunk) == 0;\n let chunkIndex = chunk.block.chunks.indexOf(chunk);\n //前一个chunk,如果存在的话\n let prevChunk = isFirstChunkInBlock ? null : chunk.block.chunks[chunkIndex - 1];\n let url = isFirstChunkInBlock ? this.getUploadBlockUrl(chunk.block.data.size) : this.getUploadChunkUrl(chunk.start, prevChunk ? prevChunk.ctx : null, prevChunk ? prevChunk.host : null);\n let xhr = new XMLHttpRequest();\n xhr.open('POST', url += (/\\?/.test(url) ? \"&\" : \"?\") + new Date().getTime(), true);\n xhr.setRequestHeader('Content-Type', 'application/octet-stream'); //设置contentType\n xhr.setRequestHeader('Authorization', `UpToken ${token}`); //添加token验证头\n //分片上传中\n xhr.upload.onprogress = e => {\n if (e.lengthComputable) {\n let progress = Math.round((this.task.finishedBlocksSize + chunk.start + e.loaded) / this.task.file.size * 100);\n if (this.task.progress < progress) {\n this.task.progress = progress;\n this.uploader.listener.onTaskProgress(this.task);\n }\n }\n };\n //分片上传完成\n xhr.upload.onload = () => {\n let progress = Math.round((this.task.finishedBlocksSize + chunk.start + chunk.data.size) / this.task.file.size * 100);\n if (this.task.progress < progress) {\n this.task.progress = progress;\n this.uploader.listener.onTaskProgress(this.task);\n }\n };\n //响应返回\n xhr.onreadystatechange = () => {\n if (xhr.readyState == XMLHttpRequest.DONE) {\n if (xhr.status == 200 && xhr.responseText != '') {\n let result = JSON.parse(xhr.responseText);\n chunk.isFinish = true;\n chunk.processing = false;\n chunk.ctx = result.ctx;\n chunk.host = result.host;\n let chunkIndex = chunk.block.chunks.indexOf(chunk);\n let hasNextChunkInThisBlock = chunkIndex != chunk.block.chunks.length - 1;\n if (!hasNextChunkInThisBlock) {\n chunk.block.isFinish = true;\n chunk.block.processing = false;\n }\n resolve();\n } else {\n reject(xhr.response);\n }\n }\n };\n xhr.send(chunk.data);\n });\n }\n concatChunks(token) {\n return new Promise((resolve, reject) => {\n let encodedKey = this.task.key ? btoa(this.task.key) : null;\n // 安全字符串 参考:https://developer.qiniu.com/kodo/api/mkfile\n if (encodedKey) {\n encodedKey = encodedKey.replace(/\\+/g, '-');\n encodedKey = encodedKey.replace(/\\//g, '_');\n }\n let url = this.getMakeFileUrl(this.task.file.size, encodedKey);\n //构建所有数据块最后一个数据片上传后得到的的组合成的列表字符串\n let ctxListString = '';\n for (let block of this.task.blocks) {\n let lastChunk = block.chunks[block.chunks.length - 1];\n ctxListString += lastChunk.ctx + ',';\n }\n if (ctxListString.endsWith(',')) {\n ctxListString = ctxListString.substring(0, ctxListString.length - 1);\n }\n let xhr = new XMLHttpRequest();\n xhr.open('POST', url += (/\\?/.test(url) ? \"&\" : \"?\") + new Date().getTime(), true);\n xhr.setRequestHeader('Content-Type', 'text/plain'); //设置contentType\n xhr.setRequestHeader('Authorization', `UpToken ${token}`); //添加token验证头\n xhr.onreadystatechange = () => {\n if (xhr.readyState == XMLHttpRequest.DONE) {\n this.task.isFinish = true;\n if (xhr.status == 200 && xhr.responseText != '') {\n let result = JSON.parse(xhr.responseText);\n this.task.isSuccess = true;\n this.task.result = result;\n this.task.endDate = new Date();\n this.uploader.listener.onTaskSuccess(this.task);\n resolve();\n } else if (this.retryTask(this.task)) {\n log.w(`${this.task.file.name}分块上传失败,准备开始重传`);\n this.uploader.listener.onTaskRetry(this.task);\n } else {\n reject(xhr.response);\n }\n }\n };\n xhr.send(ctxListString);\n });\n }\n /**\n * 获取块上传的url\n * @param blockSize\n * @returns {string}\n */\n getUploadBlockUrl(blockSize) {\n return `${this.uploader.domain}/mkblk/${blockSize}`;\n }\n /**\n * 获取片上传的url\n * @param start 片的在块中的起始位置\n * @param ctx 前一次上传返回的块级上传控制信息。\n * @param host 指定host\n */\n getUploadChunkUrl(start, ctx, host) {\n return `${host ? host : this.uploader.domain}/bput/${ctx}/${start}/`;\n }\n /**\n * 获取合并块为文件的url\n * @param fileSize 文件大小\n * @param encodedKey base64UrlEncode后的资源名称,若未指定,则使用saveKey;若未指定saveKey,则使用资源内容的SHA1值作为资源名。\n * @returns {string}\n */\n getMakeFileUrl(fileSize, encodedKey) {\n if (encodedKey) {\n return `${this.uploader.domain}/mkfile/${fileSize}/key/${encodedKey}`;\n } else {\n return `${this.uploader.domain}/mkfile/${fileSize}`;\n }\n }\n retryTask(task) {\n //达到重试次数\n if (task.retry >= this.uploader.retry) {\n log.w(`${task.file.name}达到重传次数上限${this.uploader.retry},停止重传`);\n return false;\n }\n task.retry++;\n log.w(`${task.file.name}开始重传,当前重传次数${task.retry}`);\n // this.upload(task);\n //todo\n return true;\n }\n}\nexport default ChunkUploadPattern;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/pattren/ChunkUploadPattern.ts\n// module id = 7\n// module chunks = 0","import log from \"../../util/Log\";\n/**\n * 直接上传\n */\nclass DirectUploadPattern {\n constructor(uploader) {\n this.uploader = uploader;\n }\n /**\n * 实现接口的上传方法\n * @param task\n */\n upload(task) {\n this.task = task;\n this.uploader.getToken(task).then(token => {\n task.startDate = new Date();\n this.uploadFile(token);\n });\n }\n /**\n * 创建表单\n * @param token\n * @returns {FormData}\n */\n createFormData(token) {\n let task = this.task;\n let formData = new FormData();\n //key存在,添加到formData中,若不设置,七牛服务器会自动生成hash key\n if (task.key !== null && task.key !== undefined) {\n formData.append('key', task.key);\n }\n formData.append('token', token);\n formData.append('file', task.file);\n log.d(`创建formData对象`);\n return formData;\n }\n /**\n * 上传文件\n * @param token\n */\n uploadFile(token) {\n let task = this.task;\n let xhr = new XMLHttpRequest();\n //上传中\n xhr.upload.onprogress = e => {\n if (e.lengthComputable) {\n let progress = Math.round(e.loaded * 100 / e.total);\n if (task.progress < progress) {\n task.progress = progress;\n this.uploader.listener.onTaskProgress(task);\n }\n }\n };\n //上传完成\n xhr.upload.onload = () => {\n if (task.progress < 100) {\n task.progress = 100;\n this.uploader.listener.onTaskProgress(task);\n }\n };\n let url = this.uploader.domain;\n //避免浏览器缓存http请求\n url += (/\\?/.test(this.uploader.domain) ? \"&\" : \"?\") + new Date().getTime();\n xhr.open('POST', url, true);\n xhr.onreadystatechange = () => {\n if (xhr.readyState == XMLHttpRequest.DONE) {\n if (xhr.status == 200 && xhr.responseText != '') {\n task.result = JSON.parse(xhr.responseText);\n task.isSuccess = true;\n task.isFinish = true;\n task.endDate = new Date();\n this.uploader.listener.onTaskSuccess(task);\n } else if (this.retryTask(task)) {\n log.w(`${task.file.name}上传失败,准备开始重传`);\n this.uploader.listener.onTaskRetry(task);\n } else {\n log.w(`${task.file.name}上传失败`);\n task.error = xhr.response;\n task.isSuccess = false;\n task.isFinish = true;\n task.endDate = new Date();\n this.uploader.listener.onTaskFail(task);\n }\n //所有任务都结束了\n if (this.uploader.isTaskQueueFinish()) {\n log.d('上传队列结束');\n //更改任务执行中标志\n this.uploader.tasking = false;\n //onFinish callback\n this.uploader.listener.onFinish(this.uploader.taskQueue);\n }\n }\n };\n let formData = this.createFormData(token);\n xhr.send(formData);\n log.d('发送ajax post 请求');\n }\n /**\n * 重传\n * @param task\n * @returns {boolean}\n */\n retryTask(task) {\n log.d(\"开始尝试重传\");\n //达到重试次数\n if (task.retry >= this.uploader.retry) {\n log.w(`${task.file.name}达到重传次数上限${this.uploader.retry},停止重传`);\n return false;\n }\n task.retry++;\n log.w(`${task.file.name}开始重传,当前重传次数${task.retry}`);\n this.upload(task);\n return true;\n }\n}\nexport default DirectUploadPattern;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/pattren/DirectUploadPattern.ts\n// module id = 8\n// module chunks = 0","import BaseTask from \"./BaseTask\";\n/**\n * 分块任务\n */\nclass ChunkTask extends BaseTask {\n /**\n * 构造函数\n * @param file\n * @param blockSize 块大小\n * @param chunkSize 片大小\n */\n constructor(file, blockSize, chunkSize) {\n super(file);\n //分块\n this._blocks = [];\n this._blockSize = 0;\n this._chunkSize = 0;\n this._blockSize = blockSize;\n this._chunkSize = chunkSize;\n this.spliceFile2Block();\n }\n /**\n * 将文件分块\n */\n spliceFile2Block() {\n this._blocks = [];\n let fileSize = this._file.size;\n let file = this._file;\n //总块数\n let blockCount = Math.ceil(fileSize / this._blockSize);\n for (let i = 0; i < blockCount; i++) {\n let start = i * this._blockSize; //起始位置\n let end = start + this._blockSize; //结束位置\n //构造一个块实例\n let block = new Block(start, end, file.slice(start, end), this._chunkSize, file);\n //添加到数组中\n this._blocks.push(block);\n }\n }\n /**\n * 获取所有的block\n * @returns {Block[]}\n */\n get blocks() {\n return this._blocks;\n }\n /**\n * 获取正在处理的block\n * @returns {Block}\n */\n get processingBlock() {\n for (let block of this._blocks) {\n if (!block.processing) {\n continue;\n }\n return block;\n }\n throw Error(\"找不到正在处理的Block\");\n }\n get finishedBlocksSize() {\n let size = 0;\n for (let block of this._blocks) {\n size += block.isFinish ? block.data.size : 0;\n }\n return size;\n }\n get chunks() {\n let array = [];\n for (let block of this._blocks) {\n for (let chunk of block.chunks) {\n array.push(chunk);\n }\n }\n return array;\n }\n /**\n * 获取正在处理的chunk\n * @returns {Block}\n */\n get processingChunk() {\n for (let block of this._blocks) {\n if (!block.processing) {\n continue;\n }\n for (let chunk of block.chunks) {\n if (!chunk.processing) {\n continue;\n }\n return chunk;\n }\n }\n throw Error(\"找不到正在处理的Chunk\");\n }\n /**\n * 总共分片数量(所有分块的分片数量总和)\n * @returns {number}\n */\n get totalChunkCount() {\n let count = 0;\n for (let block of this._blocks) {\n count += block.chunks.length;\n }\n return count;\n }\n}\n/**\n * 分块,分块大小七牛固定是4M\n */\nclass Block {\n /**\n *\n * @param start 起始位置\n * @param end 结束位置\n * @param data 块数据\n * @param chunkSize 分片数据的最大大小\n * @param file 分块所属文件\n */\n constructor(start, end, data, chunkSize, file) {\n this._chunks = [];\n this._isFinish = false; //是否上传完成\n this._processing = false; //是否正在上传\n this._data = data;\n this._start = start;\n this._end = end;\n this._file = file;\n this.spliceBlock2Chunk(chunkSize);\n }\n /**\n * 将块分片\n */\n spliceBlock2Chunk(chunkSize) {\n let blockSize = this._data.size;\n let data = this._data;\n //总片数\n let chunkCount = Math.ceil(blockSize / chunkSize);\n for (let i = 0; i < chunkCount; i++) {\n let start = i * chunkSize; //起始位置\n let end = start + chunkSize; //结束位置\n //构造一个片实例\n let chunk = new Chunk(start, end, data.slice(start, end), this);\n //添加到数组中\n this._chunks.push(chunk);\n }\n }\n /**\n * 是否上传中\n * @returns {boolean}\n */\n get processing() {\n return this._processing;\n }\n set processing(value) {\n this._processing = value;\n }\n /**\n * 分块所属的文件\n * @returns {File}\n */\n get file() {\n return this._file;\n }\n /**\n * 是否已经结束\n * @returns {boolean}\n */\n get isFinish() {\n return this._isFinish;\n }\n set isFinish(value) {\n this._isFinish = value;\n }\n /**\n * 返回分块数据\n * @returns {Blob}\n */\n get data() {\n return this._data;\n }\n /**\n * 返回字节起始位置\n * @returns {number}\n */\n get start() {\n return this._start;\n }\n /**\n * 返回字节结束位置\n * @returns {number}\n */\n get end() {\n return this._end;\n }\n get chunks() {\n return this._chunks;\n }\n}\n/**\n * 分片,分片大小可以自定义,至少1字节\n */\nclass Chunk {\n /**\n *\n * @param start 字节起始位置\n * @param end 字节结束位置\n * @param data 分片数据\n * @param block 分块对象\n */\n constructor(start, end, data, block) {\n this._processing = false; //是否正在上传\n this._isFinish = false; //是否上传完成\n this._start = start;\n this._end = end;\n this._data = data;\n this._block = block;\n }\n /**\n * 返回chunk所属的Block对象\n * @returns {Block}\n */\n get block() {\n return this._block;\n }\n /**\n * 返回字节起始位置\n * @returns {number}\n */\n get start() {\n return this._start;\n }\n /**\n * 返回字节结束位置\n * @returns {number}\n */\n get end() {\n return this._end;\n }\n /**\n * 返回分片数据\n * @returns {Blob}\n */\n get data() {\n return this._data;\n }\n /**\n * 是否已经结束\n * @returns {boolean}\n */\n get isFinish() {\n return this._isFinish;\n }\n set isFinish(value) {\n this._isFinish = value;\n }\n get host() {\n return this._host;\n }\n set host(value) {\n this._host = value;\n }\n /**\n * 是否上传中\n * @returns {boolean}\n */\n get processing() {\n return this._processing;\n }\n set processing(value) {\n this._processing = value;\n }\n /**\n * 返回上传控制信息(七牛服务器返回前一次上传返回的分片上传控制信息,用于下一次上传,第一个chunk此值为空)\n * @returns {string}\n */\n get ctx() {\n return this._ctx;\n }\n set ctx(value) {\n this._ctx = value;\n }\n}\nexport { ChunkTask, Block, Chunk };\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/task/ChunkTask.ts\n// module id = 9\n// module chunks = 0","import BaseTask from \"./BaseTask\";\n/**\n * 直传任务\n */\nclass DirectTask extends BaseTask {}\nexport default DirectTask;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/task/DirectTask.ts\n// module id = 10\n// module chunks = 0","class UUID {\n static uuid() {\n let d = new Date().getTime();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {\n let r = (d + Math.random() * 16) % 16 | 0;\n d = Math.floor(d / 16);\n return (c == 'x' ? r : r & 0x3 | 0x8).toString(16);\n });\n }\n}\nexport default UUID;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/upload/uuid/UUID.ts\n// module id = 11\n// module chunks = 0","/**\n * Object.assign polyfill\n */\nif (typeof Object.assign != 'function') {\n Object.assign = function (target) {\n if (target == null) {\n throw new TypeError('Cannot convert undefined or null to object');\n }\n target = Object(target);\n for (let index = 1; index < arguments.length; index++) {\n let source = arguments[index];\n if (source != null) {\n for (let key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n }\n return target;\n };\n}\n/**\n * canvas.toBlob polyfill\n */\nif (!HTMLCanvasElement.prototype.toBlob) {\n Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {\n value: function (callback, type, quality) {\n let binStr = atob(this.toDataURL(type, quality).split(',')[1]),\n len = binStr.length,\n arr = new Uint8Array(len);\n for (let i = 0; i < len; i++) {\n arr[i] = binStr.charCodeAt(i);\n }\n callback(new Blob([arr], { type: type || 'image/png' }));\n }\n });\n}\n/**\n * endsWith polyfill\n */\nif (!String.prototype.endsWith) {\n let toString = {}.toString;\n let endsWith = function (search) {\n if (this == null) {\n throw TypeError();\n }\n let string = String(this);\n if (search && toString.call(search) == '[object RegExp]') {\n throw TypeError();\n }\n let stringLength = string.length;\n let searchString = String(search);\n let searchLength = searchString.length;\n let pos = stringLength;\n if (arguments.length > 1) {\n let position = arguments[1];\n if (position !== undefined) {\n // `ToInteger`\n pos = position ? Number(position) : 0;\n if (pos != pos) {\n pos = 0;\n }\n }\n }\n let end = Math.min(Math.max(pos, 0), stringLength);\n let start = end - searchLength;\n if (start < 0) {\n return false;\n }\n let index = -1;\n while (++index < searchLength) {\n if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {\n return false;\n }\n }\n return true;\n };\n if (Object.defineProperty) {\n Object.defineProperty(String.prototype, 'endsWith', {\n 'value': endsWith,\n 'configurable': true,\n 'writable': true\n });\n } else {\n String.prototype.endsWith = endsWith;\n }\n}\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/util/Polyfill.ts\n// module id = 12\n// module chunks = 0"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;A;;;;;AChEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACxCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;AClaA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;AC1QA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;AC/EA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;;;;;;;;ACDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACZA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACRA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;AC9LA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACnHA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;ACvRA;AAAA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;A","sourceRoot":""} -------------------------------------------------------------------------------- /dist/qiniu4js.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Qiniu=t():e.Qiniu=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=4)}([function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;nh.default.BLOCK_SIZE?new a.ChunkTask(n,h.default.BLOCK_SIZE,this.size):new s.default(n),0==this._saveKey&&(r.key=this.listener.onTaskGetKey(r)),this.taskQueue.push(r)}}},{key:"handleImages",value:function(){var t=this,n=[];if(1!=this.compress||0!=this.scale[0]||0!=this.scale[1]){var r=!0,i=!1,o=void 0;try{for(var u,s=this.taskQueue[Symbol.iterator]();!(r=(u=s.next()).done);r=!0){(function(){var r=u.value;if(!r.file.type.match("image.*"))return"continue";k.default.d(r.file.name+" 处理前的图片大小:"+r.file.size/1024+" kb");var i=document.createElement("canvas"),o=new Image,s=i.getContext("2d");o.src=URL.createObjectURL(r.file);var a=t;n.push(new Promise(function(e){return o.onload=function(){var t=o.width,n=o.height,u=a.scale[0],l=a.scale[1];0==u&&l>0?(i.width=t/n*l,i.height=l):0==l&&u>0?(i.width=u,i.height=n/t*u):u>0&&l>0?(i.width=u,i.height=l):(i.width=o.width,i.height=o.height),s.drawImage(o,0,0,i.width,i.height),console.log(i),console.log(i.toBlob),i.toBlob(function(t){e(t),k.default.d(r.file.name+" 处理后的图片大小:"+t.size/1024+" kb")},"image/jpeg",.95*a.compress)}}).then(function(t){t.name=r.file.name,r.file=t,e.isChunkTask(r)&&r.spliceFile2Block()}))})()}}catch(e){i=!0,o=e}finally{try{!r&&s.return&&s.return()}finally{if(i)throw o}}}return Promise.all(n)}},{key:"validateOptions",value:function(){if(k.default.d("开始检查构建参数合法性"),!this._tokenFunc)throw new Error("你必须提供一个获取Token的回调函数");if(!this.scale||!(this.scale instanceof Array)||2!=this.scale.length||this.scale[0]<0||this.scale[1]<0)throw new Error("scale必须是长度为2的number类型的数组,scale[0]为宽度,scale[1]为长度,必须大于等于0");k.default.d("构建参数检查完毕")}},{key:"start",value:function(){if(k.default.d("上传任务遍历开始"),0==this.fileInput.files.length)throw new Error("没有选中的文件,无法开始上传");if(this.tasking)throw new Error("任务执行中,请不要重复上传");this.listener.onStart(this.taskQueue);var t=!0,n=!1,r=void 0;try{for(var i,o=this.taskQueue[Symbol.iterator]();!(t=(i=o.next()).done);t=!0){var u=i.value;if(k.default.d("上传文件名:"+u.file.name),k.default.d("上传文件大小:"+u.file.size+"字节,"+u.file.size/1024+" kb,"+u.file.size/1024/1024+" mb"),e.isDirectTask(u))k.default.d("该上传任务为直传任务"),new _.default(this).upload(u);else{if(!e.isChunkTask(u))throw new Error("非法的task类型");k.default.d("该上传任务为分片任务"),new b.default(this).upload(u)}}}catch(e){n=!0,r=e}finally{try{!t&&o.return&&o.return()}finally{if(n)throw r}}}},{key:"isTaskQueueFinish",value:function(){var e=!0,t=!1,n=void 0;try{for(var r,i=this.taskQueue[Symbol.iterator]();!(e=(r=i.next()).done);e=!0){if(!r.value.isFinish)return!1}}catch(e){t=!0,n=e}finally{try{!e&&i.return&&i.return()}finally{if(t)throw n}}return!0}},{key:"chooseFile",value:function(){this.fileInput.click()}},{key:"getToken",value:function(e){var t=this;return this._tokenShare&&void 0!=this._token?Promise.resolve(this._token):(k.default.d("开始获取上传token"),Promise.resolve(this._tokenFunc(this,e)).then(function(e){return k.default.d("上传token获取成功: "+e),t._token=e,e}))}},{key:"requestTaskToken",value:function(e,t){var n=this;return this.resolveSaveKey(e).then(function(e){return n.requestToken(t,e)})}},{key:"requestToken",value:function(e,t){return new Promise(function(n,r){"string"==typeof t&&(e+=(/\?/.test(e)?"&":"?")+"saveKey="+encodeURIComponent(t)),e+=(/\?/.test(e)?"&":"?")+(new Date).getTime();var i=new XMLHttpRequest;i.open("GET",e,!0),i.onreadystatechange=function(){if(i.readyState==XMLHttpRequest.DONE)return 200==i.status?void n(i.response.uptoken):void r(i.response)},i.onabort=function(){r("aborted")},i.responseType="json",i.send()})}},{key:"resolveSaveKey",value:function(e){var t=this,n=this._saveKey;return"string"!=typeof n?Promise.resolve(void 0):Promise.resolve(n).then(this.resolveUUID).then(function(n){return t.resolveImageInfo(e.file,n)}).then(this.onSaveKeyResolved)}},{key:"retry",get:function(){return this._retry}},{key:"size",get:function(){return this._size}},{key:"auto",get:function(){return this._auto}},{key:"multiple",get:function(){return this._multiple}},{key:"accept",get:function(){return this._accept}},{key:"compress",get:function(){return this._compress}},{key:"scale",get:function(){return this._scale}},{key:"listener",get:function(){return this._listener}},{key:"fileInput",get:function(){return this._fileInput}},{key:"chunk",get:function(){return this._chunk}},{key:"taskQueue",get:function(){return this._taskQueue}},{key:"tasking",get:function(){return this._tasking},set:function(e){this._tasking=e}},{key:"interceptors",get:function(){return this._interceptors}},{key:"domain",get:function(){return this._domain}}],[{key:"isChunkTask",value:function(e){return e.constructor.name===a.ChunkTask.name&&e instanceof a.ChunkTask}},{key:"isDirectTask",value:function(e){return e.constructor.name===s.default.name&&e instanceof s.default}}]),e}();t.default=m},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:"click";return this._button=e,this._buttonEventName=t,this}},{key:"compress",value:function(e){return this._compress=Math.max(Math.min(e,1),0),this}},{key:"scale",value:function(e){return this._scale=e,this}},{key:"saveKey",value:function(e){return this._saveKey=e,this}},{key:"tokenUrl",value:function(e){return this._tokenFunc=function(t,n){return t.requestTaskToken(n,e)},this}},{key:"tokenFunc",value:function(e){return this._tokenFunc=function(t,n){return new Promise(function(t){e(t,n)})},this}},{key:"listener",value:function(e){return this._listener=e,this}},{key:"tokenShare",value:function(e){return this._tokenShare=e,this}},{key:"chunk",value:function(e){return this._chunk=e,this}},{key:"debug",value:function(e){return this._isDebug=e,this}},{key:"build",value:function(){return new s.default(this)}},{key:"getRetry",get:function(){return this._retry}},{key:"getSize",get:function(){return this._size}},{key:"getAuto",get:function(){return this._auto}},{key:"getMultiple",get:function(){return this._multiple}},{key:"getAccept",get:function(){return this._accept}},{key:"getButton",get:function(){return this._button}},{key:"getButtonEventName",get:function(){return this._buttonEventName}},{key:"getCompress",get:function(){return this._compress}},{key:"getScale",get:function(){return this._scale}},{key:"getListener",get:function(){return this._listener}},{key:"getSaveKey",get:function(){return this._saveKey}},{key:"getTokenFunc",get:function(){return this._tokenFunc}},{key:"getTokenShare",get:function(){return this._tokenShare}},{key:"getChunk",get:function(){return this._chunk}},{key:"getIsDebug",get:function(){return this._isDebug}},{key:"getInterceptors",get:function(){return this._interceptors}},{key:"getDomain",get:function(){var t=this._domain;if(t||(t=e.UPLOAD_DOMAIN),"string"!=typeof t){var n=this._scheme;if("string"!=typeof n){var r=window.location.protocol;n=r.substring(0,r.length-1)}t=t[n]}return t.endsWith("/")?t.substring(0,t.length-1):t}}]),e}();c.MAX_CHUNK_SIZE=4194304,c.BLOCK_SIZE=c.MAX_CHUNK_SIZE,c.UPLOAD_DOMAIN={http:"http://upload.qiniu.com",https:"https://up.qbox.me"},t.default=c},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n=this.uploader.retry?(u.default.w(e.file.name+"达到重传次数上限"+this.uploader.retry+",停止重传"),!1):(e.retry++,u.default.w(e.file.name+"开始重传,当前重传次数"+e.retry),!0)}}]),e}();t.default=s},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n=this.uploader.retry?(u.default.w(e.file.name+"达到重传次数上限"+this.uploader.retry+",停止重传"),!1):(e.retry++,u.default.w(e.file.name+"开始重传,当前重传次数"+e.retry),this.upload(e),!0)}}]),e}();t.default=s},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0}),t.Chunk=t.Block=t.ChunkTask=void 0;var u=function(){function e(e,t){for(var n=0;n1){var s=arguments[1];void 0!==s&&(u=s?Number(s):0)!=u&&(u=0)}var a=Math.min(Math.max(u,0),n),l=a-o;if(l<0)return!1;for(var c=-1;++ch.default.BLOCK_SIZE?new a.ChunkTask(n,h.default.BLOCK_SIZE,this.size):new s.default(n),0==this._saveKey&&(r.key=this.listener.onTaskGetKey(r)),this.taskQueue.push(r)}}},{key:\"handleImages\",value:function(){var t=this,n=[];if(1!=this.compress||0!=this.scale[0]||0!=this.scale[1]){var r=!0,i=!1,o=void 0;try{for(var u,s=this.taskQueue[Symbol.iterator]();!(r=(u=s.next()).done);r=!0){(function(){var r=u.value;if(!r.file.type.match(\"image.*\"))return\"continue\";k.default.d(r.file.name+\" 处理前的图片大小:\"+r.file.size/1024+\" kb\");var i=document.createElement(\"canvas\"),o=new Image,s=i.getContext(\"2d\");o.src=URL.createObjectURL(r.file);var a=t;n.push(new Promise(function(e){return o.onload=function(){var t=o.width,n=o.height,u=a.scale[0],l=a.scale[1];0==u&&l>0?(i.width=t/n*l,i.height=l):0==l&&u>0?(i.width=u,i.height=n/t*u):u>0&&l>0?(i.width=u,i.height=l):(i.width=o.width,i.height=o.height),s.drawImage(o,0,0,i.width,i.height),console.log(i),console.log(i.toBlob),i.toBlob(function(t){e(t),k.default.d(r.file.name+\" 处理后的图片大小:\"+t.size/1024+\" kb\")},\"image/jpeg\",.95*a.compress)}}).then(function(t){t.name=r.file.name,r.file=t,e.isChunkTask(r)&&r.spliceFile2Block()}))})()}}catch(e){i=!0,o=e}finally{try{!r&&s.return&&s.return()}finally{if(i)throw o}}}return Promise.all(n)}},{key:\"validateOptions\",value:function(){if(k.default.d(\"开始检查构建参数合法性\"),!this._tokenFunc)throw new Error(\"你必须提供一个获取Token的回调函数\");if(!this.scale||!(this.scale instanceof Array)||2!=this.scale.length||this.scale[0]<0||this.scale[1]<0)throw new Error(\"scale必须是长度为2的number类型的数组,scale[0]为宽度,scale[1]为长度,必须大于等于0\");k.default.d(\"构建参数检查完毕\")}},{key:\"start\",value:function(){if(k.default.d(\"上传任务遍历开始\"),0==this.fileInput.files.length)throw new Error(\"没有选中的文件,无法开始上传\");if(this.tasking)throw new Error(\"任务执行中,请不要重复上传\");this.listener.onStart(this.taskQueue);var t=!0,n=!1,r=void 0;try{for(var i,o=this.taskQueue[Symbol.iterator]();!(t=(i=o.next()).done);t=!0){var u=i.value;if(k.default.d(\"上传文件名:\"+u.file.name),k.default.d(\"上传文件大小:\"+u.file.size+\"字节,\"+u.file.size/1024+\" kb,\"+u.file.size/1024/1024+\" mb\"),e.isDirectTask(u))k.default.d(\"该上传任务为直传任务\"),new _.default(this).upload(u);else{if(!e.isChunkTask(u))throw new Error(\"非法的task类型\");k.default.d(\"该上传任务为分片任务\"),new b.default(this).upload(u)}}}catch(e){n=!0,r=e}finally{try{!t&&o.return&&o.return()}finally{if(n)throw r}}}},{key:\"isTaskQueueFinish\",value:function(){var e=!0,t=!1,n=void 0;try{for(var r,i=this.taskQueue[Symbol.iterator]();!(e=(r=i.next()).done);e=!0){if(!r.value.isFinish)return!1}}catch(e){t=!0,n=e}finally{try{!e&&i.return&&i.return()}finally{if(t)throw n}}return!0}},{key:\"chooseFile\",value:function(){this.fileInput.click()}},{key:\"getToken\",value:function(e){var t=this;return this._tokenShare&&void 0!=this._token?Promise.resolve(this._token):(k.default.d(\"开始获取上传token\"),Promise.resolve(this._tokenFunc(this,e)).then(function(e){return k.default.d(\"上传token获取成功: \"+e),t._token=e,e}))}},{key:\"requestTaskToken\",value:function(e,t){var n=this;return this.resolveSaveKey(e).then(function(e){return n.requestToken(t,e)})}},{key:\"requestToken\",value:function(e,t){return new Promise(function(n,r){\"string\"==typeof t&&(e+=(/\\?/.test(e)?\"&\":\"?\")+\"saveKey=\"+encodeURIComponent(t)),e+=(/\\?/.test(e)?\"&\":\"?\")+(new Date).getTime();var i=new XMLHttpRequest;i.open(\"GET\",e,!0),i.onreadystatechange=function(){if(i.readyState==XMLHttpRequest.DONE)return 200==i.status?void n(i.response.uptoken):void r(i.response)},i.onabort=function(){r(\"aborted\")},i.responseType=\"json\",i.send()})}},{key:\"resolveSaveKey\",value:function(e){var t=this,n=this._saveKey;return\"string\"!=typeof n?Promise.resolve(void 0):Promise.resolve(n).then(this.resolveUUID).then(function(n){return t.resolveImageInfo(e.file,n)}).then(this.onSaveKeyResolved)}},{key:\"retry\",get:function(){return this._retry}},{key:\"size\",get:function(){return this._size}},{key:\"auto\",get:function(){return this._auto}},{key:\"multiple\",get:function(){return this._multiple}},{key:\"accept\",get:function(){return this._accept}},{key:\"compress\",get:function(){return this._compress}},{key:\"scale\",get:function(){return this._scale}},{key:\"listener\",get:function(){return this._listener}},{key:\"fileInput\",get:function(){return this._fileInput}},{key:\"chunk\",get:function(){return this._chunk}},{key:\"taskQueue\",get:function(){return this._taskQueue}},{key:\"tasking\",get:function(){return this._tasking},set:function(e){this._tasking=e}},{key:\"interceptors\",get:function(){return this._interceptors}},{key:\"domain\",get:function(){return this._domain}}],[{key:\"isChunkTask\",value:function(e){return e.constructor.name===a.ChunkTask.name&&e instanceof a.ChunkTask}},{key:\"isDirectTask\",value:function(e){return e.constructor.name===s.default.name&&e instanceof s.default}}]),e}();t.default=m},function(e,t,n){\"use strict\";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}Object.defineProperty(t,\"__esModule\",{value:!0});var o=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:\"click\";return this._button=e,this._buttonEventName=t,this}},{key:\"compress\",value:function(e){return this._compress=Math.max(Math.min(e,1),0),this}},{key:\"scale\",value:function(e){return this._scale=e,this}},{key:\"saveKey\",value:function(e){return this._saveKey=e,this}},{key:\"tokenUrl\",value:function(e){return this._tokenFunc=function(t,n){return t.requestTaskToken(n,e)},this}},{key:\"tokenFunc\",value:function(e){return this._tokenFunc=function(t,n){return new Promise(function(t){e(t,n)})},this}},{key:\"listener\",value:function(e){return this._listener=e,this}},{key:\"tokenShare\",value:function(e){return this._tokenShare=e,this}},{key:\"chunk\",value:function(e){return this._chunk=e,this}},{key:\"debug\",value:function(e){return this._isDebug=e,this}},{key:\"build\",value:function(){return new s.default(this)}},{key:\"getRetry\",get:function(){return this._retry}},{key:\"getSize\",get:function(){return this._size}},{key:\"getAuto\",get:function(){return this._auto}},{key:\"getMultiple\",get:function(){return this._multiple}},{key:\"getAccept\",get:function(){return this._accept}},{key:\"getButton\",get:function(){return this._button}},{key:\"getButtonEventName\",get:function(){return this._buttonEventName}},{key:\"getCompress\",get:function(){return this._compress}},{key:\"getScale\",get:function(){return this._scale}},{key:\"getListener\",get:function(){return this._listener}},{key:\"getSaveKey\",get:function(){return this._saveKey}},{key:\"getTokenFunc\",get:function(){return this._tokenFunc}},{key:\"getTokenShare\",get:function(){return this._tokenShare}},{key:\"getChunk\",get:function(){return this._chunk}},{key:\"getIsDebug\",get:function(){return this._isDebug}},{key:\"getInterceptors\",get:function(){return this._interceptors}},{key:\"getDomain\",get:function(){var t=this._domain;if(t||(t=e.UPLOAD_DOMAIN),\"string\"!=typeof t){var n=this._scheme;if(\"string\"!=typeof n){var r=window.location.protocol;n=r.substring(0,r.length-1)}t=t[n]}return t.endsWith(\"/\")?t.substring(0,t.length-1):t}}]),e}();c.MAX_CHUNK_SIZE=4194304,c.BLOCK_SIZE=c.MAX_CHUNK_SIZE,c.UPLOAD_DOMAIN={http:\"http://upload.qiniu.com\",https:\"https://up.qbox.me\"},t.default=c},function(e,t,n){\"use strict\";function r(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}Object.defineProperty(t,\"__esModule\",{value:!0});var i=function(){function e(e,t){for(var n=0;n=this.uploader.retry?(u.default.w(e.file.name+\"达到重传次数上限\"+this.uploader.retry+\",停止重传\"),!1):(e.retry++,u.default.w(e.file.name+\"开始重传,当前重传次数\"+e.retry),!0)}}]),e}();t.default=s},function(e,t,n){\"use strict\";function r(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}Object.defineProperty(t,\"__esModule\",{value:!0});var i=function(){function e(e,t){for(var n=0;n=this.uploader.retry?(u.default.w(e.file.name+\"达到重传次数上限\"+this.uploader.retry+\",停止重传\"),!1):(e.retry++,u.default.w(e.file.name+\"开始重传,当前重传次数\"+e.retry),this.upload(e),!0)}}]),e}();t.default=s},function(e,t,n){\"use strict\";function r(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function i(e,t){if(!e)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return!t||\"object\"!=typeof t&&\"function\"!=typeof t?e:t}function o(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Super expression must either be null or a function, not \"+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,\"__esModule\",{value:!0}),t.Chunk=t.Block=t.ChunkTask=void 0;var u=function(){function e(e,t){for(var n=0;n1){var s=arguments[1];void 0!==s&&(u=s?Number(s):0)!=u&&(u=0)}var a=Math.min(Math.max(u,0),n),l=a-o;if(l<0)return!1;for(var c=-1;++c; 5 | } 6 | export default TokenFunc; 7 | -------------------------------------------------------------------------------- /dist/types/upload/Uploader.d.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "./task/BaseTask"; 2 | import UploaderBuilder from "./UploaderBuilder"; 3 | import Interceptor from "./interceptor/UploadInterceptor"; 4 | import UploadListener from "./hook/UploadListener"; 5 | import "../util/Polyfill"; 6 | declare class Uploader { 7 | private FILE_INPUT_EL_ID; 8 | private _fileInputId; 9 | private _fileInput; 10 | private _token; 11 | private _taskQueue; 12 | private _tasking; 13 | private _retry; 14 | private _size; 15 | private _chunk; 16 | private _auto; 17 | private _multiple; 18 | private _accept; 19 | private _button; 20 | private _buttonEventName; 21 | private _compress; 22 | private _scale; 23 | private _listener; 24 | private _saveKey; 25 | private _tokenFunc; 26 | private _tokenShare; 27 | private _interceptors; 28 | private _domain; 29 | constructor(builder: UploaderBuilder); 30 | /** 31 | * 初始化操作 32 | */ 33 | private init(); 34 | /** 35 | * 初始化file input element 36 | */ 37 | private initFileInputEl(); 38 | /** 39 | * 上传完成或者失败后,对本次上传任务进行清扫 40 | */ 41 | private resetUploader(); 42 | /** 43 | * 处理文件 44 | */ 45 | private handleFiles; 46 | /** 47 | * 是否是分块任务 48 | * @param task 49 | * @returns {boolean} 50 | */ 51 | private static isChunkTask(task); 52 | /** 53 | * 是否是直传任务 54 | * @param task 55 | * @returns {boolean} 56 | */ 57 | private static isDirectTask(task); 58 | /** 59 | * 生成task 60 | */ 61 | private generateTask(); 62 | /** 63 | * 处理图片-缩放-质量压缩 64 | */ 65 | private handleImages(); 66 | /** 67 | * 检验选项合法性 68 | */ 69 | private validateOptions(); 70 | /** 71 | * 开始上传 72 | */ 73 | start(): void; 74 | /** 75 | * 所有任务是否完成 76 | * @returns {boolean} 77 | */ 78 | isTaskQueueFinish(): boolean; 79 | /** 80 | * 选择文件 81 | */ 82 | chooseFile(): void; 83 | getToken(task: BaseTask): Promise; 84 | requestTaskToken(task: BaseTask, url: string): Promise; 85 | private requestToken(url, saveKey); 86 | private resolveSaveKey(task); 87 | private resolveUUID; 88 | private resolveImageInfo; 89 | private onSaveKeyResolved; 90 | readonly retry: number; 91 | readonly size: number; 92 | readonly auto: boolean; 93 | readonly multiple: boolean; 94 | readonly accept: string[]; 95 | readonly compress: number; 96 | readonly scale: number[]; 97 | readonly listener: UploadListener; 98 | readonly fileInput: HTMLInputElement; 99 | readonly chunk: boolean; 100 | readonly taskQueue: BaseTask[]; 101 | tasking: boolean; 102 | readonly interceptors: Interceptor[]; 103 | readonly domain: string; 104 | } 105 | export default Uploader; 106 | -------------------------------------------------------------------------------- /dist/types/upload/UploaderBuilder.d.ts: -------------------------------------------------------------------------------- 1 | import Uploader from "./Uploader"; 2 | import TokenFunc from "./TokenFunc"; 3 | import Interceptor from "./interceptor/UploadInterceptor"; 4 | import UploadListener from "./hook/UploadListener"; 5 | import { Scheme, Domain } from "./url/Domain"; 6 | /** 7 | * UploaderBuilder 8 | * 9 | */ 10 | declare class UploaderBuilder { 11 | static MAX_CHUNK_SIZE: number; 12 | static BLOCK_SIZE: number; 13 | static UPLOAD_DOMAIN: { 14 | http: string; 15 | https: string; 16 | }; 17 | private _retry; 18 | private _domain; 19 | private _scheme; 20 | private _size; 21 | private _chunk; 22 | private _auto; 23 | private _multiple; 24 | private _accept; 25 | private _button; 26 | private _buttonEventName; 27 | private _compress; 28 | private _scale; 29 | private _listener; 30 | private _saveKey; 31 | private _tokenFunc; 32 | private _tokenShare; 33 | private _interceptors; 34 | private _isDebug; 35 | /** 36 | * 设置上传的域名,默认是 {http: 'http://upload.qiniu.com', https: 'https://up.qbox.me'} 37 | * @param domain 38 | * @returns {UploaderBuilder} 39 | */ 40 | domain(domain: Domain): UploaderBuilder; 41 | /** 42 | * 设置上传域名的协议类型,默认从 window.location.protocol 读取 43 | * @param scheme 44 | * @returns {UploaderBuilder} 45 | */ 46 | scheme(scheme: Scheme): UploaderBuilder; 47 | /** 48 | * 添加一个拦截器 49 | * @param interceptor 50 | * @returns {UploaderBuilder} 51 | */ 52 | interceptor(interceptor: Interceptor): UploaderBuilder; 53 | /** 54 | * 上传失败后的重传尝试次数 55 | * @param retry 默认0次,不尝试次重传 56 | * @returns {UploaderBuilder} 57 | */ 58 | retry(retry: number): UploaderBuilder; 59 | /** 60 | * 设置分片大小 61 | * @param size 分块大小,单位字节,默认4*1024*1024字节(4mb) 62 | * @returns {UploaderBuilder} 63 | */ 64 | size(size: number): UploaderBuilder; 65 | /** 66 | * 选择文件后,是否自动上传 67 | * @param auto 默认true 68 | * @returns {UploaderBuilder} 69 | */ 70 | auto(auto: boolean): UploaderBuilder; 71 | /** 72 | * 是否支持多文件选择 73 | * @param multiple 默认true 74 | * @returns {UploaderBuilder} 75 | */ 76 | multiple(multiple: boolean): UploaderBuilder; 77 | /** 78 | * 接受上传的文件类型 79 | * @param accept 数组形式例如:['.png','video/*'] 80 | * 81 | * 详细配置见http://www.w3schools.com/tags/att_input_accept.asp 82 | * 83 | * @returns {UploaderBuilder} 84 | */ 85 | accept(accept: string[]): UploaderBuilder; 86 | /** 87 | * 设置上传按钮 88 | * @param button 上传按钮ID 89 | * @param eventName 上传按钮的监听事件名称,默认为 "click" 。 90 | * @returns {UploaderBuilder} 91 | */ 92 | button(button: string, eventName?: string): UploaderBuilder; 93 | /** 94 | * 图片质量压缩,只在上传的文件是图片的时候有效 95 | * @param compress 0-1,默认1,不压缩 96 | * @returns {UploaderBuilder} 97 | */ 98 | compress(compress: number): UploaderBuilder; 99 | /** 100 | * 图片缩放 101 | * @returns {UploaderBuilder} 102 | * @param scale 103 | */ 104 | scale(scale: number[]): UploaderBuilder; 105 | /** 106 | * 设置 saveKey 107 | * @param saveKey 108 | * @returns {UploaderBuilder} 109 | */ 110 | saveKey(saveKey: boolean | string): UploaderBuilder; 111 | /** 112 | * 获取Token的地址 113 | * @param tokenUrl 114 | * @returns {UploaderBuilder} 115 | */ 116 | tokenUrl(tokenUrl: string): UploaderBuilder; 117 | /** 118 | * 获取Token的函数 119 | * @param tokenFunc 120 | * @returns {UploaderBuilder} 121 | */ 122 | tokenFunc(tokenFunc: Function): UploaderBuilder; 123 | /** 124 | * 上传生命周期钩子 125 | * @param listener 126 | * @returns {UploaderBuilder} 127 | */ 128 | listener(listener: UploadListener): UploaderBuilder; 129 | /** 130 | * 是否分享token,如果为false每上传一个文件都需要请求一次Token。 131 | * @param tokenShare 132 | * @returns {UploaderBuilder} 133 | */ 134 | tokenShare(tokenShare: boolean): UploaderBuilder; 135 | /** 136 | * 是否分块上传 137 | * @param chunk 默认false 138 | * @returns {UploaderBuilder} 139 | */ 140 | chunk(chunk: boolean): UploaderBuilder; 141 | /** 142 | * 是否开启debug模式 143 | * @param debug 默认false 144 | * @returns {UploaderBuilder} 145 | */ 146 | debug(debug: boolean): UploaderBuilder; 147 | readonly getRetry: number; 148 | readonly getSize: number; 149 | readonly getAuto: boolean; 150 | readonly getMultiple: boolean; 151 | readonly getAccept: string[]; 152 | readonly getButton: string; 153 | readonly getButtonEventName: string; 154 | readonly getCompress: number; 155 | readonly getScale: number[]; 156 | readonly getListener: UploadListener; 157 | readonly getSaveKey: boolean | string; 158 | readonly getTokenFunc: TokenFunc; 159 | readonly getTokenShare: boolean; 160 | readonly getChunk: boolean; 161 | readonly getIsDebug: boolean; 162 | readonly getInterceptors: Interceptor[]; 163 | readonly getDomain: string; 164 | build(): Uploader; 165 | } 166 | export default UploaderBuilder; 167 | -------------------------------------------------------------------------------- /dist/types/upload/hook/SimpleUploadListener.d.ts: -------------------------------------------------------------------------------- 1 | import Task from "../task/DirectTask"; 2 | import UploadListener from "./UploadListener"; 3 | declare class SimpleUploadListener implements UploadListener { 4 | onReady(taskQueue: Task[]): void; 5 | onStart(taskQueue: Task[]): void; 6 | onTaskProgress(task: Task): void; 7 | onTaskGetKey(task: Task): string; 8 | onTaskFail(task: Task): void; 9 | onTaskSuccess(task: Task): void; 10 | onTaskRetry(task: Task): void; 11 | onFinish(taskQueue: Task[]): void; 12 | } 13 | export default SimpleUploadListener; 14 | -------------------------------------------------------------------------------- /dist/types/upload/hook/UploadListener.d.ts: -------------------------------------------------------------------------------- 1 | import Task from "../task/DirectTask"; 2 | interface UploadListener { 3 | onReady(taskQueue: Task[]): void; 4 | onStart(taskQueue: Task[]): void; 5 | onTaskProgress(task: Task): void; 6 | onTaskGetKey(task: Task): string; 7 | onTaskFail(task: Task): void; 8 | onTaskSuccess(task: Task): void; 9 | onTaskRetry(task: Task): void; 10 | onFinish(taskQueue: Task[]): void; 11 | } 12 | export default UploadListener; 13 | -------------------------------------------------------------------------------- /dist/types/upload/interceptor/SimpleUploadInterceptor.d.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "../task/BaseTask"; 2 | import UploadInterceptor from "./UploadInterceptor"; 3 | declare class SimpleUploadInterceptor implements UploadInterceptor { 4 | onIntercept(task: BaseTask): boolean; 5 | onInterrupt(task: BaseTask): boolean; 6 | } 7 | export default SimpleUploadInterceptor; 8 | -------------------------------------------------------------------------------- /dist/types/upload/interceptor/UploadInterceptor.d.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "../task/BaseTask"; 2 | /** 3 | * 上传任务拦截器 4 | */ 5 | interface UploadInterceptor { 6 | /** 7 | * 是否拦截此Task,拦截后此Task不再上传 8 | * @param task 返回true拦截,false放行。 9 | */ 10 | onIntercept(task: BaseTask, tasks: BaseTask[]): boolean; 11 | /** 12 | * 是否中断上传任务,中断后则不会开始上传。 13 | * @param task 返回true中断,false放行。 14 | */ 15 | onInterrupt(task: BaseTask, tasks: BaseTask[]): boolean; 16 | } 17 | export default UploadInterceptor; 18 | -------------------------------------------------------------------------------- /dist/types/upload/pattren/ChunkUploadPattern.d.ts: -------------------------------------------------------------------------------- 1 | import IUploadPattern from "./IUploadPattern"; 2 | import Uploader from "../Uploader"; 3 | import { ChunkTask } from "../task/ChunkTask"; 4 | /** 5 | * 分块上传 6 | */ 7 | declare class ChunkUploadPattern implements IUploadPattern { 8 | private uploader; 9 | private task; 10 | constructor(uploader: Uploader); 11 | init(uploader: Uploader): void; 12 | upload(task: ChunkTask): void; 13 | private uploadBlock(token); 14 | private uploadChunk(chunk, token); 15 | private concatChunks(token); 16 | /** 17 | * 获取块上传的url 18 | * @param blockSize 19 | * @returns {string} 20 | */ 21 | private getUploadBlockUrl(blockSize); 22 | /** 23 | * 获取片上传的url 24 | * @param start 片的在块中的起始位置 25 | * @param ctx 前一次上传返回的块级上传控制信息。 26 | * @param host 指定host 27 | */ 28 | private getUploadChunkUrl(start, ctx, host?); 29 | /** 30 | * 获取合并块为文件的url 31 | * @param fileSize 文件大小 32 | * @param encodedKey base64UrlEncode后的资源名称,若未指定,则使用saveKey;若未指定saveKey,则使用资源内容的SHA1值作为资源名。 33 | * @returns {string} 34 | */ 35 | private getMakeFileUrl(fileSize, encodedKey); 36 | private retryTask(task); 37 | } 38 | export default ChunkUploadPattern; 39 | -------------------------------------------------------------------------------- /dist/types/upload/pattren/DirectUploadPattern.d.ts: -------------------------------------------------------------------------------- 1 | import IUploadPattern from "./IUploadPattern"; 2 | import Uploader from "../Uploader"; 3 | import DirectTask from "../task/DirectTask"; 4 | /** 5 | * 直接上传 6 | */ 7 | declare class DirectUploadPattern implements IUploadPattern { 8 | private uploader; 9 | private task; 10 | constructor(uploader: Uploader); 11 | /** 12 | * 实现接口的上传方法 13 | * @param task 14 | */ 15 | upload(task: DirectTask): void; 16 | /** 17 | * 创建表单 18 | * @param token 19 | * @returns {FormData} 20 | */ 21 | private createFormData(token); 22 | /** 23 | * 上传文件 24 | * @param token 25 | */ 26 | private uploadFile(token); 27 | /** 28 | * 重传 29 | * @param task 30 | * @returns {boolean} 31 | */ 32 | private retryTask(task); 33 | } 34 | export default DirectUploadPattern; 35 | -------------------------------------------------------------------------------- /dist/types/upload/pattren/IUploadPattern.d.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "../task/BaseTask"; 2 | /** 3 | * 4 | */ 5 | interface IUploadPattern { 6 | /** 7 | * 上传任务 8 | * @param task 9 | */ 10 | upload(task: BaseTask): void; 11 | } 12 | export default IUploadPattern; 13 | -------------------------------------------------------------------------------- /dist/types/upload/task/BaseTask.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 上传任务 3 | */ 4 | declare abstract class BaseTask { 5 | protected _file: File; 6 | protected _retry: number; 7 | protected _createDate: Date; 8 | protected _startDate: Date; 9 | protected _endDate: Date; 10 | protected _key: string; 11 | protected _progress: number; 12 | protected _isSuccess: boolean; 13 | protected _isFinish: boolean; 14 | protected _result: Object; 15 | protected _error: any; 16 | constructor(file: File); 17 | file: File; 18 | retry: number; 19 | createDate: Date; 20 | startDate: Date; 21 | endDate: Date; 22 | isSuccess: boolean; 23 | progress: number; 24 | result: Object; 25 | error: any; 26 | key: string; 27 | isFinish: boolean; 28 | } 29 | export default BaseTask; 30 | -------------------------------------------------------------------------------- /dist/types/upload/task/ChunkTask.d.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "./BaseTask"; 2 | /** 3 | * 分块任务 4 | */ 5 | declare class ChunkTask extends BaseTask { 6 | private _blocks; 7 | private _blockSize; 8 | private _chunkSize; 9 | /** 10 | * 构造函数 11 | * @param file 12 | * @param blockSize 块大小 13 | * @param chunkSize 片大小 14 | */ 15 | constructor(file: File, blockSize: number, chunkSize: number); 16 | /** 17 | * 将文件分块 18 | */ 19 | spliceFile2Block(): void; 20 | /** 21 | * 获取所有的block 22 | * @returns {Block[]} 23 | */ 24 | readonly blocks: Block[]; 25 | /** 26 | * 获取正在处理的block 27 | * @returns {Block} 28 | */ 29 | readonly processingBlock: Block; 30 | readonly finishedBlocksSize: number; 31 | readonly chunks: Chunk[]; 32 | /** 33 | * 获取正在处理的chunk 34 | * @returns {Block} 35 | */ 36 | readonly processingChunk: Chunk; 37 | /** 38 | * 总共分片数量(所有分块的分片数量总和) 39 | * @returns {number} 40 | */ 41 | readonly totalChunkCount: number; 42 | } 43 | /** 44 | * 分块,分块大小七牛固定是4M 45 | */ 46 | declare class Block { 47 | private _data; 48 | private _start; 49 | private _end; 50 | private _chunks; 51 | private _isFinish; 52 | private _processing; 53 | private _file; 54 | /** 55 | * 56 | * @param start 起始位置 57 | * @param end 结束位置 58 | * @param data 块数据 59 | * @param chunkSize 分片数据的最大大小 60 | * @param file 分块所属文件 61 | */ 62 | constructor(start: number, end: number, data: Blob, chunkSize: number, file: File); 63 | /** 64 | * 将块分片 65 | */ 66 | private spliceBlock2Chunk(chunkSize); 67 | /** 68 | * 是否上传中 69 | * @returns {boolean} 70 | */ 71 | processing: boolean; 72 | /** 73 | * 分块所属的文件 74 | * @returns {File} 75 | */ 76 | readonly file: File; 77 | /** 78 | * 是否已经结束 79 | * @returns {boolean} 80 | */ 81 | isFinish: boolean; 82 | /** 83 | * 返回分块数据 84 | * @returns {Blob} 85 | */ 86 | readonly data: Blob; 87 | /** 88 | * 返回字节起始位置 89 | * @returns {number} 90 | */ 91 | readonly start: number; 92 | /** 93 | * 返回字节结束位置 94 | * @returns {number} 95 | */ 96 | readonly end: number; 97 | readonly chunks: Chunk[]; 98 | } 99 | /** 100 | * 分片,分片大小可以自定义,至少1字节 101 | */ 102 | declare class Chunk { 103 | private _start; 104 | private _end; 105 | private _data; 106 | private _processing; 107 | private _isFinish; 108 | private _ctx; 109 | private _block; 110 | private _host; 111 | /** 112 | * 113 | * @param start 字节起始位置 114 | * @param end 字节结束位置 115 | * @param data 分片数据 116 | * @param block 分块对象 117 | */ 118 | constructor(start: number, end: number, data: Blob, block: Block); 119 | /** 120 | * 返回chunk所属的Block对象 121 | * @returns {Block} 122 | */ 123 | readonly block: Block; 124 | /** 125 | * 返回字节起始位置 126 | * @returns {number} 127 | */ 128 | readonly start: number; 129 | /** 130 | * 返回字节结束位置 131 | * @returns {number} 132 | */ 133 | readonly end: number; 134 | /** 135 | * 返回分片数据 136 | * @returns {Blob} 137 | */ 138 | readonly data: Blob; 139 | /** 140 | * 是否已经结束 141 | * @returns {boolean} 142 | */ 143 | isFinish: boolean; 144 | host: string; 145 | /** 146 | * 是否上传中 147 | * @returns {boolean} 148 | */ 149 | processing: boolean; 150 | /** 151 | * 返回上传控制信息(七牛服务器返回前一次上传返回的分片上传控制信息,用于下一次上传,第一个chunk此值为空) 152 | * @returns {string} 153 | */ 154 | ctx: string; 155 | } 156 | export { ChunkTask, Block, Chunk }; 157 | -------------------------------------------------------------------------------- /dist/types/upload/task/DirectTask.d.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "./BaseTask"; 2 | /** 3 | * 直传任务 4 | */ 5 | declare class DirectTask extends BaseTask { 6 | } 7 | export default DirectTask; 8 | -------------------------------------------------------------------------------- /dist/types/upload/url/Domain.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Scheme = "http" | "https" | string; 2 | export declare type Domain = string | { 3 | http: string; 4 | https: string; 5 | }; 6 | -------------------------------------------------------------------------------- /dist/types/upload/uuid/UUID.d.ts: -------------------------------------------------------------------------------- 1 | declare class UUID { 2 | static uuid(): string; 3 | } 4 | export default UUID; 5 | -------------------------------------------------------------------------------- /dist/types/util/Log.d.ts: -------------------------------------------------------------------------------- 1 | declare class Log { 2 | static _enable: boolean; 3 | static enable: boolean; 4 | static d(object: any): void; 5 | static l(object: any): void; 6 | static e(object: any): void; 7 | static w(object: any): void; 8 | static i(object: any): void; 9 | } 10 | export default Log; 11 | -------------------------------------------------------------------------------- /dist/types/util/Polyfill.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sloaix/qiniu4js/cf4970568af375ab1f794916c4b0c07ea88532b0/dist/types/util/Polyfill.d.ts -------------------------------------------------------------------------------- /dist/types/util/isWechat.d.ts: -------------------------------------------------------------------------------- 1 | export default function isWechat(): boolean; 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 18 | 19 | 20 | 23 |
24 | 25 | 26 | 162 | 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qiniu4js", 3 | "version": "1.0.13", 4 | "description": "Maybe is the best js sdk for qiniu", 5 | "homepage": "https://www.gihub.com/lsxiao/qiniu4js", 6 | "repository": "lsxiao/qiniu4js", 7 | "author": "lsxiao ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "qiniu", 11 | "js", 12 | "sdk" 13 | ], 14 | "types": "./types/Main.d.ts", 15 | "main": "qiniu4js.js", 16 | "jsnext:main": "qiniu4js.es.js", 17 | "bugs": { 18 | "url": "https://github.com/lsxiao/qiniu4js/issues" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.24.1", 22 | "babel-loader": "^7.0.0", 23 | "babel-polyfill": "^6.23.0", 24 | "babel-preset-env": "^1.5.1", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-stage-0": "^6.24.1", 27 | "del": "^2.2.2", 28 | "npmlog": "^4.0.2", 29 | "rimraf": "^2.6.1", 30 | "source-map-loader": "^0.2.1", 31 | "ts-loader": "^2.1.0", 32 | "typescript": "^2.3.4", 33 | "webpack": "^2.6.1" 34 | }, 35 | "scripts": { 36 | "clean": "rimraf ./dist && mkdir dist", 37 | "prebuild": "npm run clean && node copy.js", 38 | "build": "webpack -p --env=es5min && webpack --env=es5 && webpack --env=es6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Main.ts: -------------------------------------------------------------------------------- 1 | import Uploader from "./upload/Uploader"; 2 | import UploaderBuilder from "./upload/UploaderBuilder"; 3 | export {Uploader, UploaderBuilder}; -------------------------------------------------------------------------------- /src/upload/TokenFunc.ts: -------------------------------------------------------------------------------- 1 | import Uploader from "./Uploader"; 2 | import BaseTask from "./task/BaseTask"; 3 | 4 | interface TokenFunc { 5 | (uploader: Uploader, task: BaseTask): string | Promise; 6 | } 7 | 8 | export default TokenFunc; 9 | -------------------------------------------------------------------------------- /src/upload/Uploader.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "./task/BaseTask"; 2 | import DirectTask from "./task/DirectTask"; 3 | import {ChunkTask} from "./task/ChunkTask"; 4 | import TokenFunc from "./TokenFunc"; 5 | import UUID from "./uuid/UUID"; 6 | import UploaderBuilder from "./UploaderBuilder"; 7 | import log from "../util/Log"; 8 | import Interceptor from "./interceptor/UploadInterceptor"; 9 | import UploadListener from "./hook/UploadListener"; 10 | import SimpleUploadListener from "./hook/SimpleUploadListener"; 11 | import DirectUploadPattern from "./pattren/DirectUploadPattern"; 12 | import ChunkUploadPattern from "./pattren/ChunkUploadPattern"; 13 | import "../util/Polyfill"; 14 | 15 | class Uploader { 16 | private FILE_INPUT_EL_ID: string = 'qiniu4js-input'; 17 | private _fileInputId: string; 18 | private _fileInput: HTMLInputElement;//input 元素 19 | private _token: string;//token 20 | private _taskQueue: BaseTask[] = [];//任务队列 21 | private _tasking: boolean = false;//任务执行中 22 | 23 | private _retry: number;//最大重试次数 24 | private _size: number;//分片大小,单位字节,最大4mb,不能为0 25 | private _chunk: boolean;//分块上传,该选项开启后,只有在文件大于4mb的时候才会分块上传 26 | private _auto: boolean;//自动上传,每次选择文件后 27 | private _multiple: boolean;//是否支持多文件 28 | private _accept: string[];//接受的文件类型 29 | private _button: string;//上传按钮 30 | private _buttonEventName: string;//上传按钮的监听事件名称 31 | private _compress: number;//图片压缩质量 32 | private _scale: number[] = [];//缩放大小,限定高度等比缩放[h:200,w:0],限定宽度等比缩放[h:0,w:100],限定长宽[h:200,w:100] 33 | private _listener: UploadListener;//监听器 34 | private _saveKey: boolean | string = false; 35 | private _tokenFunc: TokenFunc;//token获取函数 36 | private _tokenShare: boolean;//分享token,如果为false,每一次HTTP请求都需要新获取Token 37 | private _interceptors: Interceptor[];//任务拦截器 38 | private _domain: string;//上传域名 39 | 40 | constructor(builder: UploaderBuilder) { 41 | this._retry = builder.getRetry; 42 | this._size = builder.getSize; 43 | this._chunk = builder.getChunk; 44 | this._auto = builder.getAuto; 45 | this._multiple = builder.getMultiple; 46 | this._accept = builder.getAccept; 47 | this._button = builder.getButton; 48 | this._buttonEventName = builder.getButtonEventName; 49 | this._compress = builder.getCompress; 50 | this._scale = builder.getScale; 51 | this._saveKey = builder.getSaveKey; 52 | this._tokenFunc = builder.getTokenFunc; 53 | this._tokenShare = builder.getTokenShare; 54 | this._listener = Object.assign(new SimpleUploadListener(), builder.getListener); 55 | this._interceptors = builder.getInterceptors; 56 | this._domain = builder.getDomain; 57 | this._fileInputId = `${this.FILE_INPUT_EL_ID}_${new Date().getTime()}`; 58 | log.enable = builder.getIsDebug; 59 | 60 | this.validateOptions(); 61 | 62 | this.init(); 63 | } 64 | 65 | /** 66 | * 初始化操作 67 | */ 68 | private init(): void { 69 | this.initFileInputEl(); 70 | } 71 | 72 | /** 73 | * 初始化file input element 74 | */ 75 | private initFileInputEl(): void { 76 | 77 | //查询已经存在的file input 78 | let exist: HTMLInputElement = document.getElementById(this._fileInputId); 79 | 80 | //创建input元素 81 | this._fileInput = exist ? exist : document.createElement('input'); 82 | this.fileInput.type = 'file';//type file 83 | this.fileInput.id = this._fileInputId;//id 方便后面查找 84 | this.fileInput.style.display = 'none';//隐藏file input 85 | 86 | //多文件 87 | if (this.multiple) { 88 | //多文件 89 | this.fileInput.multiple = true; 90 | } 91 | 92 | //文件类型 93 | if (this.accept && this.accept.length != 0) { 94 | let acceptValue: string = ''; 95 | for (let value of this.accept) { 96 | acceptValue += value; 97 | acceptValue += ','; 98 | } 99 | 100 | if (acceptValue.endsWith(',')) { 101 | acceptValue = acceptValue.substring(0, acceptValue.length - 1); 102 | } 103 | this.fileInput.accept = acceptValue; 104 | log.d(`accept类型 ${acceptValue}`); 105 | } 106 | 107 | //将input元素添加到body子节点的末尾 108 | document.body.appendChild(this.fileInput); 109 | 110 | //选择文件监听器 111 | this.fileInput.addEventListener('change', this.handleFiles, false); 112 | 113 | if (this._button != undefined) { 114 | let button: any = document.getElementById(this._button); 115 | button.addEventListener(this._buttonEventName, this.chooseFile.bind(this)); 116 | } 117 | } 118 | 119 | 120 | /** 121 | * 上传完成或者失败后,对本次上传任务进行清扫 122 | */ 123 | private resetUploader(): void { 124 | log.d("开始重置 uploader"); 125 | this.taskQueue.length = 0; 126 | log.d("任务队列已清空"); 127 | this._token = null; 128 | log.d("token已清空"); 129 | log.d("uploader 重置完毕"); 130 | } 131 | 132 | /** 133 | * 处理文件 134 | */ 135 | private handleFiles = () => { 136 | //如果没有选中文件就返回 137 | if (this.fileInput.files.length == 0) { 138 | return; 139 | } 140 | 141 | //生成task 142 | this.generateTask(); 143 | 144 | //是否中断任务 145 | let isInterrupt: boolean = false; 146 | let interceptedTasks: BaseTask[] = []; 147 | 148 | //任务拦截器过滤 149 | for (let task of this.taskQueue) { 150 | for (let interceptor of this.interceptors) { 151 | //拦截生效 152 | if (interceptor.onIntercept(task, this.taskQueue)) { 153 | interceptedTasks.push(task); 154 | log.d("任务拦截器拦截了任务:"); 155 | log.d(task); 156 | } 157 | //打断生效 158 | if (interceptor.onInterrupt(task, this.taskQueue)) { 159 | //将打断标志位设为true 160 | isInterrupt = true; 161 | break; 162 | } 163 | } 164 | } 165 | 166 | if (isInterrupt) { 167 | log.w("任务拦截器中断了任务队列"); 168 | return; 169 | } 170 | 171 | //从任务队列中去除任务 172 | for (let task of interceptedTasks) { 173 | let index = this.taskQueue.indexOf(task); 174 | if (index != -1) { 175 | this.taskQueue.splice(index, 1); 176 | } 177 | } 178 | 179 | //回调函数函数 180 | this.listener.onReady(this.taskQueue); 181 | 182 | 183 | //处理图片 184 | this.handleImages().then(() => { 185 | //自动上传 186 | if (this.auto) { 187 | log.d("开始自动上传"); 188 | this.start(); 189 | } 190 | }); 191 | }; 192 | 193 | 194 | /** 195 | * 是否是分块任务 196 | * @param task 197 | * @returns {boolean} 198 | */ 199 | private static isChunkTask(task: BaseTask): boolean { 200 | return task.constructor.name === ChunkTask.name && task instanceof ChunkTask; 201 | } 202 | 203 | /** 204 | * 是否是直传任务 205 | * @param task 206 | * @returns {boolean} 207 | */ 208 | private static isDirectTask(task: BaseTask): boolean { 209 | return task.constructor.name === DirectTask.name && task instanceof DirectTask; 210 | } 211 | 212 | /** 213 | * 生成task 214 | */ 215 | private generateTask() { 216 | this.resetUploader(); 217 | 218 | let files: FileList = this.fileInput.files; 219 | 220 | //遍历files 创建上传任务 221 | for (let i: number = 0; i < this.fileInput.files.length; i++) { 222 | let file: File = files[i]; 223 | 224 | let task: BaseTask; 225 | //只有在开启分块上传,并且文件大小大于4mb的时候才进行分块上传 226 | if (this.chunk && file.size > UploaderBuilder.BLOCK_SIZE) { 227 | task = new ChunkTask(file, UploaderBuilder.BLOCK_SIZE, this.size); 228 | } 229 | else { 230 | task = new DirectTask(file); 231 | } 232 | if (this._saveKey == false) { 233 | task.key = this.listener.onTaskGetKey(task); 234 | } 235 | this.taskQueue.push(task); 236 | } 237 | } 238 | 239 | /** 240 | * 处理图片-缩放-质量压缩 241 | */ 242 | private handleImages(): Promise { 243 | let promises: Promise[] = []; 244 | 245 | if (this.compress != 1 || this.scale[0] != 0 || this.scale[1] != 0) { 246 | for (let task of this.taskQueue) { 247 | if (!task.file.type.match('image.*')) { 248 | continue; 249 | } 250 | log.d(`${task.file.name} 处理前的图片大小:${task.file.size / 1024} kb`); 251 | 252 | let canvas: HTMLCanvasElement = document.createElement('canvas'); 253 | 254 | let img: HTMLImageElement = new Image(); 255 | let ctx: CanvasRenderingContext2D = canvas.getContext('2d'); 256 | img.src = URL.createObjectURL(task.file); 257 | 258 | 259 | let _this = this; 260 | 261 | promises.push(new Promise((resolve) => 262 | img.onload = () => { 263 | 264 | let imgW = img.width; 265 | let imgH = img.height; 266 | 267 | let scaleW = _this.scale[0]; 268 | let scaleH = _this.scale[1]; 269 | 270 | if (scaleW == 0 && scaleH > 0) { 271 | canvas.width = imgW / imgH * scaleH; 272 | canvas.height = scaleH; 273 | } 274 | else if (scaleH == 0 && scaleW > 0) { 275 | canvas.width = scaleW; 276 | canvas.height = imgH / imgW * scaleW; 277 | } 278 | else if (scaleW > 0 && scaleH > 0) { 279 | canvas.width = scaleW; 280 | canvas.height = scaleH; 281 | } 282 | else { 283 | canvas.width = img.width; 284 | canvas.height = img.height; 285 | } 286 | 287 | //这里的长宽是绘制到画布上的图片的长宽 288 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 289 | 290 | console.log(canvas); 291 | console.log(canvas.toBlob); 292 | //0.95是最接近原图大小,如果质量为1的话会导致比原图大几倍。 293 | canvas.toBlob((blob: Blob) => { 294 | resolve(blob); 295 | log.d(`${task.file.name} 处理后的图片大小:${blob.size / 1024} kb`); 296 | }, "image/jpeg", _this.compress * 0.95); 297 | } 298 | ).then((blob: any) => { 299 | blob.name = task.file.name; 300 | task.file = blob; 301 | if (Uploader.isChunkTask(task)) { 302 | (task).spliceFile2Block(); 303 | } 304 | })); 305 | } 306 | } 307 | return Promise.all(promises); 308 | } 309 | 310 | /** 311 | * 检验选项合法性 312 | */ 313 | private validateOptions(): void { 314 | log.d("开始检查构建参数合法性"); 315 | if (!this._tokenFunc) { 316 | throw new Error('你必须提供一个获取Token的回调函数'); 317 | } 318 | if (!this.scale || !(this.scale instanceof Array) || this.scale.length != 2 || this.scale[0] < 0 || this.scale[1] < 0) { 319 | throw new Error('scale必须是长度为2的number类型的数组,scale[0]为宽度,scale[1]为长度,必须大于等于0'); 320 | } 321 | log.d("构建参数检查完毕"); 322 | } 323 | 324 | /** 325 | * 开始上传 326 | */ 327 | public start(): void { 328 | log.d(`上传任务遍历开始`); 329 | 330 | if (this.fileInput.files.length == 0) { 331 | throw new Error('没有选中的文件,无法开始上传'); 332 | } 333 | 334 | if (this.tasking) { 335 | throw new Error('任务执行中,请不要重复上传'); 336 | } 337 | 338 | this.listener.onStart(this.taskQueue); 339 | 340 | //遍历任务队列 341 | for (let task of this.taskQueue) { 342 | log.d(`上传文件名:${task.file.name}`); 343 | log.d(`上传文件大小:${task.file.size}字节,${task.file.size / 1024} kb,${task.file.size / 1024 / 1024} mb`); 344 | //根据任务的类型调用不同的上传模式进行上传 345 | if (Uploader.isDirectTask(task)) { 346 | log.d('该上传任务为直传任务'); 347 | //直传 348 | new DirectUploadPattern(this).upload(task); 349 | } 350 | else if (Uploader.isChunkTask(task)) { 351 | log.d('该上传任务为分片任务'); 352 | //分块上传 353 | new ChunkUploadPattern(this).upload(task); 354 | } 355 | else { 356 | throw new Error('非法的task类型'); 357 | } 358 | } 359 | } 360 | 361 | /** 362 | * 所有任务是否完成 363 | * @returns {boolean} 364 | */ 365 | public isTaskQueueFinish() { 366 | for (let task of this.taskQueue) { 367 | if (!task.isFinish) { 368 | return false; 369 | } 370 | } 371 | return true; 372 | } 373 | 374 | /** 375 | * 选择文件 376 | */ 377 | public chooseFile() { 378 | this.fileInput.click(); 379 | } 380 | 381 | public getToken(task: BaseTask): Promise { 382 | if (this._tokenShare && this._token != undefined) { 383 | return Promise.resolve(this._token); 384 | } 385 | log.d(`开始获取上传token`); 386 | return Promise.resolve(this._tokenFunc(this, task)).then((token: string): string => { 387 | log.d(`上传token获取成功: ${token}`); 388 | this._token = token; 389 | return token; 390 | }); 391 | } 392 | 393 | public requestTaskToken(task: BaseTask, url: string): Promise { 394 | return this.resolveSaveKey(task).then((saveKey: string) => { 395 | return this.requestToken(url, saveKey); 396 | }); 397 | } 398 | 399 | private requestToken(url: string, saveKey: string): Promise { 400 | return new Promise((resolve, reject) => { 401 | if (typeof saveKey == "string") { 402 | url += ((/\?/).test(url) ? "&" : "?") + "saveKey=" + encodeURIComponent(saveKey); 403 | } 404 | url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime(); 405 | 406 | let xhr: XMLHttpRequest = new XMLHttpRequest(); 407 | xhr.open('GET', url, true); 408 | xhr.onreadystatechange = () => { 409 | if (xhr.readyState != XMLHttpRequest.DONE) { 410 | return; 411 | } 412 | if (xhr.status == 200) { 413 | resolve(xhr.response.uptoken); 414 | return; 415 | } 416 | reject(xhr.response); 417 | }; 418 | xhr.onabort = () => { 419 | reject('aborted'); 420 | }; 421 | xhr.responseType = 'json'; 422 | xhr.send(); 423 | }); 424 | } 425 | 426 | private resolveSaveKey(task: BaseTask): Promise { 427 | let saveKey = this._saveKey; 428 | if (typeof saveKey != "string") { 429 | return Promise.resolve(undefined); 430 | } 431 | return Promise.resolve(saveKey) 432 | .then(this.resolveUUID) 433 | .then(saveKey => this.resolveImageInfo(task.file, saveKey)) 434 | .then(this.onSaveKeyResolved); 435 | } 436 | 437 | private resolveUUID = (s: string): string => { 438 | let re = /\$\(uuid\)/; 439 | if (re.test(s)) { 440 | return s.replace(re, UUID.uuid()); 441 | } 442 | return s; 443 | }; 444 | 445 | private resolveImageInfo = (blob: Blob, s: string): Promise => { 446 | let widthRe = /\$\(imageInfo\.width\)/; 447 | let heightRe = /\$\(imageInfo\.height\)/; 448 | if (!widthRe.test(s) && !heightRe.test(s)) { 449 | return Promise.resolve(s); 450 | } 451 | return new Promise((resolve) => { 452 | let img = new Image(); 453 | img.src = URL.createObjectURL(blob); 454 | img.onload = () => { 455 | s = s.replace(widthRe, img.width.toString()); 456 | s = s.replace(heightRe, img.height.toString()); 457 | resolve(s); 458 | }; 459 | }); 460 | }; 461 | 462 | private onSaveKeyResolved = (saveKey: string): string => { 463 | this._tokenShare = this._tokenShare && this._saveKey == saveKey; 464 | return saveKey; 465 | }; 466 | 467 | get retry(): number { 468 | return this._retry; 469 | } 470 | 471 | get size(): number { 472 | return this._size; 473 | } 474 | 475 | get auto(): boolean { 476 | return this._auto; 477 | } 478 | 479 | get multiple(): boolean { 480 | return this._multiple; 481 | } 482 | 483 | get accept(): string[] { 484 | return this._accept; 485 | } 486 | 487 | get compress(): number { 488 | return this._compress; 489 | } 490 | 491 | get scale(): number[] { 492 | return this._scale; 493 | } 494 | 495 | get listener(): UploadListener { 496 | return this._listener; 497 | } 498 | 499 | get fileInput(): HTMLInputElement { 500 | return this._fileInput; 501 | } 502 | 503 | get chunk(): boolean { 504 | return this._chunk; 505 | } 506 | 507 | get taskQueue(): BaseTask[] { 508 | return this._taskQueue; 509 | } 510 | 511 | 512 | get tasking(): boolean { 513 | return this._tasking; 514 | } 515 | 516 | set tasking(value: boolean) { 517 | this._tasking = value; 518 | } 519 | 520 | get interceptors(): Interceptor[] { 521 | return this._interceptors; 522 | } 523 | 524 | get domain(): string { 525 | return this._domain; 526 | } 527 | } 528 | 529 | export default Uploader; -------------------------------------------------------------------------------- /src/upload/UploaderBuilder.ts: -------------------------------------------------------------------------------- 1 | import Uploader from "./Uploader"; 2 | import BaseTask from "./task/BaseTask"; 3 | import TokenFunc from "./TokenFunc"; 4 | import Interceptor from "./interceptor/UploadInterceptor"; 5 | import SimpleUploadInterceptor from "./interceptor/SimpleUploadInterceptor"; 6 | import UploadListener from "./hook/UploadListener"; 7 | import {Scheme, Domain} from "./url/Domain"; 8 | 9 | /** 10 | * UploaderBuilder 11 | * 12 | */ 13 | class UploaderBuilder { 14 | public static MAX_CHUNK_SIZE = 4 * 1024 * 1024;//分片最大值 15 | public static BLOCK_SIZE = UploaderBuilder.MAX_CHUNK_SIZE;//分块大小,只有大于这个数才需要分块 16 | public static UPLOAD_DOMAIN = {http: 'http://upload.qiniu.com', https: 'https://up.qbox.me'}; 17 | 18 | private _retry: number = 0;//最大重试次数 19 | private _domain: Domain = UploaderBuilder.UPLOAD_DOMAIN;//上传域名 20 | private _scheme: Scheme = null;//上传域名的 scheme 21 | private _size: number = 1024 * 1024;//分片大小,单位字节,上限4m,不能为0 22 | private _chunk: boolean = true;//分块上传 23 | private _auto: boolean = true;//自动上传,每次选择文件后 24 | private _multiple: boolean = true;//是否支持多文件 25 | private _accept: string[] = [];//接受的文件类型 26 | private _button: string;//上传按钮 27 | private _buttonEventName: string;//上传按钮的监听事件名称 28 | private _compress: number = 1;//图片压缩质量 29 | private _scale: number[] = [0, 0];//缩放大小,限定高度等比[h:200,w:0],限定宽度等比[h:0,w:100],限定长宽[h:200,w:100] 30 | private _listener: UploadListener;//监听器 31 | private _saveKey: boolean | string = false; 32 | private _tokenFunc: TokenFunc;//token获取函数 33 | private _tokenShare: boolean = true;//分享token,如果为false,每一次HTTP请求都需要新获取Token 34 | private _interceptors: Interceptor[] = [];//任务拦截器 35 | private _isDebug: boolean = false;// 36 | 37 | 38 | /** 39 | * 设置上传的域名,默认是 {http: 'http://upload.qiniu.com', https: 'https://up.qbox.me'} 40 | * @param domain 41 | * @returns {UploaderBuilder} 42 | */ 43 | public domain(domain: Domain): UploaderBuilder { 44 | this._domain = domain; 45 | return this; 46 | } 47 | 48 | /** 49 | * 设置上传域名的协议类型,默认从 window.location.protocol 读取 50 | * @param scheme 51 | * @returns {UploaderBuilder} 52 | */ 53 | public scheme(scheme: Scheme): UploaderBuilder { 54 | this._scheme = scheme; 55 | return this; 56 | } 57 | 58 | /** 59 | * 添加一个拦截器 60 | * @param interceptor 61 | * @returns {UploaderBuilder} 62 | */ 63 | public interceptor(interceptor: Interceptor): UploaderBuilder { 64 | this._interceptors.push(Object.assign(new SimpleUploadInterceptor(), interceptor)); 65 | return this; 66 | } 67 | 68 | /** 69 | * 上传失败后的重传尝试次数 70 | * @param retry 默认0次,不尝试次重传 71 | * @returns {UploaderBuilder} 72 | */ 73 | public retry(retry: number): UploaderBuilder { 74 | this._retry = retry; 75 | return this; 76 | } 77 | 78 | /** 79 | * 设置分片大小 80 | * @param size 分块大小,单位字节,默认4*1024*1024字节(4mb) 81 | * @returns {UploaderBuilder} 82 | */ 83 | public size(size: number): UploaderBuilder { 84 | this._size = Math.min(Math.max(size, 1), UploaderBuilder.MAX_CHUNK_SIZE); 85 | return this; 86 | } 87 | 88 | /** 89 | * 选择文件后,是否自动上传 90 | * @param auto 默认true 91 | * @returns {UploaderBuilder} 92 | */ 93 | public auto(auto: boolean): UploaderBuilder { 94 | this._auto = auto; 95 | return this; 96 | } 97 | 98 | /** 99 | * 是否支持多文件选择 100 | * @param multiple 默认true 101 | * @returns {UploaderBuilder} 102 | */ 103 | public multiple(multiple: boolean): UploaderBuilder { 104 | this._multiple = multiple; 105 | return this; 106 | } 107 | 108 | /** 109 | * 接受上传的文件类型 110 | * @param accept 数组形式例如:['.png','video/*'] 111 | * 112 | * 详细配置见http://www.w3schools.com/tags/att_input_accept.asp 113 | * 114 | * @returns {UploaderBuilder} 115 | */ 116 | public accept(accept: string[]): UploaderBuilder { 117 | this._accept = accept; 118 | return this; 119 | } 120 | 121 | /** 122 | * 设置上传按钮 123 | * @param button 上传按钮ID 124 | * @param eventName 上传按钮的监听事件名称,默认为 "click" 。 125 | * @returns {UploaderBuilder} 126 | */ 127 | public button(button: string, eventName = "click"): UploaderBuilder { 128 | this._button = button; 129 | this._buttonEventName = eventName; 130 | return this; 131 | } 132 | 133 | /** 134 | * 图片质量压缩,只在上传的文件是图片的时候有效 135 | * @param compress 0-1,默认1,不压缩 136 | * @returns {UploaderBuilder} 137 | */ 138 | public compress(compress: number): UploaderBuilder { 139 | this._compress = Math.max(Math.min(compress, 1), 0); 140 | return this; 141 | } 142 | 143 | /** 144 | * 图片缩放 145 | * @returns {UploaderBuilder} 146 | * @param scale 147 | */ 148 | public scale(scale: number[]): UploaderBuilder { 149 | this._scale = scale; 150 | return this; 151 | } 152 | 153 | /** 154 | * 设置 saveKey 155 | * @param saveKey 156 | * @returns {UploaderBuilder} 157 | */ 158 | public saveKey(saveKey: boolean | string): UploaderBuilder { 159 | this._saveKey = saveKey; 160 | return this; 161 | } 162 | 163 | /** 164 | * 获取Token的地址 165 | * @param tokenUrl 166 | * @returns {UploaderBuilder} 167 | */ 168 | public tokenUrl(tokenUrl: string): UploaderBuilder { 169 | this._tokenFunc = (uploader: Uploader, task: BaseTask) => { 170 | return uploader.requestTaskToken(task, tokenUrl); 171 | }; 172 | return this; 173 | } 174 | 175 | /** 176 | * 获取Token的函数 177 | * @param tokenFunc 178 | * @returns {UploaderBuilder} 179 | */ 180 | public tokenFunc(tokenFunc: Function): UploaderBuilder { 181 | this._tokenFunc = (uploader: Uploader, task: BaseTask) => { 182 | return new Promise((resolve) => { 183 | tokenFunc(resolve, task); 184 | }); 185 | }; 186 | return this; 187 | } 188 | 189 | /** 190 | * 上传生命周期钩子 191 | * @param listener 192 | * @returns {UploaderBuilder} 193 | */ 194 | public listener(listener: UploadListener): UploaderBuilder { 195 | this._listener = listener; 196 | return this; 197 | } 198 | 199 | /** 200 | * 是否分享token,如果为false每上传一个文件都需要请求一次Token。 201 | * @param tokenShare 202 | * @returns {UploaderBuilder} 203 | */ 204 | public tokenShare(tokenShare: boolean): UploaderBuilder { 205 | this._tokenShare = tokenShare; 206 | return this; 207 | } 208 | 209 | /** 210 | * 是否分块上传 211 | * @param chunk 默认false 212 | * @returns {UploaderBuilder} 213 | */ 214 | public chunk(chunk: boolean): UploaderBuilder { 215 | this._chunk = chunk; 216 | return this; 217 | } 218 | 219 | /** 220 | * 是否开启debug模式 221 | * @param debug 默认false 222 | * @returns {UploaderBuilder} 223 | */ 224 | public debug(debug: boolean): UploaderBuilder { 225 | this._isDebug = debug; 226 | return this; 227 | } 228 | 229 | get getRetry(): number { 230 | return this._retry; 231 | } 232 | 233 | get getSize(): number { 234 | return this._size; 235 | } 236 | 237 | get getAuto(): boolean { 238 | return this._auto; 239 | } 240 | 241 | get getMultiple(): boolean { 242 | return this._multiple; 243 | } 244 | 245 | get getAccept(): string[] { 246 | return this._accept; 247 | } 248 | 249 | get getButton(): string { 250 | return this._button; 251 | } 252 | 253 | get getButtonEventName(): string { 254 | return this._buttonEventName; 255 | } 256 | 257 | get getCompress(): number { 258 | return this._compress; 259 | } 260 | 261 | get getScale(): number[] { 262 | return this._scale; 263 | } 264 | 265 | get getListener(): UploadListener { 266 | return this._listener; 267 | } 268 | 269 | get getSaveKey(): boolean | string { 270 | return this._saveKey; 271 | } 272 | 273 | get getTokenFunc(): TokenFunc { 274 | return this._tokenFunc; 275 | } 276 | 277 | get getTokenShare(): boolean { 278 | return this._tokenShare; 279 | } 280 | 281 | get getChunk(): boolean { 282 | return this._chunk; 283 | } 284 | 285 | get getIsDebug(): boolean { 286 | return this._isDebug; 287 | } 288 | 289 | get getInterceptors(): Interceptor[] { 290 | return this._interceptors; 291 | } 292 | 293 | get getDomain(): string { 294 | let domain: any = this._domain; 295 | if (!domain) { 296 | domain = UploaderBuilder.UPLOAD_DOMAIN; 297 | } 298 | if (typeof domain != "string") { 299 | let scheme = this._scheme; 300 | if (typeof scheme != "string") { 301 | let protocol = window.location.protocol; 302 | scheme = protocol.substring(0, protocol.length - 1) as Scheme 303 | } 304 | domain = domain[scheme]; 305 | } 306 | return domain.endsWith('/') ? domain.substring(0, domain.length - 1) : domain; 307 | } 308 | 309 | public build(): Uploader { 310 | return new Uploader(this); 311 | } 312 | } 313 | 314 | export default UploaderBuilder; -------------------------------------------------------------------------------- /src/upload/hook/SimpleUploadListener.ts: -------------------------------------------------------------------------------- 1 | import Task from "../task/DirectTask"; 2 | import UploadListener from "./UploadListener"; 3 | class SimpleUploadListener implements UploadListener { 4 | 5 | onReady(taskQueue: Task[]): void { 6 | } 7 | 8 | onStart(taskQueue: Task[]): void { 9 | } 10 | 11 | onTaskProgress(task: Task): void { 12 | } 13 | 14 | onTaskGetKey(task: Task): string { 15 | return null; 16 | } 17 | 18 | onTaskFail(task: Task): void { 19 | } 20 | 21 | onTaskSuccess(task: Task): void { 22 | } 23 | 24 | onTaskRetry(task: Task): void { 25 | } 26 | 27 | onFinish(taskQueue: Task[]): void { 28 | } 29 | } 30 | 31 | export default SimpleUploadListener; -------------------------------------------------------------------------------- /src/upload/hook/UploadListener.ts: -------------------------------------------------------------------------------- 1 | import Task from "../task/DirectTask"; 2 | interface UploadListener { 3 | onReady(taskQueue: Task[]): void; 4 | onStart(taskQueue: Task[]): void; 5 | onTaskProgress(task: Task): void; 6 | onTaskGetKey(task: Task): string; 7 | onTaskFail(task: Task): void; 8 | onTaskSuccess(task: Task): void; 9 | onTaskRetry(task: Task): void; 10 | onFinish(taskQueue: Task[]): void; 11 | } 12 | 13 | export default UploadListener; -------------------------------------------------------------------------------- /src/upload/interceptor/SimpleUploadInterceptor.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "../task/BaseTask"; 2 | import UploadInterceptor from "./UploadInterceptor"; 3 | 4 | class SimpleUploadInterceptor implements UploadInterceptor { 5 | 6 | onIntercept(task: BaseTask): boolean { 7 | return false; 8 | } 9 | 10 | onInterrupt(task: BaseTask): boolean { 11 | return false; 12 | } 13 | 14 | } 15 | 16 | export default SimpleUploadInterceptor; 17 | -------------------------------------------------------------------------------- /src/upload/interceptor/UploadInterceptor.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "../task/BaseTask"; 2 | /** 3 | * 上传任务拦截器 4 | */ 5 | interface UploadInterceptor { 6 | /** 7 | * 是否拦截此Task,拦截后此Task不再上传 8 | * @param task 返回true拦截,false放行。 9 | */ 10 | onIntercept(task: BaseTask, tasks: BaseTask[]): boolean; 11 | /** 12 | * 是否中断上传任务,中断后则不会开始上传。 13 | * @param task 返回true中断,false放行。 14 | */ 15 | onInterrupt(task: BaseTask, tasks: BaseTask[]): boolean; 16 | } 17 | 18 | 19 | export default UploadInterceptor; 20 | -------------------------------------------------------------------------------- /src/upload/pattren/ChunkUploadPattern.ts: -------------------------------------------------------------------------------- 1 | import IUploadPattern from "./IUploadPattern"; 2 | import Uploader from "../Uploader"; 3 | import {ChunkTask, Chunk} from "../task/ChunkTask"; 4 | import log from "../../util/Log"; 5 | 6 | /** 7 | * 分块上传 8 | */ 9 | class ChunkUploadPattern implements IUploadPattern { 10 | private uploader: Uploader; 11 | private task: ChunkTask; 12 | 13 | constructor(uploader: Uploader) { 14 | this.uploader = uploader; 15 | } 16 | 17 | init(uploader: Uploader): void { 18 | this.uploader = uploader; 19 | } 20 | 21 | upload(task: ChunkTask): void { 22 | this.task = task; 23 | 24 | this.uploader.getToken(task).then((token: string) => { 25 | task.startDate = new Date(); 26 | this.uploadBlock(token); 27 | }); 28 | } 29 | 30 | private uploadBlock(token: string) { 31 | log.d(`准备开始上传块`); 32 | let chain: Promise = Promise.resolve(); 33 | log.d(`共${this.task.blocks.length}块等待上传`); 34 | log.d(`共${this.task.totalChunkCount}分片等待上传`); 35 | 36 | this.task.blocks.forEach((block, blockIndex) => { 37 | block.chunks.forEach((chunk, chunkIndex) => { 38 | chain = chain.then(() => { 39 | log.d(`开始上传第${(blockIndex + 1)}块,第${(chunkIndex + 1)}片`); 40 | return this.uploadChunk(chunk, token) 41 | }); 42 | }); 43 | }); 44 | 45 | 46 | chain.then(() => { 47 | return this.concatChunks(token); 48 | }).then(() => { 49 | //所有任务都结束了 50 | if (this.uploader.isTaskQueueFinish()) { 51 | log.d(`上传任务队列已结束`); 52 | 53 | //更改任务执行中标志 54 | this.uploader.tasking = false; 55 | 56 | //监听器调用 57 | this.uploader.listener.onFinish(this.uploader.taskQueue); 58 | } 59 | }).catch((response) => { 60 | log.w(`${this.task.file.name}分块上传失败`); 61 | this.task.error = response; 62 | this.task.isSuccess = false; 63 | this.task.isFinish = true; 64 | this.task.endDate = new Date(); 65 | this.uploader.listener.onTaskFail(this.task); 66 | }); 67 | } 68 | 69 | private uploadChunk(chunk: Chunk, token: string) { 70 | return new Promise((resolve, reject) => { 71 | let isFirstChunkInBlock = chunk.block.chunks.indexOf(chunk) == 0; 72 | let chunkIndex = chunk.block.chunks.indexOf(chunk); 73 | //前一个chunk,如果存在的话 74 | let prevChunk: any = isFirstChunkInBlock ? null : chunk.block.chunks[chunkIndex - 1]; 75 | 76 | let url: string = isFirstChunkInBlock ? this.getUploadBlockUrl(chunk.block.data.size) : this.getUploadChunkUrl(chunk.start, prevChunk ? prevChunk.ctx : null, prevChunk ? prevChunk.host : null); 77 | 78 | let xhr: XMLHttpRequest = new XMLHttpRequest(); 79 | xhr.open('POST', url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime(), true); 80 | xhr.setRequestHeader('Content-Type', 'application/octet-stream');//设置contentType 81 | xhr.setRequestHeader('Authorization', `UpToken ${token}`);//添加token验证头 82 | 83 | //分片上传中 84 | xhr.upload.onprogress = (e: ProgressEvent) => { 85 | if (e.lengthComputable) { 86 | let progress = Math.round(((this.task.finishedBlocksSize + chunk.start + e.loaded) / this.task.file.size) * 100); 87 | if (this.task.progress < progress) { 88 | this.task.progress = progress; 89 | this.uploader.listener.onTaskProgress(this.task); 90 | } 91 | } 92 | }; 93 | 94 | //分片上传完成 95 | xhr.upload.onload = () => { 96 | let progress = Math.round(((this.task.finishedBlocksSize + chunk.start + chunk.data.size) / this.task.file.size) * 100); 97 | if (this.task.progress < progress) { 98 | this.task.progress = progress; 99 | this.uploader.listener.onTaskProgress(this.task); 100 | } 101 | }; 102 | 103 | //响应返回 104 | xhr.onreadystatechange = () => { 105 | if (xhr.readyState == XMLHttpRequest.DONE) { 106 | if (xhr.status == 200 && xhr.responseText != '') { 107 | let result: any = JSON.parse(xhr.responseText); 108 | chunk.isFinish = true; 109 | chunk.processing = false; 110 | chunk.ctx = result.ctx; 111 | chunk.host = result.host; 112 | let chunkIndex: number = chunk.block.chunks.indexOf(chunk); 113 | let hasNextChunkInThisBlock: boolean = chunkIndex != chunk.block.chunks.length - 1; 114 | if (!hasNextChunkInThisBlock) { 115 | chunk.block.isFinish = true; 116 | chunk.block.processing = false; 117 | } 118 | resolve(); 119 | } 120 | else { 121 | reject(xhr.response); 122 | } 123 | } 124 | }; 125 | 126 | xhr.send(chunk.data); 127 | }); 128 | } 129 | 130 | 131 | private concatChunks(token: string) { 132 | return new Promise((resolve, reject) => { 133 | let encodedKey: any = this.task.key ? btoa(this.task.key) : null; 134 | // 安全字符串 参考:https://developer.qiniu.com/kodo/api/mkfile 135 | if (encodedKey) { 136 | encodedKey = encodedKey.replace(/\+/g, '-'); 137 | encodedKey = encodedKey.replace(/\//g, '_'); 138 | } 139 | let url = this.getMakeFileUrl(this.task.file.size, encodedKey); 140 | //构建所有数据块最后一个数据片上传后得到的的组合成的列表字符串 141 | let ctxListString = ''; 142 | 143 | for (let block of this.task.blocks) { 144 | let lastChunk = block.chunks[block.chunks.length - 1]; 145 | ctxListString += lastChunk.ctx + ','; 146 | } 147 | 148 | if (ctxListString.endsWith(',')) { 149 | ctxListString = ctxListString.substring(0, ctxListString.length - 1); 150 | } 151 | 152 | let xhr: XMLHttpRequest = new XMLHttpRequest(); 153 | xhr.open('POST', url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime(), true); 154 | xhr.setRequestHeader('Content-Type', 'text/plain');//设置contentType 155 | xhr.setRequestHeader('Authorization', `UpToken ${token}`);//添加token验证头 156 | xhr.onreadystatechange = () => { 157 | if (xhr.readyState == XMLHttpRequest.DONE) { 158 | this.task.isFinish = true; 159 | if (xhr.status == 200 && xhr.responseText != '') { 160 | let result: any = JSON.parse(xhr.responseText); 161 | this.task.isSuccess = true; 162 | this.task.result = result; 163 | this.task.endDate = new Date(); 164 | this.uploader.listener.onTaskSuccess(this.task); 165 | resolve(); 166 | } 167 | else if (this.retryTask(this.task)) { 168 | log.w(`${this.task.file.name}分块上传失败,准备开始重传`); 169 | this.uploader.listener.onTaskRetry(this.task); 170 | } 171 | else { 172 | reject(xhr.response); 173 | } 174 | } 175 | }; 176 | xhr.send(ctxListString); 177 | }); 178 | } 179 | 180 | /** 181 | * 获取块上传的url 182 | * @param blockSize 183 | * @returns {string} 184 | */ 185 | private getUploadBlockUrl(blockSize: number): string { 186 | return `${this.uploader.domain}/mkblk/${blockSize}`; 187 | } 188 | 189 | /** 190 | * 获取片上传的url 191 | * @param start 片的在块中的起始位置 192 | * @param ctx 前一次上传返回的块级上传控制信息。 193 | * @param host 指定host 194 | */ 195 | private getUploadChunkUrl(start: number, ctx: string, host?: string): string { 196 | return `${host ? host : this.uploader.domain}/bput/${ctx}/${start}/`; 197 | } 198 | 199 | /** 200 | * 获取合并块为文件的url 201 | * @param fileSize 文件大小 202 | * @param encodedKey base64UrlEncode后的资源名称,若未指定,则使用saveKey;若未指定saveKey,则使用资源内容的SHA1值作为资源名。 203 | * @returns {string} 204 | */ 205 | private getMakeFileUrl(fileSize: number, encodedKey: string): string { 206 | if (encodedKey) { 207 | return `${this.uploader.domain}/mkfile/${fileSize}/key/${encodedKey}`; 208 | } 209 | else { 210 | return `${this.uploader.domain}/mkfile/${fileSize}`; 211 | } 212 | } 213 | 214 | 215 | private retryTask(task: ChunkTask): boolean { 216 | //达到重试次数 217 | if (task.retry >= this.uploader.retry) { 218 | log.w(`${task.file.name}达到重传次数上限${this.uploader.retry},停止重传`); 219 | return false; 220 | } 221 | task.retry++; 222 | log.w(`${task.file.name}开始重传,当前重传次数${task.retry}`); 223 | // this.upload(task); 224 | 225 | //todo 226 | return true; 227 | } 228 | 229 | } 230 | 231 | 232 | export default ChunkUploadPattern; -------------------------------------------------------------------------------- /src/upload/pattren/DirectUploadPattern.ts: -------------------------------------------------------------------------------- 1 | import IUploadPattern from "./IUploadPattern"; 2 | import Uploader from "../Uploader"; 3 | import DirectTask from "../task/DirectTask"; 4 | import log from "../../util/Log"; 5 | /** 6 | * 直接上传 7 | */ 8 | class DirectUploadPattern implements IUploadPattern { 9 | private uploader: Uploader; 10 | private task: DirectTask; 11 | 12 | constructor(uploader: Uploader) { 13 | this.uploader = uploader; 14 | } 15 | 16 | /** 17 | * 实现接口的上传方法 18 | * @param task 19 | */ 20 | upload(task: DirectTask): void { 21 | this.task = task; 22 | 23 | this.uploader.getToken(task).then((token: string) => { 24 | task.startDate = new Date(); 25 | this.uploadFile(token); 26 | }); 27 | 28 | } 29 | 30 | 31 | /** 32 | * 创建表单 33 | * @param token 34 | * @returns {FormData} 35 | */ 36 | private createFormData(token: string): FormData { 37 | let task: DirectTask = this.task; 38 | let formData: FormData = new FormData(); 39 | 40 | //key存在,添加到formData中,若不设置,七牛服务器会自动生成hash key 41 | if (task.key !== null && task.key !== undefined) { 42 | formData.append('key', task.key); 43 | } 44 | 45 | formData.append('token', token); 46 | formData.append('file', task.file); 47 | 48 | log.d(`创建formData对象`); 49 | 50 | return formData; 51 | } 52 | 53 | 54 | /** 55 | * 上传文件 56 | * @param token 57 | */ 58 | private uploadFile(token: string) { 59 | let task: DirectTask = this.task; 60 | 61 | let xhr: XMLHttpRequest = new XMLHttpRequest(); 62 | 63 | //上传中 64 | xhr.upload.onprogress = (e: ProgressEvent) => { 65 | if (e.lengthComputable) { 66 | let progress = Math.round((e.loaded * 100) / e.total); 67 | if (task.progress < progress) { 68 | task.progress = progress; 69 | this.uploader.listener.onTaskProgress(task); 70 | } 71 | } 72 | }; 73 | 74 | //上传完成 75 | xhr.upload.onload = () => { 76 | if (task.progress < 100) { 77 | task.progress = 100; 78 | this.uploader.listener.onTaskProgress(task); 79 | } 80 | }; 81 | 82 | 83 | let url = this.uploader.domain; 84 | //避免浏览器缓存http请求 85 | url += ((/\?/).test(this.uploader.domain) ? "&" : "?") + (new Date()).getTime(); 86 | xhr.open('POST', url, true); 87 | 88 | xhr.onreadystatechange = () => { 89 | if (xhr.readyState == XMLHttpRequest.DONE) { 90 | if (xhr.status == 200 && xhr.responseText != '') { 91 | task.result = JSON.parse(xhr.responseText); 92 | task.isSuccess = true; 93 | task.isFinish = true; 94 | task.endDate = new Date(); 95 | this.uploader.listener.onTaskSuccess(task); 96 | } 97 | else if (this.retryTask(task)) { 98 | log.w(`${task.file.name}上传失败,准备开始重传`); 99 | this.uploader.listener.onTaskRetry(task); 100 | } 101 | else { 102 | log.w(`${task.file.name}上传失败`); 103 | task.error = xhr.response; 104 | task.isSuccess = false; 105 | task.isFinish = true; 106 | task.endDate = new Date(); 107 | this.uploader.listener.onTaskFail(task); 108 | } 109 | 110 | //所有任务都结束了 111 | if (this.uploader.isTaskQueueFinish()) { 112 | log.d('上传队列结束'); 113 | //更改任务执行中标志 114 | this.uploader.tasking = false; 115 | 116 | //onFinish callback 117 | this.uploader.listener.onFinish(this.uploader.taskQueue); 118 | } 119 | } 120 | }; 121 | 122 | let formData: FormData = this.createFormData(token); 123 | xhr.send(formData); 124 | log.d('发送ajax post 请求'); 125 | } 126 | 127 | 128 | /** 129 | * 重传 130 | * @param task 131 | * @returns {boolean} 132 | */ 133 | private retryTask(task: DirectTask): boolean { 134 | log.d("开始尝试重传"); 135 | //达到重试次数 136 | if (task.retry >= this.uploader.retry) { 137 | log.w(`${task.file.name}达到重传次数上限${this.uploader.retry},停止重传`); 138 | return false; 139 | } 140 | task.retry++; 141 | log.w(`${task.file.name}开始重传,当前重传次数${task.retry}`); 142 | this.upload(task); 143 | return true; 144 | } 145 | } 146 | 147 | 148 | export default DirectUploadPattern; -------------------------------------------------------------------------------- /src/upload/pattren/IUploadPattern.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "../task/BaseTask"; 2 | /** 3 | * 4 | */ 5 | interface IUploadPattern { 6 | 7 | /** 8 | * 上传任务 9 | * @param task 10 | */ 11 | upload(task: BaseTask): void; 12 | } 13 | 14 | export default IUploadPattern; -------------------------------------------------------------------------------- /src/upload/task/BaseTask.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 上传任务 3 | */ 4 | abstract class BaseTask { 5 | protected _file: File; 6 | protected _retry: number = 0;//已重试次数 7 | protected _createDate: Date;//创建时间 8 | protected _startDate: Date;//开始时间 9 | protected _endDate: Date;//结束时间 10 | protected _key: string;//key文件名 11 | protected _progress: number = 0;//任务进度,最大100 12 | protected _isSuccess: boolean = false;//是否上传成功 13 | protected _isFinish: boolean = false;//是否结束 14 | protected _result: Object; 15 | protected _error: any; 16 | 17 | constructor(file: File) { 18 | this._file = file; 19 | this._createDate = new Date(); 20 | } 21 | 22 | public get file(): File { 23 | return this._file; 24 | } 25 | 26 | public set file(file: File) { 27 | this._file = file; 28 | } 29 | 30 | get retry(): number { 31 | return this._retry; 32 | } 33 | 34 | set retry(value: number) { 35 | this._retry = value; 36 | } 37 | 38 | get createDate(): Date { 39 | return this._createDate; 40 | } 41 | 42 | set createDate(value: Date) { 43 | this._createDate = value; 44 | } 45 | 46 | get startDate(): Date { 47 | return this._startDate; 48 | } 49 | 50 | set startDate(value: Date) { 51 | this._startDate = value; 52 | } 53 | 54 | get endDate(): Date { 55 | return this._endDate; 56 | } 57 | 58 | set endDate(value: Date) { 59 | this._endDate = value; 60 | } 61 | 62 | get isSuccess(): boolean { 63 | return this._isSuccess; 64 | } 65 | 66 | set isSuccess(value: boolean) { 67 | this._isSuccess = value; 68 | } 69 | 70 | get progress(): number { 71 | return this._progress; 72 | } 73 | 74 | set progress(value: number) { 75 | this._progress = Math.min(Math.max(0, value), 100); 76 | } 77 | 78 | 79 | get result() { 80 | return this._result; 81 | } 82 | 83 | set result(value) { 84 | this._result = value; 85 | } 86 | 87 | get error() { 88 | return this._error; 89 | } 90 | 91 | set error(value) { 92 | this._error = value; 93 | } 94 | 95 | 96 | get key(): string { 97 | return this._key; 98 | } 99 | 100 | set key(value: string) { 101 | this._key = value; 102 | } 103 | 104 | get isFinish(): boolean { 105 | return this._isFinish; 106 | } 107 | 108 | set isFinish(value: boolean) { 109 | this._isFinish = value; 110 | } 111 | } 112 | 113 | export default BaseTask; -------------------------------------------------------------------------------- /src/upload/task/ChunkTask.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "./BaseTask"; 2 | /** 3 | * 分块任务 4 | */ 5 | class ChunkTask extends BaseTask { 6 | //分块 7 | private _blocks: Block[] = []; 8 | private _blockSize: number = 0; 9 | private _chunkSize: number = 0; 10 | 11 | /** 12 | * 构造函数 13 | * @param file 14 | * @param blockSize 块大小 15 | * @param chunkSize 片大小 16 | */ 17 | constructor(file: File, blockSize: number, chunkSize: number) { 18 | super(file); 19 | this._blockSize = blockSize; 20 | this._chunkSize = chunkSize; 21 | this.spliceFile2Block(); 22 | } 23 | 24 | /** 25 | * 将文件分块 26 | */ 27 | public spliceFile2Block(): void { 28 | this._blocks = []; 29 | let fileSize: number = this._file.size; 30 | let file: File = this._file; 31 | //总块数 32 | let blockCount = Math.ceil(fileSize / this._blockSize); 33 | 34 | for (let i = 0; i < blockCount; i++) { 35 | let start: number = i * this._blockSize;//起始位置 36 | let end: number = start + this._blockSize;//结束位置 37 | //构造一个块实例 38 | let block: Block = new Block(start, end, file.slice(start, end), this._chunkSize, file); 39 | //添加到数组中 40 | this._blocks.push(block); 41 | } 42 | } 43 | 44 | /** 45 | * 获取所有的block 46 | * @returns {Block[]} 47 | */ 48 | get blocks(): Block[] { 49 | return this._blocks; 50 | } 51 | 52 | /** 53 | * 获取正在处理的block 54 | * @returns {Block} 55 | */ 56 | get processingBlock(): Block { 57 | for (let block of this._blocks) { 58 | if (!block.processing) { 59 | continue; 60 | } 61 | return block; 62 | } 63 | throw Error("找不到正在处理的Block") 64 | } 65 | 66 | get finishedBlocksSize(): number { 67 | let size: number = 0; 68 | for (let block of this._blocks) { 69 | size += (block.isFinish ? block.data.size : 0); 70 | } 71 | return size; 72 | } 73 | 74 | get chunks(): Chunk[] { 75 | let array: Chunk[] = []; 76 | for (let block of this._blocks) { 77 | for (let chunk of block.chunks) { 78 | array.push(chunk); 79 | } 80 | } 81 | return array; 82 | } 83 | 84 | /** 85 | * 获取正在处理的chunk 86 | * @returns {Block} 87 | */ 88 | get processingChunk(): Chunk { 89 | for (let block of this._blocks) { 90 | if (!block.processing) { 91 | continue; 92 | } 93 | for (let chunk of block.chunks) { 94 | if (!chunk.processing) { 95 | continue; 96 | } 97 | return chunk; 98 | } 99 | } 100 | throw Error("找不到正在处理的Chunk") 101 | } 102 | 103 | /** 104 | * 总共分片数量(所有分块的分片数量总和) 105 | * @returns {number} 106 | */ 107 | get totalChunkCount(): number { 108 | let count = 0; 109 | for (let block of this._blocks) { 110 | count += block.chunks.length; 111 | } 112 | return count; 113 | } 114 | } 115 | 116 | /** 117 | * 分块,分块大小七牛固定是4M 118 | */ 119 | class Block { 120 | private _data: Blob;//块数据 121 | private _start: number;//起始位置 122 | private _end: number;//结束位置 123 | private _chunks: Chunk[] = []; 124 | private _isFinish: boolean = false;//是否上传完成 125 | private _processing: boolean = false;//是否正在上传 126 | private _file: File; 127 | 128 | /** 129 | * 130 | * @param start 起始位置 131 | * @param end 结束位置 132 | * @param data 块数据 133 | * @param chunkSize 分片数据的最大大小 134 | * @param file 分块所属文件 135 | */ 136 | constructor(start: number, end: number, data: Blob, chunkSize: number, file: File) { 137 | this._data = data; 138 | this._start = start; 139 | this._end = end; 140 | this._file = file; 141 | this.spliceBlock2Chunk(chunkSize); 142 | } 143 | 144 | /** 145 | * 将块分片 146 | */ 147 | private spliceBlock2Chunk(chunkSize: number): void { 148 | let blockSize: number = this._data.size; 149 | let data: Blob = this._data; 150 | //总片数 151 | let chunkCount = Math.ceil(blockSize / chunkSize); 152 | for (let i: number = 0; i < chunkCount; i++) { 153 | let start: number = i * chunkSize;//起始位置 154 | let end: number = start + chunkSize;//结束位置 155 | //构造一个片实例 156 | let chunk: Chunk = new Chunk(start, end, data.slice(start, end), this); 157 | //添加到数组中 158 | this._chunks.push(chunk); 159 | } 160 | } 161 | 162 | /** 163 | * 是否上传中 164 | * @returns {boolean} 165 | */ 166 | get processing(): boolean { 167 | return this._processing; 168 | } 169 | 170 | set processing(value: boolean) { 171 | this._processing = value; 172 | } 173 | 174 | /** 175 | * 分块所属的文件 176 | * @returns {File} 177 | */ 178 | get file(): File { 179 | return this._file; 180 | } 181 | 182 | /** 183 | * 是否已经结束 184 | * @returns {boolean} 185 | */ 186 | get isFinish(): boolean { 187 | return this._isFinish; 188 | } 189 | 190 | set isFinish(value: boolean) { 191 | this._isFinish = value; 192 | } 193 | 194 | /** 195 | * 返回分块数据 196 | * @returns {Blob} 197 | */ 198 | get data(): Blob { 199 | return this._data; 200 | } 201 | 202 | /** 203 | * 返回字节起始位置 204 | * @returns {number} 205 | */ 206 | get start(): number { 207 | return this._start; 208 | } 209 | 210 | /** 211 | * 返回字节结束位置 212 | * @returns {number} 213 | */ 214 | get end(): number { 215 | return this._end; 216 | } 217 | 218 | get chunks(): Chunk[] { 219 | return this._chunks; 220 | } 221 | } 222 | 223 | /** 224 | * 分片,分片大小可以自定义,至少1字节 225 | */ 226 | class Chunk { 227 | private _start: number;//起始位置 228 | private _end: number;//结束位置 229 | private _data: Blob;//片数据 230 | private _processing: boolean = false;//是否正在上传 231 | private _isFinish: boolean = false;//是否上传完成 232 | private _ctx: string;//前一次上传返回的块级上传控制信息,第一个chunk此值为空 233 | private _block: Block;//分片所属的块对象 234 | private _host: string;//前一次上传返回的指定上传地址 235 | 236 | /** 237 | * 238 | * @param start 字节起始位置 239 | * @param end 字节结束位置 240 | * @param data 分片数据 241 | * @param block 分块对象 242 | */ 243 | constructor(start: number, end: number, data: Blob, block: Block) { 244 | this._start = start; 245 | this._end = end; 246 | this._data = data; 247 | this._block = block; 248 | } 249 | 250 | /** 251 | * 返回chunk所属的Block对象 252 | * @returns {Block} 253 | */ 254 | get block(): Block { 255 | return this._block; 256 | } 257 | 258 | /** 259 | * 返回字节起始位置 260 | * @returns {number} 261 | */ 262 | get start(): number { 263 | return this._start; 264 | } 265 | 266 | /** 267 | * 返回字节结束位置 268 | * @returns {number} 269 | */ 270 | get end(): number { 271 | return this._end; 272 | } 273 | 274 | /** 275 | * 返回分片数据 276 | * @returns {Blob} 277 | */ 278 | get data(): Blob { 279 | return this._data; 280 | } 281 | 282 | /** 283 | * 是否已经结束 284 | * @returns {boolean} 285 | */ 286 | get isFinish(): boolean { 287 | return this._isFinish; 288 | } 289 | 290 | 291 | set isFinish(value: boolean) { 292 | this._isFinish = value; 293 | } 294 | 295 | 296 | get host(): string { 297 | return this._host; 298 | } 299 | 300 | set host(value: string) { 301 | this._host = value; 302 | } 303 | 304 | 305 | /** 306 | * 是否上传中 307 | * @returns {boolean} 308 | */ 309 | get processing(): boolean { 310 | return this._processing; 311 | } 312 | 313 | set processing(value: boolean) { 314 | this._processing = value; 315 | } 316 | 317 | 318 | /** 319 | * 返回上传控制信息(七牛服务器返回前一次上传返回的分片上传控制信息,用于下一次上传,第一个chunk此值为空) 320 | * @returns {string} 321 | */ 322 | get ctx(): string { 323 | return this._ctx; 324 | } 325 | 326 | set ctx(value: string) { 327 | this._ctx = value; 328 | } 329 | } 330 | 331 | 332 | export {ChunkTask, Block, Chunk}; -------------------------------------------------------------------------------- /src/upload/task/DirectTask.ts: -------------------------------------------------------------------------------- 1 | import BaseTask from "./BaseTask"; 2 | /** 3 | * 直传任务 4 | */ 5 | class DirectTask extends BaseTask { 6 | 7 | } 8 | 9 | export default DirectTask; -------------------------------------------------------------------------------- /src/upload/url/Domain.ts: -------------------------------------------------------------------------------- 1 | export type Scheme = "http" | "https" | string 2 | 3 | export type Domain = string | { http: string; https: string } 4 | -------------------------------------------------------------------------------- /src/upload/uuid/UUID.ts: -------------------------------------------------------------------------------- 1 | class UUID { 2 | public static uuid(): string { 3 | let d = new Date().getTime(); 4 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 5 | let r = (d + Math.random() * 16) % 16 | 0; 6 | d = Math.floor(d / 16); 7 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); 8 | }); 9 | } 10 | } 11 | 12 | export default UUID; 13 | -------------------------------------------------------------------------------- /src/util/Log.ts: -------------------------------------------------------------------------------- 1 | class Log { 2 | public static _enable: boolean = false; 3 | 4 | static get enable(): boolean { 5 | return this._enable; 6 | } 7 | 8 | static set enable(value: boolean) { 9 | this._enable = value; 10 | } 11 | 12 | public static d(object: any) { 13 | if (!Log._enable) { 14 | return; 15 | } 16 | console.debug(object); 17 | }; 18 | 19 | public static l(object: any) { 20 | if (!Log._enable) { 21 | return; 22 | } 23 | console.log(object); 24 | } 25 | 26 | public static e(object: any) { 27 | if (!Log._enable) { 28 | return; 29 | } 30 | console.error(object); 31 | } 32 | 33 | public static w(object: any) { 34 | if (!Log._enable) { 35 | return; 36 | } 37 | console.warn(object); 38 | } 39 | 40 | public static i(object: any) { 41 | if (!Log._enable) { 42 | return; 43 | } 44 | console.info(object); 45 | } 46 | } 47 | 48 | export default Log; -------------------------------------------------------------------------------- /src/util/Polyfill.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Object.assign polyfill 3 | */ 4 | if (typeof Object.assign != 'function') { 5 | Object.assign = function (target) { 6 | if (target == null) { 7 | throw new TypeError('Cannot convert undefined or null to object'); 8 | } 9 | 10 | target = Object(target); 11 | for (let index = 1; index < arguments.length; index++) { 12 | let source = arguments[index]; 13 | if (source != null) { 14 | for (let key in source) { 15 | if (Object.prototype.hasOwnProperty.call(source, key)) { 16 | target[key] = source[key]; 17 | } 18 | } 19 | } 20 | } 21 | return target; 22 | }; 23 | } 24 | 25 | /** 26 | * canvas.toBlob polyfill 27 | */ 28 | if (!HTMLCanvasElement.prototype.toBlob) { 29 | Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { 30 | value: function (callback, type, quality) { 31 | 32 | let binStr = atob(this.toDataURL(type, quality).split(',')[1]), 33 | len = binStr.length, 34 | arr = new Uint8Array(len); 35 | 36 | for (let i = 0; i < len; i++) { 37 | arr[i] = binStr.charCodeAt(i); 38 | } 39 | 40 | callback(new Blob([arr], {type: type || 'image/png'})); 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * endsWith polyfill 47 | */ 48 | if (!String.prototype.endsWith) { 49 | let toString = {}.toString; 50 | let endsWith = function (search) { 51 | if (this == null) { 52 | throw TypeError(); 53 | } 54 | let string = String(this); 55 | if (search && toString.call(search) == '[object RegExp]') { 56 | throw TypeError(); 57 | } 58 | let stringLength = string.length; 59 | let searchString = String(search); 60 | let searchLength = searchString.length; 61 | let pos = stringLength; 62 | if (arguments.length > 1) { 63 | let position = arguments[1]; 64 | if (position !== undefined) { 65 | // `ToInteger` 66 | pos = position ? Number(position) : 0; 67 | if (pos != pos) { // better `isNaN` 68 | pos = 0; 69 | } 70 | } 71 | } 72 | let end = Math.min(Math.max(pos, 0), stringLength); 73 | let start = end - searchLength; 74 | if (start < 0) { 75 | return false; 76 | } 77 | let index = -1; 78 | while (++index < searchLength) { 79 | if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) { 80 | return false; 81 | } 82 | } 83 | return true; 84 | }; 85 | if (Object.defineProperty) { 86 | Object.defineProperty(String.prototype, 'endsWith', { 87 | 'value': endsWith, 88 | 'configurable': true, 89 | 'writable': true 90 | }); 91 | } else { 92 | String.prototype.endsWith = endsWith; 93 | } 94 | } -------------------------------------------------------------------------------- /src/util/isWechat.ts: -------------------------------------------------------------------------------- 1 | export default function isWechat(): boolean { 2 | return /micromessenger/i.test(navigator.userAgent); 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./types", 4 | "declaration": true, 5 | "sourceMap": true, 6 | "module": "es6", 7 | "target": "es6", 8 | //不允许不一致包装引用相同的文件。 9 | "forceConsistentCasingInFileNames": true, 10 | //不是函数的所有返回路径都有返回值时报错。 11 | "noImplicitReturns": true, 12 | //阻止--noImplicitAny对缺少索引签名的索引对象报错。查看issue #1232了解详情。 13 | "suppressImplicitAnyIndexErrors": true, 14 | //若有未使用的局部变量则抛错。 15 | "noUnusedLocals": true, 16 | "lib": [ 17 | "es2016", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = env => ({ 4 | entry: './src/Main.ts', 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: getFileName(env), 8 | library: 'Qiniu', 9 | libraryTarget: 'umd' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | enforce: 'pre', 15 | test: /\.js$/, 16 | loader: 'source-map-loader' 17 | }, 18 | { 19 | test: /\.ts?$/, 20 | exclude: '/node_modules', 21 | use: [ 22 | { 23 | loader: 'babel-loader', 24 | options: { 25 | presets: getBabelOption(env) 26 | } 27 | }, 28 | { 29 | loader: 'ts-loader' 30 | } 31 | ] 32 | } 33 | ] 34 | }, 35 | resolve: { 36 | extensions: ['.ts', '.js'] 37 | }, 38 | devtool: 'cheap-source-map', 39 | }) 40 | 41 | function getBabelOption (env) { 42 | if (env === 'es5min') { 43 | return [ 44 | 'es2015', 45 | 'stage-0' 46 | ] 47 | } else if (env === 'es5') { 48 | return [ 49 | 'es2015', 50 | 'stage-0' 51 | ] 52 | } 53 | else if (env === 'es6') { 54 | return [] 55 | } 56 | } 57 | 58 | function getFileName (env) { 59 | if (env === 'es5min') { 60 | return 'qiniu4js.min.js' 61 | } else if (env === 'es5') { 62 | return 'qiniu4js.js' 63 | } 64 | else if (env === 'es6') { 65 | return 'qiniu4js.es.js' 66 | } 67 | } --------------------------------------------------------------------------------