【技术派后端篇】技术派实现图片上传至 OSS

1 OSS 概述

阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,可提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。国内的竞品有七牛云的 Kodo 和腾讯云的 COS。

2 开通 OSS

阿里云 OSS 价格相对较低,100GB 中国大陆标准版一个月续费价格约 11 元。开通 OSS 资源包后,进入 OSS 管理控制台,点击「Bucket 列表」,再点击「创建 Bucket」。创建时需注意将读写权限设置为“公共读”(若配置 CDN 可设置为私有)。开通成功后,从 RAM(Resource Access Management)访问控制处获取 accesskey ID 和 accesskey secret,这两个是访问阿里云 API 的关键,务必妥善保管。同时,还需获取 Endpoint(地域节点)和 Bucket(桶名),完成 OSS 的前期准备工作。
在这里插入图片描述

3 新增 OSS 配置文件

技术派的图片配置文件为 application-image.yml,将 OSS 相关配置独立出来,便于管理。
在这里插入图片描述

以下是主要的 OSS 配置项:

  • image.oss.type:ali 表示阿里云 OSS,local 为本地存储,rest 为阿里云 OSS 的中间转存服务,用于解决 OSS 跨域限制问题。
  • image.oss.prefix:上传文件的前缀路径。
  • image.oss.endpoint:地域节点。
  • image.oss.ak:accesskey ID。
  • image.oss.sk:accesskey secret。
  • image.oss.bucket:桶名。
  • image.oss.host:后续使用的 CDN 域名。

同时,对应的 JavaBean 使用 lombok 的 Data 注解,通过 Spring Boot 的 @ConfigurationProperties 注解将配置文件中的属性绑定到类的字段上,并使用 @Component 注解将类标记为 Spring 容器的组件,便于在其他地方使用。

4 在 pom.xml 文件中添加 OSS 依赖包

在 pom.xml 文件中添加 OSS 依赖包,版本可通过 https://mvnrepository.com/artifact/com.aliyun.oss/aliyun-sdk-oss查看,选择使用量最多的版本通常较为稳定。

<!-- https://mvnrepository.com/artifact/com.aliyun.oss/aliyun-sdk-oss -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.2</version>
</dependency>

5 新增图片上传接口

为方便切换图片上传服务(本地或 OSS),新增 com.github.paicoding.forum.service.image.oss.ImageUploader 接口,包含 upload(用于图片上传)和 uploadIgnore(用于图片转链)两个关键方法,以及默认的 getFileType 方法用于获取文件后缀名,限定图片格式为 png、jpg、webp、gif 等。WebP 是由 Google 开发的现代图像格式,具有更高效的压缩性能,能减少带宽和加载时间。

public interface ImageUploader {
    String DEFAULT_FILE_TYPE = "txt";
    Set<MediaType> STATIC_IMG_TYPE = new HashSet<>(Arrays.asList(MediaType.ImagePng, MediaType.ImageJpg, MediaType.ImageWebp, MediaType.ImageGif));

    /**
     * 文件上传
     *
     * @param input
     * @param fileType
     * @return
     */
    String upload(InputStream input, String fileType);

    /**
     * 判断外网图片是否依然需要处理
     *
     * @param fileUrl
     * @return true 表示忽略,不需要转存
     */
    boolean uploadIgnore(String fileUrl);

    /**
     * 获取文件类型
     *
     * @param input
     * @param fileType
     * @return
     */
    default String getFileType(ByteArrayInputStream input, String fileType) {
        if (StringUtils.isNotBlank(fileType)) {
            return fileType;
        }

        MediaType type = MediaType.typeOfMagicNum(FileReadUtil.getMagicNum(input));
        if (STATIC_IMG_TYPE.contains(type)) {
            return type.getExt();
        }
        return DEFAULT_FILE_TYPE;
    }
}

6 新增 OSS 实现类

