{"id":18880525,"url":"https://github.com/onewaytech/eluploader-oss-solution","last_synced_at":"2025-10-29T07:13:00.593Z","repository":{"id":143812443,"uuid":"110793524","full_name":"OneWayTech/ElUploader-OSS-Solution","owner":"OneWayTech","description":"ElementUI - Upload 组件结合 OSS 的封装","archived":false,"fork":false,"pushed_at":"2018-01-17T04:12:25.000Z","size":31,"stargazers_count":29,"open_issues_count":2,"forks_count":7,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-28T08:22:10.900Z","etag":null,"topics":["cdn","element","elupload","oss","upload","vue"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OneWayTech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-11-15T06:34:34.000Z","updated_at":"2024-11-03T06:44:11.000Z","dependencies_parsed_at":null,"dependency_job_id":"326b1ddd-d91b-4755-87b0-76c37ba5720f","html_url":"https://github.com/OneWayTech/ElUploader-OSS-Solution","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2FElUploader-OSS-Solution","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2FElUploader-OSS-Solution/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2FElUploader-OSS-Solution/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2FElUploader-OSS-Solution/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OneWayTech","download_url":"https://codeload.github.com/OneWayTech/ElUploader-OSS-Solution/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248946083,"owners_count":21187447,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cdn","element","elupload","oss","upload","vue"],"created_at":"2024-11-08T06:44:21.813Z","updated_at":"2025-10-29T07:13:00.514Z","avatar_url":"https://github.com/OneWayTech.png","language":"JavaScript","readme":"# § ElementUI 1.x - Upload 结合 OSS 的封装\n\n\u003e ElementUI 2.x：请看 [uploader(forElementUI2).mixin.js](./uploader(forElementUI2).mixin.js)（大同小异）\n\n[ElUpload](http://element-cn.eleme.io/1.4/#/zh-CN/component/upload) 对于直传阿里云 OSS 或其他 CDN 服务的场景显得相当不便利（因为涉及到签名校验以及有效期等）  \n此时我们需要自行封装出符合业务需求的通用化组件\n\n于我而言，我本身比较排斥组件嵌套较深的情况，且很多时候还要考虑到父子组件通信的问题  \n因此我有很多所谓的“通用化组件”实际上是以 mixin 的形式来实现的\n\n首先讲讲需要注意的问题：\n* 需要从我们自己的后端获取签名（详见 OSS 文档 - [服务端签名后直传](https://help.aliyun.com/document_detail/31926.html)）\n* 需要考虑签名的有效期（我司一般是设置 2 ~ 3 min）\n* 在签名有效期内，不能重复请求后端获取签名（签名的跨组件共享，原理可参考[这里](https://github.com/kenberkeley/vue-state-management-alternative)）\n* 是逐个上传，而非并发上传（否则网速慢的时候得卡死）\n* 需要支持各类文件的不定项上传\n* 需要支持回显操作（编辑状态下肯定是必须的）\n* 需要支持样式各异的组件形式（单单这个需求就已经没办法实现一个所谓的通用化组件了，因为模板与样式各异）\n* ElUpload 截止到 1.4.7 时还是有很多坑，需要一一避免（e.g. 令人抓狂的 `fileList`）\n\n下面是对应的 `@/mixins/uploader.js`\n\n```js\nimport moment from 'moment'\nimport urljoin from 'url-join'\nimport debounce from 'lodash/debounce'\nimport browserMD5File from 'browser-md5-file'\nconst isStr = s =\u003e typeof s === 'string'\n\n// 组件共享状态：OSS 上传签名（这只是我司后端返回的原始形式，真正 POST 到 OSS 的是 computed:access）\nconst oss = {\n  accessid: '', // 16 位字符串\n  policy: '', // Base64 编码字符串\n  signature: '', // 28 位字符串\n  dir: '', // 上传路径（根据当前用户决定）\n  expire: '', // 13 位毫秒数\n  cdnUrl: '' // 阿里云 OSS 地址\n}\n\nexport default {\n  props: {\n    // 【注意】该项请使用 .sync 修饰，形式可为 'url' 或 ['url1', 'url2', ...]\n    files: { type: [String, Array], required: true }\n  },\n  data: () =\u003e ({\n    oss,\n    key: '', // 正在上传的文件的 key（computed:access 依赖项）\n    percent: 0, // 当前任务上传进度\n    taskQueue: [], // 上传队列（基于 Promise 实现）\n    isUploading: false,\n    fileList: [] // 用于 ElUpload 组件的 $props.fileList\n  }),\n  computed: {\n    action () { // 用于 ElUpload 组件的 $props.action\n      return oss.cdnUrl\n    },\n    access () { // 用于 ElUpload 组件的 $props.data\n      return {\n        key: this.key,\n        policy: oss.policy,\n        signature: oss.signature,\n        OSSAccessKeyId: oss.accessid,\n        success_action_status: 200\n      }\n    }\n  },\n  watch: {\n    files: {\n      handler (files) {\n        if (isStr(files)) {\n          files = [files]\n        }\n        // 遵循 ElUpload 的 $props.fileList 的 [{ name, url }] 格式\n        this.fileList = files.map((url, idx) =\u003e ({ name: '' + idx, url }))\n      },\n      immediate: true\n    },\n    isUploading (isUploading) {\n      // isUploading 从 true 变成 false 时，在 nextTick 中同步 ElUpload $data.uploadFiles 到 $props.files\n      // 为什么要 nextTick？因为 onSuccess 中执行 this.nextFile() 之后还有 file.url = uploadFile 的操作\n      isUploading || this.$nextTick(() =\u003e {\n        this.syncUploadFiles()\n      })\n    }\n  },\n  methods: {\n    /**\n     * 【注意：该方法须自行实现】新增上传任务，用于 ElUpload 组件的 before-upload 钩子函数，举例如下：\n     * @param  {File}\n     * @return {Boolean/Promise} - 官方文档写道：若返回 false 或者 Promise 则停止上传\n      beforeUpload (file) {\n        // 此处进行检测 file 合法性等操作，之后就只需要调用如下函数即可\n        return this.addFile(file)\n      }\n     */\n    syncUploadFiles () {\n      // 这里最后意为排除掉 blob 开头的 URL（这算是一个坑），此时 files 有可能是空数组\n      let files = this.$refs.upload.uploadFiles.map(({ url }) =\u003e url).filter(url =\u003e url.startsWith('http'))\n\n      // 对于无论是否 multiple，ElUpload 的 $data.uploadFiles 始终都是数组类型\n      // 因此若 $props.files 为字符串类型，则应取 files 的末位元素（注：空数组时取得 undefined）\n      this.$emit('update:files', isStr(this.files) ? files.slice(-1)[0] || '' : files)\n    },\n    // 用于 ElUpload 的 on-progress\n    onProgress ({ percent }) {\n      this.percent = ~~percent\n    },\n    // 用于 ElUpload 的 on-success\n    onSuccess (res, file, uploadFiles) {\n      const uploadPath = this.nextFile()\n      file.url = uploadPath // 把 blob 链接替换成 CDN 链接\n    },\n    // 用于 ElUpload 的 on-remove\n    onRemove: debounce(function () {\n      // 手动点击删除显然会调用本函数，但如下场景也会触发调用：\n      // 限制 5 张，已传 3 张，若在文件管理器中再选 10 张上传\n      // 则溢出了 8 张，即本函数将会频繁调用 8 次（所以要 debounce 一下）\n      \n      // 若本函数仅仅就是单纯执行 syncUploadFiles，则必然报错：\n      // Uncaught TypeError: Cannot set property 'status' of null\n      // \n      // 因为此时正在上传 2 张，ElUpload 内部的 handleProgress 一直在不断执行\n      // 若直接就粗暴地调用 syncUploadFiles 则会触发 ElUpload $data.uploadFiles 的更新\n      // 导致 handleProgress 中的 var file = this.getFile(rawFile) 为 null\n      // 故随后 file.status = 'uploading' 就会立即报错\n      // （详见源码 https://github.com/ElemeFE/element/blob/1.x/packages/upload/src/index.vue#L141-L146）\n      this.isUploading\n        ? setTimeout(() =\u003e this.onRemove, 1000)\n        : this.syncUploadFiles()\n    }, 250),\n    // 用于 ElUpload 的 on-error（一般是 OSS access 过期了）\n    onError () {\n      this.isUploading = false // 重置上传状态很关键，否则之后就不能 auto run 了\n      this.$message.warning('上传功能出了点问题，请重试')\n    },\n    addFile (file) {\n      return new Promise(resolve =\u003e {\n        this.taskQueue.push({ file, start: resolve })\n\n        // auto run\n        if (!this.isUploading) {\n          this.isUploading = true\n          this.nextFile(true)\n        }\n      })\n    },\n    nextFile (isAutorun) {\n      // 当 isUploading false =\u003e true 时（auto run）：\n      // 1. 若之前没有上传过的，则 this.action 和 this.key 均为 ''，故 join 出来是 '/'\n      // 2. 若之前有上传过的，则结果为上一次的 uploadPath\n      // 鉴于两者都没有意义，故由 auto run 触发的都无需执行 urljoin\n      let uploadPath\n      if (!isAutorun) {\n        uploadPath = urljoin(this.action, this.key)\n      }\n      // 开发环境下打印出刚上传成功的文件链接以便调试\n      // （为什么不写成 if(__DEV__ \u0026\u0026 !isAutorun)？因为有利于 UglifyJS 压缩时直接剔除整块代码 ）\n      if (__DEV__) {\n        if (!isAutorun) {\n          console.info('上传成功：', uploadPath)\n        }\n      }\n\n      const { taskQueue } = this\n      if (taskQueue.length) {\n        const ensureAccessValid = isAccessExpired() ? updateAccess : doNothing\n        let nextTask\n        ensureAccessValid().then(() =\u003e {\n          nextTask = taskQueue.shift()\n          return keygen(nextTask.file)\n        }).then(key =\u003e {\n          this.key = key // 更新 key 以更新 computed:access\n          this.$nextTick(() =\u003e {\n            nextTask.start() // 相当于 resolve 掉 before-upload 钩子中返回的 promise\n          })\n        }).catch(e =\u003e console.warn(e))\n      } else {\n        this.isUploading = false\n      }\n      \n      return uploadPath\n    }\n  }\n}\n\n// 判断 access 是否过期（提前 10 秒过期）\nfunction isAccessExpired () {\n  return +moment().add(10, 's').format('x') \u003e +oss.expire\n}\n\n/**\n * 更新 OSS access\n * @return {Promise}\n */\nfunction updateAccess() {\n  return \u003c获取 OSS 签名的 API\u003e.then(re =\u003e {\n    Object.assign(oss, re)\n  })\n}\n\nfunction doNothing () {\n  return Promise.resolve()\n}\n\n/**\n * 生成上传 key（基于文件哈希）\n * @param   {File}\n * @resolve {String} 形如 '\u003c上传路径\u003e/3d3e93a9745fd21240ef3c88045cc0d1.jpg'\n */\nfunction keygen(file) {\n  detectCompatibility()\n  return new Promise((resolve, reject) =\u003e {\n    browserMD5File(file, (err, md5) =\u003e {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve(\n        urljoin(oss.dir, `${md5}.${file.name.split('.').pop()}`)\n      )\n    })\n  })\n}\n\nfunction detectCompatibility() {\n  window.File || window.FileReader || alert(\n    '当前浏览器不支持 File / FileReader，上传功能受限。\\n建议您使用特性更多，性能更好的现代浏览器。'\n  )\n}\ndetectCompatibility()\n```\n\n例如，我们有一个上传 icon 的组件（`IconUploader`），如下：\n\n```html\n\u003ctemplate\u003e\n  \u003c!-- 【注意】必须设置 ref=\"upload\" --\u003e\n  \u003cel-upload\n    ref=\"upload\"\n    :data=\"access\"\n    :action=\"action\"\n    :file-list=\"fileList\"\n    :show-file-list=\"false\"\n    :accept=\"acceptTypes.join(',')\"\n    :before-upload=\"beforeUpload\"\n    :on-progress=\"onProgress\"\n    :on-success=\"onSuccess\"\n    :on-error=\"onError\"\u003e\n    \u003cdiv class=\"-icon-uploader\"\u003e    \n      \u003cspan v-if=\"isUploading\"\u003e{{ percent }} %\u003c/span\u003e\n      \u003cimg v-else-if=\"files\" :src=\"files\"\u003e\n      \u003cspan v-else\u003e(推荐分辨率为 100 \u0026times; 100)\u003c/span\u003e\n    \u003c/div\u003e\n  \u003c/el-upload\u003e\n\u003c/template\u003e\n\u003cscript\u003e\nimport uploader from '@/mixins/uploader'\n\nexport default {\n  mixins: [uploader],\n  data: () =\u003e ({\n    acceptTypes: ['image/png', 'image/jpeg']\n  }),\n  methods: {\n    // 一般情况下只需要实现以下函数即可\n    beforeUpload (file) {\n      let isPngJpg = this.acceptTypes.includes(file.type)\n      if (!isPngJpg) {\n        this.$notify.info({\n          title: file.name,\n          message: '只能上传 PNG / JPG 格式的图片'\n        })\n        return false\n      }\n      let isLt1M = file.size / 1024 / 1024 \u003c 1\n      if (!isLt1M) {\n        this.$notify.info({\n          title: file.name,\n          message: '单张图片不得大于 1 MB 的限制'\n        })\n        return false\n      }\n      return this.addFile(file)\n    }\n  }\n}\n\u003c/script\u003e\n\u003cstyle lang=\"scss\"\u003e\n.-icon-uploader {\n  width: 100px;\n  height: 100px;\n  color: #acacac;\n  border: 1px dashed #d9d9d9;\n  border-radius: 4px;\n  font-size: 12px;\n  line-height: 100px;\n  \n  \u0026:hover {\n    border-color: #3498ff;\n  }\n\n  \u003e img {\n    width: 100%;\n    height: 100%;\n    vertical-align: top;\n  }\n}\n\u003c/style\u003e\n```\n\n用的时候相当简单，就是：\n\n```html\n\u003cicon-uploader :files.sync=\"iconUrl\" /\u003e\n```\n\n***\n\n同样地，上传 App 包体的组件（`AppUploader`）如下：\n\n```html\n\u003ctemplate\u003e\n  \u003c!-- 【注意】必须设置 ref=\"upload\" --\u003e\n  \u003cel-upload\n    ref=\"upload\"\n    accept=\".apk,.ipa\"\n    :data=\"access\"\n    :action=\"action\"\n    :show-file-list=\"false\"\n    :before-upload=\"beforeUpload\"\n    :on-progress=\"onProgress\"\n    :on-success=\"onSuccess\"\n    :on-error=\"onError\"\u003e    \n    \u003cel-button size=\"mini\" :loading=\"isUploading\"\u003e\n      \u003ctemplate v-if=\"isUploading\"\u003e\n        {{ percent }}%\n      \u003c/template\u003e\n      \u003ctemplate v-else\u003e\n        \u003ci class=\"fa fa-cloud-upload\"\u003e\u003c/i\u003e\n        上传应用\n      \u003c/template\u003e\n    \u003c/el-button\u003e\n  \u003c/el-upload\u003e\n\u003c/template\u003e\n\u003cscript\u003e\nimport uploader from '@/mixins/uploader'\n\nexport default {\n  mixins: [uploader],\n  methods: {\n    beforeUpload (file) {\n      const ext = file.name.split('.').pop()\n      return ['apk', 'ipa'].includes(ext) \u0026\u0026 this.addFile(file)\n    }\n  }\n}\n\u003c/script\u003e\n\u003cstyle lang=\"scss\"\u003e\n.el-upload__input {\n  // 覆盖 BootStrap input[type=file] { display: block; }\n  display: none !important;\n}\n\u003c/style\u003e\n```\n\n用法：\n\n```html\n\u003capp-uploader :files.sync=\"pkgUrl\" /\u003e\n```\n\n***\n\n我们来总结一下，三步走：\n1. 引入 `@/mixins/uploader`  \n2. 把 mixin 中的对应的参数以及方法传给 ElUpload，顺便实现自己的模板与样式  \n3. 实现 `beforeUpload` 方法（内部须调用 `addFile` 把文件添加到上传队列中）\n\n本人经过多次尝试才总结出当前这种较为通用的 mixin 方式，希望可以抛砖引玉，得到您改进的建议与意见\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonewaytech%2Feluploader-oss-solution","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fonewaytech%2Feluploader-oss-solution","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonewaytech%2Feluploader-oss-solution/lists"}