dcat-admin 大文件上传(前端直传解决)

昨天遇到了一个后端传输导致nginx502的问题

今天在调整了代码之后,遂将解决方案贴出

前端直传的好处:减轻应用服务器的压力,将压力分给了oss 这一点特别是在上传大文件时特别明显的,php是要消耗很大一部分内存去处理前端分片上传来的文件再传输给oss,如果文件特别大,耗时长nginx会直接502 我们没必要去调整nginx的超时时间把路走窄了。直接由客户端直传oss吧。

完成后效果图

Laravel

Laravel

因为dcat-admin是高度封装的。改它的组件基本不现实,不过$form->view()方法可以引入一个视图文件.我的想法是用vue封装一个上传的组件,然后通过该方法引入。正好laravel提供了前端脚手架laravel mix 整合了vue。

  • laravel版本7.x

    开始一套梭

composer require laravel/ui --dev //安装前端脚手架
php artisan ui vue //生成vue基本脚手架
npm install
npm install ali-oss --save //安装 oss js-sdk
npm install clipboard --save //安装 复制插件
npm run watch  //命令监视热加载、编译

使用element-ui的组件,所以我们引入它

npm i element-ui -S

app.js文件全局加载

dcat-admin 大文件上传(前端直传解决)

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
import Clipboard from 'clipboard';
Vue.prototype.Clipboard=Clipboard;

并新建组件OssFile

<template>
    <div>
        <el-upload
            class="upload-demo"
            action=""
            ref="upload"
            :file-list="fileList"
            :limit="2"
            :on-change="handleChange"
            :on-remove="handleRemove"
            :auto-upload="false"
            accept=""
        >
            <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
            <el-button style="margin-left: 10px;" size="small" type="success" @click="submitForm">直传oss</el-button>
            <el-button style="margin-left: 10px;" size="small" type="success" @click="resumeUpload">继续</el-button>
            <el-button style="margin-left: 10px;" size="small" type="success" @click="stopUplosd">暂停</el-button>
            <el-button style="margin-left: 10px;" size="small" type="success" @click="abortMultipartUpload">清除切片</el-button>
        </el-upload>
        <el-progress :percentage="percentage" :status="uploadStatus"></el-progress>
        <span
            class="copybtn"
            @click="copy"
            :data-clipboard-text="fileName"
        >
{{ fileName }}
</span>
    </div>
</template>

<script>