com.github.paicoding.forum.service.image.oss.impl.AliOssWrapper 类是一个用于阿里云 OSS 文件上传的实现类,它实现了 ImageUploader 接口,同时实现了 InitializingBeanDisposableBean 接口,以处理 Bean 的初始化和销毁逻辑。以下是对该类代码逻辑的详细解释:

6.1 类定义与注解

@Slf4j
@ConditionalOnExpression(value = "#{'ali'.equals(environment.getProperty('image.oss.type'))}")
@Component
public class AliOssWrapper implements ImageUploader, InitializingBean, DisposableBean {
}
  • @Slf4j:Lombok 注解,用于自动生成日志记录器 log,方便日志输出。
  • @ConditionalOnExpression:Spring 注解,当 image.oss.type 属性值为 ali 时,该 Bean 才会被创建。
  • @Component:Spring 注解,将该类标记为一个组件,使其能被 Spring 自动扫描并注册到容器中。
  • implements ImageUploader, InitializingBean, DisposableBean:实现 ImageUploader 接口,定义文件上传的相关方法;实现 InitializingBean 接口,用于在 Bean 属性设置完成后执行初始化操作;实现 DisposableBean 接口,用于在 Bean 销毁时执行清理操作。

6.2 初始化与销毁方法

private void init() {
    log.info("init ossClient");
    ossClient = new OSSClientBuilder().build(properties.getOss().getEndpoint(), properties.getOss().getAk(), properties.getOss().getSk());
}

@Override
public void afterPropertiesSet() {
    init();
    // 监听配置变更,然后重新初始化OSSClient实例
    // dynamicConfigContainer.registerRefreshCallback(properties, () -> {
    //     init();
    //     log.info("ossClient refreshed!");
    // });
}

@Override
public void destroy() {
    if (ossClient != null) {
        ossClient.shutdown();
    }
}
  • init():创建阿里云 OSS 客户端实例。
  • afterPropertiesSet():实现 InitializingBean 接口的方法,在 Bean 属性设置完成后调用 init() 方法进行初始化。同时可以监听配置变更,若配置发生变化则重新初始化 OSS 客户端。
  • destroy():实现 DisposableBean 接口的方法,在 Bean 销毁时关闭 OSS 客户端。

6.3 文件上传方法

  • upload(InputStream input, String fileType):将输入流中的文件上传到 OSS,先将输入流转换为字节数组,再调用 upload(byte[] bytes, String fileType) 方法。
public String upload(InputStream input, String fileType) {
    try {
        // 创建PutObjectRequest对象。
        byte[] bytes = StreamUtils.copyToByteArray(input);
        return upload(bytes, fileType);
    } catch (OSSException oe) {
        log.error("Oss rejected with an error response! msg:{}, code:{}, reqId:{}, host:{}", oe.getErrorMessage(), oe.getErrorCode(), oe.getRequestId(), oe.getHostId());
        return "";
    } catch (Exception ce) {
        log.error("Caught an ClientException, which means the client encountered "
                + "a serious internal problem while trying to communicate with OSS, "
                + "such as not being able to access the network. {}", ce.getMessage());
        return "";
    }
}
  • upload(byte[] bytes, String fileType):将字节数组形式的文件上传到 OSS,计算文件的 MD5 值作为文件名,避免重复上传。上传成功后返回文件的访问 URL,失败则返回空字符串。
