Swift 4 踩坑之 Codable 協(xié)議

所有文章已搬遷到個(gè)人站點(diǎn):me.harley-xk.studio,歡迎訪問(wèn)留言

WWDC 過(guò)去有一段時(shí)間了,最近終于有時(shí)間空閑,可以靜下心來(lái)仔細(xì)研究一下相關(guān)內(nèi)容。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),本屆WWDC 最重要的消息還是得屬 Swift 4 的推出。

Swift 經(jīng)過(guò)三年的發(fā)展,終于在 API 層面趨于穩(wěn)定。從 Swift 3 遷移代碼到 Swift 4 終于不用像 2 到 3 那樣痛苦了。這對(duì)開(kāi)發(fā)者來(lái)說(shuō)實(shí)在是個(gè)重大利好,應(yīng)該會(huì)吸引一大批對(duì) Swift 仍然處于觀望狀態(tài)的開(kāi)發(fā)者加入。

另外 Swift 4 引入了許多新的特性,像是 fileprivate 關(guān)鍵字的限制范圍更加精確了;聲明屬性終于可以同時(shí)限制類(lèi)型和協(xié)議了;新的 KeyPath API 等等,從這些改進(jìn)我們可以看到,Swift 的生態(tài)越來(lái)越完善,Swift 本身也越來(lái)越強(qiáng)大。

而 Swift 4 帶來(lái)的新特性中,最讓人眼前一亮的,我覺(jué)得非 Codable 協(xié)議莫屬,下面就來(lái)介紹下我自己對(duì) Codable 協(xié)議踩坑的經(jīng)驗(yàn)總結(jié)。

簡(jiǎn)單介紹

Swift 由于類(lèi)型安全的特性,對(duì)于像 JSON 這類(lèi)弱類(lèi)型的數(shù)據(jù)處理一直是一個(gè)比較頭疼的問(wèn)題,雖然市面上許多優(yōu)秀的第三方庫(kù)在這方面做了不少努力,但是依然存在著很多難以克服的缺陷,所以 Codable 協(xié)議的推出,一來(lái)打破了這樣的僵局,二來(lái)也給我們解決類(lèi)似問(wèn)題提供了新的思路。

通過(guò)查看定義可以看到,Codable 其實(shí)是一個(gè)組合協(xié)議,由 DecodableEncodable 兩個(gè)協(xié)議組成:

/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable

/// A type that can encode itself to an external representation.
public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

/// A type that can decode itself from an external representation.
public protocol Decodable {
    public init(from decoder: Decoder) throws
}

EncodableDecodable 分別定義了 encode(to:)init(from:) 兩個(gè)協(xié)議函數(shù),分別用來(lái)實(shí)現(xiàn)數(shù)據(jù)模型的歸檔和外部數(shù)據(jù)的解析和實(shí)例化。最常用的場(chǎng)景就是接口 JSON 數(shù)據(jù)解析和模型創(chuàng)建。但是 Codable 的能力并不止于此,這個(gè)后面會(huì)說(shuō)。

解析 JSON 對(duì)象

先來(lái)看 Decodable 對(duì) JSON 數(shù)據(jù)對(duì)象的解析。Swift 為我們做了絕大部分的工作,Swift 中的基本數(shù)據(jù)類(lèi)型比如 StringIntFloat 等都已經(jīng)實(shí)現(xiàn)了 Codable 協(xié)議,因此如果你的數(shù)據(jù)類(lèi)型只包含這些基本數(shù)據(jù)類(lèi)型的屬性,只需要在類(lèi)型聲明中加上 Codable 協(xié)議就可以了,不需要寫(xiě)任何實(shí)際實(shí)現(xiàn)的代碼,這也是 Codable 最大的優(yōu)勢(shì)所在。

比如我們有下面這樣一個(gè)學(xué)生信息的 JSON 字符串:

let jsonString =
"""
{
    "name": "小明",
    "age": 12,
    "weight": 43.2
}
"""

