从原理到实战,手把手教你在项目中使用RabbitMQ

RabbitMQ 的文章之前写过,但是当时给的示例是 Demo 版的,这篇文章主要是结合之前写的理论知识,将 RabbitMQ 集成到技术派项目中。

话不多说,上文章目录:
在这里插入图片描述
下面我们先回顾一下理论知识,如果对这块知识已经清楚的同学,可以直接跳到实战部分。

1. 消息队列

1.1 消息队列模式

消息队列目前主要 2 种模式,分别为“点对点模式”和“发布/订阅模式”。

点对点模式

一个具体的消息只能由一个消费者消费,多个生产者可以向同一个消息队列发送消息,但是一个消息在被一个消息者处理的时候,这个消息在队列上会被锁住或者被移除并且其他消费者无法处理该消息。

需要额外注意的是,如果消费者处理一个消息失败了,消息系统一般会把这个消息放回队列,这样其他消费者可以继续处理。
在这里插入图片描述

发布/订阅模式

单个消息可以被多个订阅者并发的获取和处理。一般来说,订阅有两种类型:

  • 临时(ephemeral)订阅:这种订阅只有在消费者启动并且运行的时候才存在。一旦消费者退出,相应的订阅以及尚未处理的消息就会丢失。
  • 持久(durable)订阅:这种订阅会一直存在,除非主动去删除。消费者退出后,消息系统会继续维护该订阅,并且后续消息可以被继续处理。
    在这里插入图片描述

1.2 RabbitMQ 特征

  • **消息路由(支持):**RabbitMQ可以通过不同的交换器支持不同种类的消息路由;
  • **消息有序(不支持):**当消费消息时,如果消费失败,消息会被放回队列,然后重新消费,这样会导致消息无序;
  • **消息时序(非常好):**通过延时队列,可以指定消息的延时时间,过期时间TTL等;
  • **容错处理(非常好):**通过交付重试和死信交换器(DLX)来处理消息处理故障;
  • **伸缩(一般):**伸缩其实没有非常智能,因为即使伸缩了,master queue还是只有一个,负载还是只有这一个master queue去抗,所以我理解RabbitMQ的伸缩很弱(个人理解)。
  • **持久化(不太好):**没有消费的消息,可以支持持久化,这个是为了保证机器宕机时消息可以恢复,但是消费过的消息,就会被马上删除,因为RabbitMQ设计时,就不是为了去存储历史数据的。
  • **消息回溯(支持):**因为消息不支持永久保存,所以自然就不支持回溯。
  • **高吞吐(中等):**因为所有的请求的执行,最后都是在master queue,它的这个设计,导致单机性能达不到十万级的标准。

2. RabbitMQ 原理初探

RabbitMQ 2007 年发布,是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。

2.1 基本概念

提到RabbitMQ,就不得不提AMQP协议。AMQP协议是具有现代特征的二进制协议。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

先了解一下AMQP协议中间的几个重要概念:

  • **Server:**接收客户端的连接,实现AMQP实体服务。
  • **Connection:**连接,应用程序与Server的网络连接,TCP连接。
  • **Channel:**信道,消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。
  • **Message:**消息,应用程序和服务器之间传送的数据,消息可以非常简单,也可以很复杂。由Properties和Body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body就是消息体内容。
  • **Virtual Host:**虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名称的Exchange或Queue。
  • Exchange:交换器,接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,或者返回给生产者,或者直接丢弃。RabbitMQ常用的交换器常用类型有direct、topic、fanout、headers四种,后面详细介绍。
  • Binding:绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey。
  • RoutingKey:路由键,生产者将消息发送给交换器的时候,会发送一个RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。路由键通常为一个“.”分割的字符串,例如“com.rabbitmq”。
  • Queue:消息队列,用来保存消息,供消费者消费。

2.2 工作原理

AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:

  1. 生产者是连接到 Server,建立一个连接,开启一个信道。
  2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。
  3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。
  4. 生产者发送消息,发送到服务端中的虚拟主机。
  5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。
  6. 订阅了消息队列的消费者就可以获取到消息,进行消费。
    在这里插入图片描述