public String upload(byte[] bytes, String fileType) {
    StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传");
    try {
        // 计算md5作为文件名,避免重复上传
        String fileName = stopWatchUtil.record("md5计算", () -> Md5Util.encode(bytes));
        ByteArrayInputStream input = new ByteArrayInputStream(bytes);
        fileName = properties.getOss().getPrefix() + fileName + "." + getFileType(input, fileType);
        // 创建PutObjectRequest对象。
        PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getOss().getBucket(), fileName, input);
        // 设置该属性可以返回response。如果不设置,则返回的response为空。
        putObjectRequest.setProcess("true");

        // 上传文件
        PutObjectResult result = stopWatchUtil.record("文件上传", () -> ossClient.putObject(putObjectRequest));
        if (SUCCESS_CODE == result.getResponse().getStatusCode()) {
            return properties.getOss().getHost() + fileName;
        } else {
            log.error("upload to oss error! response:{}", result.getResponse().getStatusCode());
            // Guava 不允许回传 null
            return "";
        }
    } catch (OSSException oe) {
        log.error("Oss rejected with an error response! msg:{}, code:{}, reqId:{}, host:{}", oe.getErrorMessage(), oe.getErrorCode(), oe.getRequestId(), oe.getHostId());
        return  "";
    } catch (Exception ce) {
        log.error("Caught an ClientException, which means the client encountered "
                + "a serious internal problem while trying to communicate with OSS, "
                + "such as not being able to access the network. {}", ce.getMessage());
        return  "";
    } finally {
        if (log.isDebugEnabled()) {
            log.debug("upload image size:{} cost: {}", bytes.length, stopWatchUtil.prettyPrint());
        }
    }
}

upload 方法用于将前端传递的图片上传到 OSS,主要步骤如下:

  1. 从输入流中获取字节对象:byte[] bytes = StreamUtils.copyToByteArray(input)
  2. 计算文件名:Md5Util.encode(bytes)
  3. 生成完整文件名:fileName = properties.getOss().getPrefix() + fileName + "." + getFileType(input, fileType)
  4. 创建 OSS 请求对象:PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getOss().getBucket(), fileName, input)
  5. 设置请求属性以返回 response:putObjectRequest.setProcess("true")
  6. 提交请求:ossClient.putObject(putObjectRequest)
  7. 若上传成功,返回带有 CDN 的图片 URL:properties.getOss().getHost() + fileName
  • uploadWithFileName(byte[] bytes, String fileName):将字节数组形式的文件以指定的文件名上传到 OSS,上传成功后返回文件的访问 URL,失败则返回空字符串。
public String uploadWithFileName(byte[] bytes, String fileName) {
    StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传");
    try {
        // 计算md5作为文件名,避免重复上传
        ByteArrayInputStream input = new ByteArrayInputStream(bytes);
        fileName = properties.getOss().getPrefix() + fileName;
        // 创建PutObjectRequest对象。
        PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getOss().getBucket(), fileName, input);
        // 设置该属性可以返回response。如果不设置,则返回的response为空。
        putObjectRequest.setProcess("true");

        // 上传文件
        PutObjectResult result = stopWatchUtil.record("文件上传", () -> ossClient.putObject(putObjectRequest));
        if (SUCCESS_CODE == result.getResponse().getStatusCode()) {
            return properties.getOss().getHost() + fileName;
        } else {
            log.error("upload to oss error! response:{}", result.getResponse().getStatusCode());
            // Guava 不允许回传 null
            return "";
        }
    } catch (OSSException oe) {
        log.error("Oss rejected with an error response! msg:{}, code:{}, reqId:{}, host:{}", oe.getErrorMessage(), oe.getErrorCode(), oe.getRequestId(), oe.getHostId());
        return  "";
    } catch (Exception ce) {
        log.error("Caught an ClientException, which means the client encountered "
                + "a serious internal problem while trying to communicate with OSS, "
                + "such as not being able to access the network. {}", ce.getMessage());
        return  "";
    } finally {
        if (log.isDebugEnabled()) {
            log.debug("upload image size:{} cost: {}", bytes.length, stopWatchUtil.prettyPrint());
        }
    }
}

6.4 其他方法

uploadIgnore 方法用于判断给定的文件 URL 是否应忽略上传,先判断 URL 是否为 CDN 开头,若是则忽略;再判断 URL 是否以 http 开头,若不是则可能不是有效图片资源。

