亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

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

W_BinaryTree / 2648人閱讀

摘要:但是,對(duì)于模塊化背后的加載與運(yùn)行原理,我們是否清楚呢。源碼結(jié)構(gòu)一覽這里使用版本源碼為例子來做分析。下面就來分析的原理。至此就基本講清楚了核心模塊的加載過程。所以的內(nèi)建模塊會(huì)被放入一個(gè)叫做的數(shù)組中。

原文鏈接自我的個(gè)人博客:https://github.com/mly-zju/blog/issues/10 歡迎關(guān)注。

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

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

Node.js 中的模塊支持哪些文件類型?

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

除了 JavaScript 模塊以外,怎樣去寫一個(gè) C/C++ 擴(kuò)展模塊?

……

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

1. Node.js 模塊類型

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

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

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

native 模塊,比如我們?cè)?Node.js 中常用的 buffer,fs,os 等 native 模塊,其底層都有調(diào)用 built-in 模塊。

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

JavaScript 模塊,這是最常見的,我們開發(fā)的時(shí)候一般都寫的是 JavaScript 模塊

JSON 模塊,這個(gè)很簡單,就是一個(gè) JSON 文件

C/C++ 擴(kuò)展模塊,使用 C/C++ 編寫,編譯之后后綴名為 .node

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

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

這里使用 Node.js 6.x 版本源碼為例子來做分析。去 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

其中:

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

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

./deps文件夾包含了 Node.js 依賴的各種庫,典型的如 v8,libuv,zlib 等。

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

首先,我們需要認(rèn)識(shí)一下編譯用到的組織工具,即 gyp。Node.js 源碼中我們可以看到一個(gè) node.gyp,這個(gè)文件中的內(nèi)容是由 python 寫成的一些 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 就可以看出來,是將 JavaScript 轉(zhuǎn)換為 C/C++ 代碼。這個(gè)任務(wù)我們下面還會(huì)提到。

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

gcc 和 g++ 4.9.4 及以上版本

clang 和 clang++

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

GNU MAKE 3.81 及以上版本

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

./configuration
make
make install

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

3. 從 node index.js 開始

讓我們首先從最簡單的情況開始。假設(shè)有一個(gè) index.js 文件,里面只有一行很簡單的 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 

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(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 script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
                                                        "bootstrap_node.js");
    Local 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 f = Local::Cast(f_value);
    // ...
    f->Call(Null(env->isolate()), 1, &arg);
}

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

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

OK,重點(diǎn)來了,從 Node.js 啟動(dòng)以來,我們終于看到了第一個(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ù)。通過 process.argv[1] 拿到文件名,在我們的 node index.js 中,process.argv[1] 顯然就是 index.js,然后調(diào)用 path.resolve 解析出文件路徑。在最后,run(Module.runMain) 來編譯執(zhí)行我們的 index.js

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

下文我們?cè)诜治瞿K的 require 的時(shí)候,也會(huì)來到 lib/module.js 中,也會(huì)分析到 Module._load。因此我們可以看出,Node.js 啟動(dòng)一個(gè)文件的過程,其實(shí)到最后,也是 require 一個(gè)文件的過程,可以理解為是立即 require 一個(gè)文件。下面就來分析 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。

我們這么快就又來到了 Module._load 來看看這個(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;
};
// ...

這段代碼的流程比較清晰,具體說來:

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

查看緩存 Module._cache 中是否有該模塊,如果有,直接返回

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

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

我們首先來看一下 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è)方法較長,直接附上 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 字段所寫入的文件路徑

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

嘗試在路徑后面加上 .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,判斷是否存在,存在則返回

解析文件中用到的 tryPackagetryExtensions 方法的 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è)核心模塊,所以我們先來分析核心模塊加載的這個(gè)分支。

4.1 核心模塊加載原理

核心模塊是通過 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)加載過,如果有,直接返回 exports

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

首先我們來看一下如何編譯,從代碼中看是調(diào)用了 compile 方法,而在 NativeModule.prototype.compile 方法中,首先是通過 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è)又是在哪里賦值的呢?在上述代碼中也截取了出來,是通過 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 等。這就是為什么我們寫模塊的時(shí)候,不需要定義 exports, require, module 就可以直接用的原因。

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

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

// lib/buffer.js
"use strict";

// 通過 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& 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)建模塊是通過一個(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 模塊。這里繪制一張圖來描述一下核心模塊加載過程:

而回憶我們?cè)谧铋_始介紹的,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è)方法主要做的事情是,檢測 filename 的擴(kuò)展名,然后針對(duì)不同的擴(kuò)展名,調(diào)用不同的 Module._extensions 方法來加載、編譯模塊。接著我們看看 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 類型的文件加載方法是最簡單的,直接讀取文件內(nèi)容,然后 JSON.parse 之后返回對(duì)象即可。

下面來看對(duì) .js 的處理,首先也是通過 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í)行幾乎是一模一樣的。從整體流程上來講,核心 JavaScript 模塊與第三方 JavaScript 模塊最大的不同就是,核心 JavaScript 模塊源代碼是通過 process.binding("natives") 從內(nèi)存中獲取的,而第三方 JavaScript 模塊源代碼是通過 fs.readFileSync 方法從文件中讀取的。

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

// src/node.cc
// ...
env->SetMethod(process, "dlopen", DLOpen);
// ...
void DLOpen(const FunctionCallbackInfo& args) {
  // ...
  const bool is_dlopen_error = uv_dlopen(*filename, &lib);
  // ...
}
// ...

實(shí)際上最終調(diào)用了 DLOpen 函數(shù),該函數(shù)中最重要的是使用 uv_dlopen 方法打開動(dòng)態(tài)鏈接庫,然后對(duì) C/C++ 模塊進(jìn)行加載。uv_dlopen 方法是定義在 libuv 庫中的。libuv 庫是一個(gè)跨平臺(tái)的異步 IO 庫。對(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 庫,是 Node.js 異步 IO 的核心驅(qū)動(dòng)力,這一塊本身就值得專門作為一個(gè)專題來研究,這里就不展開講了。

到此為止,我們理清楚了三種第三方模塊的加載、編譯過程。

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

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

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

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

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

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

// hello.cc
#include 

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}

void init(Local exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo

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

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

其次,看到了很多 using v8:xxx 這樣的代碼。我們知道,Node.js 是基于 v8 引擎的,而 v8 引擎,就是用 C++ 來寫的。我們要開發(fā) C++ 擴(kuò)展模塊,便需要使用 v8 中提供的很多數(shù)據(jù)類型,而這一系列代碼,正是聲明了需要使用 v8 命名空間下的這些數(shù)據(jù)類型。

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

接下來需要定義擴(kuò)展模塊的初始化方法,這里是 Init 函數(shù),只有一句簡單的 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è)模塊,我們需要通過npm安裝 node-gyp 編譯工具。該工具將 Google 的 gyp 工具封裝,用來構(gòu)建 Node.js 擴(kuò)展。安裝這個(gè)工具后,我們?cè)谠创a文件夾下面增加一個(gè)名為 bingding.gyp 的配置文件,對(duì)于我們這個(gè)例子,文件只要這樣寫:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "hello.cc" ]
    }
  ]
}

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

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

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

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

Handle Example(const Arguments& args); // 0.10.x
void Example(FunctionCallbackInfo& args); // 6.x

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

#include 

NAN_METHOD(Example) {
  // ...
}

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

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

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

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

內(nèi)存消耗較大的應(yīng)用。Node.js 是基于 v8 的,而 v8 一開始是為瀏覽器設(shè)計(jì)的,所以其在內(nèi)存方面是有比較嚴(yán)格的限制的,所以對(duì)于一些需要較大內(nèi)存的應(yīng)用,直接基于 v8 可能會(huì)有些力不從心,這個(gè)時(shí)候就需要使用擴(kuò)展模塊,來繞開 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è)測試?yán)觼韺?duì)比計(jì)算性能。測試用例是經(jīng)典的計(jì)算斐波那契數(shù)列,首先使用 Node.js 原生語言實(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++ 編寫一個(gè)實(shí)現(xiàn)同樣功能的擴(kuò)展函數(shù),取名 fibC:

// fibC.cpp
#include 
#include 

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& 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)

在測試中,分別使用這兩個(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ò)展模塊測試
var fibC = require("./build/Release/fibC"); // 這里是擴(kuò)展模塊編譯產(chǎn)物的存放路徑
testSpeed(fibC, "c++ test:");

// 使用 JavaScript 函數(shù)進(jìn)行測試
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

多次測試,擴(kuò)展模塊平均花費(fèi)時(shí)長大約 1.2s,而 JavaScript 模塊花費(fèi)時(shí)長大約 2.6s,可見在此場景下,C/C++ 擴(kuò)展性能還是要快上不少的。

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

