# 对象存储

对象存储服务,简称 OSS,是一种面向海量数据规模的分布式存储服务,具有稳定、可靠、安全、低成本的特点,能够提供十一个九的数据可靠性。OSS提供与平台无关的 RESTful API 接口,您可以在互联网任何位置存储和访问。OSS的容量和处理能力弹性扩展,并提供多种存储类型供您选择,全面优化存储成本。简单理解就是存储图片、音频、视频等非结构化数据的数据池。

在这里我选择了七牛云对象存储,大家在可能在疑惑为什么,或者它的优点是什么? 免费算是优点吗?使用七牛云的免费存储空间足够我们的练习使用了。

# 优点

# 成本低,资源弹性伸缩,按需付费

现在阿里云对象存储(简称 OSS)40G 容量一年的价格大概是 9 块钱。七牛云对象存储(简称 cos)更是有免费的 10G 容量。已经可以满足大部分个人站点的使用需求。不像服务器的流量是固定的,包含在服务器的费用里面了。无论你用或不用,都是这么多。对象存储可以是根据你的实际使用量进行计费。

# 大幅提升网页性能

一般情况下,我们都是建议使用主机服务器和对象存储分工合作的方式来存储网站数据。主机服务器主要负责存储网站的动态数据,对象存储则用来存储网站的静态文件。从而实现网站的动静分离,当用户访问一个网站时,分别从主机服务器和对象存储的服务器同步读取数据,可以大幅的提升网页性能。

# 管理方便

那个服务商都有单独的管理控制台。你不必打开网站,就可以像使用百度云盘一样使用对象存储来管理你的网站文件,除了上传、下载、预览等常用功能,还可以直接在对象存储上进行图片处理/媒体转码/数据分析等。

# 新建存储空间

注册七牛云账号 > 管理控制台 > 对象存储 > 新建储存空间

拿到 AccessKey 和 SecretKey

# 安装插件

七牛云官方提供并一直在维护的 SDK ,这个 SDK 以方便快捷的方式将数据安全的存储到七牛云上,无论是网站、云端还是终端等应用都可以让用户高速上传和下载。七牛云存储 Node.js SDK 使用指南

$ npm install qiniu --save

# 配置参数

https://github.com/push-over/egg-example/blob/70dc0ac730/config/config.default.ts

接下来将这些参数放到配置文件中:

config.qiniu = {
    AccessKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    SecretKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    Bucket: 'xxxxxxx',  // 存储空间的名字
    Domain: 'xxxxxxx',  // 融合 CDN 测试域名
};

# 保存图片

https://github.com/push-over/egg-example/blob/70dc0ac730c452f2344568a75cf5ce1f6394811d/app/service/Utils.ts

由于这个项目是一个 API 服务,没有前端页面,在这里我选择使用后端去上传图片以便给大家做演示。至于后端返回配置前端上传还是后端来做上传看大家实际需求了。

在这个方法中我们把图片以日期保存到了本地,并返回了图片所在的本地路径。

private async storeUpload(stream: any, suffix: string) {
    const { ctx, uploadDir } = this;
    const { dayjs, uuidv1 } = ctx.helper;
    const id = uuidv1();
    // 当前日期
    const dirName = dayjs(Date.now()).format('YYYYMMDD');
    const filename = `${id}.${suffix}`;
    // 创建文件夹
    if (!fs.existsSync(join(uploadDir , dirName))) fs.mkdirSync(join(uploadDir , dirName));
    const path = join(uploadDir, dirName, filename);
	// 保存图片
    return new Promise<{ id: string, path: string }>((resolve, reject) =>
      stream.pipe(fs.createWriteStream(path))
        .on('finish', () => resolve({ id, path }))
        .on('error', reject),
    );
  }

# 上传图片至七牛云

API流程:获取七牛云 Token > 携带Token上传图片

# 获取token

https://github.com/push-over/egg-example/blob/70dc0ac730c452f2344568a75cf5ce1f6394811d/app/service/Utils.ts

我们拿之前获取到的配置实例化七牛云的对象并获取到 token。

 private getToken() {
    const { app } = this;
    // config 中的配置
    const { AccessKey: accessKey, SecretKey: secretKey, Bucket } = app.config.qiniu;
    const putPolicy = new qiniu.rs.PutPolicy({ scope: Bucket });
    const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
    const uploadToken = putPolicy.uploadToken(mac);

    return uploadToken;
  }

