Chapter1:Node.js Design Fundamentals(下)

Module definition patterns

? ? 除了作為加載依賴的機制之外,模塊系統也是一種用于定義API的工具。正如針對其他與API設計相關的問題一樣,所要考慮的主要因素是:在公有函數和私有函數之間獲得平衡。其目的是最大限度的實現信息隱藏和API可用性,與此同時與其他軟件質量指標(可擴展性和代碼復用性)相平衡。

? ? 在本節中,我們將分析一些最流行的設計模式來定義Node中的模塊。每一種模式,都有它自身針對信息隱藏、可擴展性和代碼復用的平衡。

Named exports

? ? 暴露公共API最基本的方式是使用命名導出,它包括將所有要公開的值賦予exports (或module.exports)所引用的對象。在這種方式下,生成的導出對象變成了容器或者命名空間來容納一系列相關的函數。

以下代碼展示了一個采用這種模式的模塊:

//file logger.js

exports.info = function(message) {

? ? console.log('info: ' + message);

};

exports.verbose = function(message) {

? ? console.log('verbose: ' + message);

};

? ? 所導出的函數將作為已加載的模塊屬性而變得可用,如以下代碼所示:

//file main.js

var logger = require('./logger');

logger.info('This is an informational message');

logger.verbose('This is a verbose message');

大多數的Node核心模塊使用這種模式

? ? CommonJS規范只允許通過使用導出變量來暴露公共的成員。因此,命名導出模式是唯一能夠實際與CommonJS規范相互兼容的規范。使用module.exports是由Node提供的一種拓展來支持更范圍更廣的模塊定義模式,如圖我們竟會在下一節所將看到的一樣。

Exporting a function

一個模塊導出一個構造器是一個模塊導出一個函數的特例。不同之處在于,使用這種新模式,我們允許用戶使用構造器創造新的實例,但是同時我們要給它們能力去繼承原型并且構造新的類。接下來是一個以這種模式實現的例子:

//file logger.js

function Logger(name) {

? ? this.name = name;

};

Logger.prototype.log = function(message) {

? ? console.log('[' + this.name + '] ' + message);

};

Logger.prototype.info = function(message) {

? ? this.log('info: ' + message);

};

Logger.prototype.verbose = function(message) {

? ? this.log('verbose: ' + message);

};

module.exports = Logger;

? ? 并且,我們可以使用之前的模塊,如下所示:

//file logger.js var Logger = require('./logger');

var dbLogger = new Logger('DB');

dbLogger.info('This is an informational message');

var accessLogger = new Logger('ACCESS');

accessLogger.verbose('This is a verbose message');

? ? 導出一個構造器始終為模塊提供了一個單獨的入口點,但是與子棧模式相比相比,它暴露了模塊內部的內容。然而,與此同時,當需要去擴展它的函數時,它提供了更多的能力。

? ?這種模式的變化包括應用在一個確保不使用新的指令調用的機制上。這個小技巧允許我們使用我們的模塊作為一個工廠。以下的代碼向你展示了這個機制如何工作:

function Logger(name) {

? ? if(!(this instanceof Logger)) {

? ? ? ? return new Logger(name);

? ? }

? ? this.name = name;

};

? ? 技巧很簡單,我們檢查它是否存在并且作為日志記錄器的一個實例。如果這些條件的其中任意一個是錯誤的,這意味著Logger()函數被調用的時候不使用new,所以我們繼續適當的創建實例并將它返回給調用者。這種技術允許我們將模塊作為工廠使用,如下列代碼所述:

//file logger.js

var Logger = require('./logger');

var dbLogger = Logger('DB');

accessLogger.verbose('This is a verbose message');

Exporting an instance

我們可以利用require()的緩存機制來輕松定義狀態實例(對象伴隨著一個狀態由構造器或者工廠產生),這個實例可以再模塊之間被共享。以下代碼展示了這種模式:

//file logger.js

function Logger(name) {

? ? this.count = 0;

? ? this.name = name;

};

Logger.prototype.log = function(message) {

? ? this.count++;

? ? console.log('[' + this.name + '] ' + message);

};

module.exports = new Logger('DEFAULT');

? ? 新定義的模塊可以在之后被如下方式使用:

//file main.js

var logger = require('./logger');

logger.log('This is an informational message');