結(jié)語

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

首先以一個(gè)node index.js 的運(yùn)行原理開始,指出使用node 運(yùn)行一個(gè)文件,等同于立即執(zhí)行一次require 。

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

最后介紹了大家不太熟悉的c/c++擴(kuò)展模塊的開發(fā),并結(jié)合一個(gè)性能對(duì)比的例子來說明其適用場景。

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

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

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/93922.html

相關(guān)文章

  • Node.js模塊化機(jī)制原理探究

    摘要:要想讓模塊再次運(yùn)行,必須清除緩存。用戶自己編寫的模塊,稱為文件模塊。并且和指向了同一個(gè)模塊對(duì)象。模塊路徑這是在定位文件模塊的具體文件時(shí)指定的查找策略,具體表現(xiàn)為一個(gè)路徑組成的數(shù)組。 前言 Node應(yīng)用是由模塊組成的,Node遵循了CommonJS的模塊規(guī)范,來隔離每個(gè)模塊的作用域,使每個(gè)模塊在它自身的命名空間中執(zhí)行。 CommonJS規(guī)范的主要內(nèi)容: 模塊必須通過 module.exp...

    aikin 評(píng)論0 收藏0
  • Node】前后端模塊規(guī)范模塊加載原理

    摘要:例如指定一些依賴到模塊中實(shí)現(xiàn)規(guī)范的模塊化,感興趣的可以查看的文檔。 CommonJS 定義了 module、exports 和 require 模塊規(guī)范,Node.js 為了實(shí)現(xiàn)這個(gè)簡單的標(biāo)準(zhǔn),從底層 C/C++ 內(nèi)建模塊到 JavaScript 核心模塊,從路徑分析、文件定位到編譯執(zhí)行,經(jīng)歷了一系列復(fù)雜的過程。簡單的了解 Node 模塊的原理,有利于我們重新認(rèn)識(shí)基于 Node 搭建的...

    jsyzchen 評(píng)論0 收藏0
  • webpack原理

    摘要:原理查看所有文檔頁面前端開發(fā)文檔,獲取更多信息。初始化階段事件名解釋初始化參數(shù)從配置文件和語句中讀取與合并參數(shù),得出最終的參數(shù)。以上處理的相關(guān)配置如下編寫編寫的職責(zé)由上面的例子可以看出一個(gè)的職責(zé)是單一的,只需要完成一種轉(zhuǎn)換。 webpack原理 查看所有文檔頁面:前端開發(fā)文檔,獲取更多信息。原文鏈接:webpack原理,原文廣告模態(tài)框遮擋,閱讀體驗(yàn)不好,所以整理成本文,方便查找。 工作...

    trigkit4 評(píng)論0 收藏0
  • webpack優(yōu)化

    摘要:使用要給項(xiàng)目構(gòu)建接入動(dòng)態(tài)鏈接庫的思想,需要完成以下事情把網(wǎng)頁依賴的基礎(chǔ)模塊抽離出來,打包到一個(gè)個(gè)單獨(dú)的動(dòng)態(tài)鏈接庫中去。接入已經(jīng)內(nèi)置了對(duì)動(dòng)態(tài)鏈接庫的支持,需要通過個(gè)內(nèi)置的插件接入,它們分別是插件用于打包出一個(gè)個(gè)單獨(dú)的動(dòng)態(tài)鏈接庫文件。 webpack優(yōu)化 查看所有文檔頁面:全棧開發(fā),獲取更多信息。原文鏈接:webpack優(yōu)化,原文廣告模態(tài)框遮擋,閱讀體驗(yàn)不好,所以整理成本文,方便查找。 ...

    ChanceWong 評(píng)論0 收藏0
  • 校招社招必備核心前端面試問題詳細(xì)解答

    摘要:本文總結(jié)了前端老司機(jī)經(jīng)常問題的一些問題并結(jié)合個(gè)人總結(jié)給出了比較詳盡的答案。網(wǎng)易阿里騰訊校招社招必備知識(shí)點(diǎn)。此外還有網(wǎng)絡(luò)線程,定時(shí)器任務(wù)線程,文件系統(tǒng)處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機(jī)制叫做事件循環(huán)。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結(jié)了前端老司機(jī)經(jīng)常問題的一些問題并結(jié)合個(gè)...

    jonh_felix 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

W_BinaryTree

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<