Skip to content

小程序(uniapp)上传头像至OSS(阿里云)--保姆级

前言

微信小程序改版以来,现在获取用户的头像和昵称就不能直接通过wx.getUserInfo获取了。而是需要用户主动在登录后填写自己的昵称和头像,微信只是提供一个一键填写的快捷操作让用户直接使用自己已有的微信昵称或头像。

如果是想做一个比较完善的小程序系统,那么头像昵称的修改可谓是每个带用户的小程序开发都需要经历的。

昵称还好,就是一个文本字符串,但是头像的话我们就需要上传至自己的服务器或者是一些云对象存储服务,这里我选择的是阿里云OSS服务,下面开始我的保姆级教程

流程概览

如上是整个上传头像的一个时序图,总的来说有这五步:

  1. 通过微信自带组件获取用户选择头像的临时文件路径
  2. 获取对OSS的操作授权
  3. 配置后端服务生成临时授权的服务
  4. 获取授权并上传文件至OSS
  5. 将新的头像路径保存到数据库用户表中

好,接下来我们就以上述五步慢慢道来:

1. 获取头像临时路径

据描述:是要将 button 组件 open-type 的值设置为 chooseAvatar,当用户选择需要使用的头像之后,可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径。

所以笔者编写了如下代码:

vue
// template
<button class="avatar-wrapper" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
	<uni-icons type="forward" size="30" color="#D3D3D3"></uni-icons>
</button>
ts
// script
function onChooseAvatar(e: any) {
	console.log("choose avatar: ", e.detail);
	user.avatarUrl = e.detail.avatarUrl;
}

为了让button样式透明,所以style代码如下

scss
.avatar-wrapper {
	padding-left: 0;
	padding-right: 0;
	height: 50px;
	line-height: 50px;
	background-color: transparent;
	border-color: transparent;
}

.avatar-wrapper::after {
	border: none;
}

这里笔者在button里面包裹了一个icon,因为我不想要button的样式,而仅仅用户点击icon就可以修改头像。

此时我们点击就可以获取头像的临时路径了:

然后我们选择微信头像或者本地上传的图片后就可以出现如下的日志信息:

到这里,我们这一步就已经完成了,已经获取到如上的头像临时路径了...

2. 获取OSS操作授权

这里假设你已经开通了OSS服务 ,开通过程非常简单,毕竟花钱的过程一般来说都是非常简单的,一步到位🤬。这里就不过多赘述了。

如果你想要你的OSS服务拥有自己的专属域名以及CDN加速的话,可以查看我之间写的这篇文章--CDN实践配置+原理篇

首先我们创建一个我们阿里云账号的子用户,这个子用户我们后续会将它授权给我们的服务器,让我们的服务器操作该子用户,拥有其拥有的权限。你的就是我的🤭

1)配置阿里云-访问控制

按照上述的流程点击创建后,就会出现如下页面,这里值得注意的是我们需要保存好其中的accessKeyId以及accessKeySecret

拥有这个Id+Secret相当于我们就拥有了这个用户的身份,就可以操作该身份具有的资源了,但是此时我们还没有赋予其任何权限,所以这里我们赋予其操作OSS的权限,尽量不要赋予过多的权限,做好权限收敛:

当然,如果你有多个用户有同样的权限操作,也可以使用用户组进行管理,这里不展开了。

点击添加权限,选择AliyunOSSFullAccess

2)配置阿里云-OSS-细化授权

然后我们继续对OSS进行细化配置:

3)配置阿里云-OSS-跨域访问

如果你的OSS之前已经可以访问了,这里就不需要配置了,如果是第一次配置,那么记得还要对OSS进行跨域访问的配置,这也是很多网友出现403的主要原因。

好,此时我们就拥有了一个有效的AccesssKeyIdAccessKeySecretl了

3. 配置服务器

这里我使用的是服务端签名,你也可以使用客户端签名临时凭证。并且我这里使用的后端技术栈是NestJS + GraphQl,如果你是其他技术栈,可以参考原官方文档进行自定义

0)安装

sh
npm i crypto-js @types/crypto-js

1)/src/utils/oss.interface.ts

ts
export interface MpUploadOssHelperOptions {
  // 阿里云账号AccessKey
  accessKeyId: string;
  // 阿里云账号AccessId
  accessKeySecret: string;
  // 限制参数的生效实践,单位为小时,默认值为1
  timeout?: number;
  // 限制上传文件大小,单位为MB,默认值为10
  maxSize?: number;
}

2)/src/utils/oss.ts

ts
import * as crypto from 'crypto-js';
import { MpUploadOssHelperOptions } from './oss.interface';

// 详见:https://help.aliyun.com/document_detail/92883.html
export class MpUploadOssHelper {
  private accessKeyId: string;
  private accessKeySecret: string;
  private timeout: number;
  private maxSize: number;

