結(jié)合源碼分析 Node.js 模塊加載與運(yùn)行原理

作者:馬齡陽(yáng)

efe.baidu.com/blog/nodejs-module-analyze/


Node.js 的出現(xiàn),讓 JavaScript 脫離了瀏覽器的束縛,進(jìn)入了廣闊的服務(wù)端開(kāi)發(fā)領(lǐng)域。而 Node.js 對(duì) CommonJS 模塊化規(guī)范的引入茫船,則更是讓 JavaScript成為了一門真正能夠適應(yīng)大型工程的語(yǔ)言。


在 Node.js 中使用模塊非常簡(jiǎn)單,我們?nèi)粘i_(kāi)發(fā)中幾乎都有過(guò)這樣的經(jīng)歷:寫(xiě)一段 JavaScript 代碼溃肪,require 一些想要的包免胃,然后將代碼產(chǎn)物 exports 導(dǎo)出。但是惫撰,對(duì)于 Node.js 模塊化背后的加載與運(yùn)行原理羔沙,我們是否清楚呢。首先拋出以下幾個(gè)問(wèn)題:


1润绎、Node.js 中的模塊支持哪些文件類型撬碟?

2、核心模塊和第三方模塊的加載運(yùn)行流程有什么不同莉撇?

3呢蛤、除了 JavaScript 模塊以外,怎樣去寫(xiě)一個(gè) C/C++ 擴(kuò)展模塊棍郎?

4其障、……


本篇文章,就會(huì)結(jié)合 Node.js 源碼涂佃,探究一下以上這些問(wèn)題背后的答案励翼。


1. Node.js 模塊類型


在 Node.js 中,模塊主要可以分為以下幾種類型:

1辜荠、核心模塊:包含在 Node.js 源碼中汽抚,被編譯進(jìn) Node.js 可執(zhí)行二進(jìn)制文件 JavaScript 模塊,也叫 native 模塊伯病,比如常用的 http, fs 等等

2造烁、C/C++ 模塊,也叫 built-in 模塊午笛,一般我們不直接調(diào)用惭蟋,而是在 native module 中調(diào)用,然后我們?cè)?require

3药磺、native 模塊告组,比如我們?cè)?Node.js 中常用的 buffer,fs癌佩,os 等 native 模塊木缝,其底層都有調(diào)用 built-in 模塊。

4围辙、第三方模塊:非 Node.js 源碼自帶的模塊都可以統(tǒng)稱第三方模塊氨肌,比如 express,webpack 等等酌畜。

5怎囚、JavaScript 模塊,這是最常見(jiàn)的,我們開(kāi)發(fā)的時(shí)候一般都寫(xiě)的是 JavaScript 模塊

6恳守、JSON 模塊考婴,這個(gè)很簡(jiǎn)單,就是一個(gè) JSON 文件

7催烘、C/C++ 擴(kuò)展模塊沥阱,使用 C/C++ 編寫(xiě),編譯之后后綴名為 .node


本篇文章中伊群,我們會(huì)一一涉及到上述幾種模塊的加載考杉、運(yùn)行原理。


2. Node.js 源碼結(jié)構(gòu)一覽


這里使用 Node.js 6.x 版本源碼為例子來(lái)做分析舰始。去 github 上下載相應(yīng)版本的 Node.js 源碼崇棠,可以看到代碼大體結(jié)構(gòu)如下:


├── AUTHORS

├── BSDmakefile

├── BUILDING.md

├── CHANGELOG.md

├── CODE_OF_CONDUCT.md

├── COLLABORATOR_GUIDE.md

├── CONTRIBUTING.md

├── GOVERNANCE.md

├── LICENSE

├── Makefile

├── README.md

├── android-configure

├── benchmark

├── common.gypi

├── configure

├── deps

├── doc

├── lib

├── node.gyp

├── node.gypi

├── src

├── test

├── tools

└── vcbuild.bat


其中:


1、./lib文件夾主要包含了各種 JavaScript 文件丸卷,我們常用的 JavaScript native 模塊都在這里枕稀。

2、./src文件夾主要包含了 Node.js 的 C/C++ 源碼文件谜嫉,其中很多 built-in 模塊都在這里萎坷。

3、./deps文件夾包含了 Node.js 依賴的各種庫(kù)沐兰,典型的如 v8哆档,libuv,zlib 等住闯。


我們?cè)陂_(kāi)發(fā)中使用的 release 版本虐呻,其實(shí)就是從源碼編譯得到的可執(zhí)行文件。如果我們想要對(duì) Node.js 進(jìn)行一些個(gè)性化的定制寞秃,則可以對(duì)源碼進(jìn)行修改,然后再運(yùn)行編譯偶惠,得到定制化的 Node.js 版本春寿。這里以 Linux 平臺(tái)為例,簡(jiǎn)要介紹一下 Node.js 編譯流程忽孽。


首先绑改,我們需要認(rèn)識(shí)一下編譯用到的組織工具,即 gyp兄一。Node.js 源碼中我們可以看到一個(gè) node.gyp厘线,這個(gè)文件中的內(nèi)容是由 python 寫(xiě)成的一些 JSON-like 配置,定義了一連串的構(gòu)建工程任務(wù)出革。我們舉個(gè)例子造壮,其中有一個(gè)字段如下:


{

??????'target_name':?'node_js2c',

??????'type':?'none',

??????'toolsets':?['host'],

??????'actions':?[

????????{

??????????'action_name':?'node_js2c',

??????????'inputs':?[

????????????'<@(library_files)',

????????????'./config.gypi',

??????????],

??????????'outputs':?[

????????????'<(SHARED_INTERMEDIATE_DIR)/node_natives.h',

??????????],

??????????'conditions':?[

????????????[?'node_use_dtrace=="false" and node_use_etw=="false"',?{

??????????????'inputs':?[?'src/notrace_macros.py'?]

????????????}],

????????????['node_use_lttng=="false"',?{

??????????????'inputs':?[?'src/nolttng_macros.py'?]

????????????}],

????????????[?'node_use_perfctr=="false"',?{

??????????????'inputs':?[?'src/perfctr_macros.py'?]

????????????}]

??????????],

??????????'action':?[

????????????'python',

????????????'tools/js2c.py',

????????????'<@(_outputs)',

????????????'<@(_inputs)',

??????????],

????????},

??????],

????},?#?end?node_js2c


這個(gè)任務(wù)主要的作用從名稱 node_js2c 就可以看出來(lái),是將 JavaScript 轉(zhuǎn)換為 C/C++ 代碼。這個(gè)任務(wù)我們下面還會(huì)提到耳璧。


首先編譯 Node.js成箫,需要提前安裝一些工具:


1、gcc 和 g++ 4.9.4 及以上版本

2旨枯、clang 和 clang++