這時(shí)候,只需要定義一個(gè) Student 類(lèi)型,聲明實(shí)現(xiàn) Decodable 協(xié)議即可,Swift 4 已經(jīng)為我們提供了默認(rèn)的實(shí)現(xiàn):

struct Student: Decodable {   
    var name: String
    var age: Int
    var weight: Float
}

然后,只需要一行代碼就可以將 小明 解析出來(lái)了:

let xiaoming = try JSONDecoder().decode(Student.self, from: jsonString.data(using: .utf8)!)

這里需要注意的是, decode 函數(shù)需要外部數(shù)據(jù)類(lèi)型為 Data 類(lèi)型,如果是字符串需要先轉(zhuǎn)換為 Data 之后操作,不過(guò)像 Alamofire 之類(lèi)的網(wǎng)絡(luò)框架,返回?cái)?shù)據(jù)原本就是 Data 類(lèi)型的。
另外 decode 函數(shù)是標(biāo)記為 throws 的,如果解析失敗,會(huì)拋出一個(gè)異常,為了保證程序的健壯性,需要使用 do-catch 對(duì)異常情況進(jìn)行處理:

do {
    let xiaoming = try JSONDecoder().decode(Student.self, from: data)
} catch {
    // 異常處理
}

特殊數(shù)據(jù)類(lèi)型

很多時(shí)候光靠基本數(shù)據(jù)類(lèi)型并不能完成工作,往往我們需要用到一些特殊的數(shù)據(jù)類(lèi)型。Swift 對(duì)許多特殊數(shù)據(jù)類(lèi)型也提供了默認(rèn)的 Codable 實(shí)現(xiàn),但是有一些限制。

枚舉
{
    ...
    "gender": "male"
    ...
}

性別是一個(gè)很常用的信息,我們經(jīng)常會(huì)把它定義成枚舉:

enum Gender {
    case male
    case female
    case other
}

枚舉類(lèi)型也默認(rèn)實(shí)現(xiàn)了 Codable 協(xié)議,但是如果我們直接聲明 Gender 枚舉支持 Codable 協(xié)議,編譯器會(huì)提示沒(méi)有提供實(shí)現(xiàn):

其實(shí)這里有一個(gè)限制:枚舉類(lèi)型要默認(rèn)支持 Codable 協(xié)議,需要聲明為具有原始值的形式,并且原始值的類(lèi)型需要支持 Codable 協(xié)議:

enum Gender: String, Decodable {
    case male
    case female
    case other
}

由于枚舉類(lèi)型原始值隱式賦值特性的存在,如果枚舉值的名稱(chēng)和對(duì)應(yīng)的 JSON 中的值一致,不需要顯式指定原始值即可完成解析。

Bool

我們的數(shù)據(jù)模型現(xiàn)在新增了一個(gè)字段,用來(lái)表示某個(gè)學(xué)生是否是少先隊(duì)員:

{
    ...
    "isYoungPioneer": true
    ...
}

這時(shí)候,直接聲明對(duì)應(yīng)的屬性就可以了:

var isYoungPioneer: Bool

Bool 類(lèi)型原本沒(méi)什么好講的,不過(guò)因?yàn)椴鹊搅丝樱赃€是得說(shuō)一說(shuō):
目前發(fā)現(xiàn)的坑是:Bool 類(lèi)型默認(rèn)只支持 true/false 形式的 Bool 值解析。對(duì)于一些使用 0/1 形式來(lái)表示 Bool 值的后端框架,只能通過(guò) Int 類(lèi)型解析之后再做轉(zhuǎn)換了,或者可以自定義實(shí)現(xiàn) Codable 協(xié)議。

日期解析策略

說(shuō)了枚舉和 Bool,另外一個(gè)常用的特殊類(lèi)型就是 Date 了,Date 類(lèi)型的特殊性在于它有著各種各樣的格式標(biāo)準(zhǔn)和表示方式,從數(shù)字到字符串可以說(shuō)是五花八門(mén),解析 Date 類(lèi)型是任何一個(gè)同類(lèi)型的框架都必須面對(duì)的課題。