  constructor(options: MpUploadOssHelperOptions) {
    this.accessKeyId = options.accessKeyId;
    this.accessKeySecret = options.accessKeySecret;
    // 限制参数的生效时间,单位为小时,默认值为1。
    this.timeout = options.timeout || 1;
    // 限制上传文件的大小,单位为MB,默认值为10。
    this.maxSize = options.maxSize || 10;
  }

  createUploadParams() {
    const policy = this.getPolicyBase64();
    const signature = this.signature(policy);
    return {
      OSSAccessKeyId: this.accessKeyId,
      policy: policy,
      signature: signature,
    };
  }

  getPolicyBase64() {
    const date = new Date();
    // 设置policy过期时间。
    date.setHours(date.getHours() + this.timeout);
    const srcT = date.toISOString();
    const policyText = {
      expiration: srcT,
      conditions: [
        // 限制上传文件大小。
        ['content-length-range', 0, this.maxSize * 1024 * 1024],
      ],
    };
    const buffer = Buffer.from(JSON.stringify(policyText));
    return buffer.toString('base64');
  }

  signature(policy) {
    return crypto.enc.Base64.stringify(
      crypto.HmacSHA1(policy, this.accessKeySecret)
    );
  }
}

3)src/oss/models/oss.model.ts

ts
import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Oss {
  @Field()
  OSSAccessKeyId: string;

  @Field()
  policy: string;

  @Field()
  signature: string;
}

4)src/oss/oss.module.ts

ts
import { Module } from '@nestjs/common';
import { OssService } from './oss.service';
import { OssResolver } from './oss.resolver';

@Module({
  providers: [OssService, OssResolver],
})
export class OssModule {}

5)src/oss/oss.service.ts

ts
import { Injectable } from '@nestjs/common';
import { MpUploadOssHelper } from 'src/utils/oss';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class OssService {
  constructor(private readonly configService: ConfigService) {}

  getPostObjectParams() {
    const mpHelper = new MpUploadOssHelper({
      accessKeyId: this.configService.get<string>('ACCESS_KEY_ID'),
      accessKeySecret: this.configService.get<string>('ACCESS_KEY_SECRET'),
      timeout: 1,
      maxSize: 1,
    });

    // 生成参数
    const params = mpHelper.createUploadParams();

    return params;
  }
}

注意:这里我是通过环境变量获取的IDKEY,环境变量的使用可参考这篇文章

6)src/oss/oss.resolve.ts或者src/oss/oss.controller.ts

这里就体现了service作为业务层的作用了,无论我们是想使用GraphQL,还是使用REST API暴露接口,都可以非常灵活的替换,如下是GraphQL的代码:

ts
import { UseGuards } from '@nestjs/common';
import { Resolver, Query } from '@nestjs/graphql';
import { GqlAuthGuard } from 'src/auth/gql-auth.guard';
import { Oss } from './models/oss.model';
import { OssService } from './oss.service';

@Resolver()
export class OssResolver {
  constructor(private readonly ossService: OssService) {}

  @UseGuards(GqlAuthGuard)
  @Query(() => Oss)
  getPostObjectParams() {
    return this.ossService.getPostObjectParams();
  }
}

演示:

4. 上传文件至OSS

如下代码,这里使用的是GraphQL请求的接口,如果你是使用的REST API,基本流程也是一样的:

ts
async function uploadAvatar(filePath: string) {
	// 1. 请求服务端签名凭证
	const { execute } = useQuery({ query: getPostObjectParamsGQL });
	uni.showLoading({ title: "正在请求上传凭证中..." });
	const { error, data } = await execute();
	if (error) {
		uni.showToast({
			title: `上传头像失败: ${error}`,
			icon: "error",
			duration: 2000,
		});
		throw new Error(`上传头像失败: ${error}`);
	}

	// 2. 上传图片至oss
	const { OSSAccessKeyId, policy, signature } = data?.getPostObjectParams || {};

	const imgType = filePath.split(".").pop();
	const key = `wxmp/${userData?.id}.${imgType}`;
	uni.showLoading({ title: "正在上传图片中..." });
	const ossRes = await uniUploadFile({
		url: ossHost, // 开发者服务器的URL。
		filePath,
		name: "file", // 必须填file。
		formData: {
			key,
			policy,
			OSSAccessKeyId,
			signature,
		},
	});
	uni.hideLoading();

	return ossHost + "/" + key;
}

上述代码我是以用户的uid重命名图片文件的,就是key这个属性值,你也可以自定义你自己的文件命名方式。

演示:

然后OSS中就可以查看到这张图片了:

5. 保存数据库用户表

这一步其实就很简单了,也没什么参考价值,就是请求API,然后保存到数据库就可以了,具体代码就不贴了,如下是效果:

最后

小程序的开发几乎是程序员的必备操作,自带流量,前期非常好用,不过就是开发文档确实看着不舒服,平台改版我们也需要跟着改版...

然后用户登录以及自定义头像都是小程序们非常常见的操作,这里演示一下过程希望对你有所帮助,如果有操作不当,欢迎在评论区中友善指出..

参考