@Override
public boolean uploadIgnore(String fileUrl) {
    if (StringUtils.isNotBlank(properties.getOss().getHost()) && fileUrl.startsWith(properties.getOss().getHost())) {
        return true;
    }
    return !fileUrl.startsWith("http");
}

7 新增图片控制器

7.1 ImageRestController

ImageRestController 是一个 Spring Boot 的 REST 控制器,主要提供图片相关的服务,包括图片上传、二维码识别和图片转存。这些服务要求用户登录后才能操作。

7.1.1 类定义和注解

@Permission(role = UserRole.LOGIN)
@RequestMapping(path = {"image/", "admin/image/", "api/admin/image/",})
@RestController
@Slf4j
public class ImageRestController {
  • @Permission(role = UserRole.LOGIN):自定义权限注解,表明该控制器下的所有接口都要求用户处于登录状态才能访问。
  • @RequestMapping(path = {"image/", "admin/image/", "api/admin/image/",}):定义了该控制器的基础请求路径,这意味着该控制器下的接口可以通过这三个路径访问。
  • @RestController:Spring Boot 注解,表明这是一个 RESTful 风格的控制器,返回的结果会自动序列化为 JSON 格式。
  • @Slf4j:Lombok 注解,自动生成日志记录器 log,方便记录日志。

7.1.2 图片上传接口

@RequestMapping(path = "upload")
public ResVo<ImageVo> upload(HttpServletRequest request) {
    ImageVo imageVo = new ImageVo();
    try {
        String imagePath = imageService.saveImg(request);
        imageVo.setImagePath(imagePath);
    } catch (Exception e) {
        log.error("save upload file error!", e);
        return ResVo.fail(StatusEnum.UPLOAD_PIC_FAILED);
    }
    return ResVo.ok(imageVo);
}
  • @RequestMapping(path = "upload"):定义了该方法处理的请求路径为 upload,结合控制器的前缀,完整路径可能是 image/upload 等。
  • 方法尝试调用 ImageServicesaveImg 方法保存上传的图片,并将返回的图片路径设置到 ImageVo 对象中。
  • 如果保存过程中出现异常,记录错误日志并返回一个失败的响应 ResVo.fail(StatusEnum.UPLOAD_PIC_FAILED)
  • 如果保存成功,返回一个成功的响应 ResVo.ok(imageVo)

7.1.3 图片转存接口

@RequestMapping(path = "save")
public ResVo<ImageVo> save(@RequestParam(name = "img", defaultValue = "") String imgUrl) {
    ImageVo imageVo = new ImageVo();
    if (StringUtils.isBlank(imgUrl)) {
        return ResVo.ok(imageVo);
    }

    String url = imageService.saveImg(imgUrl);
    imageVo.setImagePath(url);
    return ResVo.ok(imageVo);
}
  • @RequestMapping(path = "save"):定义了该方法处理的请求路径为save,结合控制器的前缀,完整路径可能是 image/save 等。
  • @RequestParam(name = "img", defaultValue = ""):从请求参数中获取名为 img 的参数值,若未提供则默认为空字符串。
  • 检查 imgUrl 是否为空,如果为空则直接返回一个成功的响应。
  • 调用 ImageServicesaveImg 方法转存图片,并将返回的图片路径设置到 ImageVo 对象中。
  • 最后返回一个成功的响应 ResVo.ok(imageVo)

7.2 ImageServiceImpl

ImageServiceImpl 类是 ImageService 接口的具体实现,主要负责处理图片上传、外网图片转存以及 Markdown 内容中图片自动替换等功能。

7.2.1 外网图片转存缓存

private LoadingCache<String, String> imgReplaceCache = CacheBuilder.newBuilder()
        .maximumSize(300)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String img) {
                try {
                    InputStream stream = FileReadUtil.getStreamByFileName(img);
                    URI uri = URI.create(img);
                    String path = uri.getPath();
                    int index = path.lastIndexOf(".");
                    String fileType = null;
                    if (index > 0) {
                        fileType = path.substring(index + 1);
                    }
                    return imageUploader.upload(stream, fileType);
                } catch (Exception e) {
                    log.error("外网图片转存异常! img:{}", img, e);
                    return "";
                }
            }
        });
  • 使用 Google GuavaLoadingCache 构建缓存,最多存储 300 个条目,写入 5 分钟后过期。
  • 当缓存中不存在某个图片 URL 时,CacheLoader 会尝试从该 URL 获取图片流,解析文件类型,然后调用 ImageUploader 上传图片,并将结果存入缓存。

