react

✍️ Tangxt ⏳ 2021-04-09 🏷️ epic

03-epic 项目文件上传、上传历史

实现的效果

1)leancloud 实现文件上传、图片信息全局状态管理

文档:数据类型文件

API

model -> 接口层、模型层;store -> 全局状态;components -> UI 层

1、与服务器打交道的 Uploader

const Uploader = {
  add(file, filename) {
    const item = new AV.Object("Image");
    const avFile = new AV.File(filename, file);
    item.set("filename", filename);
    item.set("owner", AV.User.current());
    item.set("url", avFile);
    return new Promise((resolve, reject) => {
      item.save().then(
        (serverFile) => resolve(serverFile),
        (error) => reject(error)
      );
    });
  },
};

💡:new AV.Object("Image")

在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将 LeanCloud 里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。

所以创建了一张表:

创建一张表

💡:new AV.File(filename, file)

有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 AV.Object 保存,此时文件对象 AV.File 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。

示例:

const data = { base64: 'TGVhbkNsb3Vk' };
// resume.txt 是文件名
const file = new AV.File('resume.txt', data);

💡:item配合avFile使用?

AV.Object旗下的属性支持两种特殊的数据类型 PointerFile,可以分别用来存储指向其他 AV.Object 的指针以及二进制数据

所以:

你上传了一张图片,就是在Image表里创建一条记录:

记录

此刻,Image表里的这个item记录的这个url字段指向了File表里avFile这条记录

表与表之间的关联

存储服务似乎用了七牛提供的存储服务!

💡:item.save()

是个异步操作,把数据存储到远程数据库,如果存储成功,响应回来给我们的serverFile是这样一个东西:

serverFile

2、全局状态 ImageStore

import { observable, action, makeObservable } from "mobx";
import { Uploader } from "../models";

class ImageStore {
  constructor() {
    makeObservable(this);
  }
  @observable filename = "";
  @observable file = null;
  @observable serverFile = null;
  @observable isUploading = false;
  @action setFilename(newFilename) {
    this.filename = newFilename;
  }
  @action setFile(newFile) {
    this.file = newFile;
  }
  @action upload() {
    this.isUploading = true;
    return new Promise((resolve, reject) => {
      Uploader.add(this.file, this.filename)
        .then((serverFile) => {
          this.serverFile = serverFile;
          resolve(serverFile);
        })
        .catch((err) => {
          console.error("上传失败");
          reject(err);
        })
        .finally(() => (this.isUploading = false));
    });
  }
}

export default new ImageStore();

Uploader组件使用全局数据:

上传组件

这跟之前的登录注册功能是一样的写代码姿势! -> Store维护了我们要传给后台的参数

效果:

效果

💡:为什么抽离出一个 Uploader 组件?

这个组件是给 Home 页面用的!而这个页面需要用到很多东西,如果不把上传功能抽离成一个组件,那么 Home 页面就很乱了,而这也体现不了使用 React 的优势了!

💡:非受控表单?

所谓受控和非受控,指的是我们对某个组件状态的掌控,如果它的值只能由用户设置,也就是使用 Web 应用的人,那么这个组件的状态是「非受控」的,如果开发者可以通过 JS 代码来控制组件的状态,那么这个组件就是「受控」的

在 HTML 的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件

受控组件特点 -> 需要单独的为每个表单元素维护一个状态

而非受控组件 -> 利用ref属性,可以获取 input 元素的 DOM 属性信息,如获取用户输入的值 -> 用defaultValue属性来指定表单元素的默认值

对于 file 类型的表单控件它始终是一个不受控制的组件,因为它的值只能由用户设置,而不是以编程方式设置。

针对type类型的fileinput元素,你要用非受控组件的方式去获取它的值! -> 这就是使用const ref = useRef()的原因!

总之你不能通过这样:

<input type="file" value={this.state.files} onChange={(e) => this.handleFile(e)} />

去改变这个元素的值!

file 表单元素的值是fileRef.current.files -> files(一个或多个文件,是个数组,没开启多选multiple的话,那么files的长度始终为1

总之:

React 的官方说法是:绝大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React组件负责处理;当然如果选择受控组件的话,表单数据就由DOM本身处理了。

➹:受控和非受控组件真的那么难理解吗?(React 实际案例详解)

💡:onChange事件?

监听输入内容的改变

➹:你真的了解 onChange 事件吗 - 知乎

💡:IDL 属性?

IDL

我很好奇为啥是用files来获取元素的内容?而不是用value呢?

➹:Browser APIs · 语雀

➹:Web 技巧(07). 在这一期中咱们一起来聊聊 HTML5 中的表单。说到 HTML

2)使用 antd 上传组件实现点击和拖拽上传

文档:上传 Upload - Ant Design

上传组件

最终实现的效果:

实现效果

代码:

import React from "react";
import { useStores } from "../stores";
import { observer } from "mobx-react";

import { Upload } from "antd";
import { InboxOutlined } from "@ant-design/icons";

const { Dragger } = Upload;

