RxSwift遷移Combine指南

譯自 RxSwift to Combine: The Complete Transition Guide

不逐字翻譯了,只翻主要的知識(shí)點(diǎn)信息。

簡(jiǎn)介

Combine 是swift新推出的一種面向 響應(yīng)式編程的框架。
很多開發(fā)者都想從RxSwift 切換到Combine,兩者之間有很多的相似之處。
但細(xì)節(jié)方面還是有很多區(qū)別的,如果你想切換過來,這篇文章會(huì)列出 RxSwift和Combine之間的 操作符,函數(shù),類型等等的映射。

響應(yīng)式編程可以讓 數(shù)據(jù)狀態(tài)在對(duì)象之間,工程之間,甚至app之間互相同步。
在ios開發(fā)中,最常見的就是在UI和Model之間進(jìn)行狀態(tài)同步。在之前很長(zhǎng)一段時(shí)間里,RxSwift都是最好的選擇。但apple推出了 跟SwiftUI 更契合的Combine 作為響應(yīng)式編程的框架,基本可以完全替代RxSwift的所有功能了。

此文章將分為 Combine是怎么工作的 切換到Combine是好的選擇嗎 怎么更簡(jiǎn)單地從RxSwift切換到Combine 三個(gè)部分。

RxSwift 和 Combine的主要區(qū)別

RxSwift Combine
支持的iOS版本 iOS 8.0+ iOS 13.0+
支持的平臺(tái) iOS, macOS, tvOS, watchOS, Linux iOS, macOS, tvOS, watchOS, UIKit for Mac
框架所屬 第三方 Apple第一方,SDK內(nèi)置
誰來維護(hù)? 開源社區(qū) Apple技術(shù)團(tuán)隊(duì)
協(xié)作的UI框架 RxCocoa SwiftUI

Combine是怎么工作的

考慮到本文章是介紹如何從RxSwift遷移到Combine, 假設(shè)讀者已經(jīng)對(duì)RxSwift有一定的了解了。 如果需要了解RxSwift,可以參考 RxSwift repository on GitHub.

Combine在很多方面跟RxSwift都有相似之處以及對(duì)等的映射概念映射,比如 方法,類型聲明等。
如果要找出兩者之間更深層次的差異,需要對(duì)Combine進(jìn)行更深地挖掘,看看它的底層機(jī)制。

Publisher

跟RxSwift的Observable 相對(duì)應(yīng)的是Combine里的Publisher。
在RxSwift里是一個(gè)類,而在Combine里是一個(gè)協(xié)議。

protocol Publisher {
    associatedtype Output
    associatedtype Failure: Error
    func receive<S: Subscriber>(subscriber: S) 
        where Self.Failure == S.Failure, 
              Self.Output == S.Input
}

Combine 不會(huì)指定 Wrapper types來描述它的唯一特征,比如RxSwift里的Infallible, Maybe or Single。
但每個(gè)Publisher也有自己的自定義類型,借由該類型,Publisher的特征也能被推導(dǎo)出來。

RxSwift的Observable 需要實(shí)現(xiàn)subscribe 函數(shù),對(duì)應(yīng)的Publisher需要實(shí)現(xiàn) receive函數(shù),它們的功能基本一致。相比于RxSwift而言,Publisher指定了一個(gè) Failure類型,可以表明該publisher 是否/如何 失敗。對(duì)于那些不用處理失敗的Publisher,我們?cè)O(shè)定Failure類型為Never 即可。

Combine的Publisher 可以是 值類型(比如struct)或是 引用類型(比如類)。大多數(shù)的Publisher都是值類型的。

在RxSwift里,操作符一般就是簡(jiǎn)單地返回一個(gè)Observable 類型,而Combine里返回的是一個(gè)混合類型,比較復(fù)雜;比如Timer.TimerPublisherCombineLatest<Just<Int>, Just<Int>>. 鑒于自己定義一個(gè)混合類型比較麻煩,我們可以就用 AnyPublisher 來簡(jiǎn)化Publisher的使用。
舉個(gè)例子:AnyPublisher<Int, Never>,可以作為一個(gè) 不會(huì)失敗的拋出Int值的Publisher。

Image

Subscriber