對(duì)此,Codable 給出的解決方案是:定義解析策略。JSONDecoder 類(lèi)聲明了一個(gè) DateDecodingStrategy 類(lèi)型的屬性,用來(lái)制定 Date 類(lèi)型的解析策略,同樣先看定義:

/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
    
    /// Defer to `Date` for decoding. This is the default strategy.
    case deferredToDate
    
    /// Decode the `Date` as a UNIX timestamp from a JSON number.
    case secondsSince1970
    
    /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
    case millisecondsSince1970
    
    /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
    case iso8601
    
    /// Decode the `Date` as a string parsed by the given formatter.
    case formatted(DateFormatter)
    
    /// Decode the `Date` as a custom value decoded by the given closure.
    case custom((Decoder) throws -> Date)
}

Codable 對(duì)幾種常用格式標(biāo)準(zhǔn)進(jìn)行了支持,默認(rèn)啟用的策略是 deferredToDate,即從 **UTC 時(shí)間2001年1月1日 **開(kāi)始的秒數(shù),對(duì)應(yīng) Date 類(lèi)型中 timeIntervalSinceReferenceDate 這個(gè)屬性。比如 519751611.125429 這個(gè)數(shù)字解析后的結(jié)果是 2017-06-21 15:26:51 +0000

另外可選的格式標(biāo)準(zhǔn)有 secondsSince1970millisecondsSince1970iso8601 等,這些都是有詳細(xì)說(shuō)明的通用標(biāo)準(zhǔn),不清楚的自行谷歌吧 :)

同時(shí) Codable 提供了兩種方自定義 Date 格式的策略:

  • formatted(DateFormatter)
    這種策略通過(guò)設(shè)置 DateFormatter 來(lái)指定 Date 格式
  • custom((Decoder) throws -> Date)
    custom 策略接受一個(gè) (Decoder) -> Date 的閉包,基本上是把解析任務(wù)完全丟給我們自己去實(shí)現(xiàn)了,具有較高的自由度
小數(shù)解析策略

小數(shù)類(lèi)型(FloatDouble) 默認(rèn)也實(shí)現(xiàn)了 Codable 協(xié)議,但是小數(shù)類(lèi)型在 Swift 中有許多特殊值,比如圓周率(Float.pi)等。這里要說(shuō)的是另外兩個(gè)屬性,先看定義:

/// Positive infinity.
///
/// Infinity compares greater than all finite numbers and equal to other
/// infinite values.
public static var infinity: Double { get }

/// A quiet NaN ("not a number").
///
/// A NaN compares not equal, not greater than, and not less than every
/// value, including itself. Passing a NaN to an operation generally results
/// in NaN.
public static var nan: Double { get }

infinity 表示正無(wú)窮(負(fù)無(wú)窮寫(xiě)作:-infinity),nan 表示沒(méi)有值,這些特殊值沒(méi)有辦法使用數(shù)字進(jìn)行表示,但是在 Swift 中它們是確確實(shí)實(shí)的值,可以參與計(jì)算、比較等。
不同的語(yǔ)言、框架對(duì)此會(huì)有類(lèi)似的實(shí)現(xiàn),但是表達(dá)方式可能不完全相同,因此如果在某些場(chǎng)景下需要解析這樣的值,就需要做特殊轉(zhuǎn)換了。

Codable 的實(shí)現(xiàn)方式比較簡(jiǎn)單粗暴,JSONDecoder 類(lèi)型有一個(gè)屬性 nonConformingFloatDecodingStrategy ,用來(lái)指定不一致的小數(shù)轉(zhuǎn)換策略,默認(rèn)值為 throw, 即直接拋出異常,解析失敗。另外一個(gè)選擇就是自己指定 infinity-infinitynan 三個(gè)特殊值的表示方式:

let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "infinity", negativeInfinity: "-infinity", nan: "nan")
// 另外一種表示方式
// decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "∞", negativeInfinity: "-∞", nan: "n/a")

目前看來(lái)只支持這三個(gè)特殊值的轉(zhuǎn)換,不過(guò)這種特殊值的使用場(chǎng)景應(yīng)該非常有限,至少在我自己五六年的開(kāi)發(fā)生涯中還沒(méi)有遇到過(guò)。

