Clojure component 設(shè)計(jì)哲學(xué)

這是 Clojure component 框架的簡介揩晴,里面涉及了關(guān)于狀態(tài)管理和依賴注入的設(shè)計(jì)思路蔫巩,值得借鑒髓梅。

Component 是一個微型的 Clojure 框架用于管理那些包含運(yùn)行時狀態(tài)的軟件組件的生命周期和依賴褒脯。

這主要是一種用幾個輔助函數(shù)實(shí)現(xiàn)的設(shè)計(jì)模式追他》啬迹可以被看成是使用不可變數(shù)據(jù)結(jié)構(gòu)的依賴注入風(fēng)格。

觀看 Clojure/West 2014 年的視頻 (YouTube, 40 minutes)(YouTube, 40分鐘)

發(fā)布和依賴信息

[Leingen] 依賴信息邑狸;

[com.stuartsierra/component "0.3.2"]

Maven 依賴信息

    <dependency>
      <groupId>com.stuartsierra</groupId>
      <artifactId>component</artifactId>
      <version>0.3.2</version>
    </dependency>

Gradle 依賴信息:

compile "com.stuartsierra:component:0.3.2"

依賴和兼容性

從 0.3.0 版本的 Component 開始懈糯,需要 1.7.0 及其以上版本的 Clojure 或 ClojureScript 以便提供 Conditional Read 支持。

0.2.3 版本的 Component 兼容 Clojure 1.4.0 及其以上版本单雾。

Component 需要依賴我的 dependency

討論

請?jiān)?Clojure Mailling List 提問赚哗。

介紹

顧名思義,一個 component 就是一組共享運(yùn)行時某些狀態(tài)的函數(shù)或過程硅堆。

一些 component 的例子:

  • 數(shù)據(jù)庫訪問:共享數(shù)據(jù)庫連接的查詢屿储、插入函數(shù)
  • 外部的 API 服務(wù):共享一個 HTTP 連接池的數(shù)據(jù)發(fā)送和接收函數(shù)
  • Web 服務(wù)器:共享所有應(yīng)用程序運(yùn)行時狀態(tài)枝冀,比如 session store挥唠,的函數(shù),用于處理不同的路由试疙。
  • 內(nèi)存式緩存:在一個共享的可變引用當(dāng)中獲取或者設(shè)置數(shù)據(jù)的函數(shù)茄菊,比如 Clojure 中的 Atom 或 Ref疯潭。

Component 和面向?qū)ο缶幊汤锏膶ο蠖x在理念上很類似赊堪。但這并不會動搖 Clojure 這門編程語言中純函數(shù)和不可變數(shù)據(jù)結(jié)構(gòu)的地位。大部分函數(shù)依然是函數(shù)竖哩,大多數(shù)數(shù)據(jù)也還是數(shù)據(jù)哭廉。而 Component 嘗試在函數(shù)式編程范式中輔助管理有狀態(tài)的資源。

Component 模型的優(yōu)點(diǎn)

大型應(yīng)用經(jīng)常由多個有狀態(tài)的進(jìn)程構(gòu)成相叁,這些進(jìn)程必須以特定的順序啟動和關(guān)閉遵绰。Component 模型讓這些關(guān)系變得比命令式代碼更直觀且表意。

Component 為構(gòu)建 Clojure 應(yīng)用提供了一些基本的指導(dǎo)钝荡,包括系統(tǒng)不同部分間的邊界街立。Component 提供了一些封裝以便將相關(guān)的實(shí)體聚合。每個 component 僅僅持有它所需的引用埠通,拒絕不必要的共享狀態(tài)赎离。有別于遍歷深層嵌套的 map,component 至多需要查找一個 map 就能獲取任何東西端辱。

與將可變的狀態(tài)分散到不同的命名空間的做法不同梁剔,應(yīng)用的所有有狀態(tài)的部分都可以被聚合到一起。某些情況下舞蔽,使用 component 可以不需要共享可變引用荣病。舉個例子,存儲當(dāng)前的數(shù)據(jù)庫資源鏈接渗柿。與此同時个盆,通過單個 system 對象維護(hù)所有可達(dá)狀態(tài),可以更加容易地從REPL 查看任意部分的應(yīng)用狀態(tài)朵栖。

出于測試目的颊亮,我們需要來回切換 stub 和 mock。Component 依賴模型讓 這種實(shí)現(xiàn)方式變得容易陨溅,因?yàn)椴恍枰蕾嚺c時間相關(guān)的構(gòu)造了终惑,比如with-redefs 或者 binding,它們在多線程的代碼中經(jīng)常會導(dǎo)致競爭條件门扇。

