錯誤處理 Alamofire

Alamofire 對錯誤的處理考慮使用枚舉,只要結(jié)果可能是有限的集合的情況下,其實(shí)枚舉本身還是數(shù)據(jù)的一種載體,swift中,枚舉有著很豐富的使用方法.
先總結(jié)一下swfit中enum中的用法:

1.正常用法

enum Movement {
    case Left
    case Right
    case Top
    case Bottom
}
let aMovement = Movement.Left
switch aMovement {
case .Left:
    print("left")
default:
    print("Unknow")
}
if case .Left = aMovement {
    print("Left")
}
if .Left == aMovement {
    print("Left")
}

2.聲明為整型

enum Season: Int {
    case Spring = 0
    case Summer = 1
    case Autumn = 2
    case Winter = 3
}

.3.聲明為字符串類型

enum House: String {
    case ZhangSan = "I am zhangsan"
    case LiSi = "I am lisi"
}
let zs = House.ZhangSan
print(zs.rawValue)
enum CompassPoint: String {
case North, South, East, West
}
let n = CompassPoint.North

4.聲明為浮點(diǎn)類型

enum Constants: Double {
    case π = 3.14159
    case e = 2.71828
    case φ = 1.61803398874
    case λ = 1.30357
}
let pai = Constants.π
print(pai.rawValue)

5.其他類型

enum VNodeFlags : UInt32 {
    case Delete = 0x00000001
    case Write = 0x00000002
    case Extended = 0x00000004
    case Attrib = 0x00000008
    case Link = 0x00000010
    case Rename = 0x00000020
    case Revoke = 0x00000040
    case None = 0x00000080
}

6.enum包含enum

enum Character {

    enum Weapon {
        case Bow
        case Sword
        case Lance
        case Dagger
    }

    enum Helmet {
        case Wooden
        case Iron
        case Diamond
    }

    case Thief
    case Warrior
    case Knight
}

7.結(jié)構(gòu)體和枚舉

struct Scharacter {
    enum CharacterType {
        case Thief
        case Warrior
        case Knight
    }
    enum Weapon {
        case Bow
        case Sword
        case Lance
        case Dagger
    }
    let type: CharacterType
    let weapon: Weapon
}
let sc = Scharacter(type: .Thief, weapon: .Bow)
print(sc.type)

8.值關(guān)聯(lián)

enum Trade {
    case Buy(stock: String, amount: Int)
    case Sell(stock: String, amount: Int)
}
let trade = Trade.Buy(stock: "Car", amount: 100)
if case let Trade.Buy(stock, amount) = trade {
    print("buy \(amount) of \(stock)")
}
enum Trade0 {
    case Buy(String, Int)
    case Sell(String, Int)
}
let trade0 = Trade0.Buy("Car0", 100)
if case let Trade0.Buy(stock, amount) = trade0 {
    print("buy \(amount) of \(stock)")
}

9.枚舉中的函數(shù)

enum Wearable {
    enum Weight: Int {
        case Light = 2
    }

    enum Armor: Int {
        case Light = 2
    }

    case Helmet(weight: Weight, armor: Armor)


    func attributes() -> (weight: Int, armor: Int) {
        switch self {
        case .Helmet(let w, let a):
            return (weight: w.rawValue * 2, armor: a.rawValue * 4)

        }
    }
}

let test = Wearable.Helmet(weight: .Light, armor: .Light).attributes()
print(test)

enum Device {
    case iPad, iPhone, AppleTV, AppleWatch
    func introduced() -> String {
        switch self {
        case .AppleTV: return "\(self) was introduced 2006"
        case .iPhone: return "\(self) was introduced 2007"
        case .iPad: return "\(self) was introduced 2010"
        case .AppleWatch: return "\(self) was introduced 2014"
        }
    }
}
print (Device.iPhone.introduced())

10.枚舉中的屬性

enum Device1 {
    case iPad, iPhone
    var year: Int {
        switch self {
        case .iPad:
            return 2010
        case .iPhone:
            return 2007
    }
    }
}

let iPhone = Device1.iPhone
print(iPhone.year)

ParameterEncodingFailureReason