? ? 因為模塊是緩存的,每個引入logger模塊的模塊實際上總要檢索這對象相同的實例,由此來分享它的狀態。這個模式非常像是創建了一種單例模式,然而,它不能確保遍及整個應用范圍時這個實例的唯一性(就想它所發生在傳統單例模式中那樣)。當分析解析算法時,我們實際上看到,一個模塊會在一個應用程序的依賴樹種反復被安裝很多次。這一結果伴隨著同一個模塊的很多個實例,全部都運行在同一個Node應用的上下文上。在第五章,寫模塊的部分中,我們將分析導出具有狀態的實例所帶來的影響和一些我們可以作為替代的模式。

? ? 針對我們剛才所描述模式的一個拓展,包括:暴露一個構造器來用來創建這個實例,更近一步的針對這個實例本身。這一機制允許用戶去創建同一個對象中新的實例。甚至在需要的時候能夠擴展這個實例。為了實現這個設計,我們只需要將一個新的屬性賦予給實例,這個過程如下代碼所示:

module.exports.Logger = Logger;

? ? 然后,我們可以使用導出控制器來創建這個類其他的實例,如下所示:

var customLogger = new logger.Logger('CUSTOM');

customLogger.log('This is an informational message');

? ? 從可用性的角度來看,這種機制與將導出函數用作命名空間的做法相似。模塊導出一個對象的默認實例,作為我們大多數時間需要使用的函數部件。然而更多的特性,比如建立新實例或者繼承對象的特性,仍然可以通過很少暴露的屬性來使其可用。

Modifying other modules or the global scope

? ? 模塊甚至可以到處任何內容,這似乎看起來有一點不恰當,然而,我們應該不會忘記一個模塊可以改變全局作用域以及所有在它之內的對象,包括其他在緩存中的模塊。請注意這些做法在通常情況下被認為是不好的實踐,但是因為這種模式在某些場景下(比如說:測試環境下)可以是有用且安全的,并且有時候是在非生產環境下使用的,因此它很值得去了解和理解。所以,我們說一個模塊可以改變其他摩羯和全局作用域中的對象。好的,這種方式被稱為猴子補丁,這種機制通常用來指在運行時間修改現存對象或者改變或者擴充他們的行為來應用臨時修補的實踐。

? ? 以下實例向你展示了,如何向其他模塊添加一個函數:

//file patcher.js

// ./logger is another module

require('./logger').customMessage = function() {

? ? console.log('This is a new functionality');

};

使用我們新的補丁(patcher)模塊,將會很容易寫出如下代碼:

//file main.js

require('./patcher');

var logger = require('./logger');

logger.customMessage();

? ? 在前面的代碼中,補丁patcher必須在使用logger日志模塊之前被第一次引入,來允許補丁patch被使用。

? ? 這里所描述的技術都是應用起來很危險的。最主要的關注點如下:具備一個模塊可以改變全局的命名空間或者其他模塊,這個過程是一個具有副作用的操作。換句話來說,它影響它們作用域以外實體的狀態,這將會產生不能被預知的影響,特別是當很多模塊與相同的實體交互。假設有兩個不同的模塊試圖去設置同一個全局變量。或者改變同一個模塊的同一個屬性。影響將是無法預測的(哪個模塊能夠贏?)但是更重要的是,它將對整個應用程序產生影響。

The observer pattern

? ? 另一個重要但是基礎的模式被采用在Node中,是觀察observer模式。與反應器reactor、回調callbacks、以及模塊module一樣,這個模式是平臺的基石,并且是針對使用很多node核心模塊與用戶平面模塊的絕對先決條件。

? ? 觀察者模式是一種針對Node反應特性建模的理想的解決方式,而且對于回調來說是一種理想的補充。讓我們給出一個正確的定義如下:

? ? 觀察者模式(observe):定義一個對象(被稱為主體),其可以通知一系列的觀察者(或者被稱為監聽者),當狀態的一個改變發生時。

? ? 這種模式與回調模式最主要的區別是主體實際上可以通知很多觀察者,而一個傳統的持續傳遞模式的回調一般情況下只能傳遞它的結果給一個唯一的監聽者,即那個回調。

The EventEmitter

? ? 在傳統的面向對象編程中,觀察者模式需要接口、具體的類、層級概念。而在Node當中,一切都變得更加簡潔,觀察者模式已經成為核心并且通過EventEmitter類來提供使用。EventEmitter類允許我們注冊一個或者多個函數來作為監聽者,這些監聽器將會被調用當一個特殊的事件類型開始啟用。下面的圖象直觀的展示了這種概念:


? ? EventEmitter是一種原型,并且它是從events核心模塊導出的。下述代碼展示了我們如何對它進行引用:

var EventEmitter = require('events').EventEmitter;

var eeInstance = new EventEmitter();