對于和應(yīng)用相關(guān)聯(lián)的狀態(tài)雹有,如果能連貫地創(chuàng)建并清除這些狀態(tài),就能夠保證無需啟動 JVM 就能快速構(gòu)建出開發(fā)環(huán)境臼寄,這也可以讓單元測試變得更快更獨(dú)立霸奕,由于創(chuàng)建和啟動一個 system 的開銷很小,所以每個測試都能夠創(chuàng)建一個新的 system 實(shí)例吉拳。

Component 模型的缺點(diǎn)

首先特別重要地质帅,當(dāng)應(yīng)用的所有部件都遵循相同的模式,那么這個框架會工作得很好。不過临梗,對于一個遺留系統(tǒng),除非進(jìn)行大量重構(gòu)稼跳,否則很難設(shè)施 Component 模型盟庞。

Component 假設(shè)所有的應(yīng)用狀態(tài)都是通過參數(shù)的形式傳遞給使用到它的函數(shù)中的。這樣會導(dǎo)致很難應(yīng)用到那些依賴全局或者單例引用的代碼汤善。

對于小型的應(yīng)用什猖,在 component 之間聲明依賴關(guān)系可能比手工按序啟動所有 component 來的麻煩。不過即便如此红淡,你也可以單獨(dú)使用 Lifecycle protocol 而不去使用依賴注入特性不狮,只不過 component 的附加價值就變小了。

框架產(chǎn)生的 system 對象是一個巨大并且有很多重復(fù)的復(fù)雜 map在旱。同樣的 component 可能會在 map 的多個地方出現(xiàn)摇零。盡管這種因?yàn)槌志没臄?shù)據(jù)結(jié)構(gòu)導(dǎo)致的重復(fù)產(chǎn)生的內(nèi)存開銷可以忽略不計(jì),但是 system map 一般都因?yàn)樘蠖鴽]法可視化出來以方便檢測桶蝎。

你必須顯式地在 component 之間指定依賴關(guān)系驻仅,代碼本身不能自動發(fā)現(xiàn)這些關(guān)系。

最后登渣,component 之間不允許有環(huán)依賴噪服。我相信環(huán)形依賴通常都暗示架構(gòu)有瑕疵,可以通過重新構(gòu)造應(yīng)用得以消除胜茧。在極少數(shù)的情況下粘优,環(huán)形依賴無法避免,那么你可以使用可變的引用來管理它呻顽,不過這就超出了 component 的范圍雹顺。

使用

(ns com.example.your-application
  (:require [com.stuartsierra.component :as component]))

創(chuàng)建 component

通過定義實(shí)現(xiàn)了Lifecycle協(xié)議的 Clojure record 創(chuàng)建一個 component。

(defrecord Database [host port connection]
  ;; Implement the Lifecycle protocol
  component/Lifecycle

  (start [component]
    (println ";; Starting database")
    ;; In the 'start' method, initialize this component
    ;; and start it running. For example, connect to a
    ;; database, create thread pools, or initialize shared
    ;; state.
    (let [conn (connect-to-database host port)]
      ;; Return an updated version of the component with
      ;; the run-time state assoc'd in.
      (assoc component :connection conn)))

  (stop [component]
    (println ";; Stopping database")
    ;; In the 'stop' method, shut down the running
    ;; component and release any external resources it has
    ;; acquired.
    (.close connection)
    ;; Return the component, optionally modified. Remember that if you
    ;; dissoc one of a record's base fields, you get a plain map.
    (assoc component :connection nil)))

可以選擇提供一個構(gòu)造函數(shù)芬位,接收 component 的初始化配置參數(shù)无拗,讓運(yùn)行時狀態(tài)為空。

(defn new-database [host port]
  (map->Database {:host host :port port}))

定義實(shí)現(xiàn)了 component 行為的函數(shù)昧碉,并接收一個 component 的實(shí)例作為參數(shù)英染。

(defn get-user [database username]
  (execute-query (:connection database)
    "SELECT * FROM users WHERE username = ?"
    username))

(defn add-user [database username favorite-color]
  (execute-insert (:connection database)
    "INSERT INTO users (username, favorite_color)"
    username favorite-color))

定義該 component 所依賴的其他 component。