自定義數(shù)據(jù)類(lèi)型

純粹的基本數(shù)據(jù)類(lèi)型依然不能很好地工作,實(shí)際項(xiàng)目的數(shù)據(jù)結(jié)構(gòu)往往是很復(fù)雜的,一個(gè)數(shù)據(jù)類(lèi)型經(jīng)常會(huì)包含另一個(gè)數(shù)據(jù)類(lèi)型的屬性。比如說(shuō)我們這個(gè)例子中,每個(gè)學(xué)生信息中還包含了所在學(xué)校的信息:

{
    "name": "小明",
    "age": 12,
    "weight": 43.2
    "school": {
      "name": "市第一中學(xué)",
      "address": "XX市人民中路 66 號(hào)"
    }
}

這時(shí)候就需要 Student 和 School 兩個(gè)類(lèi)型來(lái)組合表示:

struct School: Decodable {
    var name: String
    var address: String
}
struct Student: Decodable {   
    var name: String
    var age: Int
    var weight: Float
    var school: School
}

由于所有基本類(lèi)型都實(shí)現(xiàn)了 Codable 協(xié)議,因此 SchoolStudent 一樣,只要所有屬性都實(shí)現(xiàn)了 Codable 協(xié)議,就不需要手動(dòng)提供任何實(shí)現(xiàn)即可獲得默認(rèn)的 Codable 實(shí)現(xiàn)。由于 School 支持了 Codable 協(xié)議,保證了 Student 依然能夠獲得默認(rèn)的 Codable 實(shí)現(xiàn),因此,嵌套類(lèi)型的解析同樣不需要額外的代碼了。

自定義字段

很多時(shí)候前后端不一定能完全步調(diào)一致,觀念相同。所以往往后端給出的數(shù)據(jù)結(jié)構(gòu)中會(huì)有一些比較個(gè)性的字段名,當(dāng)然有時(shí)候是我們自己。另外有一些框架(比如我正在用的 Laravel)習(xí)慣使用蛇形命名法,而 iOS 的代碼規(guī)范推薦使用駝峰命名法,為了保證代碼風(fēng)格和平臺(tái)特色,這時(shí)候就必須要自行指定字段名了。

在研究自定義字段之前我們需要深入底層,了解下 Codable 默認(rèn)是怎么實(shí)現(xiàn)屬性的名稱(chēng)識(shí)別及賦值的。通過(guò)研究底層的 C++ 源代碼可以發(fā)現(xiàn),Codable 通過(guò)巧(kai)妙(guà)的方式,在編譯代碼時(shí)根據(jù)類(lèi)型的屬性,自動(dòng)生成了一個(gè) CodingKeys 的枚舉類(lèi)型定義,這是一個(gè)以 String 類(lèi)型作為原始值的枚舉類(lèi)型,對(duì)應(yīng)每一個(gè)屬性的名稱(chēng)。然后再給每一個(gè)聲明實(shí)現(xiàn) Codable 協(xié)議的類(lèi)型自動(dòng)生成 init(from:)encode(to:) 兩個(gè)函數(shù)的具體實(shí)現(xiàn),最終完成了整個(gè)協(xié)議的實(shí)現(xiàn)。

所以我們可以自己實(shí)現(xiàn) CodingKeys 的類(lèi)型定義,并且給屬性指定不同的原始值來(lái)實(shí)現(xiàn)自定義字段的解析。這樣編譯器會(huì)直接采用我們已經(jīng)實(shí)現(xiàn)好的方案而不再重新生成一個(gè)默認(rèn)的。

比如 Student 需要增加一個(gè)出生日期的屬性,后端接口使用蛇形命名,JSON 數(shù)據(jù)如下:

{
    "name": "小明",
    "age": 12,
    "weight": 43.2
    "birth_date": "1992-12-25"
}

這時(shí)候在 Student 類(lèi)型聲明中需要增加 CodingKeys 定義,并且將 birthday 的原始值設(shè)置為 birth_date