2.3 常用交换器

RabbitMQ常用的交换器类型有direct、topic、fanout、headers四种:

  • Direct Exchange:见文知意,直连交换机意思是此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配。简单点说就是一对一的,点对点的发送。
    在这里插入图片描述
  • Fanout Exchange:这种类型的交换机需要将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。简单点说就是发布订阅。
    在这里插入图片描述
  • Topic Exchange:直接翻译的话叫做主题交换机,如果从用法上面翻译可能叫通配符交换机会更加贴切。这种交换机是使用通配符去匹配,路由到对应的队列。通配符有两种:“*” 、 “#”。需要注意的是通配符前面必须要加上"."符号。
  • *符号:有且只匹配一个词。比如 a.*可以匹配到"a.b"、“a.c”,但是匹配不了"a.b.c"。
  • #符号:匹配一个或多个词。比如"rabbit.#“既可以匹配到"rabbit.a.b”、“rabbit.a”,也可以匹配到"rabbit.a.b.c"。
    在这里插入图片描述
  • Headers Exchange:这种交换机用的相对没这么多。它跟上面三种有点区别,它的路由不是用routingKey进行路由匹配,而是在匹配请求头中所带的键值进行路由。创建队列需要设置绑定的头部信息,有两种模式:全部匹配和部分匹配。如上图所示,交换机会根据生产者发送过来的头部信息携带的键值去匹配队列绑定的键值,路由到对应的队列。
    在这里插入图片描述

3. RabbitMQ环境搭建

因为我用的是Mac,所以直接可以参考官网:

https://www.rabbitmq.com/install-homebrew.html

需要注意的是,一定需要先执行:

brew update

然后再执行:

brew install rabbitmq

之前没有执行brew update,直接执行brew install rabbitmq时,会报各种各样奇怪的错误,其中“403 Forbidde”居多。

但是在执行“brew install rabbitmq”,会自动安装其它的程序,如果你使用源码安装Rabbitmq,因为启动该服务依赖erlang环境,所以你还需手动安装erlang,但是目前官方已经一键给你搞定,会自动安装Rabbitmq依赖的所有程序,是不是很棒!
在这里插入图片描述
最后执行成功的输出如下:
在这里插入图片描述
启动服务:

# 启动方式1:后台启动
brew services start rabbitmq
# 启动方式2:当前窗口启动
cd /usr/local/Cellar/rabbitmq/3.8.19
rabbitmq-server

在浏览器输入:

http://localhost:15672/

会出现RabbitMQ后台管理界面(用户名和密码都为guest):
在这里插入图片描述
通过brew安装,一行命令搞定,真香!

4. RabbitMQ 集成

4.1 前置工作

添加账号:

## 添加账号
./rabbitmqctl add_user admin admin
## 添加访问权限
./rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
## 设置超级权限
./rabbitmqctl set_user_tags admin administrator

pom 引入依赖:

<dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>5.5.1</version>
</dependency>

4.2 代码实现

核心代码

先整一个 ConnectionFactory 单例,每台机器都有自己的 ConnectionFactory,防止每次都初始化(在后面的迭代中,我会把这个去掉,整成连接池)。

/*** @author Louzai* @date 2023/5/10*/
public class RabbitmqUtil {/*** 每个key都有自己的工厂*/private static Map<String, ConnectionFactory> executors = new ConcurrentHashMap<>();/*** 初始化一个工厂** @param host* @param port* @param username* @param passport* @param virtualhost* @return*/public static ConnectionFactory init(String host,Integer port,String username,String passport,String virtualhost) {ConnectionFactory factory = new ConnectionFactory();factory.setHost(host);factory.setPort(port);factory.setUsername(username);factory.setPassword(passport);factory.setVirtualHost(virtualhost);return factory;}/*** 工厂单例,每个key都有属于自己的工厂** @param key* @param host* @param port* @param username* @param passport* @param virtualhost* @return*/public static ConnectionFactory getOrInitConnectionFactory(String key,String host,Integer port,String username,String passport,String virtualhost) {ConnectionFactory connectionFactory = executors.get(key);if (null == connectionFactory) {synchronized (RabbitmqUtil.class) {connectionFactory = executors.get(key);if (null == connectionFactory) {connectionFactory = init(host, port, username, passport, virtualhost);executors.put(key, connectionFactory);}}}return connectionFactory;}
}