(defrecord ExampleComponent [options cache database scheduler]
  component/Lifecycle

  (start [this]
    (println ";; Starting ExampleComponent")
    ;; In the 'start' method, a component may assume that its
    ;; dependencies are available and have already been started.
    (assoc this :admin (get-user database "admin")))

  (stop [this]
    (println ";; Stopping ExampleComponent")
    ;; Likewise, in the 'stop' method, a component may assume that its
    ;; dependencies will not be stopped until AFTER it is stopped.
    this))

不用把 Component 的依賴傳入構(gòu)造函數(shù)
System 負(fù)責(zé)把運(yùn)行時依賴注入到其中的 Component被饿,下個章節(jié)會提到:

(defn example-component [config-options]
  (map->ExampleComponent {:options config-options
                          :cache (atom {})}))

System

component 被組合到 system 中四康。一個 system 就是一個知道如果啟停其他 component 的 component。它也負(fù)責(zé)將依賴注入到 component 中狭握。

創(chuàng)建 system 最簡單的方式就是使用system-map函數(shù)闪金,就像hash-map或者array-map構(gòu)造方法一樣,接收一系列的 key/value 對。Key 在 system map 中都是 keyword哎垦,Value 在其中則是 Component 的實(shí)例囱嫩,一般是 record 或者 map。

(defn example-system [config-options]
  (let [{:keys [host port]} config-options]
    (component/system-map
      :db (new-database host port)
      :scheduler (new-scheduler)
      :app (component/using
             (example-component config-options)
             {:database  :db
              :scheduler :scheduler}))))

使用using函數(shù)在 component 之間指定依賴關(guān)系漏设。using接收一個component 和一組描述依賴的 key墨闲。

如果 component 和 system 使用了相同的 key,那么你可以用一個 vector 的 key 指定依賴郑口。

    (component/system-map
      :database (new-database host port)
      :scheduler (new-scheduler)
      :app (component/using
             (example-component config-options)
             [:database :scheduler]))
             ;; Both ExampleComponent and the system have
             ;; keys :database and :scheduler

如果 component 和 system 使用不同的 key鸳碧,那么得以 {:component-key :system-key} 的方式指定依賴,也就是犬性,using 的 key 和 component 中的 key 匹配瞻离,而 value 則和 System 中的 key 匹配。

    (component/system-map
      :db (new-database host port)
      :sched (new-scheduler)
      :app (component/using
             (example-component config-options)
             {:database  :db
              :scheduler :sched}))
        ;;     ^          ^
        ;;     |          |
        ;;     |          \- Keys in the system map
        ;;     |
        ;;     \- Keys in the ExampleComponent record

system map 提供了自己對于 Lifecycle 協(xié)議的實(shí)現(xiàn)乒裆,使用依賴信息(存儲在每個 component 的元數(shù)據(jù))以正確的順序啟動 component套利。

在開始啟動每個 component 之前,System 會基于 using 提供的元數(shù)據(jù) assoc 它的依賴缸兔。

還是用上面的例子日裙,ExampleComponent 將會像下面那樣啟動起來。

(-> example-component
    (assoc :database (:db system))
    (assoc :scheduler (:sched system))
    (start))

調(diào)用stop方法關(guān)停 System惰蜜,這會逆序地關(guān)閉每個 component昂拂,然后重新關(guān)聯(lián)每個 component 的依賴。

什么時間給 component 關(guān)聯(lián)上依賴是無關(guān)緊要的抛猖,只要發(fā)生在調(diào)用start方法之前格侯。如果你事先知道 system 中所有 component 的名字,你就可以選擇添加元數(shù)據(jù)到 component 的構(gòu)造方法中:

(defrecord AnotherComponent [component-a component-b])

(defrecord AnotherSystem [component-a component-b component-c])

(defn another-component []   ; constructor
  (component/using
    (map->AnotherComponent {})
    [:component-a :component-b]))

作為可選項(xiàng)财著,component 依賴可以通過 system-using 方法給所有 component 一次性指定联四,接收一個從 component 名稱指向其依賴的 map。

(defn example-system [config-options]
  (let [{:keys [host port]} config-options]
    (-> (component/system-map
          :config-options config-options
          :db (new-database host port)
          :sched (new-scheduler)
          :app (example-component config-options))
        (component/system-using
          {:app {:database  :db
                 :scheduler :sched}}))))

生產(chǎn)環(huán)境的入口

component 并沒有規(guī)定你如何存儲 system map 或者使用包含其中的 component撑教,這完全看你個人朝墩。