通過ParameterEncodingFailureReason我們能夠很清楚的看出來這是一個參數(shù)編碼的錯誤原因。大家注意reason這個詞,在命名中,有或者沒有這個詞,表達(dá)的意境完全不同,因此,Alamofire牛逼就體現(xiàn)在這些細(xì)節(jié)之中。

public enum AFError: Error {
    /// The underlying reason the parameter encoding error occurred.
    ///
    /// - missingURL:                 The URL request did not have a URL to encode.
    /// - jsonEncodingFailed:         JSON serialization failed with an underlying system error during the
    ///                               encoding process.
    /// - propertyListEncodingFailed: Property list serialization failed with an underlying system error during
    ///                               encoding process.
    public enum ParameterEncodingFailureReason {
        case missingURL
        case jsonEncodingFailed(error: Error)
        case propertyListEncodingFailed(error: Error)
    }
 }

ParameterEncodingFailureReason本身是一個enum,同時,它又被包含在AFError之中,這說明枚舉之中可以有另一個枚舉。那么像這種情況我們怎么使用呢?看下邊的代碼:

let parameterErrorReason = AFError.ParameterEncodingFailureReason.missingURL

枚舉的訪問是一級一級進(jìn)行的。我們再看這行代碼:case jsonEncodingFailed(error: Error)。jsonEncodingFailed(error: Error)并不是函數(shù),就是枚舉的一個普通的子選項(xiàng)。(error: Error)是它的一個關(guān)聯(lián)值,相對于任何一個子選項(xiàng),我們都可以關(guān)聯(lián)任何值,它的意義就在于,把這些值與子選項(xiàng)進(jìn)行綁定,方便在需要的時候調(diào)用。我們會在下邊講解如何獲取關(guān)聯(lián)值。

參數(shù)編碼有一下幾種方式:

把參數(shù)編碼到URL中
把參數(shù)編碼到httpBody中
Alamofire中是如何進(jìn)行參數(shù)編碼的,這方面的內(nèi)容會在后續(xù)的ParameterEncoding.swift這一篇文章中給出詳細(xì)的解釋。那么編碼失敗的原因可能為:

missingURL 給定的urlRequest.url為nil的情況拋出錯誤
jsonEncodingFailed(error: Error) 當(dāng)選擇把參數(shù)編碼成JSON格式的情況下,參數(shù)JSON化拋出的錯誤
propertyListEncodingFailed(error: Error) 這個同上
綜上所述,ParameterEncodingFailureReason封裝了參數(shù)編碼的錯誤,可能出現(xiàn)的錯誤類型為Error,說明這些所謂一般是調(diào)用系統(tǒng)Api產(chǎn)生的錯誤。

MultipartEncodingFailureReason

public enum MultipartEncodingFailureReason {
        case bodyPartURLInvalid(url: URL)
        case bodyPartFilenameInvalid(in: URL)
        case bodyPartFileNotReachable(at: URL)
        case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
        case bodyPartFileIsDirectory(at: URL)
        case bodyPartFileSizeNotAvailable(at: URL)
        case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
        case bodyPartInputStreamCreationFailed(for: URL)

        case outputStreamCreationFailed(for: URL)
        case outputStreamFileAlreadyExists(at: URL)
        case outputStreamURLInvalid(url: URL)
        case outputStreamWriteFailed(error: Error)

        case inputStreamReadFailed(error: Error)
    }

多部分編碼錯誤一般發(fā)生在上傳或下載請求中對數(shù)據(jù)的處理過程中,這里邊最重要的是對上傳數(shù)據(jù)的處理過程,會在后續(xù)的MultipartFormData.swift這一篇文章中給出詳細(xì)的解釋,我們就簡單的分析下MultipartEncodingFailureReason子選項(xiàng)錯誤出現(xiàn)的原因:

bodyPartURLInvalid(url: URL) 上傳數(shù)據(jù)時,可以通過fileURL的方式,讀取本地文件數(shù)據(jù),如果fileURL不可用,就會拋出這個錯誤
bodyPartFilenameInvalid(in: URL) 如果使用fileURL的lastPathComponent或者pathExtension獲取filename為空拋出的錯誤
bodyPartFileNotReachable(at: URL) 通過fileURL不能訪問數(shù)據(jù),也就是不可達(dá)的
bodyPartFileNotReachableWithError(atURL: URL, error: Error) 這個不同于bodyPartFileNotReachable(at: URL),當(dāng)嘗試檢測fileURL是不是可達(dá)的情況下拋出的錯誤
bodyPartFileIsDirectory(at: URL) 當(dāng)fileURL是一個文件夾時拋出錯誤
bodyPartFileSizeNotAvailable(at: URL) 當(dāng)使用系統(tǒng)Api獲取fileURL指定文件的size出現(xiàn)錯誤
bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error) 查詢fileURL指定文件size出現(xiàn)錯誤
bodyPartInputStreamCreationFailed(for: URL) 通過fileURL創(chuàng)建inputStream出現(xiàn)錯誤
outputStreamCreationFailed(for: URL) 當(dāng)嘗試把編碼后的數(shù)據(jù)寫入到硬盤時,創(chuàng)建outputStream出現(xiàn)錯誤
outputStreamFileAlreadyExists(at: URL) 數(shù)據(jù)不能被寫入,因?yàn)橹付ǖ膄ileURL已經(jīng)存在
outputStreamURLInvalid(url: URL) fileURL不是一個file URL
outputStreamWriteFailed(error: Error) 數(shù)據(jù)流寫入錯誤
inputStreamReadFailed(error: Error) 數(shù)據(jù)流讀入錯誤
綜上所述,這些錯誤基本上都跟數(shù)據(jù)的操作相關(guān),這個在后續(xù)會做出很詳細(xì)的說明。

ResponseValidationFailureReason

public enum ResponseValidationFailureReason {
        case dataFileNil
        case dataFileReadFailed(at: URL)
        case missingContentType(acceptableContentTypes: [String])
        case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
        case unacceptableStatusCode(code: Int)
    }

Alamofire不管請求是否成功,都會返回response。它提供了驗(yàn)證ContentType和StatusCode的功能,關(guān)于驗(yàn)證,再后續(xù)的文章中會有詳細(xì)的解答,我們先看看這些原因:

dataFileNil 保存數(shù)據(jù)的URL不存在,這種情況一般出現(xiàn)在下載任務(wù)中,指的是下載代理中的fileURL缺失
dataFileReadFailed(at: URL) 保存數(shù)據(jù)的URL無法讀取數(shù)據(jù),同上
missingContentType(acceptableContentTypes: [String]) 服務(wù)器返回的response不包含ContentType且提供的acceptableContentTypes不包含通配符(通配符表示可以接受任何類型)
unacceptableContentType(acceptableContentTypes: [String], responseContentType: String) ContentTypes不匹配
unacceptableStatusCode(code: Int) StatusCode不匹配

ResponseSerializationFailureReason

public enum ResponseSerializationFailureReason {
    case inputDataNil
    case inputDataNilOrZeroLength
    case inputFileNil
    case inputFileReadFailed(at: URL)
    case stringSerializationFailed(encoding: String.Encoding)
    case jsonSerializationFailed(error: Error)
    case propertyListSerializationFailed(error: Error)
}

我們在上篇中已經(jīng)提到,Alamofire支持把服務(wù)器的response序列成幾種數(shù)據(jù)格式。

response 直接返回HTTPResponse,未序列化
responseData 序列化為Data
responseJSON 序列化為Json
responseString 序列化為字符串
responsePropertyList 序列化為Any
那么在序列化的過程中,很可能會發(fā)生下邊的錯誤:

inputDataNil 服務(wù)器返回的response沒有數(shù)據(jù)
inputDataNilOrZeroLength 服務(wù)器返回的response沒有數(shù)據(jù)或者數(shù)據(jù)的長度是0
inputFileNil 指向數(shù)據(jù)的URL不存在
inputFileReadFailed(at: URL) 指向數(shù)據(jù)的URL無法讀取數(shù)據(jù)

AFError

上邊內(nèi)容中介紹的ParameterEncodingFailureReason MultipartEncodingFailureReason ResponseValidationFailureReason和 ResponseSerializationFailureReason,他們是定義在AFError中獨(dú)立的枚舉,他們之間是包含和被包含的關(guān)系,理解這一點(diǎn)很重要,因?yàn)橛辛诉@種包含的管理,在使用中就需要通過AFError.ParameterEncodingFailureReason這種方式進(jìn)行操作。