获取 RabbitmqClient:

/*** @author Louzai* @date 2023/5/10*/
@Component
public class RabbitmqClient {@Autowiredprivate RabbitmqProperties rabbitmqProperties;/*** 创建一个工厂* @param key* @return*/public ConnectionFactory getConnectionFactory(String key) {String host = rabbitmqProperties.getHost();Integer port = rabbitmqProperties.getPort();String userName = rabbitmqProperties.getUsername();String password = rabbitmqProperties.getPassport();String virtualhost = rabbitmqProperties.getVirtualhost();return RabbitmqUtil.getOrInitConnectionFactory(key, host, port, userName,password, virtualhost);}
}

重点!敲黑板!!!这里就是 RabbmitMQ 的核心逻辑了。

我们使用的交换机类型是 Direct Exchange,此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配,简单点说就是一对一的,点对点的发送。

至于为什么不用广播和主题交换机模式,因为技术派的使用场景就是发送单个消息,点到点发送和消费的模式完全可以满足我们的需求。

下面 3 个方法都很简单:

  • 发送消息:拿到工厂 -> 创建链接 -> 创建通道 -> 声明交换机 -> 发送消息 -> 关闭链接;
  • 消费消息:拿到工厂 -> 创建链接 -> 创建通道 -> 确定消息队列 -> 绑定队列到交换机 -> 接受并消费消息;
  • 消费消息永动模式:非阻塞模式消费 RabbitMQ 消息。