import Clipboard from 'clipboard';
let OSS = require('ali-oss') // 引入ali-oss插件
const client = new OSS({
    region: 'oss-cn-shenzhen',//根据那你的Bucket地点来填写
    accessKeyId: '',//自己账户的accessKeyId
    accessKeySecret: '',//自己账户的accessKeySecret
    bucket: '',//bucket名字
});
export default {
    name: "OssFile",
    data () {
        return {
            fileName:"",
            fileList:[],
            file: null,
            tempCheckpoint: null, // 用来缓存当前切片内容
            uploadId: '',
            uploadStatus: null, // 进度条上传状态
            percentage: 0, // 进度条百分比
            uploadName: '',  //Object所在Bucket的完整路径
        }
    },
    mounted() {
         window.addEventListener('online',  this.resumeUpload);
    },
    methods: {
        copy()
        {
            var clipboard = new Clipboard(".copybtn");
            clipboard.on("success", (e) => {
                this.$message({
                    message: '复制成功',
                    type: 'success'
                });
                // 释放内存
                clipboard.destroy();
            });
            clipboard.on("error", (e) => {
                // 不支持复制
                this.$message({
                    message: '该浏览器不支持自动复制',
                    type: 'success'
                });
                // 释放内存
                clipboard.destroy();
            });
        },
        // 点击上传至服务器
        submitForm(file) {
           this.multipartUpload();
        },
        // 取消分片上传事件
        async abortMultipartUpload() {
            window.removeEventListener('online', this.resumeUpload)
            const name = this.uploadName; // Object所在Bucket的完整路径。
            const uploadId = this.upload; // 分片上传uploadId。
            const result = await client.abortMultipartUpload(name, uploadId);
            console.log(result, '=======清除切片====');
        },
        // 暂停分片上传。
        stopUplosd () {
            window.removeEventListener('online', this.resumeUpload) // 暂停时清除时间监听
            let result = client.cancel();
            console.log( result, '---------暂停上传-----------')
        },
        // 切片上传
        async multipartUpload () {
            if (!this.file) {
                this.$message.error('请选择文件')
                return
            }

            console.log("this.uploadStatus",this.file, this.uploadStatus);
            console.log("文件列表:"+this.fileList)
            console.log("文件:"+this.file)
            this.percentage = 0
            try {
                //object-name可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
                let result = await client.multipartUpload(this.file.name, this.file, {
                    headers: {
                        'Content-Disposition': 'inline',
                        'Content-Type': this.file.type //注意:根据图片或者文件的后缀来设置,我试验用的‘.png’的图片,具体为什么下文解释
                    },
                    progress: (p, checkpoint) => {
                        this.tempCheckpoint = checkpoint;
                        this.upload = checkpoint.uploadId
                        this.uploadName = checkpoint.name
                        this.percentage = p * 100
                        // console.log(p, checkpoint, this.percentage, '---------uploadId-----------')
                        // 断点记录点。浏览器重启后无法直接继续上传,您需要手动触发上传操作。
                    },
                    meta: { year: 2020, people: 'dev' },
                    mime: this.file.type
                });
                console.log(result, this.percentage, 'result= 切片上传完毕=');


                this.$nextTick(()=>{
                    this.fileName = 'https://image.mythinkcar.cn/'+result.name
                })
                console.log(this.fileName)

            } catch (e) {
                console.log(e)
                window.addEventListener('online',  this.resumeUpload) // 该监听放在断网的异常处理
                // 捕获超时异常。
                if (e.code === 'ConnectionTimeoutError') { // 请求超时异常处理
                    this.uploadStatus = 'exception'
                    console.log("TimeoutError");
                }

            }
        },
        // 恢复上传。
        async resumeUpload () {
            window.removeEventListener('online', this.resumeUpload)
            if (!this.tempCheckpoint) {
                this.$message.error('请先上传')
                return
            }
            this.uploadStatus = null
            try {
                let result = await client.multipartUpload(this.file.name, this.file, {
                    headers: {
                        'Content-Disposition': 'inline',
                        'Content-Type': this.file.type //注意:根据图片或者文件的后缀来设置,我试验用的‘.png’的图片,具体为什么下文解释
                    },

                    progress: (p, checkpoint) => {
                        this.percentage = p * 100
                        console.log(p, checkpoint, 'checkpoint----恢复上传的切片信息-------')
                        this.tempCheckpoint = checkpoint;
                    },
                    checkpoint: this.tempCheckpoint,
                    meta: { year: 2020, people: 'dev' },
                    mime: this.file.type
                })
                console.log(result, 'result-=-=-恢复上传完毕')
            } catch (e) {
                console.log(e, 'e-=-=-');
            }
        },

        // 选择文件发生改变
        handleChange(file, fileList) {
            this.fileList = fileList.filter(row => row.uid == file.uid)
            this.file = file.raw
            // 文件改变时上传
            // this.submitForm(file)
        },
        handleRemove(file, fileList) {
            this.percentage = 0 //进度条置空
            this.fileList = []
        },
    }
}
</script>


<style>
.avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
}
.avatar-uploader .el-upload:hover {
    border-color: #409EFF;
}
.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 150px;
    height: 150px;
    line-height: 150px;
    text-align: center;
}
.avatar {
    width: 150px;
    height: 150px;
    display: block;
}
</style>

app.js中引入组件

Vue.component('oss-file', require('./components/uploads/OssFile.vue').default);

resources/views目录新建oss.blade.php 引入组件<oss-file></oss-file>

<link rel="stylesheet" href="{{mix('css/app.css')}}">
<div id="app">
<div class="container">
    <oss-file></oss-file>
</div>
</div>
<script src="{{mix('js/app.js')}}"></script>

最后

  $form->html(view('uploads.oss'));
  $form->text('link','直传后填入地址');

完美解决~~~

碰到的问题

  • oss 跨域问题
  • oss 出现 RequestId 错误处理方法 新增ETag x-oss-request-id

参考文章


vue+element存在兼容问题。后续jq重写了


{{--<link rel="stylesheet" type="text/css" href="{{asset('/oss/style.css')}}"/>--}}

<style>
    .progress{
        margin-top:2px;
        width: 200px;
        height: 14px;
        margin-bottom: 10px;
        overflow: hidden;
        border-radius: 4px;
    }
    .progress-bar{
        background-color: rgb(92, 184, 92);
        background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.14902) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.14902) 50%, rgba(255, 255, 255, 0.14902) 75%, transparent 75%, transparent);
        background-size: 40px 40px;
        box-shadow: rgba(0, 0, 0, 0.14902) 0px -1px 0px 0px inset;
        box-sizing: border-box;
        color: rgb(255, 255, 255);
        display: block;
        float: left;
        font-size: 12px;
        height: 20px;
        line-height: 20px;
        text-align: center;
        transition-delay: 0s;
        transition-duration: 0.6s;
        transition-property: width;
        transition-timing-function: ease;
        width: 266.188px;
    }

</style>
<div id="ossfile"></div>
<br/>
<div id="container">
    <a id="selectfiles" href="javascript:void(0);" class=' btn-xs btn-info'>选择文件</a>
    <a id="postfiles" href="javascript:void(0);" class=' btn-xs btn-success'>开始上传</a>