? ? EventEmitter的關鍵方法被給出,如下所示:

?on(event, listener):此方法允許你注冊一個新的監聽器(a function)來針對于給定的事件類型(a string)進行監聽。

?once(event, listener):此方法注冊一個新的監聽器,在事件第一次被發射后會刪除這個監聽器。

?emit(event, [arg1], […]):這種方法會產生一個新的事件并提供附加的參數并傳遞給監聽器。

?removeListener(event, listener):這個方法針對一個特殊的事件類型,刪除一個監聽器。

? ? 上述的所有方法將會返回EventEmitter實例來允許鏈式。監聽器函數具有簽名,function([arg1], […]),所以它只有當事件發射時,簡單的接受所提供的參數。在監聽器內部,這種參考EventEmitter實例的做法產生了事件。

? ? 我們已經看到在監聽器和傳統的Node回調之間巨大的不同。特別是,它的第一個參數不是error,而是在它被調用的時刻,可以放置任何傳遞給emit()函數的數據。

Create and use an EventEmitter

? ? 讓我們來看看如何在實踐中使用EventEmitter。最簡潔的方法是創建一個新實例并直接使用它,以下代碼展示了一個函數,當一個特殊模式在文件列表中找到時,它可以使用EventEmitter來實時通知所有用戶:

var EventEmitter = require('events').EventEmitter;

var fs = require('fs');

function findPattern(files, regex) {

? ? var emitter = new EventEmitter();

? ? files.forEach(function(file) {

? ? ? ? fs.readFile(file, 'utf8', function(err, content) {

? ? ? ? ? ? if(err)

? ? ? ? ? ? ? ? return emitter.emit('error', err);

? ? ? ? ? ? emitter.emit('fileread', file);

? ? ? ? ? ? var match = null;

? ? ? ? ? ? if(match = content.match(regex))

? ? ? ? ? ? ? ? match.forEach(function(elem) {

? ? ? ? ? ? ? ? ? ? emitter.emit('found', file, elem);

? ? ? ? ? ? ? ? ?});

? ? ? ? ? ? ?});

? ? });

? ? return emitter;

}

? ? 由之前函數創建的EventEmitter,將會創造以下三種事件:

? fileread:這種事件發生在當一個文件被讀取時。

? found:這種發生在當一個匹配被找到時。

? error:這種事件發生在當文件在讀取過程中一個錯誤發生時。

? ? 讓我們來看看,我們的findPattern()函數如何被使用:

findPattern(

? ? ['fileA.txt', 'fileB.json'],

? ? /hello \w+/g

)

.on('fileread', function(file) {

? ? console.log(file + ' was read');

})

.on('found', function(file, match) {

? ? console.log('Matched "' + match + '" in file ' + file);

})

.on('error', function(err) {

? ? console.log('Error emitted: ' + err.message);

});

? ? 在前面的例子中,我們針對于由EventEmitter創建的三種類型事件,每一種事件都注冊一個監聽器,而EventEmitter由findPattern()函數創建。

Propagating errors

? ? EventEmitter和回調一樣,不能拋出異常,當錯誤條件發生時,如果事件發射是異步的,它們將在事件循環(event loop)里丟失。相反,約定發射一個特殊事件,稱為error,并將Error對象作為參數傳遞。而這正是我們在之前定義的函數findPattern()中所做的事情。

? ? 針對error事件注冊一個監聽器一直是一種很好的實踐,因為Node將會以特殊的方式對待它,并且當沒有相關聯的監聽器被找到時,可以自動拋出異常并且從程序中退出。

Make any object observable

? ? 有時候,從EventEmitter類中直接創建一個新的觀察對象是不夠的,因為這使得提供函數,其功能超越了僅僅針對于新事件創造的職能。事實上更常見的是,建立一種通用對象的觀察,這個是通過繼承EventEmitter類而變得可能的。

? ? 為了論證這種模式,我們嘗試在對象中實現findPattern()函數,如下所示:

var EventEmitter = require('events').EventEmitter;

var util = require('util');

var fs = require('fs');

function FindPattern(regex) {

? ? EventEmitter.call(this);

? ? this.regex = regex; this.files = [];

}

util.inherits(FindPattern, EventEmitter);

FindPattern.prototype.addFile = function(file) {

? ? this.files.push(file);

? ? return this;

};