Subscriber (RxSwift: Observer)可以接收訂閱信息(subscriptions), 輸入數(shù)據(jù)(inputs),以及結(jié)束回調(diào)(completions). 這點(diǎn)跟RxSwift不同,沒有聲明包含了數(shù)據(jù)回調(diào)以及 結(jié)束回調(diào)的event的枚舉,但提供了單獨(dú)的方法來分別處理對(duì)應(yīng)的事件。

protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error

    func receive(subscription: Subscription)
    func receive(_ input: Self.Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Self.Failure>)
}

在上面的代碼片段里,你可以看到Subscriber 也聲明了一個(gè)Failure 類型,決定了它能失敗,以及失敗的相關(guān)類型。對(duì)應(yīng)的RxSwift也聲明了獨(dú)立的Error事件。Combine非常明顯的表明了 失敗會(huì)導(dǎo)致Publisher 終結(jié)(RxSwift里,error事件也表明了 Observable的終結(jié))。

Subscribers 必須是類,因?yàn)?subscription要引用它來做數(shù)據(jù)傳遞,而不能在訂閱期間重新創(chuàng)建多次Subscribers(值類型比如struct 會(huì)在數(shù)據(jù)變化時(shí)會(huì)重新創(chuàng)建實(shí)例)。

Subscription

每當(dāng)你訂閱時(shí),就會(huì)創(chuàng)建一個(gè) Subscription,它會(huì)持有所有必要的資源的引用,而且可以隨時(shí)cancel(對(duì)應(yīng)RxSwift里的DisposeBag)。這也是為什么Subscription 必須是類。

protocol Subscription: Cancellable, ... {
    func request(_ demand: Subscribers.Demand)
}

protocol Cancellable {
    func cancel()
}

與RxSwift相比,Combine允許訂閱者隨時(shí)可以去獲取更多數(shù)據(jù),并以 backpressure support 的形式返回;因此 實(shí)際上是訂閱者在主動(dòng)請(qǐng)求數(shù)據(jù)而不是 Publisher來決定什么時(shí)候?qū)?shù)據(jù)傳出去。
這能讓訂閱者不會(huì)被大量數(shù)據(jù)堵塞而導(dǎo)致無法及時(shí)處理,這看上去更像是可控制的數(shù)據(jù)流。

如果想更深入地了解 backpressure support ,可以參考 Processing Published Elements with Subscribers

Subject

Subject 跟RxSwift的差不多,可以看作是 Subscriber 和 Publisher 的組合,既可以發(fā)送數(shù)據(jù)也能接收數(shù)據(jù)。

protocol Subject: AnyObject, Publisher {
    func send(_ value: Self.Output)
    func send(completion: Subscribers.Completion<Self.Failure>)
    func send(subscription: Subscription)
}

一方面你可以將數(shù)據(jù)傳給 Subject,另一方面由于 Subject 遵從了 Publisher協(xié)議,它也可以被監(jiān)聽來獲取數(shù)據(jù)。

在Combine里,訂閱者 receive 而 Subject send 事件。盡管如此,在每個(gè)subject里,每當(dāng)value 遵從 Subscriber協(xié)議時(shí),你都必須創(chuàng)建一個(gè) AnySubscriber。

綜上所述,RxSwift和Combine 大部分功能都是一樣的,但在接口方面有一些很小的區(qū)別。 那么哪個(gè)更適合我們呢?

切換到Combine是好的選擇嗎

最大的爭(zhēng)議點(diǎn)在于,Combine是apple的第一方框架,使用它你就不用做一些多余的framework 依賴配置工作,app也體積也會(huì)小一些。

當(dāng)然,RxSwift也沒有明確表明是否會(huì)有錯(cuò)誤發(fā)生。一般情況下,直接就用 Observable 然后等著數(shù)據(jù)過來就行了。 但在Combine里,就明確表明了Publisher 可能會(huì)失敗,而且失敗后產(chǎn)生的錯(cuò)誤是啥。

但這些可失敗和不會(huì)失敗的publisher的交互可能會(huì)非常復(fù)雜,你得在每個(gè)使用處都加上 eraseToAnyPublisher(),但幫助也不大。