那么最重要的問題就是,如何把上邊4個獨(dú)立的枚舉進(jìn)行串聯(lián)呢?Alamofire巧妙的地方就在這里,有4個獨(dú)立的枚舉,分別代表4大錯誤。也就是說這個網(wǎng)絡(luò)框架肯定有這4大錯誤模塊,我們只需要給AFError設(shè)計(jì)4個子選項(xiàng),每個子選項(xiàng)關(guān)聯(lián)上上邊4個獨(dú)立枚舉的值就ok了。

這個設(shè)計(jì)真的很巧妙,試想,如果把所有的錯誤都放到AFError中,就顯得非常冗余。那么下邊的代碼就呼之欲出了,大家好好體會體會在swift下這么設(shè)計(jì)的妙用:

case invalidURL(url: URLConvertible)
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
case responseValidationFailed(reason: ResponseValidationFailureReason)
case responseSerializationFailed(reason: ResponseSerializationFailureReason)

AFError的擴(kuò)展

也許在開發(fā)中,我們完成了上邊的代碼就認(rèn)為夠用了,但對于一個開源框架而言,遠(yuǎn)遠(yuǎn)是不夠的。我們一點(diǎn)點(diǎn)進(jìn)行剖析:

現(xiàn)在給定一條數(shù)據(jù):

 func findErrorType(error: AFError) {

  }

我只需要知道這個error是不是參數(shù)編碼錯誤,應(yīng)該怎么辦?因此為AFError提供5個布爾類型的屬性,專門用來獲取當(dāng)前的錯誤是不是某個指定的類型。這個功能的實(shí)現(xiàn)比較簡單,代碼如下:

extension AFError {
    /// Returns whether the AFError is an invalid URL error.
    public var isInvalidURLError: Bool {
        if case .invalidURL = self { return true }
        return false
    }

    /// Returns whether the AFError is a parameter encoding error. When `true`, the `underlyingError` property will
    /// contain the associated value.
    public var isParameterEncodingError: Bool {
        if case .parameterEncodingFailed = self { return true }
        return false
    }

    /// Returns whether the AFError is a multipart encoding error. When `true`, the `url` and `underlyingError` properties
    /// will contain the associated values.
    public var isMultipartEncodingError: Bool {
        if case .multipartEncodingFailed = self { return true }
        return false
    }

    /// Returns whether the `AFError` is a response validation error. When `true`, the `acceptableContentTypes`,
    /// `responseContentType`, and `responseCode` properties will contain the associated values.
    public var isResponseValidationError: Bool {
        if case .responseValidationFailed = self { return true }
        return false
    }

    /// Returns whether the `AFError` is a response serialization error. When `true`, the `failedStringEncoding` and
    /// `underlyingError` properties will contain the associated values.
    public var isResponseSerializationError: Bool {
        if case .responseSerializationFailed = self { return true }
        return false
    }
}

總而言之,這些都是給AFError這個枚舉擴(kuò)展的屬性,還包含下邊這些屬性
urlConvertible: URLConvertible? 獲取某個屬性,這個屬性實(shí)現(xiàn)了URLConvertible協(xié)議,在AFError中只有case invalidURL(url: URLConvertible)這個選項(xiàng)符合要求

/// The `URLConvertible` associated with the error.
      public var urlConvertible: URLConvertible? {
          switch self {
          case .invalidURL(let url):
              return url
          default:
              return nil
          }
      }

url: URL? 獲取AFError中的URL,當(dāng)然這個URL只跟MultipartEncodingFailureReason這個子選項(xiàng)有關(guān)

/// The `URL` associated with the error.
      public var url: URL? {
          switch self {
          case .multipartEncodingFailed(let reason):
              return reason.url
          default:
              return nil
          }
      }

underlyingError: Error? AFError中封裝的所有的可能出現(xiàn)的錯誤中,并不是每種可能都會返回Error這個錯誤信息,因此這個屬性是可選的