FindPattern.prototype.find = function() {

? ? var self = this;

? ? self.files.forEach(function(file) {

? ? ? ? fs.readFile(file, 'utf8', function(err, content) {

? ? ? ? ? ? if(err)

? ? ? ? ? ? ? ? return self.emit('error', err);

? ? ? ? ? ? self.emit('fileread', file);

? ? ? ? ? ? var match = null;

? ? ? ? ? ? if(match = content.match(self.regex))

? ? ? ? ? ? ? ? match.forEach(function(elem) {

? ? ? ? ? ? ? ? ? ? self.emit('found', file, elem);

? ? ? ? ? ? ? ? });

? ? ? ? ? ? });

? ? });

? ? return this;

};

? ? 我們所定義的FindPattern原型,繼承了EventEmitter,使用了核心模塊util所提供的inherits()函數。通過這種方式,它變成了完全可觀察類。以下是使用它的實例:

var findPatternObject = new FindPattern(/hello \w+/);

findPatternObject

? ? .addFile('fileA.txt')

? ? .addFile('fileB.json')

? ? .find() .on('found', function(file, match) {

? ? ? ? console.log('Matched "' + match + '" in file ' + file);

? ? ?})

? ? .on('error', function(err) {

? ? ? ? console.log('Error emitted ' + err.message);

? ? })

? ? 我們可以看到FindPattern如何擁有一套完整的方法,除此之外能夠被EventEmitter的函數所繼承。

? ? 這是一個在Node生態系統中非常通用的模式,例如,核心http模塊中服務器對象所定義的方法,比如:listen(), close(), setTimeout(),在內部它也是繼承于EventEmitter函數,由此允許它去創造事件(比如:request),當一個新的請求被接收時,或者連接時,當一個新的連接被建立時,或者關閉時,當服務器關閉時。

? ? 其他關于對象繼承自EventEmitter的例子是Node.js streams(流),我們將在第3章?Coding with Streams中更細節的分析流。

Synchronous and asynchronous events

? ? 正如回調一樣,事件可以是同步或者異步發射的,并且關鍵的是我們可以把這兩種方式在同一個EventEmitter中進行混合,但是更重要的是,當發射相同的事件類型時,避免創造我們在Unleashing Zalgo章節所討論過的同樣的問題。

? ? 發射同步與異步事件時,主要的區別在于,監聽器被注冊的方式。當事件異步發射時,用戶具有全部的時間來注冊新的監聽器,即使當EventEmitter已經被初始化了。因為事件被確保不會被拋棄,直到事件循環(event loop)的下一輪周期。這正是findPattern()函數中所發生的事情。我們在之前定義了這個函數,并且它代表了在大多數Node模塊中使用的通用方式。

? ? 相反,發射事件是同步的,需要所有的監聽器在EventEmitter函數開始發射任何事件之前被注冊。讓我們一起來看看一個例子:

function SyncEmit() {

? ? this.emit('ready');

}

util.inherits(SyncEmit, EventEmitter);

var syncEmit = new SyncEmit();

syncEmit.on('ready', function() {

? ? console.log('Object is ready to be used');

});

? ? 如果準備好的事件是異步發出的,之前的代碼將能夠完美工作。然而,事件是同步創建的,并且監聽器在事件已經發出之后被注冊,所以結果是:監聽器永遠 不會被調用。代碼不會向控制臺打印任何內容。

? ? 相對于回調而言,這里有一些情況下以同步方式使用EventEmitter是有意義的,能夠給它一個不同的意圖。因為這個原因,在EventEmitter的文檔中去強調它的表現是非常重要的,這樣能夠避免誤解和潛在的錯誤使用。

EventEmitter vs Callbacks

? ? 當定義異步接口時一個常見的困境是:去檢查是否使用EventEmitter或者只是簡單接收一個回調callback。通用的區分法則是語義:當結果必須以異步方式返回時,回調callback方式需要被使用。當需要討論一個剛剛發生的事情時,要轉而去使用事件方式。

? ? 但是除了這個簡單的原則之外,很多困惑由此產生:這兩種范式在大多時間下是等價的,并且允許你獲得相同的結果。考慮以下代碼為例:

function helloEvents() {

? ? var eventEmitter = new EventEmitter();

? ? setTimeout(function() {

? ? ? ? eventEmitter.emit('hello', 'world');

? ? }, 100);

? ? return eventEmitter;

}

function helloCallback(callback) {

? ? setTimeout(function() {

? ? ? ? callback('hello', 'world');

? ? }, 100);

}