3蹬昌、python 2.6 或者 2.7,這里要注意攀隔,只能是這兩個(gè)版本皂贩,不可以為python 3+

4、GNU MAKE 3.81 及以上版本


有了這些工具昆汹,進(jìn)入 Node.js 源碼目錄明刷,我們只需要依次運(yùn)行如下命令:


./configuration

make

make install


即可編譯生成可執(zhí)行文件并安裝了。


3. 從 node index.js 開(kāi)始


讓我們首先從最簡(jiǎn)單的情況開(kāi)始筹煮。假設(shè)有一個(gè) index.js 文件遮精,里面只有一行很簡(jiǎn)單的 console.log('hello world') 代碼。當(dāng)輸入 node index.js 的時(shí)候败潦,Node.js 是如何編譯本冲、運(yùn)行這個(gè)文件的呢?


當(dāng)輸入 Node.js 命令的時(shí)候劫扒,調(diào)用的是 Node.js 源碼當(dāng)中的 main 函數(shù)檬洞,在 src/node_main.cc 中:


// src/node_main.cc

#include "node.h"


#ifdef _WIN32

#include <VersionHelpers.h>


int?wmain(int?argc,?wchar_t?*wargv[])?{

????// windows下面的入口

}

#else

// UNIX

int?main(int?argc,?char?*argv[])?{

??// Disable stdio buffering, it interacts poorly with printf()

??// calls elsewhere in the program (e.g., any logging from V8.)

??setvbuf(stdout,?nullptr,?_IONBF,?0);

??setvbuf(stderr,?nullptr,?_IONBF,?0);

??// 關(guān)注下面這一行

??return?node::Start(argc,?argv);

}

#endif


這個(gè)文件只做入口用,區(qū)分了 Windows 和 Unix 環(huán)境沟饥。我們以 Unix 為例添怔,在 main 函數(shù)中最后調(diào)用了 node::Start,這個(gè)是在 src/node.cc 文件中:


// src/node.cc


int?Start(int?argc,?char**?argv)?{

??// ...

??{

????NodeInstanceData instance_data(NodeInstanceType::MAIN,

???????????????????????????????????uv_default_loop(),

???????????????????????????????????argc,

???????????????????????????????????const_cast<const?char**>(argv),

???????????????????????????????????exec_argc,

???????????????????????????????????exec_argv,

???????????????????????????????????use_debug_agent);

????StartNodeInstance(&instance_data);

????exit_code?=?instance_data.exit_code();

??}

??// ...

}

// ...


static?void?StartNodeInstance(void*?arg)?{

????// ...

????{

????????Environment::AsyncCallbackScope callback_scope(env);

????????LoadEnvironment(env);

????}

????// ...

}

// ...


void?LoadEnvironment(Environment*?env)?{

????// ...

????Local<String>?script_name?=?FIXED_ONE_BYTE_STRING(env->isolate(),

????????????????????????????????????????????????????????"bootstrap_node.js");

????Local<Value>?f_value?=?ExecuteString(env,?MainSource(env),?script_name);

????if?(try_catch.HasCaught())??{

????????ReportException(env,?try_catch);

????????exit(10);

????}

????// The bootstrap_node.js file returns a function 'f'

????CHECK(f_value->IsFunction());

????Local<Function>?f?=?Local<Function>::Cast(f_value);

????// ...

????f->Call(Null(env->isolate()),?1,?&arg);

}


整個(gè)文件比較長(zhǎng)贤旷,在上面代碼段里广料,只截取了我們最需要關(guān)注的流程片段,調(diào)用關(guān)系如下: Start -> StartNodeInstance -> LoadEnvironment幼驶。


在 LoadEnvironment 需要我們關(guān)注艾杏,主要做的事情就是,取出 bootstrap_node.js 中的代碼字符串盅藻,解析成函數(shù)购桑,并最后通過(guò) f->Call 去執(zhí)行。


OK氏淑,重點(diǎn)來(lái)了勃蜘,從 Node.js 啟動(dòng)以來(lái),我們終于看到了第一個(gè) JavaScript 文件 bootstrap_node.js假残,從文件名我們也可以看出這個(gè)是一個(gè)入口性質(zhì)的文件缭贡。那么我們快去看看吧,該文件路徑為 lib/internal/bootstrap_node.js:


// lib/internal/boostrap_node.js

(function(process)?{


??function?startup()?{

????// ...

????else?if?(process.argv[1])?{

??????const?path?=?NativeModule.require('path');

??????process.argv[1]?=?path.resolve(process.argv[1]);


??????const?Module?=?NativeModule.require('module');

??????// ...

??????preloadModules();

??????run(Module.runMain);

????}

????// ...

??}

??// ...

??startup();

}


// lib/module.js

// ...

// bootstrap main module.

Module.runMain?=?function()?{

??// Load the main module--the command line argument.

??Module._load(process.argv[1],?null,?true);

??// Handle any nextTicks added in the first tick of the program

??process._tickCallback();

};

// ...


這里我們依然關(guān)注主流程,可以看到匀归,bootstrap_node.js 中坑资,執(zhí)行了一個(gè) startup() 函數(shù)。通過(guò) process.argv[1] 拿到文件名穆端,在我們的 node index.js 中袱贮,process.argv[1] 顯然就是 index.js,然后調(diào)用 path.resolve 解析出文件路徑体啰。在最后攒巍,run(Module.runMain) 來(lái)編譯執(zhí)行我們的 index.js。


而 Module.runMain 函數(shù)定義在 lib/module.js 中荒勇,在上述代碼片段的最后柒莉,列出了這個(gè)函數(shù),可以看到沽翔,主要是調(diào)用 Module._load 來(lái)加載執(zhí)行 process.argv[1]兢孝。


下文我們?cè)诜治瞿K的 require 的時(shí)候,也會(huì)來(lái)到 lib/module.js 中仅偎,也會(huì)分析到 Module._load跨蟹。因此我們可以看出,Node.js 啟動(dòng)一個(gè)文件的過(guò)程橘沥,其實(shí)到最后窗轩,也是 require 一個(gè)文件的過(guò)程,可以理解為是立即 require 一個(gè)文件座咆。下面就來(lái)分析 require 的原理痢艺。


4. 模塊加載原理的關(guān)鍵:require


我們進(jìn)一步,假設(shè)我們的 index.js 有如下內(nèi)容:


var http = require('http');


那么當(dāng)執(zhí)行這一句代碼的時(shí)候介陶,會(huì)發(fā)生什么呢堤舒?


require的定義依然在 lib/module.js 中:


// lib/module.js

// ...

Module.prototype.require?=?function(path)?{

??assert(path,?'missing path');

??assert(typeof?path?===?'string',?'path must be a string');

??return?Module._load(path,?this,?/* isMain */?false);

};

// ...