/// The `Error` returned by a system framework associated with a `.parameterEncodingFailed`,
      /// `.multipartEncodingFailed` or `.responseSerializationFailed` error.
      public var underlyingError: Error? {
          switch self {
          case .parameterEncodingFailed(let reason):
              return reason.underlyingError
          case .multipartEncodingFailed(let reason):
              return reason.underlyingError
          case .responseSerializationFailed(let reason):
              return reason.underlyingError
          default:
              return nil
          }
      }

acceptableContentTypes: [String]? 可接受的ContentType

/// The response `Content-Type` of a `.responseValidationFailed` error.
      public var responseContentType: String? {
          switch self {
          case .responseValidationFailed(let reason):
              return reason.responseContentType
          default:
              return nil
          }
      }

responseCode: Int? 響應(yīng)碼

/// The response code of a `.responseValidationFailed` error.
  public var responseCode: Int? {
      switch self {
      case .responseValidationFailed(let reason):
          return reason.responseCode
      default:
          return nil
      }
  }

failedStringEncoding: String.Encoding? 錯誤的字符串編碼

/// The `String.Encoding` associated with a failed `.stringResponse()` call.
      public var failedStringEncoding: String.Encoding? {
          switch self {
          case .responseSerializationFailed(let reason):
              return reason.failedStringEncoding
          default:
              return nil
          }
      }

這里是一個小的分割線,在上邊屬性的獲取中,也是用到了下邊代碼中的擴(kuò)展功能:

extension AFError.ParameterEncodingFailureReason {
      var underlyingError: Error? {
          switch self {
          case .jsonEncodingFailed(let error), .propertyListEncodingFailed(let error):
              return error
          default:
              return nil
          }
      }
  }

  extension AFError.MultipartEncodingFailureReason {
      var url: URL? {
          switch self {
          case .bodyPartURLInvalid(let url), .bodyPartFilenameInvalid(let url), .bodyPartFileNotReachable(let url),
               .bodyPartFileIsDirectory(let url), .bodyPartFileSizeNotAvailable(let url),
               .bodyPartInputStreamCreationFailed(let url), .outputStreamCreationFailed(let url),
               .outputStreamFileAlreadyExists(let url), .outputStreamURLInvalid(let url),
               .bodyPartFileNotReachableWithError(let url, _), .bodyPartFileSizeQueryFailedWithError(let url, _):
              return url
          default:
              return nil
          }
      }

      var underlyingError: Error? {
          switch self {
          case .bodyPartFileNotReachableWithError(_, let error), .bodyPartFileSizeQueryFailedWithError(_, let error),
               .outputStreamWriteFailed(let error), .inputStreamReadFailed(let error):
              return error
          default:
              return nil
          }
      }
  }

  extension AFError.ResponseValidationFailureReason {
      var acceptableContentTypes: [String]? {
          switch self {
          case .missingContentType(let types), .unacceptableContentType(let types, _):
              return types
          default:
              return nil
          }
      }

      var responseContentType: String? {
          switch self {
          case .unacceptableContentType(_, let responseType):
              return responseType
          default:
              return nil
          }
      }

      var responseCode: Int? {
          switch self {
          case .unacceptableStatusCode(let code):
              return code
          default:
              return nil
          }
      }
  }

  extension AFError.ResponseSerializationFailureReason {
      var failedStringEncoding: String.Encoding? {
          switch self {
          case .stringSerializationFailed(let encoding):
              return encoding
          default:
              return nil
          }
      }

      var underlyingError: Error? {
          switch self {
          case .jsonSerializationFailed(let error), .propertyListSerializationFailed(let error):
              return error
          default:
              return nil
          }
      }
  }

錯誤描述

在開發(fā)中,如果程序遇到錯誤,我們往往會給用戶展示更加直觀的信息,這就要求我們把錯誤信息轉(zhuǎn)換成易于理解的內(nèi)容。因此我們只要實(shí)現(xiàn)LocalizedError協(xié)議就好了。這里邊的內(nèi)容很簡單,在這里就直接把代碼寫上了,不做分析

extension AFError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .invalidURL(let url):
            return "URL is not valid: \(url)"
        case .parameterEncodingFailed(let reason):
            return reason.localizedDescription
        case .multipartEncodingFailed(let reason):
            return reason.localizedDescription
        case .responseValidationFailed(let reason):
            return reason.localizedDescription
        case .responseSerializationFailed(let reason):
            return reason.localizedDescription
        }
    }
}