? ? helloEvents() 和 helloCallback()這兩個函數,就功能而言是完全等價的。第一個函數通過使用事件來交流超時情況,第二個函數轉而使用回調函數來通知調用者,將事件類型(event type)作為參數而傳遞。但真正區分開它們的標準是:語義、可讀性、需要被實現和使用的代碼量。盡管我們不能給出一組確定的規則來在其中之一與另外一個模式之間進行選擇,一些建議仍然能夠幫助我們作出決定:

? ? 作為第一個觀察,我們可以說當需要支持不同類型的事件時,回調方式有一些局限性。事實上,我們仍然可以區分多個事件通過:傳遞事件類型作為回調的參數,或者通過接受很多回調函數,其中每一個都支持事件。然而,但這些事實上不會被認為是優雅的API。在這種情況下,EventEmitter方式可以提供更優雅的接口和更精簡的代碼。

? ? 另外一個EventEmitter方式更被偏向于使用的情境是:當一個事件發生很多次或不再發生時。一個回調,事實上,被認為僅僅能夠被調用一次,無論執行是成功還是失敗。事實上,當我們面對一個可能重復的情況,需要我們再次深入思考事件發生的語義特性,這似乎意味著對于一個事件來說需要去通信而不僅僅是作為一個結果。在這種情況下,EventEmitter方式是更被偏愛的一個選擇。

? ? 最后,一個使用API的回調只能通知特殊的回調,而使用EventEmitter函數,可以使眾多的監聽器接收同一個通知變為可能。

Combine callbacks and EventEmitter

? ? 也有一些情境下,EventEmitter方式可以與回調callback方式結合。當我們想要通過導出一個傳統的異步函數作為主功能,同時要提供豐富的特性并通過返回一個EventEmitter來完善控制時,想要實現最小接觸面原則時,這種模式非常有用。

? ? 這種模式的一個例子是使用node-glob模塊(一個庫,能夠提供全局范圍文件搜索功能)。模塊的主要入口點是它所導出的函數,具有以下簽名:

glob(pattern, [options], callback)

? ? 函數將模式作為第一個參數,接下來的參數是一組選項、以及一個回調函數,該函數被調用伴隨著與所提供模式匹配的文件列表。與此同時,該函數返回了一個EventEmitter來提供一個關于流程狀態的更細粒度的報告。舉個例子,當一個匹配通過監聽所匹配的事件而發生時,它很可能被實時通知,來獲得與最后一個事件相關的所有匹配文件列表,或者去確定流程是否因監聽廢止事件而人為中止。以下代碼展示了這種機制:

var glob = require('glob');

glob('data/*.txt', function(error, files) {

? ? console.log('All files found: ' + JSON.stringify(files));

}).on('match', function(match) {

? ? console.log('Match found: ' + match);

});

? ? 正如我們所看到的,在Node中暴露一個簡潔、干凈、并且最小化的入口點并且仍然提供高級或者次重要的特性,以第二種方式,是一種非常普遍的做法。并且將傳統回調callback方式與提到的EventEmitter方式相結合,是一種能達到這個目標的方案:

? ? 模式:創建一個函數接受一個回調,并且返回一個EventEmitter,由此提供一個簡潔但是干凈的入口點給主要功能,同時通過EventEmitter來發射一些更細粒度的事件。

Summary

? ? 在這一章中,我們看到了Node平臺是如何基于幾個重要原則來提供基礎來建立有效和可重用的代碼。在平臺背后的哲學和設計選擇,事實上,對于我們所創建的每個應用程序和模塊的表現來說都有著強烈影響。通常,對于一個從另一種技術中遷移出來的開發者來說,這些原則可能看起來并不熟悉,通常的本能反應是以嘗試尋找在一個世界中更加熟悉的模式挑戰這些改變,而這實際上需要思維方式上的真正轉變。一方面,反應器(reactor)模式的異步特性,需要不同編程風格的回調和稍后發生的工序,不需要擔心太多線程和競爭的條件。另一方面,模塊模式和它簡潔化、微小化的原則,針對代碼的可重用性、可維護性和可用性方面,創造了有趣的新場景。

? ? 最終除了顯然的技術優勢,比如說:快速、高效。基于JavaScript,Node吸引了如此多開發者的興趣是因為我們剛才所發現的那一系列原則。對很多人而言,把握世界的本質就像回到起源,用更人性化的方式編程來針對規模和復雜度進行優化,這就是為什么那么多開發者最后愛上了Node。

? ? 在下一章節中,我們將把經歷放在處理異步代碼的機制上,我們將看到回調如何輕易地變成了我們的敵人,并且我們將學會如何修復這個問題通過簡單的原則、模式甚至構造來實現一個不需要持續傳遞風格的編程方式。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容