# 上传图片

https://github.com/push-over/egg-example/blob/70dc0ac730c452f2344568a75cf5ce1f6394811d/app/service/Utils.ts

这一步我们将正式把图上上传至七牛云,携带从上一步获取到的 token 和 本地图片路径把本地图片上传至七牛云并返回图片名称。

// localFile 本地路径
// suffix ['.jpg', '.jepg', '.png']
private uploader(localFile: string, suffix: string) {
    const { ctx, app } = this;
    const { Domain } = app.config.qiniu;
    const config: any = new qiniu.conf.Config();
    //   Zone_z0: qiniu.zone.Zone_z0, 	// 华东
    //   Zone_z1: qiniu.zone.Zone_z1, 	// 华北
    //   Zone_z2: qiniu.zone.Zone_z2, 	// 华南
    //   Zone_na0: qiniu.zone.Zone_na0, // 北美
    config.zone = qiniu.zone.Zone_z0;
    const formUploader = new qiniu.form_up.FormUploader(config);
    // 获取 token
    const token = this.getToken();
    const putExtra = new qiniu.form_up.PutExtra();
    // 图片名称
    const key = ctx.helper.uuidv1() + '.' + suffix;
    // 上传图片
    return new Promise(resolve => {
      formUploader.putFile(token, key, localFile, putExtra, (respErr: any, respBody: any, respInfo: any) => {
        if (respErr) {
          throw respErr;
        }
        if (respInfo.statusCode === 200) {
          const { hash, key } = respBody;
          const result = {
            hash,
            key: `${Domain}/${key}`,
          };
          resolve(result);
        }
      });
    });
  }

# 合并方法

https://github.com/push-over/egg-example/blob/70dc0ac730c452f2344568a75cf5ce1f6394811d/app/service/Utils.ts

上传图片到本地 > 拿到路径 > 上传图片至七牛云 我们根据这个流程把我们需要的方法合并一下做一个入口。实际上就是在之前就做了代码的拆分,不然放在一个方法中显得太臃肿。我们按照顺序组合一下。

 public async processUpload(file: any) {
    const { _ } = this.ctx.helper;
    const { stream, mimetype, encoding } = file;
    const suffix = _.split(mimetype, '/', 2)[1];
    // 获取本地路径
    const { path } = await this.storeUpload(stream, suffix);
    // 上传至七牛云
    const result: any = await this.uploader(path, suffix);
    const { key } = result;
    // 返回图片信息以及路径
    return {
      filename: key,
      mimetype,
      encoding,
    };
  }

# 实践操作

我们开始正式编写 Graphql 服务,因为 Graphql 开发者工具是不支持上传文件的。所以在这里我也会新起一个 Vue 的项目基于 Vue Apollo (opens new window) 来为大家演示上传图片。

# 定义类型

https://github.com/push-over/egg-example/blob/c72d08141f/app/graphql/utils/schema.graphql

type File {
  filename: String!
  mimetype: String!
  encoding: String!
}

# 执行逻辑

https://github.com/push-over/egg-example/blob/c72d08141f/app/graphql/utils/connector.ts

在 connector 调用 service 中的上传图片方法。

  public async singleUpload(file: any) {
    const { ctx } = this;
    return await ctx.service.utils.processUpload(file);
  }

# 数据操作

https://github.com/push-over/egg-example/blob/70dc0ac730/app/graphql/user/resolver.ts

export default {
  Mutation: {
    async singleUpload(_root: any, { file }, { connector }) {
      return await connector.utils.singleUpload(await file);
    },
  },
};

上传文件这种操作是一个动作,在这里我使用了 Mutation。要解决文件上传详细信息以进行处理和存储需要依赖于这个 graphql-upload 插件。

$ npm i graphql-upload --save

大家有没有注意 Upload 这个类型,其实就是我们 graphql-upload 这个插件中的标量类型。

type Mutation {
  singleUpload(file: Upload!) : File!
}

# 演示

我们看一下七牛云上存不存在我们刚刚上传的这张图片:

# 小结

在这一节中我们学习了对象存储的优点与好处,并通过代码实例完成了一次把图片上传至七牛云对象存储。其实七牛云还有很多好玩的,如果图片裁切、加水印等等,有兴趣的可以研究研究。

阅读全文