通常區(qū)別開發(fā)和生產(chǎn)的方法是:

在生產(chǎn)環(huán)境下,system map 是生命短暫的伟姐,它被用于啟動所有 component收苏,然后就銷毀了。

當(dāng)你的應(yīng)用啟動后愤兵,例如在main函數(shù)中鹿霸,構(gòu)造了一個system的實(shí)例并且在其上調(diào)用了component/start方法,之后就無法控制在你的應(yīng)用中代表“入口點(diǎn)”的一個或多個 component 了秆乳。

舉個例子懦鼠,你有個 web server component 開始監(jiān)聽 HTTP 請求钻哩,或者是一個事件輪訓(xùn)的 component 在等待輸入。這些 component 每個都可以在它生命周期的start方法中創(chuàng)建一個或者多個線程肛冶。那么main函數(shù)可以是這樣的:

(defn main [] (component/start (new-system)))

注意:你還是得保證應(yīng)用的主線程一直運(yùn)行著以免JVM關(guān)閉了街氢。一種方法就是阻塞主線程,等待關(guān)閉的信號睦袖;另一種方法就是使用Thread/join(轉(zhuǎn)讓)主線程給你的 component 線程阳仔。

該方式也能配合類似 Apache Commons Daemon 的命令行驅(qū)動一起很好地工作。

開發(fā)環(huán)境的入口

開發(fā)過程中扣泊,一般引用一個 system map 然后在 REPL 中測試它是很有用的。

最簡單的方式就是在 development 命名空間中使用def定義一個持有 system map 的 Var嘶摊。使用alter-var-root啟停延蟹。

RELP 會話的例子:

(def system (example-system {:host "dbhost.com" :port 123}))
;;=> #'examples/system