@Component
public class RabbitmqServiceImpl implements RabbitmqService {@Autowiredprivate RabbitmqClient rabbitmqClient;@Autowiredprivate NotifyService notifyService;@Overridepublic void publishMsg(String exchange,BuiltinExchangeType exchangeType,String toutingKey,String message) throws IOException, TimeoutException {ConnectionFactory factory = rabbitmqClient.getConnectionFactory(toutingKey);// TODO: 这种并发量起不来,需要改造成连接池//创建连接Connection connection = factory.newConnection();//创建消息通道Channel channel = connection.createChannel();// 声明exchange中的消息为可持久化,不自动删除channel.exchangeDeclare(exchange, exchangeType, true, false, null);// 发布消息channel.basicPublish(exchange, toutingKey, null, message.getBytes());System.out.println("Publish msg:" + message);channel.close();connection.close();}@Overridepublic void consumerMsg(String exchange,String queue,String routingKey) throws IOException, TimeoutException {ConnectionFactory factory = rabbitmqClient.getConnectionFactory(routingKey);// TODO: 这种并发量起不来,需要改造成连接池//创建连接Connection connection = factory.newConnection();//创建消息信道final Channel channel = connection.createChannel();//消息队列channel.queueDeclare(queue, true, false, false, null);//绑定队列到交换机channel.queueBind(queue, exchange, routingKey);Consumer consumer = new DefaultConsumer(channel) {@Overridepublic void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {String message = new String(body, "UTF-8");System.out.println("Consumer msg:" + message);// 获取Rabbitmq消息,并保存到DB// 说明:这里仅作为示例,如果有多种类型的消息,可以根据消息判定,简单的用 if...else 处理,复杂的用工厂 + 策略模式notifyService.saveArticleNotify(JsonUtil.toObj(message, UserFootDO.class), NotifyTypeEnum.PRAISE);channel.basicAck(envelope.getDeliveryTag(), false);}};// 取消自动ackchannel.basicConsume(queue, false, consumer);}@Overridepublic void processConsumerMsg() {System.out.println("Begin to processConsumerMsg.");Integer stepTotal = 1;Integer step = 0;// TODO: 这种方式非常 Low,后续会改造成阻塞 I/O 模式while (true) {step ++;try {System.out.println("processConsumerMsg cycle.");consumerMsg(CommonConstants.EXCHANGE_NAME_DIRECT, CommonConstants.QUERE_NAME_PRAISE,CommonConstants.QUERE_KEY_PRAISE);if (step.equals(stepTotal)) {Thread.sleep(10000);step = 0;}} catch (Exception e) {}}}
}

这里只是给个示例,如果要真正用到生产环境,你觉得有哪些问题呢? 你自己先想想,文末再告诉你。

调用入口

其实之前我们是通过 Java 的内置异步调用方式,为了方便验证,我把文章点赞的功能迁移到 RabbitMQ 中,只要是点赞,就走 RabbitMQ 模式。

// 点赞消息走 RabbitMQ,其它走 Java 内置消息机制
if (notifyType.equals(NotifyTypeEnum.PRAISE) && rabbitmqProperties.getSwitchFlag()) {rabbitmqService.publishMsg(CommonConstants.EXCHANGE_NAME_DIRECT,BuiltinExchangeType.DIRECT,CommonConstants.QUERE_KEY_PRAISE,JsonUtil.toStr(foot));
} else {Optional.ofNullable(notifyType).ifPresent(notify -> SpringUtil.publishEvent(new NotifyMsgEvent<>(this, notify, foot)));
}

那消费入口放哪里呢?其实是在程序启动的时候,我们就启动 RabbitMQ 进行消费,然后整个进程一直在程序中跑。

@Override
public void run(ApplicationArguments args) {// 设置类型转换, 主要用于mybatis读取varchar/json类型数据据,并写入到json格式的实体Entity中JacksonTypeHandler.setObjectMapper(new ObjectMapper());// 应用启动之后执行GlobalViewConfig config = SpringUtil.getBean(GlobalViewConfig.class);if (webPort != null) {config.setHost("http://127.0.0.1:" + webPort);}// 启动 RabbitMQ 进行消费if (rabbitmqProperties.getSwitchFlag()) {taskExecutor.execute(() -> rabbitmqService.processConsumerMsg());}log.info("启动成功,点击进入首页: {}", config.getHost());
}

4.3 演示一下

我们多次点击“点赞”按钮,触发 RammitMQ 消息发送。
在这里插入图片描述
可以通过日志,也可以看到发送和消费过的消息。
在这里插入图片描述
我靠!好多没有关闭的链接。。。
在这里插入图片描述
还有一堆没有关闭的 channel。。。

在这里插入图片描述
估计再多跑一会,内存全部吃光,机器就死机了,怎么破?答案是连接池!

4.4 代码分支

为了方便大家学习功能演变的过程,每个模块都会单独开个分支,包括后面的升级版:

  • 代码仓库:https://github.com/itwanger/paicoding
  • 代码分支:feature/add_rabbitmq_20230506

如果需要运行 RabbitMQ,下面的配置需要改成 true,因为代码默认是 false
在这里插入图片描述

5 后记

这篇文章,让大家知道 RabbitMQ 的基本原理,以及如何去集成 RabbitMQ,但是还不能用到实际生产环境,但是这个确实是我写的第一个版本,存粹是搞着玩的,因为里面存在的问题还非常多。

我简单列举一下:

  1. 需要给 Connection 加个连接池,否则内存会持续消耗,机器肯定扛不住;
  2. 需要对 RabbitMQ 的消费方式进行改造,因为 while + sleep 的方式过于简单粗暴;
  3. 假如消费的任务挂掉了,你需要有重启 RabbitMQ 的消费机制;
  4. 假如机器挂了,重启后,RabbitMQ 内部的消息不能丢失。

如果你对上面的问题也非常感兴趣,可以直接基于分支 feature/add_rabbitmq_20230506,然后给我提 PR,技术嘛,我喜欢边玩边学。

预告一下,我后面会给 RabbitMQ 加个连接池,代码已经写完了,还是用 ChatGPT 帮忙完成的,下一篇文章发出,敬请期待!

如果感觉本文对你有帮助,点赞关注支持一下!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/30352.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GPT-5将死于GPT-4背刺?牛津剑桥研究警告:AI训AI成「剧毒」,会让模型崩溃!...

点击下方卡片&#xff0c;关注“CVer”公众号 AI/CV重磅干货&#xff0c;第一时间送达 点击进入—>【目标检测和Transformer】交流群 转载自&#xff1a;新智元 【导读】最差的人类语料&#xff0c;也要胜过AI生成的文本。 随着GPT-4、Stable Diffusion和Midjourney的爆火&a…

GPT-5将死于GPT-4背刺?牛津剑桥研究警告:AI训AI成「剧毒」,会让模型崩溃!

【导读】最差的人类语料&#xff0c;也要胜过AI生成的文本。 随着GPT-4、Stable Diffusion和Midjourney的爆火&#xff0c;越来越多的人开始在工作和生活中引入生成式AI技术。 甚至&#xff0c;有人已经开始尝试用AI生成的数据来训练AI了。难道&#xff0c;这就是传说中的「数…

微软、OpenAI用上“数据永动机” 合成数据是晨曦还是暮光?

微软、OpenAI、Cohere等公司已经开始测试使用合成数据来训练AI模型。Cohere首席执行官Aiden Gomez表示&#xff0c;合成数据可以适用于很多训练场景&#xff0c;只是目前尚未全面推广。 已有的&#xff08;通用&#xff09;数据资源似乎接近效能极限&#xff0c;开发人员认为&a…

论道AIGC:如何看待用于内容生成的永动机?| 大咖思辨-38

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 12月28日&#xff0c;“科技未来看青年”AI TIME 2022 年度嘉年华完美收官。本次年度嘉年华开启“青年学者说”、“科技新势力”、“Debate AIGC”三大板块&#xff0c;邀请AI领域的青年科学家、PhD等青年学者分…

让博客支持使用 ChatGPT 生成文章摘要是一种什么样的体验?

让博客支持使用 ChatGPT 生成文章摘要是一种什么样的体验&#xff1f; 起因 Sakurairo 主题支持了基于 ChatGPT 的 AI 摘要功能&#xff0c;我有点眼红&#xff0c;但是因为那是个主题限定功能&#xff0c;而我用的又是 Argon&#xff0c;遂想着让 Argon 也支持 AI 摘要功能。…

ChatGPT专业应用:生成立论稿

正文共 1107 字&#xff0c;阅读大约需要 7 分钟 辩论赛选手必备技巧&#xff0c;您将在7分钟后获得以下超能力&#xff1a; 生成立论稿 Beezy评级 &#xff1a;A级 *经过寻找和一段时间的学习&#xff0c;一部分人能掌握。主要提升效率并增强自身技能。 推荐人 | Kim 编辑者…

马斯克将起诉微软,称其使用 Twitter 数据“非法”训练GPT,吃瓜网友:事情变得更有趣了!...

点击上方“AI遇见机器学习”&#xff0c;选择“星标”公众号 第一时间获取价值内容 整理 &#xff5c; Tina 北京时间 4 月 20 日早间消息&#xff0c;埃隆马斯克表示将对微软提起“诉讼”&#xff0c;理由是微软使用 Twitter 数据“非法”训练其产品。 今天早些时候微软宣布…

Imported target “dart“ includes non-existent path 笔记

详细问题&#xff1a; 关联功能包&#xff0c;源码编译 Failed <<< gazebo_ros2_control [11.8s, exited with code 1] 网上查了很多资料&#xff0c;都是比较旧版&#xff0c;这里注意由于ROS2系统发展很快&#xff0c;功能包在快速集成。 使用源码编译可能会遇到环境…

Spring Boot 3.1.0 发布,添加大量新功能和改进

来源&#xff1a;JAVA架构日记 Spring Boot 3.1.0 现已发布&#xff0c;此版本添加了大量新功能和改进。 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.0</v…

拒绝白嫖!马斯克将起诉微软,称其使用 Twitter 数据“非法”训练GPT

来源&#xff1a;InfoQ、整理&#xff1a;Tina 北京时间 4 月 20 日早间消息&#xff0c;埃隆马斯克表示将对微软提起“诉讼”&#xff0c;理由是微软使用 Twitter 数据“非法”训练其产品。 今天早些时候微软宣布将停止在其智能广告平台上支持 Twitter&#xff0c;马斯克此举…

【GPT4技术揭秘】GPT-4 Architecture,Infrastructure,Training Dataset,Costs,Vision,MoE

本文是对《GPT-4 Architecture,Infrastructure,Training Dataset, Costs, Vision, MoE 》的中文翻译。介绍了GPT-4使用的相关技术&#xff0c;希望对大家有一些帮助。群友分享了总结内容如下&#xff1a; 13T tokens预训练语料 &#xff08;llama和palm是1.4T&#xff09;MoE&…

3 分钟为英语学习神器 Anki 部署一个专属同步服务器

原文链接&#xff1a;https://icloudnative.io/posts/anki-sync-server/ Anki 介绍 Anki 是一个辅助记忆软件&#xff0c;其本质是一个卡片排序工具--即依据使用者对卡片上的自定义内容进行主动测试、自我评判后&#xff0c;其内部算法根据评判结果更改每张卡片下次测试时间的…

chatgpt赋能python:Python中的{:>8}

Python中的 {:>8} 介绍 在Python中&#xff0c;{:>8}是一个字符串格式化的方法&#xff0c;它的作用是将字符串向右对齐&#xff0c;并在字符串左侧填充指定字符。这个方法非常有用&#xff0c;尤其是在需要对齐输出结果或者生成格式化的文本时。 这个方法在Python中非…

Heartbeat安装

安装Heartbeat前的准备 Heartbeat集群必须的硬件 从下图看出&#xff0c;构建一个Heartbeat集群系统必须的硬件设备有&#xff1a; 节点服务器&#xff1b; 网络和网卡&#xff1b; 共享磁盘。 节点服务器 安装Heartbeat至少需要两台主机&#xff0c;并且对主机的要求不高&…

ansible企业级实战(Markdown)

ansible经典实战 一、配置文件解释二、实战2.1 脚本分发 && 使用copy生成新文件2.2 列出所有被管理主机list2.3 基于key验证来避免每次输入密码2.3.1 机器密码不一样的解决方案 2.4 fetch 抓取被控制端的文件2.4.1 fetch 如何抓取多个文件&#xff1f; 2.5 file 模块设…

【Python】词云之 wordcloud库 全解析

有用的话&#xff0c;欢迎姗莲✨✨✨✨✨✨✨✨✨✨✨✨✨ 目录 一基础用法二、WordCloud类 形参说明2.1 常用参数2.11 字体 font_path2.12 画布尺寸 width、hight2.13 比例&#xff08;缩放&#xff09;scale2.14 颜色(表) colormap2.15 颜色函数 color_func2.16 词语组合频率…

【云炬COMSOL模型】自己做的激光超声的COSMOL模型

模拟结果 表面温度 等值线 表面应力 模型设置 全局定义 几何 材料 固体传热物理场 固体力学物理场 多物理场 网格 研究&计算日志 2e-06 - out- 1.43e-06 - out- 1.44e-06 - out- 1.45e-06 - out- 1.46e-06 …

uniapp生成商品分享海报

uniapp用canvas生成一个分享商品的海报 文章目录 前言一、展示效果二、使用步骤1.HTML部分2.CSS部分3.JS部分 总结 前言 uniapp用canvas生成一个分享商品的海报&#xff0c;因为用到了uni.downloadFile这个API&#xff0c;所以要注意图片在H5端跨域的问题。 以下是本篇文章正…

postman错误提示“Current request is not a multipart request”

今天在写业务进行批量上传文件时&#xff0c;用postman测试发现报500错误----Current request is not a multipart request&#xff0c;翻译了一下大概意思是 当前请求不是多部分请求&#xff0c;所以就很纳闷。今天特意记录一下 首先我们上传文件时&#xff0c;在postman里面…

如何为豆瓣FM写一个chrome的歌词插件

对于喜欢豆瓣FM的同学来说&#xff0c;没有歌词是件令人苦恼的事&#xff0c;下面我就来总结下怎样为豆瓣FM写一个chrome的歌词插件。 --------------------------------- 1.需要的技能 首先&#xff0c;你要会javascript&#xff0c;其次你要掌握一点chrome的hack&#xff…