7.2.2 从 HttpServletRequest 上传图片

@Override
public String saveImg(HttpServletRequest request) {
    MultipartFile file = null;
    if (request instanceof MultipartHttpServletRequest) {
        file = ((MultipartHttpServletRequest) request).getFile("image");
    }
    if (file == null) {
        throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "缺少需要上传的图片");
    }

    String fileType = validateStaticImg(file.getContentType());
    if (fileType == null) {
        throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "图片只支持png,jpg,gif");
    }

    try {
        return imageUploader.upload(file.getInputStream(), fileType);
    } catch (IOException e) {
        log.error("Parse img from httpRequest to BufferedImage error! e:", e);
        throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED);
    }
}
  • 检查请求是否为 MultipartHttpServletRequest,若是则获取名为 image 的文件。
  • 调用 validateStaticImg 方法验证图片格式,若格式不支持则抛出异常。
  • 调用 ImageUploaderupload 方法上传图片,若出现异常则记录日志并抛出异常。

7.2.3 外网图片转存

@Override
public String saveImg(String img) {
    if (imageUploader.uploadIgnore(img)) {
        return img;
    }

    try {
        String ans = imgReplaceCache.get(img);
        if (StringUtils.isBlank(ans)) {
            return buildUploadFailImgUrl(img);
        }
        return ans;
    } catch (Exception e) {
        log.error("外网图片转存异常! img:{}", img, e);
        return buildUploadFailImgUrl(img);
    }
}
  • ImageUploader 认为该图片无需转存,则直接返回原 URL。
  • 尝试从缓存中获取转存后的图片 URL,若缓存中不存在则触发缓存加载逻辑。
  • 若转存失败,调用 buildUploadFailImgUrl 方法生成失败标识的 URL。

7.2.4 Markdown 内容中图片自动替换

@Override
@MdcDot
@AsyncExecute(timeOutRsp = "#content")
public String mdImgReplace(String content) {
    List<MdImgLoader.MdImg> imgList = MdImgLoader.loadImgs(content);
    if (CollectionUtils.isEmpty(imgList)) {
        return content;
    }

    if (imgList.size() == 1) {
        MdImgLoader.MdImg img = imgList.get(0);
        String newImg = saveImg(img.getUrl());
        return StringUtils.replace(content, img.getOrigin(), "![" + img.getDesc() + "](" + newImg + ")");
    }

    AsyncUtil.CompletableFutureBridge bridge = AsyncUtil.concurrentExecutor("MdImgReplace");
    Map<MdImgLoader.MdImg, String> imgReplaceMap = Maps.newHashMapWithExpectedSize(imgList.size());
    for (MdImgLoader.MdImg img : imgList) {
        bridge.runAsyncWithTimeRecord(() -> {
            imgReplaceMap.put(img, saveImg(img.getUrl()));
        }, img.getUrl());
    }
    bridge.allExecuted().prettyPrint();

    for (Map.Entry<MdImgLoader.MdImg, String> entry : imgReplaceMap.entrySet()) {
        MdImgLoader.MdImg img = entry.getKey();
        String newImg = entry.getValue();
        content = StringUtils.replace(content, img.getOrigin(), "![" + img.getDesc() + "](" + newImg + ")");
    }
    return content;
}
  • 使用 MdImgLoader 从 Markdown 内容中提取图片信息。
  • 若只有一张图片,直接调用 saveImg 方法转存并替换。
  • 若有多张图片,使用 AsyncUtil 并发转存图片,提升性能。
  • 最后将 Markdown 内容中的原图片链接替换为转存后的链接。