require 方法定義在Module的原型鏈上〔肝兀可以看到這個(gè)方法中舌缤,調(diào)用了 Module._load。


我們這么快就又來(lái)到了 Module._load 來(lái)看看這個(gè)關(guān)鍵的方法究竟做了什么吧:


// lib/module.js

// ...

Module._load?=?function(request,?parent,?isMain)?{

??if?(parent)?{

????debug('Module._load REQUEST %s parent: %s',?request,?parent.id);

??}


??var?filename?=?Module._resolveFilename(request,?parent,?isMain);


??var?cachedModule?=?Module._cache[filename];

??if?(cachedModule)?{

????return?cachedModule.exports;

??}


??if?(NativeModule.nonInternalExists(filename))?{

????debug('load native module %s',?request);

????return?NativeModule.require(filename);

??}


??var?module?=?new?Module(filename,?parent);


??if?(isMain)?{

????process.mainModule?=?module;

????module.id?=?'.';

??}


??Module._cache[filename]?=?module;


??tryModuleLoad(module,?filename);


??return?module.exports;

};

// ...


這段代碼的流程比較清晰弦牡,具體說(shuō)來(lái):


1、根據(jù)文件名漂羊,調(diào)用 Module._resolveFilename 解析文件的路徑

2驾锰、查看緩存 Module._cache 中是否有該模塊,如果有走越,直接返回

3椭豫、通過(guò) NativeModule.nonInternalExists 判斷該模塊是否為核心模塊,如果核心模塊,調(diào)用核心模塊的加載方法 NativeModule.require

4赏酥、如果不是核心模塊喳整,新創(chuàng)建一個(gè) Module 對(duì)象,調(diào)用 tryModuleLoad 函數(shù)加載模塊


我們首先來(lái)看一下 Module._resolveFilename裸扶,看懂這個(gè)方法對(duì)于我們理解 Node.js 的文件路徑解析原理很有幫助:


// lib/module.js

// ...

Module._resolveFilename?=?function(request,?parent,?isMain)?{

??// ...

??var?filename?=?Module._findPath(request,?paths,?isMain);

??if?(!filename)?{

????var?err?=?new?Error("Cannot find module '"?+?request?+?"'");

????err.code?=?'MODULE_NOT_FOUND';

????throw?err;

??}

??return?filename;

};

// ...


在 Module._resolveFilename 中調(diào)用了 Module._findPath框都,模塊加載的判斷邏輯實(shí)際上集中在這個(gè)方法中,由于這個(gè)方法較長(zhǎng)呵晨,直接附上 github 該方法代碼:


https://github.com/nodejs/node/blob/v6.x/lib/module.js#L158


可以看出魏保,文件路徑解析的邏輯流程是這樣的:


先生成 cacheKey,判斷相應(yīng) cache 是否存在摸屠,若存在直接返回

如果 path 的最后一個(gè)字符不是 /:

如果路徑是一個(gè)文件并且存在谓罗,那么直接返回文件的路徑