struct Student: Codable {
    ...
    var birthday: Date
    
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case weight
        case birthday = "birth_date"
    }
}

需要注意的是,即使屬性名稱(chēng)與 JSON 中的字段名稱(chēng)一致,如果自定義了 CodingKeys,這些屬性也是無(wú)法省略的,否則會(huì)得到一個(gè) Type 'Student' does not conform to protocol 'Codable' 的編譯錯(cuò)誤,這一點(diǎn)還是有點(diǎn)坑的。不過(guò)在編譯時(shí)給 CodingKeys 補(bǔ)全其他默認(rèn)的屬性的聲明在理論上是可行的,期待蘋(píng)果后續(xù)的優(yōu)化了。

可選值

有些字段有可能會(huì)是空值。還是用學(xué)生的出生日期來(lái)舉例,假設(shè)有些學(xué)生的出生日期沒(méi)有統(tǒng)計(jì)到,這時(shí)候后臺(tái)返回?cái)?shù)據(jù)格式有兩種選擇,一種是對(duì)于沒(méi)有出生日期的數(shù)據(jù),直接不包含 birth_date 字段,另一種是指定為空值:"birth_date": null

對(duì)于這兩種形式,都只需要將 birthday 屬性聲明為可選值即可正常解析:

...
var birthday: Date?
...

解析 JSON 數(shù)組

Codable 協(xié)議同樣支持?jǐn)?shù)組類(lèi)型,只需要滿(mǎn)足一個(gè)前提:只要數(shù)組中的元素實(shí)現(xiàn)了 Codable 協(xié)議,數(shù)組將自動(dòng)獲得 Codable 協(xié)議的實(shí)現(xiàn)。

使用 JSONDecoder 解析時(shí)只需要指定類(lèi)型為對(duì)應(yīng)的數(shù)組即可:

do {
    let students = try JSONDecoder().decode([Student].self, from: data)
} catch {
    // 異常處理
}

歸檔數(shù)據(jù)

歸檔數(shù)據(jù)使用 Encodable 協(xié)議,使用方式與 Decodable 一致。

導(dǎo)出為 JSON

將數(shù)據(jù)模型轉(zhuǎn)換為 JSON 與解析過(guò)程類(lèi)似,將 JSONDecoder 更換為 JSONEncoder 即可:

let data = try JSONEncoder().encode(xiaomin)
let json = String(data: data, encoding: .utf8)

JSONEncoder 有一個(gè) outputFormatting 的屬性,可以指定輸出 JSON 的排版風(fēng)格,看定義:

public enum OutputFormatting {
    
    /// Produce JSON compacted by removing whitespace. This is the default formatting.
    case compact
    
    /// Produce human-readable JSON with indented output.
    case prettyPrinted
}
  • compact

    默認(rèn)的 compact 風(fēng)格會(huì)移除 JSON 數(shù)據(jù)中的所有格式信息,比如換行、空格和縮緊等,以減小 JSON 數(shù)據(jù)所占的空間。如果導(dǎo)出的 JSON 數(shù)據(jù)用戶(hù)程序間的通訊,對(duì)閱讀要求不高時(shí),推薦使用這個(gè)設(shè)置。

  • prettyPrinted

    如果輸出的 JSON 數(shù)據(jù)是用來(lái)閱讀查看的,那么可以選擇 prettyPrinted,這時(shí)候輸出的 JSON 會(huì)自動(dòng)進(jìn)行格式化,添加換行、空格和縮進(jìn),以便于閱讀。類(lèi)似于上面文中使用的 JSON 排版風(fēng)格。

屬性列表(PropertyList)

Codable 協(xié)議并非只支持 JSON 格式的數(shù)據(jù),它同樣支持屬性列表,即 mac 上常用的 plist 文件格式。這在我們做一些系統(tǒng)配置之類(lèi)的工作時(shí)會(huì)很有用。