const Component = observer(() => {
  const { ImageStore } = useStores();

  const props = {
    showUploadList: true,
    beforeUpload: (file) => {
      ImageStore.setFile(file);
      ImageStore.setFilename(file.name);
      // 异步操作
      ImageStore.upload()
        .then((serverFile) => console.log("上传成功", serverFile))
        .catch((err) => console.log(err));
      // 先结束
      return false;
    },
  };

  return (
    <div>
      <Dragger {...props}>
        <p className="ant-upload-drag-icon">
          <InboxOutlined />
        </p>
        <p className="ant-upload-text">
          Click or drag file to this area to upload
        </p>
        <p className="ant-upload-hint">
          Support for a single or bulk upload. Strictly prohibit from uploading
          company data or other band files
        </p>
      </Dragger>
      <div>
        <h1>上传结果</h1>
        {ImageStore.serverFile ? (
          <div>{ImageStore.serverFile.attributes.url.attributes.url}</div>
        ) : null}
      </div>
    </div>
  );
});

export default Component;

解析:

showUploadList:它的值为true,那就展示这个:

showUploadList

有种记录本地上传历史的调调……

beforeUpload:是个钩子,在上传文件到服务器之前触发,其实就是给上传文件到服务器这个操作用的!(resolve 时开始上传、返回false就停止文件上传) -> 我们从本地把文件拖进去,相当于给这个盒子输入了一个文件 -> 文件输入后,就可以着手把文件上传到服务器的事儿了!

💡:ImageStore.serverFile.attributes.url.attributes.url

serverFile

3)自定义 Tips 组件实现登录后上传限制

目前察觉到的问题:「不登录,也能上传图片」

不登录上传图片

不能让所有用户都可以无限上传,不然这免费的 API,就凉凉了……

用到的第三方组件:Message -> 文档:全局提示 Message - Ant Design

自定义的 Tips 组件:

Tips 组件

代码:

import React from "react";
import { useStores } from "../stores";
import { observer } from "mobx-react";
import styled from "styled-components";

const Tips = styled.div`
  padding: 10px;
  margin: 30px 0;
  color: #fff;
  border-radius: 4px;
  background-color: orange;
`;

const Component = observer(({ children }) => {
  const { UserStore } = useStores();
  return <>{UserStore.currentUser ? null : <Tips>{children}</Tips>}</>;
});

export default Component;

关键代码:

关键代码

💡:消息弹出框默认会停留多久才会消失?

默认是 3 s!

4)上传结果展示

要做的:

上传结果

要展示哪些关于图片的信息呢?

实现的效果:

效果

代码思路:判断ImageStore.serverFile是否存在,如果存在就把图片的信息展示到这个「上传结果」UI 里边就行了

5)图片尺寸定制与 useLocalStore

完成后的效果:

效果

💡:图片尺寸定制?

Leancloud 的文件商使用的是七牛的服务,根据七牛的文档,在 url 后面加上设定会获得相应的缩略图:

imageView2 提供简单快捷的图片格式转换、缩略、剪裁功能。只需要填写几个参数,即可对图片进行缩略操作,生成各种缩略图。imageView2接口可支持处理的原图片格式有psdjpegpnggifwebptiffbmp。(webp 不支持动图)

API 规格:

API

➹:获得缩略图问题 url - 问题讨论 / SDK / API - LeanCloud 用户社区

➹:图片基本处理(imageView2)_API 文档_智能多媒体服务 - 七牛开发者中心

💡:useLocalStore

视频里用的是useLocalStore,我用的是useLocalObservable

局部的store

store

组件依赖这个store<Observer>{fn}<Observer>

UI

mobx-reactmobx-react-lite可以混着用。..

为什么我不用useLocalStore,因为官方说它将要被废弃了 -> 官方推荐我们使用useLocalObservable代替!

总之:

➹:MobX 在 hook 中的使用 - SegmentFault 思否

💡:mobx-react-lite 介绍?

Mobx-react-lite —— Lightweight React bindings for MobX based on React 16.8 and Hooks

直至 2018 年底,React 项目中统一使用 mobx 的方式都是 mobx-react。然而随着 react hooks 的诞生,mobx-react-lite 也出现了,它是专门服务于 react hooks 的 mobx-react 轻量级版本。虽然 mobx-react@6 已经包含了 mobx-react-lite,但官方是推荐在没有类组件的 react 项目中直接使用 mobx-react-lite 的。

➹:初探 mobx-react-lite - 慕雪

💡:rel="noreferrer"

超链接 target="_blank" 要增加 rel="nofollow noopener noreferrer" 来堵住钓鱼安全漏洞。如果你在链接上使用 target="_blank"属性,并且不加上rel="noopener"属性,那么你就让用户暴露在一个非常简单的钓鱼攻击之下。

也就是说:

当你使用 target='_blank' 打开一个新的标签页时,不加 rel="..."的话,那么新页面的 window 对象上有一个属性 opener,它指向的是前一个页面的 window 对象,因此,后一个页面就获得了前一个页面的控制权,而这是很可怕的!

➹:聊聊 rel=noopener

➹:a 标签里的 rel=”noreferrer noopener”,是什么意思?-墨子学院 seo