7.2.5 辅助方法

  • buildUploadFailImgUrl:为转存失败的图片 URL 添加失败标识。
private String buildUploadFailImgUrl(String img) {
        return img.contains("saveError") ? img : img + "?&cause=saveError!";
}
  • validateStaticImg:验证图片的 MIME 类型,只支持指定的静态图片格式。
/**
 * 图片格式校验
 *
 * @param mime
 * @return
 */
private String validateStaticImg(String mime) {
    if ("svg".equalsIgnoreCase(mime)) {
        // fixme 上传文件保存到服务器本地时,做好安全保护, 避免上传了要给攻击性的脚本
        return "svg";
    }

    if (mime.contains(MediaType.ImageJpg.getExt())) {
        mime = mime.replace("jpg", "jpeg");
    }
    for (MediaType type : ImageUploader.STATIC_IMG_TYPE) {
        if (type.getMime().equals(mime)) {
            return type.getExt();
        }
    }
    return null;
}

8 admin 端增加图片上传功能

8.1 复制粘贴上传图片

技术派 admin 端使用 bytemd 开源组件库,通过 uploadImages 属性实现复制粘贴上传图片功能。
代码路径:src/views/article/edit/index.tsx#EditorProps.uploadImages

uploadImages={(files) => {
    // 使用Promise.all处理多个文件上传
    return Promise.all(
        files.map((file) => {
            // 限制图片大小,不超过5MB
            if (file.size > 5 * 1024 * 1024) {
                return  {
                    url: "图片大小不能超过 5M", // 如果超过大小限制,返回错误信息
                }
            }

            // 创建FormData对象用于文件上传
            const formData = new FormData();
            formData.append('image', file);  // 将文件添加到FormData中

            // 调用上传API
            return uploadImgApi(formData).then(({ status, result }) => {
                const { code, msg } = status || {};
                const { imagePath } = result || {};
                if (code === 0) {
                    return {
                        url: imagePath,  // 上传成功,返回图片路径
                    }
                }
                return {
                    url: msg,  // 上传失败,返回错误信息
                }
            })
        })
    )
}}

主要逻辑流程:

  1. 接收一个文件数组files作为参数
  2. 使用Promise.all并行处理所有文件的上传
  3. 对每个文件:
    • 首先检查文件大小,如果超过5MB,直接返回错误信息
    • 创建FormData对象,将文件添加到其中
    • 调用uploadImgApi接口进行文件上传
  4. 处理上传结果:
    • 如果上传成功(code === 0),返回图片路径
    • 如果上传失败,返回错误信息
  5. 最终返回一个包含所有文件上传结果的Promise

这个函数会被Markdown编辑器调用,用于处理用户插入图片时的上传操作。

8.2 图片转链

handleReplaceImgUrl 方法的主要功能是处理文章内容中的图片链接替换。
代码路径:src/views/article/edit/index.tsx#handleReplaceImgUrl

const handleReplaceImgUrl = async () => {
    // 从表单中获取当前的文章内容
    const { content } = form;
    
    // 调用uploadImages方法处理图片链接替换
    const newContent = await uploadImages(content);
    
    // 如果返回了新的内容
    if (newContent) {
        // 更新编辑器中的内容
        setContent(newContent);
        
        // 更新表单中的内容字段
        handleChange({ content: newContent });
    }
}

主要逻辑流程:

  1. 获取当前内容:从表单状态form中获取当前的文章内容content
  2. 调用图片处理函数:调用uploadImages方法,传入当前内容,该方法会处理内容中的所有图片链接(主要是将外链图片转换为内链)。
  3. 更新内容:
    • 如果uploadImages返回了新的内容newContent,则更新编辑器中的内容setContent(newContent)
    • 同时更新表单中的内容字段handleChange({ content: newContent }),确保表单数据与编辑器内容同步。