屬性列表的解析和歸檔秉承了蘋(píng)果API一貫的簡(jiǎn)潔易用的特點(diǎn),使用方式 JSON 格式一致,并不需要對(duì)已經(jīng)實(shí)現(xiàn)的 Codable 協(xié)議作任何修改,只需要將 JSONEncoderJSONDecoder 替換成對(duì)應(yīng)的 PropertyListEncoderPropertyListDecoder 即可。

屬性列表本質(zhì)上是特殊格式標(biāo)準(zhǔn)的 XML 文檔,所以理論上來(lái)說(shuō),我們可以參照系統(tǒng)提供的 Decoder/Encoder 自己實(shí)現(xiàn)任意格式的數(shù)據(jù)序列化與反序列化方案。同時(shí)蘋(píng)果也隨時(shí)可能通過(guò)實(shí)現(xiàn)新的 Decoder/Encoder 類(lèi)來(lái)擴(kuò)展其他數(shù)據(jù)格式的處理能力。這也正是文章開(kāi)頭所說(shuō)的,Codable 的能力并不止于此,它具有很大的可擴(kuò)展空間。

結(jié)語(yǔ)

到此 Codable 的核心用法基本講完了。相比目前比較常用的幾個(gè)框架:

ObjectMapper 使用范型機(jī)制進(jìn)行模型解析,但是需要手動(dòng)對(duì)每一個(gè)屬性寫(xiě)映射關(guān)系,比較繁瑣。我自己項(xiàng)目中也是用的這個(gè)框架,后來(lái)自己對(duì)其做了些優(yōu)化,利用反射機(jī)制對(duì)基本數(shù)據(jù)類(lèi)型實(shí)現(xiàn)了自動(dòng)解析,但是自定義類(lèi)型仍然需要手動(dòng)寫(xiě)映射,并且必須繼承實(shí)現(xiàn)了自動(dòng)解析的 Model 基類(lèi),限制較多。

SwiftyJSON 簡(jiǎn)單了解過(guò),其本質(zhì)其實(shí)只是將 JSON 解析成了字典類(lèi)型的數(shù)據(jù),而實(shí)際使用時(shí)依然需要使用下標(biāo)方式去取值,非常繁瑣且容易出錯(cuò),不易閱讀和維護(hù),個(gè)人認(rèn)為這是很糟糕的設(shè)計(jì)。

HandyJSON 是阿里推出的框架,思路與 Codable 殊途同歸,之前也用過(guò)一陣,當(dāng)時(shí)因?yàn)閷?duì)枚舉和 Date 等類(lèi)型的支持還不夠完善,最終還是用回了ObjectMapper。不過(guò)目前看來(lái)完善程度已經(jīng)很高了,或許可以再次嘗試踩下坑。

總體來(lái)說(shuō),Codable 作為語(yǔ)言層面對(duì)模型解析的支持方案,有其自身的優(yōu)勢(shì)。不過(guò)在靈活性上稍有欠缺,對(duì)自定義字段的支持也還不夠人性化,期待后續(xù)的完善。

對(duì)于第三方庫(kù)來(lái)說(shuō),Codable 的推出既是一種挑戰(zhàn),但同時(shí)也是一個(gè)機(jī)遇,相信這些框架的作者們都會(huì)從 Codaable 獲得許多靈感來(lái)優(yōu)化提升自己的框架,在不久的將來(lái)制造一個(gè)百家爭(zhēng)鳴的局面。

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

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

  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 11,145評(píng)論 6 13
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,923評(píng)論 18 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,229評(píng)論 4 61
  • 一天當(dāng)中,我們起碼應(yīng)該,擠出十分鐘的寧?kù)o,讓自己有喘一口氣的閑暇,有一個(gè)可以讓陽(yáng)光,照進(jìn)來(lái)的間隙。至少給自己十分鐘...
    FAB小涵閱讀 148評(píng)論 0 0
  • 這句話說(shuō)的冷曦爸爸的心里一陣暖流流過(guò),他一直在笑,這下葉子也放下心來(lái)了也不拘謹(jǐn)了。這讓冷曦的心里松了一口氣,晚...
    丶葉子丶丿閱讀 254評(píng)論 0 0