示例效果图:
1、开通COS对象存储服务
腾讯云COS官网地址:https://cloud.tencent.com/product/cos
PS:建议选择使用七牛云,七牛云是完全免费的;腾讯云COS有半年免费50G的存储服务,但腾讯云的流量是收费的,每天都给你发扣费短信,真是烦死人 ; 阿里云的OSS或者华为云的OBS都要收费(不推荐)。
2、创建密钥
自定义域名的教程,看官方文档:https://cloud.tencent.com/document/product/436/6252
3、thinkphp后端:计算腾讯云COS对象存储签名
特别说明:由于签名计算放在前端会暴露 SecretId 和 SecretKey, 所以我们把签名计算过程放在后端实现,前端通过 ajax 向后端获取签名结果,正式部署时请在后端加一层自己网站本身的权限检验。
thinkphp控制器controller中的代码:
<?phpnamespace app\index\controller;use think\Controller;use think\Request;/*引入[腾讯云](https://l.gushuji.site/tencent)cos类库(extend/sts.php)*/import('sts', EXTEND_PATH); class Car extends Controller{ /* * 获取[腾讯云](https://l.gushuji.site/tencent)COS对象存储签名 * 官网:https://cloud.tencent.com/product/cos */ public function getSts(){ $sts = new \STS(); // 配置参数 $config = array( 'url' => 'https://sts.tencentcloudapi.com/', 'domain' => 'sts.tencentcloudapi.com', 'proxy' => '', 'secretId' => '你cos密钥中的secretId', // 固定密钥 'secretKey' => '你cos密钥中的secretKey', // 固定密钥 'bucket' => 'myfaka-1256433534',//你的存储桶名称bucket 'region' => 'ap-guangzhou', // bucket所在地域 'durationSeconds' => 1800, // 密钥有效期 'allowPrefix' => '*', // 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的目录,例子:* 或者 a/* 或者 a.jpg // 密钥的权限列表。简单上传需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923 'allowActions' => array ( //上传权限 'name/cos:PutObject', 'name/cos:PostObject', //下载权限 "name/cos:GetObject", //查询权限 "name/cos:GetBucket", "name/cos:HeadObject", //删除权限 "name/cos:DeleteObject", ) ); // 获取临时密钥,计算签名 $tempKeys = $sts->getTempKeys($config); // 返回数据给前端 header('Content-Type: application/json'); header('Access-Control-Allow-Origin: http://127.0.0.1'); // 这里修改允许跨域访问的网站 header('Access-Control-Allow-Headers: origin,accept,content-type'); echo json_encode($tempKeys); }} ?>
extend/sts.php的代码:(这是官方提供的类)
<?php /** * 代码出处: * https://github.com/tencentyun/qcloud-cos-sts-sdk */ class STS{ // 临时密钥计算样例 function _hex2bin($data) { $len = strlen($data); return pack("H" . $len, $data); } // obj 转 query string function json2str($obj, $notEncode = false) { ksort($obj); $arr = array(); if(!is_array($obj)){ throw new Exception($obj + " must be a array"); } foreach ($obj as $key => $val) { array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val))); } return join('&', $arr); } // 计算临时密钥用的签名 function getSignature($opt, $key, $method, $config) { $formatString = $method . $config['domain'] . '/?' . $this->json2str($opt, 1); $sign = hash_hmac('sha1', $formatString, $key); $sign = base64_encode($this->_hex2bin($sign)); return $sign; } // v2接口的key首字母小写,v3改成大写,此处做了向下兼容 function backwardCompat($result) { if(!is_array($result)){ throw new Exception($result + " must be a array"); } $compat = array(); foreach ($result as $key => $value) { if(is_array($value)) { $compat[lcfirst($key)] = $this->backwardCompat($value); } elseif ($key == 'Token') { $compat['sessionToken'] = $value; } else { $compat[lcfirst($key)] = $value; } } return $compat; } // 获取临时密钥 function getTempKeys($config) { if(array_key_exists('bucket', $config)){ $ShortBucketName = substr($config['bucket'],0, strripos($config['bucket'], '-')); $AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-')); } if(array_key_exists('policy', $config)){ $policy = $config['policy']; }else{ $policy = array( 'version'=> '2.0', 'statement'=> array( array( 'action'=> $config['allowActions'], 'effect'=> 'allow', 'principal'=> array('qcs'=> array('*')), 'resource'=> array( 'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':prefix//' . $AppId . '/' . $ShortBucketName . '/' . $config['allowPrefix'] ) ) ) ); } $policyStr = str_replace('\\/', '/', json_encode($policy)); $Action = 'GetFederationToken'; $Nonce = rand(10000, 20000); $Timestamp = time(); $Method = 'POST'; $params = array( 'SecretId'=> $config['secretId'], 'Timestamp'=> $Timestamp, 'Nonce'=> $Nonce, 'Action'=> $Action, 'DurationSeconds'=> $config['durationSeconds'], 'Version'=>'2018-08-13', 'Name'=> 'cos', 'Region'=> 'ap-guangzhou', 'Policy'=> urlencode($policyStr) ); $params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config); $url = $config['url']; $ch = curl_init($url); if(array_key_exists('proxy', $config)){ $config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']); } curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0); curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params)); $result = curl_exec($ch); if(curl_errno($ch)) $result = curl_error($ch); curl_close($ch); $result = json_decode($result, 1); if (isset($result['Response'])) { $result = $result['Response']; $result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds']; } $result = $this->backwardCompat($result); return $result; } // get policy function getPolicy($scopes){ if (!is_array($scopes)){ return null; } $statements = array(); for($i=0, $counts=count($scopes); $i < $counts; $i++){ $actions=array(); $resources = array(); array_push($actions, $scopes[$i]->get_action()); array_push($resources, $scopes[$i]->get_resource()); $principal = array( 'qcs' => array('*') ); $statement = array( 'actions' => $actions, 'effect' => 'allow', 'principal' => $principal, 'resource' => $resources ); array_push($statements, $statement); } $policy = array( 'version' => '2.0', 'statement' => $statements ); return $policy; }} class Scope{ var $action; var $bucket; var $region; var $resourcePrefix; function __construct($action, $bucket, $region, $resourcePrefix){ $this->action = $action; $this->bucket = $bucket; $this->region = $region; $this->resourcePrefix = $resourcePrefix; } function get_action(){ return $this->action; } function get_resource(){ $index = strripos($this->bucket, '-'); $bucketName = substr($this->bucket, 0, $index); $appid = substr($this->bucket, $index + 1); if(!(strpos($this->resourcePrefix, '/') === 0)){ $this->resourcePrefix = '/' . $this->resourcePrefix; } return 'qcs::cos:' . $this->region . ':uid/' . $appid . ':prefix//' . $appid . '/' . $bucketName . $this->resourcePrefix; }}?>
4、下载cos-wx-sdk-v5.js
下载地址:https://github.com/tencentyun/cos-wx-sdk-v5/blob/master/demo/lib/cos-wx-sdk-v5.js
5、微信小程序创建config.js,用于保存对象存储参数,代码如下:
module.exports = { stsUrl: 'https://后端网址/car/getSts.html',//后端获取签名 Bucket: 'myfaka-1256433534',//存储桶名称 Region: 'ap-guangzhou',//所属地域};
把cos-wx-sdk-v5.js和config.js都放到项目里:
6、index.wxml代码:
<!--图片上传--> <view class="container"> <van-uploader file-list="{{ fileList }}" upload-text="添加图片" bind:after-read="afterRead" bind:delete="delFile"multiple="{{true}}" /> </view>
特别说明:这里使用的是vant-weapp的文件上传组件,vant框架地址:https://github.com/youzan/vant-weapp
7、index.js代码:
//获取应用实例const app = getApp() var COS = require('./cos-wx-sdk-v5')var config = require('./config');var toastMsg = '';//初始化COS对象var cos = new COS({ // 获取签名 getAuthorization: function(options, callback) { wx.request({ url: config.stsUrl, // 服务端获取签名 dataType: 'json', success: function(result) { var data = result.data; var credentials = data.credentials; callback({ TmpSecretId: credentials.tmpSecretId, TmpSecretKey: credentials.tmpSecretKey, XCosSecurityToken: credentials.sessionToken, ExpiredTime: data.expiredTime, }); } }); }});Page({ /** * 页面的初始数据 */ data: { fileList: [], date: '' }, /** * 生命周期函数--监听页面加载 */ onLoad: function(options) { //获取时间,作为图片文件夹名,如20191207 this.setData({ date: app.globalData.util.dateFormat(new Date(), "YMD") }); //清除缓存 //wx.removeStorageSync('fileList'); //获取缓存中的地址 this.updateData(); }, afterRead(event) { toastMsg = "上传"; var that = this; // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式 /* 单个上传 */ /* const { file } = event.detail; var filePath = file.path; var filename = new Date().getTime() + '.'+ filePath.substr(filePath.lastIndexOf('.') + 1); //文件相对路径名 var relativePath = 'upload/' + that.data.date + '/' + filename; cos.postObject({ Bucket: config.Bucket, Region: config.Region, Key: relativePath, FilePath: filePath, onProgress: function (info) { } }, requestCallback); //添加到预览中 var img = { id: i, url: app.globalData.cosUrl + relativePath, name: filename } //读取缓存 let list = wx.getStorageSync('fileList'); if (list) { list.push(img); } else { list = [img]; } //存入缓存 wx.setStorageSync('fileList', list); //延迟更新数据 setTimeout(function () { that.updateData(); }, 5000); */ /* 批量上传 */ var files = event.detail.file; //数组 for (var i = 0; i < files.length; i++) { var filePath = files[i].path; var filename = new Date().getTime() + '.' + filePath.substr(filePath.lastIndexOf('.') + 1); //文件相对路径名 var relativePath = 'upload/' + that.data.date + '/' + filename; cos.postObject({ Bucket: config.Bucket, Region: config.Region, Key: relativePath, FilePath: filePath, onProgress: function(info) { } }, requestCallback); //添加到预览中 var img = { id: i, url: app.globalData.cosUrl + relativePath, name: filename } //读取缓存 let list = wx.getStorageSync('fileList'); if (list) { list.push(img); } else { list = [img]; } //存入缓存 wx.setStorageSync('fileList', list); } //延迟更新数据 setTimeout(function () { that.updateData(); }, 5000); }, delFile(event) { toastMsg = "删除"; var that = this; wx.showModal({ title: '提示', content: '确定要删除这张图片吗?', success(res) { if (res.confirm) { var index = event.detail.index; //读取缓存 let list = wx.getStorageSync('fileList'); var filename = list[index].name; //更新fileList中的数据 for (let i = 0; i < list.length; i++) { //如果item是选中的话,就删除它。 if (filename == list[i].name) { // 删除对应的索引 list.splice(i, 1); break; } } //更新缓存 wx.setStorageSync('fileList', list); //更新数据 that.updateData(); //删除cos对象存储中的图片 cos.deleteObject({ Bucket: config.Bucket, Region: config.Region, Key: 'upload/' + that.data.date + '/' + filename, }, requestCallback); } else if (res.cancel) { //console.log('用户点击取消') } } }) }, //更新数据 updateData() { this.setData({ fileList: wx.getStorageSync('fileList') }); },}) // 回调函数var requestCallback = function(err, data) { //console.log(err || data); if (err && err.error) { wx.showModal({ title: '返回错误', content: '请求失败:' + (err.error.Message || err.error) + ';状态码:' + err.statusCode, showCancel: false }); } else if (err) { wx.showModal({ title: '返回错误', content: '请求出错:' + err + ';状态码:' + err.statusCode, showCancel: false }); } else { wx.showToast({ title: toastMsg + '成功', icon: 'success', duration: 3000 }); }};
上面以当天日期为目录是用了工具类的,附加util.js时间格式化的代码:
const formatTime = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')} const formatNumber = n => { n = n.toString() return n[1] ? n : '0' + n} /** * 时间戳转化为年 月 日 时 分 秒 * number: 传入时间戳 * format:返回格式,支持自定义,但参数必须与formateArr里保持一致*/function dateFormat(number, format) { var formateArr = ['Y', 'M', 'D', 'h', 'm', 's']; var returnArr = []; var date = new Date(number); returnArr.push(date.getFullYear()); returnArr.push(formatNumber(date.getMonth() + 1)); returnArr.push(formatNumber(date.getDate())); returnArr.push(formatNumber(date.getHours())); returnArr.push(formatNumber(date.getMinutes())); returnArr.push(formatNumber(date.getSeconds())); for (var i in returnArr) { format = format.replace(formateArr[i], returnArr[i]); } return format;} module.exports = { formatTime: formatTime, dateFormat: dateFormat}