backpressure support 的幫助下,Combine 能考慮得更加周到。訂閱者可以隨時(shí)獲取數(shù)據(jù),而不用等待Publisher 來發(fā)送數(shù)據(jù)。這更符合我們的直覺,特別是在一些很耗時(shí)的操作發(fā)生時(shí),大量的數(shù)據(jù)同時(shí)在publisher 進(jìn)出。

簡(jiǎn)單來說,Combine 更適應(yīng)整個(gè)swift的環(huán)境。很多擴(kuò)展方法已經(jīng)在常用的類型里實(shí)現(xiàn)了(比如 Timer.publisher, (0...3).publisher, Optional<String>.none.publisher),這些方法讓Combine的功能更加的全面。

不過Combine不像RxSwift那樣全面和強(qiáng)大,是么?

RxSwift 沒有像Combine 那么多的編譯問題。RxSwift不用管 失敗的錯(cuò)誤類型,而且單個(gè)泛型類型的檢查比兩個(gè)要簡(jiǎn)單得多。

RxSwift支持iOS9以及以上版本,但Combine只能用在iOS13 以及以上版本。它倆在所有的apple 平臺(tái)上都能work,但Combine對(duì)于linux的支持還是有缺失。OpenCombine 可以幫助解決這個(gè)問題,它用的是跟Combine一樣的接口,且能支持更多平臺(tái)。

Combine的命名規(guī)則更契合iOS平臺(tái),而RxSwift 更契合跨平臺(tái)模式的規(guī)則。

RxSwift的技術(shù)社區(qū)已經(jīng)存在很長(zhǎng)一段時(shí)間了,在維護(hù)和擴(kuò)展框架方面已經(jīng)做了很多改進(jìn)了。社區(qū)成員們?yōu)镽xSwift 擴(kuò)展了很多關(guān)于特定場(chǎng)景的功能,包括很多自定義的操作符和類型等。而Combine是一個(gè)封閉的框架,而且只推出了三年而已,只在少部分的項(xiàng)目里被使用到,如果你需要自己寫一些字定義的操作符,還不如直接使用現(xiàn)有的知識(shí)點(diǎn)來實(shí)現(xiàn)。

現(xiàn)在你要寫一個(gè)字定義的Publisher比自定義的Observable要麻煩許多。在RxSwift里,你只要用 Observable.create 來發(fā)送數(shù)據(jù),發(fā)送錯(cuò)誤,以及完成事件即可。在Combine里卻沒有對(duì)應(yīng)的操作,你可以用Future (基本上就是一個(gè)帶completion回調(diào)的Publisher,且completion只會(huì)調(diào)用一次) 或是寫一個(gè)自定義的Publisher 來模擬 Observable.create 的行為(我們接下來就會(huì)講到這點(diǎn))

總結(jié)一下

回到我們之前的 該用RxSwift還是Combine的問題,這個(gè)要看情況的。
如果你想要干掉之前那些“多余的”依賴關(guān)系,而且你的app支持iOS13 以及以上版本,且不用支持iOS 以外的平臺(tái),那Combine就是比較好的選擇了。

怎么更簡(jiǎn)單地從RxSwift切換到Combine

我們搜集了下面的切換引導(dǎo)信息,你可以直接搜索RxSwift的方法,操作符來找到Combine里的對(duì)應(yīng)替換方案。

一些簡(jiǎn)單的說明

一般來說,Combine在處理錯(cuò)誤時(shí),相關(guān)接口看上去比較復(fù)雜。給個(gè)小提示, setFailureType(to: Failure.self) 更容易理解,而不是當(dāng)作 mapError { _ -> Failure in } 來看待。

下面的這些鏈接在我們從RxSwift切換到Combine時(shí)提供了很大的幫助。

  • CombineExt 里是Combine技術(shù)社區(qū)提供的一些 當(dāng)前系統(tǒng)Combine里沒有支持的操作符。
  • cheat sheet 也提供了不少幫助。

類型

Disposable

RxSwift Combine
Disposable Cancellable Use AnyCancellable to create them like RxSwift’s Disposables.create
DisposeBag Set<AnyCancellable> any RandomAccessCollection of AnyCancellable of course, you can also create references one by one