(alter-var-root #'system component/start)
;; Starting database
;; Opening database connection
;; Starting scheduler
;; Starting ExampleComponent
;; execute-query
;;=> #examples.ExampleSystem{ ... }

(alter-var-root #'system component/stop)
;; Stopping ExampleComponent
;; Stopping scheduler
;; Stopping database
;; Closing database connection
;;=> #examples.ExampleSystem{ ... }

查看 reloaded 模板獲取更詳細(xì)的例子

Web Applications

很多 Clojure 的 web 框架和教程都圍繞一個假設(shè),即 handler 會作為全局的 defn 存在叶堆,而無需任何上下文阱飘。在這個假設(shè)底下,如果不把 handler 中的任意應(yīng)用級別的上下文變成全局的def虱颗,就很難去使用它沥匈。

component 傾向于假設(shè)任意 handler 函數(shù)都會接收 state/context 作為其參數(shù),而不依賴任何全局的狀態(tài)忘渔。

為了調(diào)和這兩種方法高帖,就得創(chuàng)建一種 handler 方法作為 Lifecycle start 方法的包含一個或多個 component 的閉包。然后把這個閉包作為 handler 傳遞給 web 框架畦粮。

大部分 web 框架或者類庫都會提供一個靜態(tài)的defroutes或者類似的宏會提供一個相等的非靜態(tài)的routes方法來創(chuàng)建一個閉包散址。

看上去像這樣:

(defn app-routes
  "Returns the web handler function as a closure over the
  application component."
  [app-component]
  ;; Instead of static 'defroutes':
  (web-framework/routes
   (GET "/" request (home-page app-component request))
   (POST "/foo" request (foo-page app-component request))
   (not-found "Not Found")))

(defrecord WebServer [http-server app-component]
  component/Lifecycle
  (start [this]
    (assoc this :http-server
           (web-framework/start-http-server
             (app-routes app-component))))
  (stop [this]
    (stop-http-server http-server)
    this))

(defn web-server
  "Returns a new instance of the web server component which
  creates its handler dynamically."
  []
  (component/using (map->WebServer {})
                   [:app-component]))

更多高級使用方式

錯誤

在啟停 system 的時候,如果任何 component 的 start 或者 stop 方法拋出了異常宣赔,start-system 或者 stop-system 方法就會捕獲并把它包裝成 ex-info 異常和一個包含下列 key 的 ex-data map预麸。

  • :system是當(dāng)前的 system,包含所有已經(jīng)啟動的 component儒将。

  • :component是導(dǎo)致該異常的 component 及其已經(jīng)注入的依賴吏祸。

這個 component 拋出的原始異常,可以調(diào)用該異常的 .getCause 方法獲取钩蚊。

Component 不會對 component 進(jìn)行從錯誤中恢復(fù)的嘗試贡翘,不過你可以使用 :system 附著到這個 exception 然后清除任何部分構(gòu)造的var

由于 component map 可能很大且有許多的重復(fù),你最好不要記日志或者打印出異常两疚。這個 ex-without-components 幫助方法會從 exception 中去除大對象床估。

ex-component? 幫助方法可以告訴你一個異常是否來源于 component 或者被一個 component 包裝過。

冪等

你可能發(fā)現(xiàn)了把 startstop 方法定義成冪等的是很有用的诱渤。例如丐巫,僅僅當(dāng) component 沒有啟動或者沒有關(guān)閉時才進(jìn)行操作。

(defrecord IdempotentDatabaseExample [host port connection]
  component/Lifecycle
  (start [this]
    (if connection  ; already started
      this
      (assoc this :connection (connect host port))))
  (stop [this]
    (if (not connection)  ; already stopped
      this
      (do (.close connection)
          (assoc this :connection nil)))))

Component 沒有要求 stop/start 是冪等的,但是在發(fā)生錯誤后递胧,冪等會易于清除狀態(tài)碑韵。由于你可以隨意地在任何東西上調(diào)用 stop 方法。

除此之外缎脾,你可以把 stop 包在 try/catch 中從而忽略所有異常祝闻。這種方式下,導(dǎo)致一個 component 停止工作的錯誤并不能保證其他 component 完全關(guān)閉遗菠。

(try (.close connection)
  (catch Throwable t
    (log/warn t "Error when stopping component")))

無狀態(tài)的 Component

Lifecycle 的默認(rèn)實(shí)現(xiàn)是個空操作联喘。如果一個 component 省略了 Lifecycle 的協(xié)議,它還是能參與到依賴注入的過程中辙纬。

無需 lifecycle 的 component 可以是一個普通的 Clojure map豁遭。

對于任何實(shí)現(xiàn)了 Lifecycle 的 component,你不能忽略 start 或者 stop贺拣,必須都提供蓖谢。

Reloading

我開發(fā)了這種結(jié)合我的"reloaded"工作流的 workflow 模式,為了進(jìn)行開發(fā)譬涡,我會創(chuàng)建一個 user 的命名空間如下:

(ns user
  (:require [com.stuartsierra.component :as component]
            [clojure.tools.namespace.repl :refer (refresh)]
            [examples :as app]))

(def system nil)

(defn init []
  (alter-var-root #'system
    (constantly (app/example-system {:host "dbhost.com" :port 123}))))

(defn start []
  (alter-var-root #'system component/start))

(defn stop []
  (alter-var-root #'system
    (fn [s] (when s (component/stop s)))))

(defn go []
  (init)
  (start))

(defn reset []
  (stop)
  (refresh :after 'user/go))

使用說明

不要把 system 到處亂傳

頂級的system記錄只是用來啟停其它 component 的闪幽,主要是為了交互開發(fā)時比較方便。

上面的 “xx入口”有詳細(xì)介紹涡匀。

任何函數(shù)都不應(yīng)該接收 system 作為參數(shù)

應(yīng)用層的函數(shù)絕對不該接收 system 作為參數(shù)盯腌,因?yàn)楣蚕砣譅顟B(tài)是沒有道理的。

除此之外陨瘩,每個函數(shù)都應(yīng)該依據(jù)至多依賴一個 component 的原則來定義自己腊嗡。

如果一個函數(shù)依賴了幾個 component,那么它應(yīng)該有一個自己的 component拾酝,在這個 component 里包含對其它 component 的依賴燕少。

任何 component 都不應(yīng)該知曉包含自己的 system

每個 component 只能接受它所依賴 component 的引用。

不要嵌套 system

在技術(shù)上蒿囤,嵌套system-map是可能的客们。但是,這種依賴的影響是微妙的材诽,并且也容易迷惑人底挫。

你應(yīng)該給每個 component 唯一的鍵,然后把他們合并到同一個 system 中脸侥。

其它類型的 component

應(yīng)用或者業(yè)務(wù)邏輯可能需要一個或多個 component 來表達(dá)建邓。

當(dāng)然,component 記錄除了Lifecycle睁枕,可能還實(shí)現(xiàn)了其它的協(xié)議官边。

除了map和record沸手,任何類型的對象都可以是 component,除非它擁有生命周期和依賴注簿。舉個例子契吉,你可以把一個簡單的Atom或者core.async Channel放到 system map 中讓其它 component 依賴。

測試替身

component 的不同實(shí)現(xiàn)(舉個例子诡渴,測試樁)可以在調(diào)用start之前捐晶,通過assoc注入到system當(dāng)中。

寫給庫作者的注意事項(xiàng)

Component旨在作為一個工具提供給應(yīng)用程序妄辩,而不是可復(fù)用的庫惑灵。我不希望通用庫在使用它的應(yīng)用程序上強(qiáng)加任何特定的框架。

也就是說眼耀,庫作者可以通過遵循下面的指導(dǎo)原則輕松地讓應(yīng)用程序?qū)⑵鋷旌虲omponent 模式結(jié)合起來使用:

  • 絕對不要創(chuàng)建全局的可變狀態(tài)(舉個例子泣棋,用def定義的Atom或者Ref)

  • 絕對不要依賴動態(tài)綁定來傳達(dá)狀態(tài)(例如,當(dāng)前數(shù)據(jù)庫的鏈接)畔塔,除非該狀態(tài)有必要局限于單個線程。

  • 絕對不要頂級的源代碼文件上操作副作用鸯屿。

  • 用單個數(shù)據(jù)結(jié)構(gòu)封裝庫依賴的運(yùn)行時狀態(tài)澈吨。

  • 提供構(gòu)建和銷毀數(shù)據(jù)結(jié)構(gòu)的函數(shù)。

  • 把任何庫函數(shù)依賴的封裝好的運(yùn)行時狀態(tài)作為參數(shù)傳進(jìn)來寄摆。

定制化

system map 只是實(shí)現(xiàn)Lifecycle協(xié)議的記錄谅辣,通過兩個公共函數(shù),start-systemstop-system婶恼。這兩個函數(shù)只是其它兩個函數(shù)的特例桑阶,
update-systemupdate-system-reverse。 (在0.2.0中添加)

例如勾邦,您可以將自己的生命周期函數(shù)定義為新的協(xié)議蚣录。你甚至不必使用協(xié)議和記錄;多方法和普通的map也可以。

update-systemupdate-system-reverse都是將函數(shù)作為參數(shù)眷篇,并在system的每個 component 上調(diào)用它萎河。遵循這種方式,他們會把更新后的依賴關(guān)聯(lián)到每個 component 上蕉饼。

update-system函數(shù)按照 component 依賴順序進(jìn)行更新:每個 component 將在其依賴之后被調(diào)用虐杯。
update-system-reverse函數(shù)按反向依賴順序排列:每個 component 將在其依賴項(xiàng)之前調(diào)用。

使用identity函數(shù)調(diào)用update-system相當(dāng)于只使用 Component 的依賴注入部分而不使用Lifecycle昧港。擎椰。

參考,更多信息



于 2018-10-08

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末创肥,一起剝皮案震驚了整個濱河市达舒,隨后出現(xiàn)的幾起案子值朋,更是在濱河造成了極大的恐慌,老刑警劉巖休弃,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吞歼,死亡現(xiàn)場離奇詭異,居然都是意外死亡塔猾,警方通過查閱死者的電腦和手機(jī)篙骡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丈甸,“玉大人糯俗,你說我怎么就攤上這事∧览蓿” “怎么了得湘?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長顿仇。 經(jīng)常有香客問我淘正,道長,這世上最難降的妖魔是什么臼闻? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任鸿吆,我火速辦了婚禮,結(jié)果婚禮上述呐,老公的妹妹穿的比我還像新娘惩淳。我一直安慰自己,他們只是感情好乓搬,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布思犁。 她就那樣靜靜地躺著,像睡著了一般进肯。 火紅的嫁衣襯著肌膚如雪激蹲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天江掩,我揣著相機(jī)與錄音托呕,去河邊找鬼。 笑死频敛,一個胖子當(dāng)著我的面吹牛项郊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斟赚,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼着降,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拗军?” 一聲冷哼從身側(cè)響起任洞,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤蓄喇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后交掏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妆偏,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年盅弛,在試婚紗的時候發(fā)現(xiàn)自己被綠了钱骂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡挪鹏,死狀恐怖见秽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情讨盒,我是刑警寧澤解取,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站返顺,受9級特大地震影響禀苦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜遂鹊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一振乏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧稿辙,春花似錦、人聲如沸气忠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旧噪。三九已至吨娜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淘钟,已是汗流浹背宦赠。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留米母,地道東北人勾扭。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像铁瞒,于是被迫代替她去往敵國和親妙色。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

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