这个方法通常会在用户点击某个按钮(如“替换图片链接”)时触发,用于将文章中的外链图片转换为内链图片,确保图片资源的稳定性和可控性。

图片转链主要在 uploadImages 方法中实现

const uploadImages = async (newVal: string) => {
    // 判断新增内容与上次处理内容的关系
    let add;
    if (newVal.startsWith(lastContent)) {
        // 如果新内容以上次处理内容开头,则只处理新增部分
        add = newVal.substring(lastContent.length);
    } else if (lastContent.startsWith(newVal)) {
        // 如果删除了部分内容,则更新lastContent并返回
        setLastContent(newVal);
        return;
    } else {
        // 否则处理全部内容
        add = newVal;
    }

    // 正则表达式匹配Markdown图片语法 ![alt](src)
    const reg = /!\[(.*?)\]\((.*?)\)/mg;
    let match;

    // 存储上传任务和图片信息
    let uploadTasks = [];
    let imageInfos: ImageInfo[] = [];

    // 遍历匹配所有图片
    while ((match = reg.exec(add)) !== null) {
        const [img, alt, src] = match;
        // 如果是外链图片且不包含"saveError"
        if (src.length > 0 && src.startsWith("http") && src.indexOf("saveError") < 0) {
            // 收集图片信息
            imageInfos.push({ img, alt, src, index: match.index });
            // 判断是否允许上传(防止30秒内重复上传)
            if (!canUpload(src)) {
                continue;
            } else {
                // 添加上传任务
                uploadTasks.push(saveImgApi(src));
            }
        }
    }

    // 并行上传所有图片
    const results = await Promise.all(uploadTasks);

    // 替换图片链接
    let newContent = newVal;
    results.forEach((result, i) => {
        if (result.status && result.status.code === 0 && result.result) {
            // 生成新的图片链接
            const newSrc = `![${imageInfos[i].alt}](${result.result.imagePath})`;
            // 替换原内容中的图片链接
            newContent = newContent.replace(imageInfos[i].img, newSrc);
        }
    });

    // 更新lastContent
    setLastContent(newVal);

    // 返回处理后的内容
    return newContent;
}

主要逻辑流程:

  1. 内容差异处理:通过比较新内容和上次处理内容,确定需要处理的部分(新增内容或全部内容)。

  2. 图片匹配:使用正则表达式匹配Markdown语法中的图片链接 ![alt](src)

  3. 图片信息收集:收集所有需要处理的外链图片信息,并检查是否允许上传(防止30秒内重复上传)。

  4. 并行上传:使用 Promise.all 并行上传所有符合条件的图片。

  5. 链接替换:将上传成功的内链图片替换原内容中的外链图片。

  6. 更新状态:更新 lastContent 状态,记录本次处理的内容。

  7. 返回结果:返回处理后的新内容。

这个方法主要用于将文章中的外链图片转换为内链图片,确保图片资源的稳定性和可控性。

9 总结

本文详细介绍了技术派实现图片上传至阿里云 OSS 服务器的过程,涵盖了开通 OSS、配置文件设置、接口和实现类的编写、控制器的处理以及 admin 端的相关功能实现。其中涉及的图片复制粘贴上传、自动转链、防止 30s 内重复上传、利用 Guava Cache 提高效率、配置文件更新时自动初始化阿里云 Client 以及自由切换本地和 OSS 服务等细节,在技术面试中都可能成为很好的引申点。通过深入理解和掌握这些内容,能够更好地应对实际开发中的相关需求,也有助于在面试中展示自己的技术能力。

10 参考链接

  1. 技术派如何实现图片上传至OSS
  2. OSS SDK快速入门
  3. 项目仓库(paicoding)
  4. 项目仓库(paicoding-admin)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值