如果路徑是一個(gè)目錄,調(diào)用 tryPackage 函數(shù)去解析目錄下的 package.json季二,然后取出其中的 main 字段所寫(xiě)入的文件路徑

  • 判斷路徑如果存在檩咱,直接返回

  • 嘗試在路徑后面加上 .js, .json, .node 三種后綴名,判斷是否存在胯舷,存在則返回

  • 嘗試在路徑后面依次加上 index.js, index.json, index.node刻蚯,判斷是否存在,存在則返回

  • 如果還不成功需纳,直接對(duì)當(dāng)前路徑加上 .js, .json, .node 后綴名進(jìn)行嘗試

    如果 path 的最后一個(gè)字符是 /:

    調(diào)用 tryPackage 芦倒,解析流程和上面的情況類似

    如果不成功,嘗試在路徑后面依次加上 index.js, index.json, index.node不翩,判斷是否存在兵扬,存在則返回


    解析文件中用到的 tryPackage 和 tryExtensions 方法的 github 鏈接: https://github.com/nodejs/node/blob/v6.x/lib/module.js#L108 https://github.com/nodejs/node/blob/v6.x/lib/module.js#L146


    整個(gè)流程可以參考下面這張圖:



    而在文件路徑解析完成之后,根據(jù)文件路徑查看緩存是否存在口蝠,存在直接返回器钟,不存在的話,走到 3 或者 4 步驟妙蔗。


    這里傲霸,在 3、4 兩步產(chǎn)生了兩個(gè)分支眉反,即核心模塊和第三方模塊的加載方法不一樣昙啄。由于我們假設(shè)了我們的 index.js 中為 var http = require('http'),http 是一個(gè)核心模塊寸五,所以我們先來(lái)分析核心模塊加載的這個(gè)分支梳凛。


    4.1 核心模塊加載原理


    核心模塊是通過(guò) NativeModule.require 加載的,NativeModule的定義在 bootstrap_node.js 中梳杏,附上 github 鏈接: https://github.com/nodejs/node/blob/v6.x/lib/internal/bootstrap_node.js#L401


    從代碼中可以看到韧拒,NativeModule.require 的流程如下:


    判斷 cache 中是否已經(jīng)加載過(guò)淹接,如果有,直接返回 exports

    新建 nativeModule 對(duì)象叛溢,然后緩存塑悼,并加載編譯


    首先我們來(lái)看一下如何編譯,從代碼中看是調(diào)用了 compile 方法楷掉,而在 NativeModule.prototype.compile 方法中厢蒜,首先是通過(guò) NativeModule.getSource 獲取了要加載模塊的源碼,那么這個(gè)源碼是如何獲取的呢靖诗?看一下 getSource 方法的定義:


    // lib/internal/bootstrap_node.js

    // ...

    NativeModule._source?=?process.binding('natives');

    // ...

    NativeModule.getSource?=?function(id)?{

    ??return?NativeModule._source[id];

    };


    直接從 NativeModule._source 獲取的郭怪,而這個(gè)又是在哪里賦值的呢?在上述代碼中也截取了出來(lái)刊橘,是通過(guò) NativeModule._source = process.binding('natives') 獲取的鄙才。


    這里就要插入介紹一下 JavaScript native 模塊代碼是如何存儲(chǔ)的了。Node.js 源碼編譯的時(shí)候促绵,會(huì)采用 v8 附帶的 js2c.py 工具攒庵,將 lib 文件夾下面的 js 模塊的代碼都轉(zhuǎn)換成 C 里面的數(shù)組,生成一個(gè) node_natives.h 頭文件败晴,記錄這個(gè)數(shù)組:


    namespace?node?{

    ??const?char?node_native[]?=?{47,?47,?32,?67,?112?…}


    ??const?char?console_native[]?=?{47,?47,?32,?67,?112?…}


    ??const?char?buffer_native[]?=?{47,?47,?32,?67,?112?…}


    ??…


    }


    struct?_native?{const?char?name;??const?char*?source;??size_t?source_len;};


    static?const?struct?_native?natives[]?=?{


    ??{?“node”,?node_native,?sizeof(node_native)-1?},


    ??{“dgram”,?dgram_native,?sizeof(dgram_native)-1?},


    ??{“console”,?console_native,?sizeof(console_native)-1?},


    ??{“buffer”,?buffer_native,?sizeof(buffer_native)-1?},


    ??…


    ??}


    而上文中 NativeModule._source = process.binding('natives'); 的作用菩浙,就是取出這個(gè) natives 數(shù)組肠骆,賦值給NativeModule._source芥备,所以在 getSource 方法中夯接,直接可以使用模塊名作為索引,從數(shù)組中取出模塊的源代碼慢味。


    在這里我們插入回顧一下上文场梆,在介紹 Node.js 編譯的時(shí)候,我們介紹了 node.gyp纯路,其中有一個(gè)任務(wù)是 node_js2c或油,當(dāng)時(shí)筆者提到從名稱看這個(gè)任務(wù)是將 JavaScript 轉(zhuǎn)換為 C 代碼,而這里的 natives 數(shù)組中的 C 代碼驰唬,正是這個(gè)構(gòu)建任務(wù)的產(chǎn)物顶岸。而到了這里,我們終于知道了這個(gè)編譯任務(wù)的作用了叫编。


    知道了源碼的獲取辖佣,繼續(xù)往下看 compile 方法,看看源碼是如何編譯的:


    // lib/internal/bootstrap_node.js

    ??NativeModule.wrap?=?function(script)?{

    ????return?NativeModule.wrapper[0]?+?script?+?NativeModule.wrapper[1];

    ??};


    ??NativeModule.wrapper?=?[

    ????'(function (exports, require, module, __filename, __dirname) { ',

    ????'});'

    ??];


    ??NativeModule.prototype.compile?=?function()?{

    ????var?source?=?NativeModule.getSource(this.id);

    ????source?=?NativeModule.wrap(source);


    ????this.loading?=?true;


    ????try?{

    ??????const?fn?=?runInThisContext(source,?{

    ????????filename:?this.filename,

    ????????lineOffset:?0,

    ????????displayErrors:?true

    ??????});

    ??????fn(this.exports,?NativeModule.require,?this,?this.filename);


    ??????this.loaded?=?true;

    ????}?finally?{

    ??????this.loading?=?false;

    ????}

    ??};

    ??// ...


    NativeModule.prototype.compile 在獲取到源碼之后搓逾,它主要做了:使用 wrap 方法處理源代碼卷谈,最后調(diào)用 runInThisContext 進(jìn)行編譯得到一個(gè)函數(shù),最后執(zhí)行該函數(shù)恃逻。其中 wrap 方法雏搂,是給源代碼加上了一頭一尾,其實(shí)相當(dāng)于是將源碼包在了一個(gè)函數(shù)中寇损,這個(gè)函數(shù)的參數(shù)有 exports, require, module 等凸郑。這就是為什么我們寫(xiě)模塊的時(shí)候,不需要定義 exports, require, module 就可以直接用的原因矛市。


    至此就基本講清楚了 Node.js 核心模塊的加載過(guò)程芙沥。說(shuō)到這里大家可能有一個(gè)疑惑,上述分析過(guò)程浊吏,好像只涉及到了核心模塊中的 JavaScript native模塊而昨,那么對(duì)于 C/C++ built-in 模塊呢?


    其實(shí)是這樣的找田,對(duì)于 built-in 模塊而言歌憨,它們不是通過(guò) require 來(lái)引入的,而是通過(guò) precess.binding('模塊名') 引入的墩衙。一般我們很少在自己的代碼中直接使用 process.binding 來(lái)引入built-in模塊务嫡,而是通過(guò) require 引用native模塊,而 native 模塊里面會(huì)引入 built-in 模塊漆改。比如我們常用的 buffer 模塊心铃,其內(nèi)部實(shí)現(xiàn)中就引入了 C/C++ built-in 模塊,這是為了避開(kāi) v8 的內(nèi)存限制:


    // lib/buffer.js

    'use strict';


    // 通過(guò) process.binding 引入名為 buffer 的 C/C++ built-in 模塊

    const?binding?=?process.binding('buffer');

    // ...


    這樣挫剑,我們?cè)?require('buffer') 的時(shí)候去扣,其實(shí)是間接的使用了 C/C++ built-in 模塊。


    這里再次出現(xiàn)了 process.binding樊破!事實(shí)上愉棱,process.binding 這個(gè)方法定義在 node.cc 中:


    // src/node.cc

    // ...

    static?void?Binding(const?FunctionCallbackInfo<Value>&?args)?{

    ??// ...

    ??node_module*?mod?=?get_builtin_module(*module_v);

    ??// ...

    }

    // ...

    env->SetMethod(process,?"binding",?Binding);

    // ...


    Binding 這個(gè)函數(shù)中關(guān)鍵的一步是 get_builtin_module。這里需要再次插入介紹一下 C/C++ 內(nèi)建模塊的存儲(chǔ)方式:


    在 Node.js 中捶码,內(nèi)建模塊是通過(guò)一個(gè)名為 node_module_struct 的結(jié)構(gòu)體定義的羽氮。所以的內(nèi)建模塊會(huì)被放入一個(gè)叫做 node_module_list 的數(shù)組中。而 process.binding 的作用惫恼,正是使用 get_builtin_module 從這個(gè)數(shù)組中取出相應(yīng)的內(nèi)建模塊代碼档押。


    綜上,我們就完整介紹了核心模塊的加載原理祈纯,主要是區(qū)分 JavaScript 類型的 native 模塊和 C/C++ 類型的 built-in 模塊令宿。這里繪制一張圖來(lái)描述一下核心模塊加載過(guò)程:




    而回憶我們?cè)谧铋_(kāi)始介紹的,native 模塊在源碼中存放在 lib/ 目錄下腕窥,而 built-in 模塊在源碼中存放在 src/ 目錄下粒没,下面這張圖則從編譯的角度梳理了 native 和 built-in 模塊如何被編譯進(jìn) Node.js 可執(zhí)行文件:



    4.2 第三方模塊加載原理


    下面讓我們繼續(xù)分析第二個(gè)分支,假設(shè)我們的 index.js 中 require 的不是 http簇爆,而是一個(gè)用戶自定義模塊癞松,那么在 module.js 中, 我們會(huì)走到 tryModuleLoad 方法中:


    // lib/module.js

    // ...

    function?tryModuleLoad(module,?filename)?{

    ??var?threw?=?true;

    ??try?{

    ????module.load(filename);

    ????threw?=?false;

    ??}?finally?{

    ????if?(threw)?{

    ??????delete?Module._cache[filename];

    ????}

    ??}

    }

    // ...

    Module.prototype.load?=?function(filename)?{

    ??debug('load %j for module %j',?filename,?this.id);


    ??assert(!this.loaded);

    ??this.filename?=?filename;

    ??this.paths?=?Module._nodeModulePaths(path.dirname(filename));


    ??var?extension?=?path.extname(filename)?||?'.js';

    ??if?(!Module._extensions[extension])?extension?=?'.js';

    ??Module._extensions[extension](this,?filename);

    ??this.loaded?=?true;

    };

    // ...


    這里看到爽撒,tryModuleLoad 中實(shí)際調(diào)用了 Module.prototype.load 定義的方法,這個(gè)方法主要做的事情是响蓉,檢測(cè) filename 的擴(kuò)展名硕勿,然后針對(duì)不同的擴(kuò)展名,調(diào)用不同的 Module._extensions 方法來(lái)加載枫甲、編譯模塊源武。接著我們看看 Module._extensions:


    // lib/module.js

    // ...

    // Native extension for .js

    Module._extensions['.js']?=?function(module,?filename)?{

    ??var?content?=?fs.readFileSync(filename,?'utf8');

    ??module._compile(internalModule.stripBOM(content),?filename);

    };



    // Native extension for .json

    Module._extensions['.json']?=?function(module,?filename)?{

    ??var?content?=?fs.readFileSync(filename,?'utf8');

    ??try?{

    ????module.exports?=?JSON.parse(internalModule.stripBOM(content));

    ??}?catch?(err)?{

    ????err.message?=?filename?+?': '?+?err.message;

    ????throw?err;

    ??}

    };



    //Native extension for .node

    Module._extensions['.node']?=?function(module,?filename)?{

    ??return?process.dlopen(module,?path._makeLong(filename));

    };

    // ...


    可以看出,一共支持三種類型的模塊加載:.js, .json, .node想幻。其中 .json 類型的文件加載方法是最簡(jiǎn)單的粱栖,直接讀取文件內(nèi)容,然后 JSON.parse 之后返回對(duì)象即可脏毯。


    下面來(lái)看對(duì) .js 的處理闹究,首先也是通過(guò) fs 模塊同步讀取文件內(nèi)容,然后調(diào)用了 module._compile食店,看看相關(guān)代碼:


    // lib/module.js

    // ...

    Module.wrap?=?NativeModule.wrap;

    // ...

    Module.prototype._compile?=?function(content,?filename)?{

    ??// ...


    ??// create wrapper function

    ??var?wrapper?=?Module.wrap(content);


    ??var?compiledWrapper?=?vm.runInThisContext(wrapper,?{

    ????filename:?filename,

    ????lineOffset:?0,

    ????displayErrors:?true

    ??});


    ??// ...

    ??var?result?=?compiledWrapper.apply(this.exports,?args);

    ??if?(depth?===?0)?stat.cache?=?null;

    ??return?result;

    };

    // ...


    首先調(diào)用 Module.wrap 對(duì)源代碼進(jìn)行包裹跋核,之后調(diào)用 vm.runInThisContext 方法進(jìn)行編譯執(zhí)行,最后返回 exports 的值叛买。而從 Module.wrap = NativeModule.wrap 這一句可以看出砂代,第三方模塊的 wrap 方法,和核心模塊的 wrap 方法是一樣的率挣。我們回憶一下剛才講到的核心js模塊加載關(guān)鍵代碼:


    // lib/internal/bootstrap_node.js

    NativeModule.wrap?=?function(script)?{

    ????return?NativeModule.wrapper[0]?+?script?+?NativeModule.wrapper[1];

    ??};


    ??NativeModule.wrapper?=?[

    ????'(function (exports, require, module, __filename, __dirname) { ',

    ????'});'

    ??];


    ??NativeModule.prototype.compile?=?function()?{

    ????var?source?=?NativeModule.getSource(this.id);

    ????source?=?NativeModule.wrap(source);


    ????this.loading?=?true;


    ????try?{

    ??????const?fn?=?runInThisContext(source,?{

    ????????filename:?this.filename,

    ????????lineOffset:?0,

    ????????displayErrors:?true

    ??????});

    ??????fn(this.exports,?NativeModule.require,?this,?this.filename);


    ??????this.loaded?=?true;

    ????}?finally?{

    ??????this.loading?=?false;

    ????}

    ??};


    兩廂對(duì)比刻伊,發(fā)現(xiàn)二者對(duì)源代碼的編譯執(zhí)行幾乎是一模一樣的。從整體流程上來(lái)講椒功,核心 JavaScript 模塊與第三方 JavaScript 模塊最大的不同就是捶箱,核心 JavaScript 模塊源代碼是通過(guò) process.binding('natives') 從內(nèi)存中獲取的,而第三方 JavaScript 模塊源代碼是通過(guò) fs.readFileSync 方法從文件中讀取的动漾。


    最后丁屎,再來(lái)看一下加載第三方 C/C++模塊(.node后綴)。直觀上來(lái)看旱眯,很簡(jiǎn)單晨川,就是調(diào)用了 process.dlopen 方法。這個(gè)方法的定義在 node.cc 中:


    // src/node.cc

    // ...

    env->SetMethod(process,?"dlopen",?DLOpen);

    // ...

    void?DLOpen(const?FunctionCallbackInfo<Value>&?args)?{

    ??// ...

    ??const?bool?is_dlopen_error?=?uv_dlopen(*filename,?&lib);

    ??// ...

    }

    // ...


    實(shí)際上最終調(diào)用了 DLOpen 函數(shù)删豺,該函數(shù)中最重要的是使用 uv_dlopen 方法打開(kāi)動(dòng)態(tài)鏈接庫(kù)共虑,然后對(duì) C/C++ 模塊進(jìn)行加載。uv_dlopen 方法是定義在 libuv 庫(kù)中的呀页。libuv 庫(kù)是一個(gè)跨平臺(tái)的異步 IO 庫(kù)妈拌。對(duì)于擴(kuò)展模塊的動(dòng)態(tài)加載這部分功能,在 *nix 平臺(tái)下蓬蝶,實(shí)際上調(diào)用的是 dlfcn.h 中定義的 dlopen() 方法尘分,而在 Windows 下猜惋,則為 LoadLibraryExW() 方法,在兩個(gè)平臺(tái)下培愁,他們加載的分別是 .so 和 .dll 文件惨奕,而 Node.js 中,這些文件統(tǒng)一被命名了 .node 后綴竭钝,屏蔽了平臺(tái)的差異。


    關(guān)于 libuv 庫(kù)雹洗,是 Node.js 異步 IO 的核心驅(qū)動(dòng)力香罐,這一塊本身就值得專門作為一個(gè)專題來(lái)研究,這里就不展開(kāi)講了时肿。


    到此為止庇茫,我們理清楚了三種第三方模塊的加載、編譯過(guò)程螃成。


    5. C/C++ 擴(kuò)展模塊的開(kāi)發(fā)以及應(yīng)用場(chǎng)景


    上文分析了 Node.js 當(dāng)中各類模塊的加載流程旦签。大家對(duì)于 JavaScript 模塊的開(kāi)發(fā)應(yīng)該是駕輕就熟了,但是對(duì)于 C/C++ 擴(kuò)展模塊開(kāi)發(fā)可能還有些陌生寸宏。這一節(jié)就簡(jiǎn)單介紹一下擴(kuò)展模塊的開(kāi)發(fā)宁炫,并談?wù)勂鋺?yīng)用場(chǎng)景。


    關(guān)于 Node.js 擴(kuò)展模塊的開(kāi)發(fā)氮凝,在 Node.js 官網(wǎng)文檔中專門有一節(jié)予以介紹羔巢,大家可以移步官網(wǎng)文檔查看:https://nodejs.org/docs/latest-v6.x/api/addons.html 。這里僅僅以其中的 hello world 例子來(lái)介紹一下編寫(xiě)擴(kuò)展模塊的一些比較重要的概念:


    假設(shè)我們希望通過(guò)擴(kuò)展模塊來(lái)實(shí)現(xiàn)一個(gè)等同于如下 JavaScript 函數(shù)的功能:


    module.exports.hello = () => 'world';


    首先創(chuàng)建一個(gè) hello.cc 文件罩阵,編寫(xiě)如下代碼:


    // hello.cc

    #include?<node.h>


    namespace?demo?{


    using?v8::FunctionCallbackInfo;

    using?v8::Isolate;

    using?v8::Local;

    using?v8::Object;

    using?v8::String;

    using?v8::Value;


    void?Method(const?FunctionCallbackInfo<Value>&?args)?{

    ??Isolate*?isolate?=?args.GetIsolate();

    ??args.GetReturnValue().Set(String::NewFromUtf8(isolate,?"world"));

    }


    void?init(Local<Object>?exports)?{

    ??NODE_SET_METHOD(exports,?"hello",?Method);

    }


    NODE_MODULE(NODE_GYP_MODULE_NAME,?init)


    }??// namespace demo


    文件雖短竿秆,但是已經(jīng)出現(xiàn)了一些我們比較陌生的代碼,這里一一介紹一下稿壁,對(duì)于了解擴(kuò)展模塊基礎(chǔ)知識(shí)還是很有幫助的幽钢。


    首先在開(kāi)頭引入了 node.h,這個(gè)是編寫(xiě) Node.js 擴(kuò)展時(shí)必用的頭文件傅是,里面幾乎包含了我們所需要的各種庫(kù)匪燕、數(shù)據(jù)類型。


    其次喧笔,看到了很多 using v8:xxx 這樣的代碼谎懦。我們知道,Node.js 是基于 v8 引擎的溃斋,而 v8 引擎界拦,就是用 C++ 來(lái)寫(xiě)的。我們要開(kāi)發(fā) C++ 擴(kuò)展模塊梗劫,便需要使用 v8 中提供的很多數(shù)據(jù)類型享甸,而這一系列代碼截碴,正是聲明了需要使用 v8 命名空間下的這些數(shù)據(jù)類型。


    然后來(lái)看 Method 方法蛉威,它的參數(shù)類型 FunctionCallbackInfo<Value>& args日丹,這個(gè) args 就是從 JavaScript 中傳入的參數(shù),同時(shí)蚯嫌,如果想在 Method 中為 JavaScript 返回變量哲虾,則需要調(diào)用 args.GetReturnValue().Set 方法。


    接下來(lái)需要定義擴(kuò)展模塊的初始化方法择示,這里是 Init 函數(shù)束凑,只有一句簡(jiǎn)單的 NODE_SET_METHOD(exports, "hello", Method);,代表給 exports 賦予一個(gè)名為 hello 的方法栅盲,這個(gè)方法的具體定義就是 Method 函數(shù)汪诉。


    最后是一個(gè)宏定義:NODE_MODULE(NODE_GYP_MODULE_NAME, init),第一個(gè)參數(shù)是希望的擴(kuò)展模塊名稱谈秫,第二個(gè)參數(shù)就是該模塊的初始化方法扒寄。


    為了編譯這個(gè)模塊,我們需要通過(guò)npm安裝 node-gyp 編譯工具拟烫。該工具將 Google 的 gyp 工具封裝该编,用來(lái)構(gòu)建 Node.js 擴(kuò)展。安裝這個(gè)工具后硕淑,我們?cè)谠创a文件夾下面增加一個(gè)名為 bingding.gyp 的配置文件上渴,對(duì)于我們這個(gè)例子,文件只要這樣寫(xiě):


    {

    ??"targets":?[

    ????{

    ??????"target_name":?"addon",

    ??????"sources":?[?"hello.cc"?]

    ????}

    ??]

    }


    這樣喜颁,運(yùn)行 node-gyp build 即可編譯擴(kuò)展模塊稠氮。在這個(gè)過(guò)程中,node-gyp 還會(huì)去指定目錄(一般是 ~/.node-gyp)下面搜我們當(dāng)前 Node.js 版本的一些頭文件和庫(kù)文件半开,如果不存在隔披,它還會(huì)幫我們?nèi)?Node.js 官網(wǎng)下載。這樣寂拆,在編寫(xiě)擴(kuò)展的時(shí)候奢米,通過(guò) #include <>,我們就可以直接使用所有 Node.js 的頭文件了纠永。


    如果編譯成功鬓长,會(huì)在當(dāng)前文件夾的 build/Release/ 路徑下看到一個(gè) addon.node,這個(gè)就是我們編譯好的可 require 的擴(kuò)展模塊尝江。


    從上面的例子中涉波,我們能大體看出擴(kuò)展模塊的運(yùn)作模式,它可以接收來(lái)自 JavaScript 的參數(shù),然后中間可以調(diào)用 C/C++ 語(yǔ)言的能力去做各種運(yùn)算啤覆、處理苍日,然后最后可以將結(jié)果再返回給 JavaScript。


    值得注意的是窗声,不同 Node.js 版本相恃,依賴的 v8 版本不同,導(dǎo)致很多 API 會(huì)有差別笨觅,因此使用原生 C/C++ 開(kāi)發(fā)擴(kuò)展的過(guò)程中拦耐,也需要針對(duì)不同版本的 Node.js 做兼容處理。比如說(shuō)见剩,聲明一個(gè)函數(shù)杀糯,在 v6.x 和 v0.12 以下的版本中,分別需要這樣寫(xiě):


    Handle<Value> Example(const Arguments& args); // 0.10.x

    void Example(FunctionCallbackInfo<Value>& args); // 6.x


    可以看到炮温,函數(shù)的聲明,包括函數(shù)中參數(shù)的寫(xiě)法牵舵,都不盡相同柒啤。這讓人不由得想起了在 Node.js 開(kāi)發(fā)中,為了寫(xiě) ES6畸颅,也是需要使用 Babel 來(lái)幫忙進(jìn)行兼容性轉(zhuǎn)換担巩。那么在 Node.js 擴(kuò)展開(kāi)發(fā)領(lǐng)域,有沒(méi)有類似 Babel 這樣幫助我們處理兼容性問(wèn)題的庫(kù)呢没炒?答案是肯定的涛癌,它的名字叫做 NAN (Native Abstraction for Node.js)。它本質(zhì)上是一堆宏送火,能夠幫助我們檢測(cè) Node.js 的不同版本拳话,并調(diào)用不同的 API。例如种吸,在 NAN 的幫助下弃衍,聲明一個(gè)函數(shù),我們不需要再考慮 Node.js 版本坚俗,而只需要寫(xiě)一段這樣的代碼:


    #include <nan.h>


    NAN_METHOD(Example)?{

    ??// ...

    }


    NAN 的宏會(huì)在編譯的時(shí)候自動(dòng)判斷镜盯,根據(jù) Node.js 版本的不同展開(kāi)不同的結(jié)果,從而解決了兼容性問(wèn)題猖败。對(duì) NAN 更詳細(xì)的介紹速缆,感興趣的同學(xué)可以移步該項(xiàng)目的 github 主頁(yè):https://github.com/nodejs/nan。


    介紹了這么多擴(kuò)展模塊的開(kāi)發(fā)恩闻,可能有同學(xué)會(huì)問(wèn)了艺糜,像這些擴(kuò)展模塊實(shí)現(xiàn)的功能,看起來(lái)似乎用js也可以很快的實(shí)現(xiàn),何必大費(fèi)周折去開(kāi)發(fā)擴(kuò)展呢倦踢?這就引出了一個(gè)問(wèn)題:C/C++ 擴(kuò)展的適用場(chǎng)景送滞。


    筆者在這里大概歸納了幾類 C/C++ 適用的情景:


    1、計(jì)算密集型應(yīng)用辱挥。我們知道犁嗅,Node.js 的編程模型是單線程 + 異步 IO,其中單線程導(dǎo)致了它在計(jì)算密集型應(yīng)用上是一個(gè)軟肋晤碘,大量的計(jì)算會(huì)阻塞 JavaScript 主線程褂微,導(dǎo)致無(wú)法響應(yīng)其他請(qǐng)求。對(duì)于這種場(chǎng)景园爷,就可以使用 C/C++ 擴(kuò)展模塊宠蚂,來(lái)加快計(jì)算速度,畢竟童社,雖然 v8 引擎的執(zhí)行速度很快求厕,但終究還是比不過(guò) C/C++。另外扰楼,使用 C/C++呀癣,還可以允許我們開(kāi)多線程,避免阻塞 JavaScript 主線程弦赖,社區(qū)里目前已經(jīng)有一些基于擴(kuò)展模塊的 Node.js 多線程方案项栏,其中最受歡迎的可能是一個(gè)叫做 thread-a-gogo 的項(xiàng)目,具體可以移步 github:https://github.com/xk/node-threads-a-gogo蹬竖。

    2沼沈、內(nèi)存消耗較大的應(yīng)用。Node.js 是基于 v8 的币厕,而 v8 一開(kāi)始是為瀏覽器設(shè)計(jì)的列另,所以其在內(nèi)存方面是有比較嚴(yán)格的限制的,所以對(duì)于一些需要較大內(nèi)存的應(yīng)用旦装,直接基于 v8 可能會(huì)有些力不從心访递,這個(gè)時(shí)候就需要使用擴(kuò)展模塊,來(lái)繞開(kāi) v8 的內(nèi)存限制同辣,最典型的就是我們常用的 buffer.js 模塊拷姿,其底層也是調(diào)用了 C++,在 C++ 的層面上去申請(qǐng)內(nèi)存旱函,避免 v8 內(nèi)存瓶頸响巢。


    關(guān)于第一點(diǎn),筆者這里也分別用原生 Node.js 以及 Node.js 擴(kuò)展實(shí)現(xiàn)了一個(gè)測(cè)試?yán)觼?lái)對(duì)比計(jì)算性能棒妨。測(cè)試用例是經(jīng)典的計(jì)算斐波那契數(shù)列踪古,首先使用 Node.js 原生語(yǔ)言實(shí)現(xiàn)一個(gè)計(jì)算斐波那契數(shù)列的函數(shù)含长,取名為 fibJs:


    function?fibJs(n)?{

    ????if?(n?===?0?||?n?===?1)?{

    ????????return?n;

    ????}

    ????else?{

    ????????return?fibJs(n?-?1)?+?fibJs(n?-?2);

    ????}

    }


    然后使用 C++ 編寫(xiě)一個(gè)實(shí)現(xiàn)同樣功能的擴(kuò)展函數(shù),取名 fibC:


    // fibC.cpp

    #include <node.h>

    #include <math.h>


    using?namespace?v8;


    int?fib(int?n)?{

    ????if?(n?==?0?||?n?==1)?{

    ????????return?n;

    ????}

    ????else?{

    ????????return?fib(n?-?1)?+?fib(n?-?2);

    ????}

    }


    void?Method(const?FunctionCallbackInfo<Value>&?args)?{

    ????Isolate*?isolate?=?args.GetIsolate();


    ????int?n?=?args[0]->NumberValue();

    ????int?result?=?fib(n);

    ????args.GetReturnValue().Set(result);

    }


    void?init(Local?<?Object?>?exports,?Local?<?Object?>?module)?{

    ????NODE_SET_METHOD(module,?"exports",?Method);

    }


    NODE_MODULE(fibC,?init)


    在測(cè)試中伏穆,分別使用這兩個(gè)函數(shù)計(jì)算從 1~40 的斐波那契數(shù)列:


    function?testSpeed(fn,?testName)?{

    ????var?start?=?Date.now();

    ????for?(var?i?=?0;?i?<?40;?i++)?{

    ????????fn(i);

    ????}

    ????var?spend?=?Date.now()?-?start;

    ????console.log(testName,?'spend time: ',?spend);

    }


    // 使用擴(kuò)展模塊測(cè)試

    var?fibC?=?require('./build/Release/fibC');?// 這里是擴(kuò)展模塊編譯產(chǎn)物的存放路徑

    testSpeed(fibC,?'c++ test:');


    // 使用 JavaScript 函數(shù)進(jìn)行測(cè)試

    function?fibJs(n)?{

    ????if?(n?===?0?||?n?===?1)?{

    ????????return?n;

    ????}

    ????else?{

    ????????return?fibJs(n?-?1)?+?fibJs(n?-?2);

    ????}

    }

    testSpeed(fibJs,?'js test:');


    // c++ test: spend time:??1221

    // js test: spend time:??2611


    多次測(cè)試拘泞,擴(kuò)展模塊平均花費(fèi)時(shí)長(zhǎng)大約 1.2s,而 JavaScript 模塊花費(fèi)時(shí)長(zhǎng)大約 2.6s枕扫,可見(jiàn)在此場(chǎng)景下陪腌,C/C++ 擴(kuò)展性能還是要快上不少的。


    當(dāng)然烟瞧,這幾點(diǎn)只是基于筆者的認(rèn)識(shí)诗鸭。在實(shí)際開(kāi)發(fā)過(guò)程中,大家在遇到問(wèn)題的時(shí)候参滴,也可以嘗試著考慮如果使用 C/C++ 擴(kuò)展模塊强岸,問(wèn)題是不是能夠得到更好的解決。


    結(jié)語(yǔ)


    文章讀到這里砾赔,我們?cè)倩厝タ匆幌乱婚_(kāi)始提出的那些問(wèn)題蝌箍,是否在文章分析的過(guò)程中都得到了解答?再來(lái)回顧一下本文的邏輯脈絡(luò):


    1暴心、首先以一個(gè)node index.js 的運(yùn)行原理開(kāi)始妓盲,指出使用node 運(yùn)行一個(gè)文件,等同于立即執(zhí)行一次require 酷勺。

    2本橙、然后引出了node中的require方法扳躬,在這里脆诉,區(qū)分了核心模塊、內(nèi)建模塊和非核心模塊幾種情況贷币,分別詳述了加載击胜、編譯的流程原理。在這個(gè)過(guò)程中役纹,還分別涉及到了模塊路徑解析偶摔、模塊緩存等等知識(shí)點(diǎn)的描述。

    3促脉、最后介紹了大家不太熟悉的c/c++擴(kuò)展模塊的開(kāi)發(fā)辰斋,并結(jié)合一個(gè)性能對(duì)比的例子來(lái)說(shuō)明其適用場(chǎng)景。


    事實(shí)上瘸味,通過(guò)學(xué)習(xí) Node.js 模塊加載流程宫仗,有助于我們更深刻的了解 Node.js 底層的運(yùn)行原理,而掌握了其中的擴(kuò)展模塊開(kāi)發(fā)旁仿,并學(xué)會(huì)在適當(dāng)?shù)膱?chǎng)景下使用藕夫,則能夠使得我們開(kāi)發(fā)出的 Node.js 應(yīng)用性能更高。


    學(xué)習(xí) Node.js 原理是一條漫長(zhǎng)的路徑。建議了解了底層模塊機(jī)制的讀者毅贮,可以去更深入的學(xué)習(xí) v8, libuv 等等知識(shí)办悟,對(duì)于精通 Node.js,必將大有裨益滩褥。

    感興趣的小伙伴病蛉,可以關(guān)注公眾號(hào)【grain先森】,回復(fù)關(guān)鍵詞 “vue”铸题,獲取更多資料铡恕,更多關(guān)鍵詞玩法期待你的探索~

    最后編輯于
    ?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
    • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市丢间,隨后出現(xiàn)的幾起案子探熔,更是在濱河造成了極大的恐慌,老刑警劉巖烘挫,帶你破解...
      沈念sama閱讀 222,464評(píng)論 6 517
    • 序言:濱河連續(xù)發(fā)生了三起死亡事件诀艰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡饮六,警方通過(guò)查閱死者的電腦和手機(jī)其垄,發(fā)現(xiàn)死者居然都...
      沈念sama閱讀 95,033評(píng)論 3 399
    • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)卤橄,“玉大人绿满,你說(shuō)我怎么就攤上這事】咂耍” “怎么了喇颁?”我有些...
      開(kāi)封第一講書(shū)人閱讀 169,078評(píng)論 0 362
    • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嚎货。 經(jīng)常有香客問(wèn)我橘霎,道長(zhǎng),這世上最難降的妖魔是什么殖属? 我笑而不...
      開(kāi)封第一講書(shū)人閱讀 59,979評(píng)論 1 299
    • 正文 為了忘掉前任姐叁,我火速辦了婚禮,結(jié)果婚禮上洗显,老公的妹妹穿的比我還像新娘外潜。我一直安慰自己,他們只是感情好挠唆,可當(dāng)我...
      茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
    • 文/花漫 我一把揭開(kāi)白布处窥。 她就那樣靜靜地躺著,像睡著了一般损搬。 火紅的嫁衣襯著肌膚如雪碧库。 梳的紋絲不亂的頭發(fā)上柜与,一...
      開(kāi)封第一講書(shū)人閱讀 52,584評(píng)論 1 312
    • 那天,我揣著相機(jī)與錄音嵌灰,去河邊找鬼弄匕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛沽瞭,可吹牛的內(nèi)容都是我干的迁匠。 我是一名探鬼主播,決...
      沈念sama閱讀 41,085評(píng)論 3 422
    • 文/蒼蘭香墨 我猛地睜開(kāi)眼驹溃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼城丧!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起豌鹤,我...
      開(kāi)封第一講書(shū)人閱讀 40,023評(píng)論 0 277
    • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤亡哄,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后布疙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蚊惯,經(jīng)...
      沈念sama閱讀 46,555評(píng)論 1 319
    • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
      茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
    • 正文 我和宋清朗相戀三年灵临,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了截型。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
      茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
    • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡儒溉,死狀恐怖宦焦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情顿涣,我是刑警寧澤波闹,帶...
      沈念sama閱讀 36,439評(píng)論 5 351
    • 正文 年R本政府宣布,位于F島的核電站园骆,受9級(jí)特大地震影響舔痪,放射性物質(zhì)發(fā)生泄漏寓调。R本人自食惡果不足惜锌唾,卻給世界環(huán)境...
      茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
    • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夺英。 院中可真熱鬧晌涕,春花似錦、人聲如沸痛悯。這莊子的主人今日做“春日...
      開(kāi)封第一講書(shū)人閱讀 32,601評(píng)論 0 25
    • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)载萌。三九已至惧财,卻和暖如春巡扇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背垮衷。 一陣腳步聲響...
      開(kāi)封第一講書(shū)人閱讀 33,702評(píng)論 1 274
    • 我被黑心中介騙來(lái)泰國(guó)打工厅翔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搀突。 一個(gè)月前我還...
      沈念sama閱讀 49,191評(píng)論 3 378
    • 正文 我出身青樓刀闷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親仰迁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子甸昏,可洞房花燭夜當(dāng)晚...
      茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

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