?? 注意,subscriptions 會(huì)在 Cancellable 析構(gòu)后直接cancel掉。

Publishers

RxSwift Combine
Observable<Element> AnyPublisher<Element, Error>或者 其他的 Publisher 類型
同樣根據(jù)拋出的錯(cuò)誤類型,Combine也能創(chuàng)建對(duì)應(yīng)的Failure類型
Single<Element> AnyPublisher<Element, Error>
Future<Element, Error>
可惜不能保證只拋出一個(gè)值,除非使用Future publisher
ConnectableObservable<Element> ConnectablePublisher
Infallible<Element> AnyPublisher<Element, Never>
注意:Failure在這里的類型是 Never
Maybe<Element> 在Combine里沒有對(duì)應(yīng)的東東

Subjects

RxSwift Combine
BehaviorSubject CurrentValueSubject
PublishSubject PassthroughSubject
ReplaySubject Combine里沒有
?? CombineExt里的替代方案
AsyncSubject Combine 里沒有

Relays

Relay是Subject的一個(gè)變種,他跟Subject的不同之處在于 無法發(fā)送/接收 完成事件。

class Relay<SubjectType: Subject>: Publisher,
    CustomCombineIdentifierConvertible where SubjectType.Failure == Never {

    typealias Output = SubjectType.Output
    typealias Failure = SubjectType.Failure

    let subject: SubjectType

    init(subject: SubjectType) {
        self.subject = subject
    }

    func send(_ value: Output) {
        subject
            .send(value)
    }

    func receive<S: Subscriber>(subscriber: S) 
        where Failure == S.Failure, Output == S.Input {
        subject
            .subscribe(on: DispatchQueue.main)
            .receive(subscriber: subscriber)
    }

}

typealias CurrentValueRelay<Output> = Relay<CurrentValueSubject<Output, Never>>
typealias PassthroughRelay<Output> = Relay<PassthroughSubject<Output, Never>>

extension Relay {

    convenience init<O>(_ value: O) 
        where SubjectType == CurrentValueSubject<O, Never> {
        self.init(subject: CurrentValueSubject(value))
    }

    convenience init<O>() 
        where SubjectType == PassthroughSubject<O, Never> {
        self.init(subject: PassthroughSubject())
    }

}

Observer Operators

RxSwift Combine
asObserver() AnySubscriber
on(_:) Subscriber
? receive(_:)
? receive(completion:)
Subject
? send(_:)
? send(completion:)
onNext(_:) Subscriber
? receive(_:)
Subject
? send(_:)
onError(_:) Subscriber
?receive(completion: .failure(<error>))
Subject
? send(completion: .failure(<error>))
onCompleted() Subscriber
? receive(completion: .finished)
Subject
? send(completion: .finished)
mapObserver(_:) Combine里沒有對(duì)應(yīng)的,不過實(shí)現(xiàn)起來很簡(jiǎn)單
??replacement in CombineExt

如果要修改subscriber的 InputFailure 類型,可以使用下面這個(gè)擴(kuò)展方式

extension Subscriber {

    func map<Input>(
        _ map: @escaping (Input) -> Self.Input
    ) -> AnySubscriber<Input, Failure> {
        .init(
            receiveSubscription: receive,
            receiveValue: { self.receive(map($0)) },
            receiveCompletion: receive
        )
    }

    func mapError<Failure>(
        _ map: @escaping (Failure) -> Self.Failure
    ) -> AnySubscriber<Input, Failure> {
        .init(
            receiveSubscription: receive,
            receiveValue: receive,
            receiveCompletion: { completion in
                switch completion {
                case let .failure(error):
                    self.receive(completion: .failure(map(error)))
                case .finished:
                    self.receive(completion: .finished)
                }
            }
        )
    }
}

Scheduling

RxSwift 有很多不同的Scheduler 類型,比如:MainSchedulerConcurrentDispatchQueueScheduler, SerialDispatchQueueScheduler, CurrentThreadScheduler, HistoricalScheduler, OperationQueueScheduler,以及其他更多類型。