</div>
<br/>
<pre id="console"></pre>
<p>&nbsp;</p>

<script>
    var policyText = {
        "expiration": "2029-01-01T12:00:00.000Z", //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了
        "conditions": [
            ["content-length-range", 0, 1048576000] // 设置上传文件的大小限制
        ]
    };
    accessid= "{{env('OSS_ACCESS_KEY')}}";
    accesskey= "{{env('OSS_SECRET_KEY')}}";
    host = 'http://xxx.oss-cn-shenzhen.aliyuncs.com';
    var policyBase64 = Base64.encode(JSON.stringify(policyText))
    message = policyBase64
    var bytes = Crypto.HMAC(Crypto.SHA1, message, accesskey, { asBytes: true }) ;
    var signature = Crypto.util.bytesToBase64(bytes);
    var uploader = new plupload.Uploader({
        runtimes : 'html5,flash,silverlight,html4',
        browse_button : 'selectfiles',
        //runtimes : 'flash',
        container: document.getElementById('container'),
        flash_swf_url : 'lib/plupload-2.1.2/js/Moxie.swf',
        silverlight_xap_url : 'lib/plupload-2.1.2/js/Moxie.xap',

        url : host,

        multipart_params: {
            'Filename': '${filename}',
            'key' : '${filename}',
            'policy': policyBase64,
            'OSSAccessKeyId': accessid,
            'success_action_status' : '200', //让服务端返回200,不然,默认会返回204
            'signature': signature,
        },

        init: {
            PostInit: function() {
                // document.getElementById('ossfile').innerHTML = '';
                document.getElementById('postfiles').onclick = function() {
                    uploader.start();
                    return false;
                };
            },

            FilesAdded: function(up, files) {
                plupload.each(files, function(file) {
                    document.getElementById('ossfile').innerHTML += '<div id="' + file.id + '">' + file.name + ' (' + plupload.formatSize(file.size) + ')<b></b>'
                        +'<div class="progress"><div class="progress-bar" style="width: 0%"></div></div>'
                        +'</div>';
                });
            },

            UploadProgress: function(up, file) {
                var d = document.getElementById(file.id);
                d.getElementsByTagName('b')[0].innerHTML = '<span>' + file.percent + "%</span>";

                var prog = d.getElementsByTagName('div')[0];
                var progBar = prog.getElementsByTagName('div')[0]
                progBar.style.width= 2*file.percent+'px';
                progBar.setAttribute('aria-valuenow', file.percent);
            },

            FileUploaded: function(up, file, info) {
                //alert(info.status)
                if (info.status >= 200 || info.status < 200)
                {
                    console.log(up,file,info)
                    document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = 'http://image.mythinkcar.cn/'+file.name;
                }
                else
                {
                    document.getElementById(file.id).getElementsByTagName('b')[0].innerHTML = info.response;
                }
            },

            Error: function(up, err) {
                document.getElementById('console').appendChild(document.createTextNode("\nError xml:" + err.response));
            }
        }
    });

    uploader.init();

</script>

app/admin/bootstrap.js 加入oss的一些js文件

Admin::js('/oss/lib/crypto1/crypto/crypto.js');
Admin::js('/oss/lib/crypto1/hmac/hmac.js');
Admin::js('/oss/lib/crypto1/sha1/sha1.js');
Admin::js('/oss/lib/base64.js');
Admin::js('/oss/lib/plupload-2.1.2/js/plupload.full.min.js');
....
php
本作品采用《CC 协议》,转载必须注明作者和本文链接
不成大牛,不改個簽
本帖由系统于 1个月前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 11

怎么追加上link,手动复制地址体验不好吧

2个月前 评论

@Asuna 点击地址复制 注意看代码

2个月前 评论
TELstatic 1个月前
TELstatic 1个月前
TELstatic 1个月前
Asuna 2个月前
Latent (作者) (楼主) 2个月前
Latent (作者) (楼主) 1个月前

你这oss的密钥直接写在前端也太猛了吧,不怕被人爆破吗

2个月前 评论
ncccc1 1个月前

@zhangsansan957 这是个后台噢 而且只能进入了这个页面才会加载app.js 才能找到密钥 懂了吧 :speak_no_evil:

2个月前 评论
gema 1个月前

@Latent 你可以用STS获取临时的 acceskey 和 secret 上传,还可以设置权限和时效

2个月前 评论

@Latent 前端能加载的都不安全,建议后端生成可控的STS临时 acceskey 和 secret

2个月前 评论

dcat-admin 把上传那部分写的太死了

2个月前 评论

dcat-admin 可以改上传地址的 直接用 oss 地址直传 + 回调就可以实现 没必要这么麻烦

1个月前 评论

@TELstatic 哈哈哈 后面用jq重写了 vue +element 写有点不兼容的问题

1个月前 评论

@TELstatic 那么你实现 了吗?比如如何获取上传token

1个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!