关于响应式编程ReactiveX,RxGo

ReactiveX,简称为 Rx,是一个异步编程的 API。与 callback(回调)、promise(JS 提供这种方式)和 deferred(Python 的 twisted 网络编程库就是使用这种方式)这些异步编程方式有所不同,Rx 是基于事件流的。这里的事件可以是系统中产生或变化的任何东西,在代码中我们一般用对象表示。在 Rx 中,事件流被称为 Observable(可观察的,被观察者)。事件流需要被 Observer(观察者)处理才有意义。

ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。它是一套API,针对不同的编程语言会有不同的实现,比如 RxJS, RxJava, RxGo

ReactiveX官网:https://reactivex.io/

ReactiveX仓库:https://github.com/ReactiveX

RxGo 是 Rx 的 Go 语言实现。借助于 Go 语言简洁的语法和强大的并发支持(goroutine、channel),Rx 与 Go 语言的结合非常完美。

pipelines (官方博客:https://blog.golang.org/pipelines)是 Go 基础的并发编程模型。其中包含,fan-in——多个 goroutine 产生数据,一个goroutine 处理数据,fan-out——一个 goroutine 产生数据,多个 goroutine 处理数据,fan-inout——多个 goroutine 产生数据,多个 goroutine 处理数据。它们都是通过 channel 连接。RxGo 的实现就是基于 pipelines 的理念,并且提供了方便易用的包装和强大的扩展。

通常来说,Go写异步程序很容易,完全可以自己封装实现,而RxGo的封装更加标准化,遵循了 ReactiveX 规范,易于理解。

在这里插入图片描述

RxGo: https://github.com/ReactiveX/RxGo

go get -u github.com/reactivex/rxgo/v2

简单使用
func t1() {observable := rxgo.Just(1, 2, 3, 4, 5)()ch := observable.Observe()for item := range ch {fmt.Println(item.V)}
}

使用 RxGo 的一般流程如下:

  • 使用相关的 Operator 创建 ObservableOperator 就是用来创建 Observable 的。这些术语都比较难贴切地翻译,而且英文也很好懂,就不强行翻译了;
  • 中间各个阶段可以使用过滤操作筛选出我们想要的数据,使用转换操作对数据进行转换;
  • 调用 ObservableObserve()方法,该方法返回一个<- chan rxgo.Item。然后for range遍历即可。

实际上rxgo.Item还可以包含错误。所以在使用时,我们应该做一层判断

func t2() {observable := rxgo.Just(1, 2, errors.New("unknown"), 4, 5)()ch := observable.Observe()for item := range ch {if item.Error() {fmt.Println("Error:", item.E)} else {fmt.Println(item.V)}}
}

除了使用for range之外,我们还可以调用 ObservableForEach()方法来实现遍历。ForEach()接受 3 个回调函数:

  • NextFunc:类型为func (v interface {}),处理数据;
  • ErrFunc:类型为func (err error),处理错误;
  • CompletedFunc:类型为func ()Observable 完成时调用。
func t3() {observable := rxgo.Just(1, 2, 3, 4, 5)()<-observable.ForEach(func(v interface{}) {fmt.Println("onNext:", v)}, func(err error) {fmt.Println("onError:", err)}, func() {fmt.Println("onComplete")})
}
onNext: 1
onNext: 2
onNext: 3
onNext: 4
onNext: 5
onComplete

ForEach()实际上是在 goroutine 里执行的,它返回一个接收通知的 channel。当 Observable 数据发送完毕时,该 channel 会关闭。所以如果要等待ForEach()执行完成,我们需要使用<-。上面的示例中如果去掉<-,可能就没有输出了,因为主 goroutine 结束了,整个程序就退出了。

创建 Observable

上面使用最简单的方式创建 Observable:直接调用Just()方法传入一系列数据。下面再介绍几种创建 Observable 的方式。

Create

传入一个[]rxgo.Producer的切片,其中rxgo.Producer的类型为func(ctx context.Context, next chan<- Item)。我们可以在代码中调用rxgo.Of(value)生成数据,rxgo.Error(err)生成错误,然后发送到next通道中:

func t4() {observable := rxgo.Create([]rxgo.Producer{func(ctx context.Context, next chan<- rxgo.Item) {next <- rxgo.Of(1)},func(ctx context.Context, next chan<- rxgo.Item) {next <- rxgo.Of(2)next <- rxgo.Error(errors.New("unknown"))next <- rxgo.Of(4)next <- rxgo.Of(5)},})<-observable.ForEach(func(v interface{}) {fmt.Println("onNext:", v)}, func(err error) {fmt.Println("onError:", err)}, func() {fmt.Println("onComplete")})
}
FromChannel

直接从一个已存在的<-chan rxgo.Item对象中创建 Observable

func t5() {ch := make(chan rxgo.Item) // no buffergo func() {for i := 1; i <= 5; i++ {ch <- rxgo.Of(i)}close(ch)}()observable := rxgo.FromChannel(ch)<-observable.ForEach(func(v interface{}) {fmt.Println("onNext:", v)}, func(err error) {fmt.Println("onError:", err)}, func() {fmt.Println("onComplete")})
}

注意:通道需要手动调用close()关闭,上面Create()方法内部rxgo自动帮我们执行了这个步骤。

Interval

以传入的时间间隔生成一个无穷的数字序列,从 0 开始

func t6() {observable := rxgo.Interval(rxgo.WithDuration(3 * time.Second))for item := range observable.Observe() {fmt.Println(item.V)}
}
Range

生成一个范围内的数字,达到最大值就结束,不包含右值

func t7() {observable := rxgo.Range(0, 5)for item := range observable.Observe() {fmt.Println(item.V)}
}
Repeat

每隔指定时间,重复一次该序列,一共重复指定次数:

func t8() {observable := rxgo.Just(1, 2, 3)()// 每隔指定时间,重复一次该序列,一共重复指定次数observable = observable.Repeat(5, rxgo.WithDuration(5*time.Second))for item := range observable.Observe() {fmt.Println(item.V)}
}
Start

可以给Start方法传入[]rxgo.Supplier作为参数,它可以包含任意数量的rxgo.Supplier类型。rxgo.Supplier的底层类型为 func(ctx context.Context) Item

func t9() {observable := rxgo.Start([]rxgo.Supplier{func(ctx context.Context) rxgo.Item {return rxgo.Of(1)},func(ctx context.Context) rxgo.Item {return rxgo.Of(2)},func(ctx context.Context) rxgo.Item {return rxgo.Of(3)},})for item := range observable.Observe() {fmt.Println(item.V)}
}
Observable 分类

根据数据在何处生成,Observable 被分为 HotCold 两种类型(类比热启动和冷启动)。数据在其它地方生成的被成为 Hot Observable。相反,在 Observable 内部生成数据的就是 Cold Observable

使用上面介绍的方法创建的实际上都是 Hot Observable

ch := make(chan rxgo.Item)
go func() {for i := 0; i < 3; i++ {ch <- rxgo.Of(i)}close(ch)
}()observable := rxgo.FromChannel(ch)for item := range observable.Observe() {fmt.Println(item.V)
}for item := range observable.Observe() {fmt.Println(item.V)
}

上面创建的是 Hot Observable。但是有个问题,第一次Observe()消耗了所有的数据,第二个就没有数据输出了。

Cold Observable 就不会有这个问题,因为它创建的流是独立于每个观察者的。即每次调用Observe()都创建一个新的 channel。我们使用Defer()方法创建 Cold Observable,它的参数与Create()方法一样。

Defer
func t10() {// Defer does not create the Observable until the observer subscribes,// and creates a fresh Observable for each observer.//// Cold Observable: 也就是在 subscribe 的时候才去生产数据流;// 与之相反的是 Hot Observable,也就是在创建 Observable 的时候就同时创建了数据流,// 这样第一次 subscribe 的时候就把数据消耗完了,再次 subscribe 是没有数据的,前面的// 例子都是 Hot Observable.observable := rxgo.Defer([]rxgo.Producer{func(_ context.Context, ch chan<- rxgo.Item) {for i := 0; i < 3; i++ {ch <- rxgo.Of(i)}}})// 有数据for item := range observable.Observe() {fmt.Println(item.V)}// 有数据for item := range observable.Observe() {fmt.Println(item.V)}
}
0
1
2
0
1
2
可连接的 Observable

可连接的(Connectable)Observable 对普通的 Observable 进行了一层组装。调用它的Observe()方法时并不会立刻产生数据。使用它,我们可以等所有的观察者都准备就绪了(即调用了Observe()方法)之后,再调用其Connect()方法开始生成数据。我们通过两个示例比较使用普通的 Observable 和可连接的 Observable 有何不同。

普通的:

func t11() {ch := make(chan rxgo.Item)go func() {for i := 1; i <= 3; i++ {ch <- rxgo.Of(i)}close(ch)}()// 普通的 Observable,只要有一个观察者注册成功就会释放数据observable := rxgo.FromChannel(ch)// 注册观察者1observable.DoOnNext(func(i interface{}) {fmt.Printf("First observer: %d\n", i)})time.Sleep(3 * time.Second)fmt.Println("before subscribe second observer")// 注册观察者2observable.DoOnNext(func(i interface{}) {fmt.Printf("Second observer: %d\n", i)})time.Sleep(3 * time.Second)
}

上例中我们使用DoOnNext()方法来注册观察者。由于DoOnNext()方法是异步执行的,所以为了等待结果输出,在最后增加了一行time.Sleep。运行:

First observer: 1
First observer: 2
First observer: 3
before subscribe second observer

由输出可以看出,注册第一个观察者之后就开始产生数据了。

我们通过在创建 Observable 的方法中指定rxgo.WithPublishStrategy()选项就可以创建可连接的 Observable

func t12() {ch := make(chan rxgo.Item)go func() {for i := 1; i <= 3; i++ {ch <- rxgo.Of(i)}close(ch)}()// 可连接的 Observableobservable := rxgo.FromChannel(ch, rxgo.WithPublishStrategy())// 注册观察者1observable.DoOnNext(func(i interface{}) {fmt.Printf("First observer: %d\n", i)})time.Sleep(3 * time.Second)fmt.Println("before subscribe second observer")// 注册观察者2observable.DoOnNext(func(i interface{}) {fmt.Printf("Second observer: %d\n", i)})// 通知 Observable,意味着所有的观察者已经注册完毕,才开始释放数据// 另外,可连接的 Observable 是 Cold Observable,即每个观察者都会收到一份相同的拷贝。observable.Connect(context.Background())time.Sleep(5 * time.Second)fmt.Println("over.")
}
before subscribe second observer
Second observer: 1
First observer: 1
First observer: 2
First observer: 3
Second observer: 2
Second observer: 3
over.

上面是等两个观察者都注册之后,并且手动调用了 Observable 的Connect()方法才产生数据。而且可连接的 Observable 有一个特性:它是 Cold Observable !!!,即每个观察者都会收到一份相同的拷贝。

其他常用操作符
Map

转换操作符,Map()方法简单修改它收到的rxgo.Item然后发送到下一个阶段(转换或过滤)。Map()接受一个类型为func (context.Context, interface{}) (interface{}, error)的函数。第二个参数就是rxgo.Item中的数据,返回转换后的数据。如果出错,则返回错误。

func t13() {observable := rxgo.Just(1, 2, 3)()// Map 转换或者过滤// 如果出现一个 error,整个数据流都是无效的observable = observable.Map(func(ctx context.Context, v interface{}) (interface{}, error) 	  {// vv := v.(int)// if vv%2 == 0 {// 	return vv * 2, nil// } else {// 	return vv, errors.New("Error")// }return v.(int) * 2, nil}).Map(func(ctx context.Context, v interface{}) (interface{}, error) {return v.(int) + 1, nil})for item := range observable.Observe() {fmt.Println(item.V)}
}
Marshal

Marshal对经过它的数据进行一次Marshal。这个Marshal可以是json.Marshal/proto.Marshal,甚至我们自己写的Marshal函数。它接受一个类型为func(interface{}) ([]byte, error)的函数用于对数据进行处理。

type User struct {Name string `json:"name"`Age  int    `json:"age"`
}func t14() {observable := rxgo.Just(User{Name: "dj",Age:  18,},User{Name: "jw",Age:  20,},)()observable = observable.Marshal(json.Marshal)for item := range observable.Observe() {fmt.Println(string(item.V.([]byte)))}
}
Unmarshal
func t15() {observable := rxgo.Just(`{"name":"dj","age":18}`,`{"name":"jw","age":20}`,)()observable = observable.Map(func(_ context.Context, i interface{}) (interface{}, error) {return []byte(i.(string)), nil}).Unmarshal(json.Unmarshal, func() interface{} {return &User{}})for item := range observable.Observe() {fmt.Println(item.V)}
}
Buffer

Buffer按照一定的规则收集接收到的数据,然后一次性发送出去(作为切片),而不是收到一个发送一个。有 3 种类型的Buffer

  • BufferWithCount(n):每收到n个数据发送一次,最后一次可能少于n个;
  • BufferWithTime(n):发送在一个时间间隔n内收到的数据;
  • BufferWithTimeOrCount(d, n):收到n个数据,或经过d时间间隔,发送当前收到的数据。
func t16() {observable := rxgo.Just(1, 2, 3, 4)().BufferWithCount(3)for item := range observable.Observe() {fmt.Println(item.V) // item.V 此时是切片类型}
}
[1 2 3]
[4]
GroupBy

GroupBy根据传入一个 Hash 函数,为每个不同的结果分别创建新的 Observable。换句话说,GroupBy生成一个数据类型为 ObservableObservable

func t17() {count := 3observable := rxgo.Range(0, 10).GroupBy(count, func(item rxgo.Item) int {return item.V.(int) % count}, rxgo.WithBufferedChannel(10))for subObservable := range observable.Observe() {fmt.Println("New observable:")for item := range subObservable.V.(rxgo.Observable).Observe() {fmt.Printf("item: %v\n", item.V)}}
}
New observable:
item: 0
item: 3
item: 6
item: 9
New observable:
item: 1
item: 4
item: 7
New observable:
item: 2
item: 5
item: 8

注意rxgo.WithBufferedChannel(10)的使用,由于我们的数字是连续生成的,依次为 0->1->2->…->9->10。而 Observable 默认是惰性的,即由Observe()驱动。内层的Observe()在返回一个 0 之后就等待下一个数,但是下一个数 1 不在此 Observable 中。所以会陷入死锁。使用rxgo.WithBufferedChannel(10),设置它们之间的连接 channel 缓冲区大小为 10,这样即使我们未取出 channel 里面的数字,上游还是能发送数字进来。

并行操作

默认情况下,这些转换操作都是串行的,即只有一个 goroutine 负责执行转换函数。我们也可以使用rxgo.WithPool(n)选项设置运行n个 goroutine,或者rxgo.WitCPUPool()选项设置运行与逻辑 CPU 数量相等的 goroutine。

func t18() {observable := rxgo.Range(1, 20)observable = observable.Map(func(_ context.Context, i interface{}) (interface{}, error) {time.Sleep(time.Duration(rand.Int31()))return i.(int)*2 + 1, nil}, rxgo.WithCPUPool())for item := range observable.Observe() {fmt.Println(item.V)}
}

由于是并行,所以输出顺序就不确定了。为了让不确定性更明显一点,我在代码中加了一行time.Sleep

Filter

Filter()接受一个类型为func (i interface{}) bool的参数,通过的数据使用这个函数断言,返回true的将发送给下一个阶段。否则,丢弃。

func t19() {observable := rxgo.Range(1, 10)observable = observable.Filter(func(i interface{}) bool {return i.(int)%2 == 0})for item := range observable.Observe() {fmt.Println(item.V)}
}
ElementAt

ElementAt()只发送指定索引的数据,如ElementAt(2)只发送索引为 2 的数据,即第 3 个数据。

func t20() {observable := rxgo.Just(0, 1, 2, 3, 4)().ElementAt(2)for item := range observable.Observe() {fmt.Println(item.V)}
}
Debounce

Debounce()比较有意思,它收到数据后还会等待指定的时间间隔,后续间隔内没有收到其他数据才会发送刚开始的数据。

func t21() {ch := make(chan rxgo.Item)go func() {ch <- rxgo.Of(1)time.Sleep(2 * time.Second)ch <- rxgo.Of(2)ch <- rxgo.Of(3)time.Sleep(2 * time.Second)close(ch)}()observable := rxgo.FromChannel(ch).Debounce(rxgo.WithDuration(1 * time.Second))for item := range observable.Observe() {fmt.Println(item.V)}
}

上面示例,先收到 1,然后 2s 内没收到数据,所以发送 1。接着收到了数据 2,由于马上又收到了 3,所以 2 不会发送。收到 3 之后 2s 内没有收到数据,发送了 3。所以最后输出为 1,3。

Distinct

Distinct()会记录它发送的所有数据,它不会发送重复的数据。由于数据格式多样,Distinct()要求我们提供一个函数,根据原数据返回一个唯一标识码(有点类似哈希值)。基于这个标识码去重。

func t22() {observable := rxgo.Just(1, 2, 2, 3, 3, 4, 4)().Distinct(func(_ context.Context, i interface{}) (interface{}, error) {return i, nil})for item := range observable.Observe() {fmt.Println(item.V)}
}
Skip

跳过前面若干个数据

func t23() {observable := rxgo.Just(1, 2, 3, 4, 5)().Skip(2)for item := range observable.Observe() {fmt.Println(item.V)}
}
Take

只取前面若干个数据

func t24() {observable := rxgo.Just(1, 2, 3, 4, 5)().Take(2)for item := range observable.Observe() {fmt.Println(item.V)}
}
选项 rxgo.Option

rxgo 提供的大部分方法的最后一个参数是一个可变长的选项类型。这是 Go 中特有的、经典的选项设计模式。我们前面已经使用了:

  • rxgo.WithBufferedChannel(10):设置 channel 的缓存大小;
  • rxgo.WithPool(n) / rxgo.WithCpuPool():使用多个 goroutine 执行转换操作;
  • rxgo.WithPublishStrategy():使用发布策略,即创建可连接的 Observable

除此之外,rxgo 还提供了很多其他选项。

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

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

相关文章

C语言实现冒泡排序(超详细)

排序算法 - 冒泡排序 什么是冒泡排序&#xff1f;冒泡排序有啥用呢&#xff1f;冒泡排序的实现代码讲解冒泡排序的总结 什么是冒泡排序&#xff1f; 冒泡排序是一种简单的排序算法&#xff0c;它重复地遍历要排序的列表&#xff0c;一次比较两个元素&#xff0c;如果它们的顺序…

Springboot框架中使用 Redis + Lua 脚本进行限流功能

Springboot框架中使用 Redis Lua 脚本进行限流功能 限流是一种用于控制系统资源利用率或确保服务质量的策略。在Web应用中&#xff0c;限流通常用于控制接口请求的频率&#xff0c;防止过多的请求导致系统负载过大或者防止恶意攻击。 什么是限流&#xff1f; 限流是一种通过…

MySql操作

Mysql数据库项目学习笔记 1.条件查询后排序 (SELECT counter : 0) temp设定临时变量ORDER BY id ASC用于将id以升序形式进行排列 SELECTcounter : counter 1 AS ROW,username,type,content FROMtest_info,( SELECTcounter : 0 ) temp WHEREusername 2 AND type 3 ORDER BYi…

项目交互-选择器交互

选择器交互 <div><el-select v-model"valueOne" placeholder"年级"><el-option v-for"item in optionsOne" :key"item.gradeId" :label"item.gradeName" :value"item.gradeId"></el-option&…

UnitTest + Selenium 完成在线加法器自动化测试

1. 任务概述 利用 UnitTest 与 Selenium 编写自动化用例&#xff0c;测试在线加法器中的整数单次加法功能【如123 】 人工操作流程&#xff08;测试 12 是否等于 3&#xff09;&#xff1a; 打开在线加法器点击按钮1&#xff0c;再点击按钮&#xff0c;再点击按钮2&#xff0c…

接口测试 —— 接口测试的意义

1、接口测试的意义&#xff08;优势&#xff09; &#xff08;1&#xff09;更早的发现问题&#xff1a; 不少的测试资料中强调&#xff0c;测试应该更早的介入到项目开发中&#xff0c;因为越早的发现bug&#xff0c;修复的成本越低。 然而功能测试必须要等到系统提供可测试…

scss的高级用法——循环

周末愉快呀&#xff01;一起来学一点简单但非常有用的css小知识。 最近在一个项目中看到以下css class写法&#xff1a; 了解过tailwind css或者unocss的都知道&#xff0c;从命名就可以看出有以下样式&#xff1a; font-size: 30pxmargin-left: 5px;margin-top: 10px; 于是…

SASS/SCSS精华干货教程

目录 介绍 基本说明 特点 sass语法格式sass的语法格式一共有两种&#xff0c;一种是以".scss"作为拓展名&#xff0c;一种是以".sass"作为拓展名&#xff0c;这里我们只讲拓展名&#xff1a; 编译环境安装 Vscode安装编译插件 简单使用 sass语法扩张…

西南科技大学814考研一

C语言基础 字节大小 char&#xff1a;1 字节 unsigned char&#xff1a;1 字节 short&#xff1a;2 字节 unsigned short&#xff1a;2 字节 int&#xff1a;通常为 4 字节&#xff08;32 位平台&#xff09;或 8 字节&#xff08;64 位平台&#xff09; unsigned int&#x…

Sublime Text:代码编辑器的卓越典范

Sublime Text是一款高效、强大且灵活的代码编辑器&#xff0c;在开发社区中广受欢迎。它不仅提供了丰富的功能&#xff0c;还具备美观的界面和卓越的性能&#xff0c;成为了众多开发者的首选工具。 Sublime Text的优点 高性能&#xff1a;Sublime Text具有极高的启动速度和响…

数智赋能,众创众治|易知微为“浙江省数字监管应用建模技能竞赛”提供技术支撑!

11月6日至8日&#xff0c;2023年浙江省数字监管应用建模技能竞赛在省金华监狱举行。浙江省监狱管理局党委书记、局长王争&#xff0c;司法部监狱管理局规划科技处处长常家瑛&#xff0c;浙江省监狱管理局党委委员、副局长朱永忠出席本次活动。 本次建模大赛共有来自全省监狱系…

Genio 500_MT8385安卓核心板:功能强大且高效

Genio 500(MT8385)安卓核心板是一款功能强大且高效的AIoT平台&#xff0c;内置的AI处理器(APU)工作频率可达500MHz&#xff0c;支持深度学习、神经网络加速和计算机视觉应用。配合高达2500万像素的摄像头&#xff0c;可以为AI相机应用提供清晰、精确的图像&#xff0c;如人脸识…

有哪些相见恨晚的stm32学习的方法?

有哪些相见恨晚的stm32学习的方法&#xff1f; 单片机用处这么广&#xff0c;尤其是STM32生态这么火&#xff01;如何快速上手学习呢&#xff1f; 你要考虑的是&#xff0c;要用STM32实现什么&#xff1f;为什么使用STM32而不是用8051&#xff1f;是因为51的频率太低&#xff…

NC Cloud uploadChunk文件上传漏洞复现

简介 NC Cloud是指用友公司推出的大型企业数字化平台。支持公有云、混合云、专属云的灵活部署模式。该产品uploadChunk文件存在任意文件上传漏洞。 漏洞复现 FOFA语法&#xff1a; app"用友-NC-Cloud" 访问页面如下所示&#xff1a; POC&#xff1a;/ncchr/pm/fb/…

2023年A特种设备相关管理(锅炉压力容器压力管道)证模拟考试题库及A特种设备相关管理(锅炉压力容器压力管道)理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年A特种设备相关管理&#xff08;锅炉压力容器压力管道&#xff09;证模拟考试题库及A特种设备相关管理&#xff08;锅炉压力容器压力管道&#xff09;理论考试试题是由安全生产模拟考试一点通提供&#xff0c;A特…

STM32H743 RTC精密数字校准 深度剖析

一、问题 项目中数据报文收到的RTC时间总是会慢一些,经过实际几天的测试得出结论:24小时要慢5S左右。根据手册我了解到可以有误差但不会差这么多,所以进行了如下分析并解决问题。 二、分析 1.影响RTC准确性的因素罗列 硬件基础误差(也就是待校准部分) …

亚马逊,shopee,lazada自养号测评:提高店铺曝光,增加产品销量

如何在较短的时间内让自己的店铺排名升高&#xff0c;提高产品销量&#xff0c;除了依靠选品和广告之外&#xff0c;亚马逊测评 在店铺的运营中也是必不可少的环节。 自养号测评对亚马逊卖家来说&#xff0c;是运营店铺的重要手段之一。一个产品想要有更好的曝光、更高的转化率…

一个开源的汽修rbac后台管理系统项目,基于若依框架,实现了activiti工作流,附源码

文章目录 前言&源码项目参考图&#xff1a; e店邦O2O平台项目总结一、springboot1.1、springboot自动配置原理1.2、springboot优缺点1.3、springboot注解 二、rbac2.1、概括2.2、三个元素的理解 三、数据字典3.1、概括与作用3.2、怎么设计3.3、若依中使用字典 四、工作流—…

剪辑视频怎么把说话声音转成文字?

短视频已然成为了一种生活潮流&#xff0c;我们每天都在浏览各种短视频&#xff0c;或者用视频的形式记录生活&#xff0c;在制作视频的时候&#xff0c;字幕是一个很大的问题&#xff0c;给视频添加字幕可以更直观、更方便浏览。手动添加太费时间&#xff0c;下面就给大家分享…

使用VSCode调试全志R128的C906 RISC-V核心

使用 VSCode 调试 调试 XuanTie C906 核心 准备工具 T-Head DebugServer&#xff08;CSkyDebugServer&#xff09; - 搭建调试服务器 下载地址&#xff1a;T-Head DebugServer手册&#xff1a;T-Head Debugger Server User Guide驱动&#xff1a;cklink_dirvers VSCode - 开…