而在Combine里,這個(gè)就很簡(jiǎn)單了??梢灾苯邮褂肎CD(Grand Central Dispatc)的相關(guān)類型就可以了。比如 DispatchQueue, OperationQueue 以及 RunLoop。
Combine里唯一的Scheduler類型只有 ImmediateScheduler,它能讓任務(wù)直接同步執(zhí)行(跟CurrentThreadScheduler 差不多)。

這篇文章里可以獲取更多關(guān)于Combine里的scheduling 的信息。

Factory Functions

RxSwift Combine
amb(...) Combine里沒有
??replacement in CombineExt
catch(sequence:) Combine里沒有
combineLatest(...) Publishers.CombineLatest
Publishers.CombineLatest3
Publishers.CombineLatest4
Publisher.combineLatest(_:)
注意:你可以把它們串起來使用,然后用map轉(zhuǎn)成特殊的數(shù)據(jù)類型來使用
concat(...) Publishers.Concatenate
Publisher.append
不支持多于兩個(gè)值,可以用reduce 來達(dá)到目的
create(_:) 單個(gè)值的Publisher可以用 Future
多個(gè)值的Publisher可以參考 replacement in CombineExt
deferred(_:) Deferred
empty() Empty
error(_:) Fail
from(_:) Collection.publisher
Optional.publisher
generate(
initialState:
condition:
iterate:
)
Combine里沒有
just(_:) Just
merge(...) Publishers.Merge
Publishers.Merge3
Publishers.Merge4
Publishers.Merge5
Publishers.Merge6
Publishers.Merge7
Publishers.Merge8
Publishers.MergeMany
never() Empty(completeImmediately: false)
of(...) Collection.publisher
range() Collection.publisher可以用Range或ClosedRange
用 stride
repeatElement(_:) Combine里沒有
timer(_:period:scheduler:) Timer.publish
+ delay
+ autoconnect
using(_:observableFactory:) Combine里沒有
zip(...) Publishers.Zip
Publishers.Zip3
Publishers.Zip4
??replacement in CombineExt

Publisher / Observable Operators

RxSwift Combine
amb(_:) Combine里沒有
?? replacement in CombineExt
asCompletable() Combine里沒有
asObservable() eraseToAnyPublisher()
buffer(
timeSpan:
count:
scheduler:
)
collect(
.byTimeOrCount(::_:),
options: )
catch(:)
catchError(
:)
catch(_:)
tryCatch(_:)
catchAndReturn(:)
catchErrorJustReturn(
:)
replaceError(with:)
compactMap(_:) compactMap(_:)
tryCompactMap(_:)
concat(...) append
Publishers.Concatenate
concatMap(_:) Combine里沒有
可以用 reduce(::)append(_:)來實(shí)現(xiàn)
debounce(_:scheduler:) debounce(
for:
scheduler:
options: )
debug(
_:
trimOutput:
file:
line:
function:
)
print(_:to:)
delay(_:scheduler:) delay(
for:
tolerance:
scheduler:
options:
)
delaySubscription(
_:
scheduler:
)
Combine里沒有
可以用 Deferreddelay(for:scheduler:options:)的組合來替代
dematerialize() Combine里沒有
?? replacement in CombineExt
distinctUntilChanged(...) removeDuplicates()
當(dāng)Output是Equatable時(shí)用
removeDuplicates(by:)
tryRemoveDuplicates(by:)
do(
onNext:
afterNext:
onError:
afterError:
onCompleted:
afterCompleted:
onSubscribe:
onSubscribed:
onDispose:
)
handleEvents(
receiveSubscription:
receiveOutput:
receiveCompletion:
receiveCancel:
receiveRequest:
)