extension AFError.ParameterEncodingFailureReason {
    var localizedDescription: String {
        switch self {
        case .missingURL:
            return "URL request to encode was missing a URL"
        case .jsonEncodingFailed(let error):
            return "JSON could not be encoded because of error:\n\(error.localizedDescription)"
        case .propertyListEncodingFailed(let error):
            return "PropertyList could not be encoded because of error:\n\(error.localizedDescription)"
        }
    }
}

extension AFError.MultipartEncodingFailureReason {
    var localizedDescription: String {
        switch self {
        case .bodyPartURLInvalid(let url):
            return "The URL provided is not a file URL: \(url)"
        case .bodyPartFilenameInvalid(let url):
            return "The URL provided does not have a valid filename: \(url)"
        case .bodyPartFileNotReachable(let url):
            return "The URL provided is not reachable: \(url)"
        case .bodyPartFileNotReachableWithError(let url, let error):
            return (
                "The system returned an error while checking the provided URL for " +
                "reachability.\nURL: \(url)\nError: \(error)"
            )
        case .bodyPartFileIsDirectory(let url):
            return "The URL provided is a directory: \(url)"
        case .bodyPartFileSizeNotAvailable(let url):
            return "Could not fetch the file size from the provided URL: \(url)"
        case .bodyPartFileSizeQueryFailedWithError(let url, let error):
            return (
                "The system returned an error while attempting to fetch the file size from the " +
                "provided URL.\nURL: \(url)\nError: \(error)"
            )
        case .bodyPartInputStreamCreationFailed(let url):
            return "Failed to create an InputStream for the provided URL: \(url)"
        case .outputStreamCreationFailed(let url):
            return "Failed to create an OutputStream for URL: \(url)"
        case .outputStreamFileAlreadyExists(let url):
            return "A file already exists at the provided URL: \(url)"
        case .outputStreamURLInvalid(let url):
            return "The provided OutputStream URL is invalid: \(url)"
        case .outputStreamWriteFailed(let error):
            return "OutputStream write failed with error: \(error)"
        case .inputStreamReadFailed(let error):
            return "InputStream read failed with error: \(error)"
        }
    }
}

extension AFError.ResponseSerializationFailureReason {
    var localizedDescription: String {
        switch self {
        case .inputDataNil:
            return "Response could not be serialized, input data was nil."
        case .inputDataNilOrZeroLength:
            return "Response could not be serialized, input data was nil or zero length."
        case .inputFileNil:
            return "Response could not be serialized, input file was nil."
        case .inputFileReadFailed(let url):
            return "Response could not be serialized, input file could not be read: \(url)."
        case .stringSerializationFailed(let encoding):
            return "String could not be serialized with encoding: \(encoding)."
        case .jsonSerializationFailed(let error):
            return "JSON could not be serialized because of error:\n\(error.localizedDescription)"
        case .propertyListSerializationFailed(let error):
            return "PropertyList could not be serialized because of error:\n\(error.localizedDescription)"
        }
    }
}

extension AFError.ResponseValidationFailureReason {
    var localizedDescription: String {
        switch self {
        case .dataFileNil:
            return "Response could not be validated, data file was nil."
        case .dataFileReadFailed(let url):
            return "Response could not be validated, data file could not be read: \(url)."
        case .missingContentType(let types):
            return (
                "Response Content-Type was missing and acceptable content types " +
                "(\(types.joined(separator: ","))) do not match \"*/*\"."
            )
        case .unacceptableContentType(let acceptableTypes, let responseType):
            return (
                "Response Content-Type \"\(responseType)\" does not match any acceptable types: " +
                "\(acceptableTypes.joined(separator: ","))."
            )
        case .unacceptableStatusCode(let code):
            return "Response status code was unacceptable: \(code)."
        }
    }
}

總結(jié)

通過閱讀AFError這篇代碼,給了我很大的震撼,在代碼的設(shè)計(jì)上,可以參考這種設(shè)計(jì)方式。
由于知識水平有限,如有錯誤,還望指出

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容