沒有onDispose: 在publisher complete時(shí),cancel不會(huì)被調(diào)用。
element(at:) output(at:)
enumerated() Combine里沒有
建議可以用 scan(::)魔改一下
filter(_:) filter(_:)
tryFilter(_:)
first() first()
flatMapFirst(_:) Combine里沒有
flatMap(_:) flatMap(_:)
tryFlatMap(_:)
flatMapLatest(_:) map(_:)接上 switchToLatest()
?? replacement in CombineExt
groupBy(keySelector:) 沒有
ifEmpty(default:) replaceEmpty(with:)
ifEmpty(switchTo:) 沒有
ignoreElements() ignoreOutput()
map(_:) map(_:)
tryMap(_:)
materialize() 沒有
?? replacement in CombineExt
multicast(_:) multicast(subject:)
multicast(makeSubject:) multicast(_:)
observe(on:) observe(on:)
Combine用的是不同的scheduler類型!
publish() multicast { PassthroughSubject() }
makeConnectable()
reduce(into:_:) 沒有
不過可以用 reduce 來實(shí)現(xiàn)
reduce(::) reduce(::)
tryReduce(::)
reduce(
_:
accumulator:
mapResult:
)
reduce(::)接上 map(_:)tryMap(_:)
refCount() autoconnect()
replay(_:) 沒有
可以用 multicast(_:)和 ReplaySubject 來實(shí)現(xiàn)
replayAll() 沒有
retry() retry(.max)
可能有微小的意義上的差別,但實(shí)際使用中一般沒區(qū)別
retry(_:) retry(_:)
retry(when:) 沒有
sample(_:defaultValue:) 沒有
scan(into:_:) 沒有
可以用一般的 scan(::)來實(shí)現(xiàn)
scan(::) scan(::)
tryScan(::)
share(replay:scope:) share()
或者用 multicast(_:)加上 PassthroughSubjectautoconnect()
或者用 multicast(_:)加上 ReplaySubject
?? extension in CombineExt
single() 沒有
當(dāng)有且只有一個(gè)值時(shí),用 first(),否則會(huì)報(bào)錯(cuò)
single(_:) 沒有
可以用 filter(_:)接上 single()的替代方案
skip(_:) dropFirst(_:)
skip(while:) drop(while:)
skip(until:) drop(untilOutputFrom:)
startWith(...) prepend(...)
subscribe(_:) 最類似的接口:
sink(
receiveValue:
receiveCompletion:
)
subscribe(on:) subscribe(on:options:)
Combine用的是不同的scheduler類型!
subscribe(
onNext:
onError:
onCompleted:
onDisposed:
)
最類似的接口:
sink(
receiveValue:
receiveCompletion:
)
subscribe(
with:
onNext:
onError:
onCompleted:
onDisposed:
)
最類似的接口:
sink(
receiveValue:
receiveCompletion:
)
[weak object]
switchLatest() switchToLatest()
take(_:) prefix(_:)
take(for:scheduler:) 沒有
可以用以下方式解決:
prefix(untilOutputFrom:
Timer.publish(
every: <time>,
on: <scheduler> )
.autoconnect()
.prefix(1)
)

?? replacement in CombineExt
take(until:) prefix(untilOutputFrom:)
take(until:behavior:) prefix(while:)的相反條件來替代,但沒有 behavior參數(shù)功能了
take(while:behavior:) prefix(while:),但沒有 behavior參數(shù)功能了
takeLast(_:) 沒有
最接近的方案:
reduce([]) { 0 + [1] }
.flatMap { $0.suffix(<count>).publisher
}
throttle(
_:
latest:
scheduler:
)
throttle(
for:
scheduler:
latest:
)
timeout(
_:
other:
scheduler:
)
最接近的方案:
timeout(
_:
scheduler:
options:
customError:
)

然后再 catch(_:), 用map轉(zhuǎn)到其他publisher上
timeout(_:scheduler:) timeout(
_:
scheduler:
options:
customError:
)
toArray() collect()
window(
timeSpan:
count:
scheduler:
)
collect(
.byTime(<scheduler>, <time>)
)


collect(
.byTimeOrCount(
<scheduler>,
<time>,
<count>
)
)
withLatestFrom(_:) 沒有
?? replacement in CombineExt
withLatestFrom(
_:
resultSelector:
)
沒有
?? replacement in CombineExt
withUnretained(_:) 最接近的方案:
compactMap { [weak object] value in
object.map { ($0, value) }
}
withUnretained(
_:
resultSelector:
)
最接近的方案:
compactMap { [weak object] value in
object.map {
resultSelector($0, value)
}
}

總結(jié)一下

對(duì)我們來說,總是要調(diào)用 setFailureType(to:)eraseToAnyPublisher() 感覺不舒坦,除此之外,我們還是覺得Combine 很棒的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容