前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入浅出 Nodejs( 三 ):Nodejs 核心模块机制

深入浅出 Nodejs( 三 ):Nodejs 核心模块机制

原创
作者头像
serena
修改2021-08-03 14:56:07
2.2K0
修改2021-08-03 14:56:07
举报
文章被收录于专栏:社区的朋友们社区的朋友们

作者:郭泽豪

导语

本篇教程关于Nodejs的核心模块机制,具体讲Nodejs核心模块的原理、C/C++扩展模块的原理、包、模块调用栈以及NPM。

本章的重点内容:

  • JavaScript核心模块的编译过程
  • C/C++核心模块的编译过程
  • C/C++扩展模块的编写、编译、加载过程
  • 模块调用栈
  • NPM

一、Nodejs核心模块的原理

前面提及,Node的核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块其实分为C/C++编写的和JavaScript编写的两部分,其中C/C++文件存在在Node项目的src目录下,JavaScript文件存放在lib目录下。

1.1 JavaScript核心模块的编译过程

在编译所有C/C++文件之前,编译程序需要将所有的JavaScript文件编译成C/C++代码,此时是否直接将其编译成可执行文件?其实不是。

(1)转存为C/C++代码

Node采用了V8附带的js2c.py工具,将全部内置的JavaScript代码(即src/node.js和lib/*.js)转换成C++里的数组,生成node_natives.h头文件,相关代码如下:

代码语言:javascript
复制
namespace node{
    const char node_native[] = {47, 47, ..};
    const char dgram_native[] = {47, 47, ..};
    const char console_native[] = {47, 47, ..};
    const char buffer_native[] = {47, 47, ..};
    const char querystring_native[] = {47, 47, ..};
    const char punycode_native[] = {47, 42, ..};
    ...
    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", node_native, sizeof(node_native) -1};
        ...
    };
}

在这个过程中,JavaScript代码以字符串的形式存储在node命名空间中,是不可直接执行的。在启动Node进程时,JavaScript代码直接加载进内存中。在加载的过程中,JavaScript核心模块通过标识符分析直接定位到内存中,找到JavaScript代码后编译执行,比普通的文件模块从磁盘查找快很多。

(2)编译JavaScript核心模块

lib目录下的所有模块文件也没有定义require、module、exports这些变量。在引入JavaScript核心模块的过程中,也经历头尾包装的过程,然后执行和导出exports对象。与文件模块有区别的地方在于:获取源代码的方式(核心模块从内存加载),以及缓存执行结果的位置。

JavaScript核心模块对象的定义如下面的代码所示,源文件可以通过process.binding(‘natives’)取出,编译执行成功生成的模块对象缓存在NativeModule._cache对象上,文件模块则缓存在Module._cache对象上。

代码语言:javascript
复制
function NativeModule(id){
    this.filename = id + ".js";
    this.id = id;
    this.exports = {};
    this.loaded = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};

1.2 C/C++核心模块的编译过程

在核心模块中,有些模块全部由C/C++编写,也有些模块则由C/C++完成核心部分,其他部分则由JavaScript实现包装或向外导出,以满足性能需求。后者这种C++模块主内完成核心,JavaScript主外实现封装的模式是Node能够提高性能的常见方式。通常,脚本的开发速度快于静态语言,但性能却弱于静态语言。而Node的这种复合模式可以在开发速度和性能之间找到平衡点。这里我们将那么纯由C/C++编写的部分模块统一称为内建模块,因为它们通常不会被用户直接调用。

(1)内建模块的内部结构

在Node中,内建模块的内部结构如下:

代码语言:javascript
复制
struct node_module_struct{
    int version;
    void *dso_handle;
    const char *filename;
    void (*register_func) (v8:Handle<v8::Object> target);
    const char *modname;
}

每一个内建模块在定义之后,都通过NODE_MODULE宏将模块定义到node的命名空间中,模块的具体初始化方法挂载为结构的register_func成员。

代码语言:javascript
复制
#define NODE_MODULE(modname, regfunc)
    extern "c" {
        NODE_MODULE_EXPORT node::node_module_struct modname ## _module = 
        {
            NODE_STANDARD_MODULE_STUFF,
            regfunc,
            NODE_STRINGIFY(modname)
        };
    }

node_extension.h文件将散列的内建模块统一放进一个叫node_module_list的数组中,Node也提供了get_buildin_module()方法从node_module_list数组中取出这些模块。

内建模块的优势在于,首先它们本身有C/C++编写,其性能优于JavaScript脚本语言;其次在进行文件编译时,它们被编译进二进制文件。一旦Node开始执行,它们被直接加载进内存中,无须做标识符定位、文件定位、编译等过程,直接可执行。

(2)内建模块的导出

在Node的所有模块类型中,存在着一种依赖层级关系,即文件模块依赖于JavaScript核心模块,JavaScript核心模块依赖于内建模块。通常不推荐文件模块直接调用内建模块。如需调用,直接调用核心模块即可,因为其实核心模块中基本封装了内建模块。那么内建模块是如何将内部变量和方法导出,以供外部JavaScript核心模块调用的呢?Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。Binding()的实现代码在src/node.cc中,具体如下所示。

代码语言:javascript
复制
static Handle<Value> Binding(const Argument& args){
    HandleScope scope;
    Local<String> module = args[0]->ToString();//获取模块标识符
    String::Utf8Value module_v(module);

    if(binding_cache.isEmpty()){
        binding_cache = Persistent<Object>::New(Object::New());
    }

    Local<Object> exports;

    if(binding_cache->Has(module)){
        exports = binding_cache->Get(module).ToObject();
        return scope.Close(exports);
    }

    char buf[1024];
    snprintf(buf, 1024, "Binding %s","*module_v");
    uint32_t l = module_load_list->Length();
    module_load_list->Set(l, String::New(buf));
        //从node_module_list中取出标识符对应的node_module_struct对象
    if((modp = get_buildin_module(*module_v)) != NULL){
        exports = Object::New();
                //调用node_module_strut对象的register_func()函数填充exports,返回给调用方
        modp->register_func(exports);//
        binding_cache->Set(module, exports);
    }else if(!strcmp(*module_v, "constants")){
        exports = Object::New();
        DefineConstants(exports);
        binding_cache->Set(module, exports);

    #ifdef __POSIX__
    }else if(!strcmp(*module_v, "io_watcher")){
        exports = Object::New();
        IOWatcher::Initialize(exports);
        binding_cache->Set(module, exports);
    #endif

    }else if(!strcmp(*module_v, "natives")){
        exports = Object::New();
        DefineJavaScript(exports);
        binding_cache->Set(module, exports);
    }else{
        return ThrowException(Exception::Error(String::New("No such module")));
    }
    return scope.close(exports);
}

在加载内建模块时,我们先创建一个exports空对象,然后调用get_buildin_module方法取出内建模块对象,通过执行register_func()填充exprots对象,最后将exports对象按模块名缓存,并返回调用方。

(3)核心模块的引入流程

从图1所示的os原生模块的引入流程可以看出,为了符合CommonJs模块规范,从JavaScript到C/C++的过程相当复杂,get_buildin_module(‘node_os’)中的node_os是通过宏命令NODE_MODULE(node_os, reg_func)注册到node命名空间,同时node_extension.h将内建模块放在node_module_list数组中,get_buidin_module实际上是根据参数从node_module_list取出内建模块。但是对于用户而言,require()十分简洁、友好。

图1 os原生模块的引入流程

(4)编写核心模块

核心模块被编译进二进制文件需要遵循一定规则。作为Node的使用者,尽管几乎没有机会参与核心模块的开发,但是了解如何开发核心模块有助于我们更加深入地了解Node。下面我们以C/C++模块为例演示如何编写内建模块。为了便于理解,我们先编写一个极其简单的JavaScript版本的原型,这个方法返回一个Hello World!字符串。

代码语言:javascript
复制
exports.sayHello = function(){
    return “Hello world!”;
};

编写内建模块通常分两步完成,编写头文件和编写C/C++文件。

(1)将以下代码保存在node_hello.h,存放到Node的src目录下:

代码语言:javascript
复制
#ifndef NODE_HEELO_H_
#define NODE_HELLO_H_
#include<v8.h>

namespace node{
    v8::Handle<v8::Value> SayHello(const v8::Argument& args);
}
#endif

(2)编写node_hello.cc,并存储在src目录下:

代码语言:javascript
复制
#include<node.h>
#include<node_hello.h>
#include<v8.h>
namespace node{
    using namespace v8;
    Handle<Value> SayHello(const Argument& args){
        HandleScope scope;
        return scope.Close(String::New("Hello world!"));
    }

    void Init_Hello(Handle<Object> target){
        target->Set(String::NewSymbol("sayHello"), FunctionTemplate::New(SayHello)->GetFunction());
    }
}    
NODE_MODULE(node_hello, node::Init_Hello);

以上两步完成了内建模块的编写,但是真正让Node认为它是内建模块,还需要更改src/node_extensions.h,在NODE_EXT_LIST_END前添加NODE_EXT_LIST_ITEM(node_hello),以将node_hello模块添加到node_module_list数组中。

其次,还需要让编写的两份代码编译进执行文件,同时需要更改Node的项目生成文件node.gyp,并在’target_name’:’node’节点的sources中添加上新编写的两个文件。然后编译整个Node项目。编译和安装后,直接在命令行运行以下代码,将会得到期望的效果。

代码语言:javascript
复制
$node
> var hello = process.binding(‘hello’);
Undefined
> hello.sayHello();
‘Hello world!’

1.3 C/C++扩展模块

C/C++扩展模块对于前端工程师来说或许比较生疏和晦涩,但是如果了解它,那么在出现性能瓶颈时将会带来极大的帮助。

C/C++扩展模块属于文件模块的一类。前面讲述文件模块的编译部分时提到,C/C++模块通过预先编译成.node文件,然后调用process.dlopen()方法加载执行。在这里,我们将分析整个C/C++扩展模块的编写、编译、加载、导出的过程。

在开始编写扩展模块之前,需要强调的一点是,Node的原生模块一定程度上是跨平台的,其前提条件是源代码可以支持在window和nix上编译,其中windows需要通过Visual C++的编译器编译为动态链接库文件(.dll),nix通过g++/gcc等编译器编译成动态链接共享对象文件(.so)。这里有一个让人疑惑的地方,那就是引用加载时却是.node文件。其实.node的扩展名只是为了看起来更自然一点,不会因为平台差异产生不同的感觉。实际上,在windows下它是一个.dll文件,在*nix下则是一个.so文件。为了实现跨平台,dlopen()方法在内部实现时区分了平台,分别用加载.so和.dll的方式。下图是扩展模块在不同平台编译和加载的详细过程。

图2 扩展模块不同平台上的编译和加载过程

1.3.1 C/C++扩展模块的编写

在介绍C/C++内建模块时,其实已经介绍了C/C++模块的编写方式。普通的扩展模块与内建模块的区别在于无须将源代码编译进Node,而是通过dlopen()方法动态加载。所以在编写普通扩展模块时,无须将源代码写入node命名空间,也不需要提供头文件。下面将通过一个例子来介绍C/C++扩展模块的编写。

它的JavaScript原型代码与前面的例子一样:

代码语言:javascript
复制
exports.sayHello = function(){
    return “Hello world!”;
};

新建hello目录作为自己的项目位置,编写hello.cc并将其存储在src目录下,相关代码如下:

代码语言:javascript
复制
#include<node.h>
#include<node_hello.h>
#include<v8.h>
namespace node{
    using namespace v8;
    Handle<Value> SayHello(const Argument& args){
        HandleScope scope;
        return scope.Close(String::New("Hello world!"));
    }

    void Init_Hello(Handle<Object> target){
        target->Set(String::NewSymbol("sayHello"), FunctionTemplate::New(SayHello)->GetFunction());
    }
}    
NODE_MODULE(node_hello, node::Init_Hello);

C/C++扩展模块与内建模块的套路一样,将方法挂载在target对象上,然后通过NODE_MODULE声明即可。

由于不像编写内建模块那样将对象声明到node_module_list链表中,所以无法被认作一个原生模块,只能通过dlopen()来动态加载,然后导出给JavaScript调用。

1.3.2 C/C++扩展模块的编译

在GYP工具的帮助下,C/C++扩展模块的编译很容易,无须为每个平台编写不同的项目编译文件。写好.gyp项目文件是除编码外的头等大事,然而无须担心此事太难,因为.gyp项目文件是足够简单的。Node-gyp约定.gyp文件为binding.gyp,其内容如下:

代码语言:javascript
复制
{
    'targets':[
        {
            'target_name' : 'hello',
            'sources' : [
                'hello.cc'
            ],
            'conditions' : [
                ['OS == "win',
                {
                    'libraries' : ['-lnode.lib']
                }
                ]
            ]
        }
    ]
}

然后调用:node-gyp configure,会在当前目录创建build目录,并生成系统相关的项目文件。在*nix平台下,build目录中会出现Makefile文件,在windows下会生成vcxproj文件。继续执行如下代码:node-gyp build,编译过程会根据平台不同,分别通过make或vcbuild进行编译。编译完成后,hello.node文件会生成在build/Release目录下。

1.3.3 C/C++扩展模块的加载

得到hello.node结果文件后,如何调用扩展模块其实前面已经提及。require()方法通过解析标识符、路径分析、文件定位,然后加载执行即可。下面的代码引入前面编译得到的.node文件,并调用执行其中的方法:

代码语言:javascript
复制
var hello = require('./build/Release/hello.node');
Console.log(hello.sayHello());

以上代码保存为hello.js,调用node hello.js命令即可得到如下输出结果:Hello world!

对于以.node为扩展名的文件,Node将会调用process.dlopen()方法来加载文件:Module._extension[‘.node’] = process.dlopen;

如图3,require()方法在引入.node文件的过程中,实际上经历了4个层面的调用。

加载.node文件实际上经历了两个步骤,第一个步骤是调用uv_dlopen()方法去打开动态链接库,第二个步骤是调用uv_dlsm()方法找到动态链接库通过NODE_MODULE宏定义的register_func方法地址。这两个过程都是通过libuv库进行封装的;在*nix平台实际上调用的是dlfcn.h头文件定义的dlopen()和dlsym()两个方法;在windows平台则通过LoadLibraryExW()和GetProcAddress()这两个方法实现的,它们分别加载.so和.dll文件(实际为.node文件)。

图3 require()引入node文件的过程

由于编写模块时通过NODE_MODULE将模块定义为node_module_struct结构,所以在获取函数地址之后,将它映射为node_module_struct结构几乎是无缝对接的。接下来的过程就是将传入的exports对象作为实参运行,将C++中定义的方法挂载在exports对象上,然后调用者就可以轻松调用了。

二、模块调用栈

结束文件模块、核心模块、内建模块、C/C++扩展模块等阐述之后,有必要明确一下各种模块之间的调用关系,如图。

图4 模块之间的调用关系

C/C++内建模块属于最底层的模块,它属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。JavaScript核心模块主要扮演的职责有两类:一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用;一类是纯粹的功能模块,它不需要跟底层打交道,但是又十分重要。文件模块通过由第三方编写,包括普通JavaScript模块和C/C++扩展模块,主要调用方向是普通JavaScript模块调用扩展模块。

三、包与NPM

Node组织了自身的核心模块,也使得第三方文件模块可以有序地编写和使用。但是在第三方模块中,模块与模块仍然是散列在各地的,相互之间不能直接引用。而在模块之外,包和NPM则是将模块联系起来的一种机制。

CommonJs的包规范的定义其实十分简单,它由包结构和包描述文件两个部分组成,前者是用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析。

3.1 包结构

包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJs规范的包目录应该包含以下这些文件。

  • Package.json:包描述文件
  • Bin: 用于存放可执行二进制文件的目录
  • Lib:用于存放JavaScript代码的目录
  • Doc: 用于存放文档的目录
  • Test:用于存放单元测试用例的代码

可以看到,CommonJs包规范从文档、测试等方面都做过考虑。当一个包完成向外公布时,用户看到单元测试和文档的时候,会给他们一种踏实可靠的感觉。

3.2 包描述文件和NPM

包描述文件用于表达非代码相关的信息,它是一个JSON格式的文件——package.json,位于包的根目录下,是包的重要组成部分。而NPM所有行为都跟包描述文件的字段息息相关。下面是express框架的package.json文件。

代码语言:javascript
复制
{
  "name": "express",
  "description": "Fast, unopinionated, minimalist web framework",
  "version": "4.13.3",
  "author": {
    "name": "TJ Holowaychuk",
    "email": "tj@vision-media.ca"
  },
  "contributors": [
    {
      "name": "Aaron Heckmann",
      "email": "aaron.heckmann+github@gmail.com"
    },
    {
      "name": "Ciaran Jessup",
      "email": "ciaranj@gmail.com"
    },
    {
      "name": "Douglas Christopher Wilson",
      "email": "doug@somethingdoug.com"
    },
    {
      "name": "Guillermo Rauch",
      "email": "rauchg@gmail.com"
    },
    {
      "name": "Jonathan Ong",
      "email": "me@jongleberry.com"
    },
    {
      "name": "Roman Shtylman",
      "email": "shtylman+expressjs@gmail.com"
    },
    {
      "name": "Young Jae Sim",
      "email": "hanul@hanul.me"
    }
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/strongloop/express.git"
  },
  "homepage": "http://expressjs.com/",
  "keywords": [
    "express",
    "framework",
    "sinatra",
    "web",
    "rest",
    "restful",
    "router",
    "app",
    "api"
  ],
  "dependencies": {
    "accepts": "~1.2.12",
    "array-flatten": "1.1.1",
    "content-disposition": "0.5.0",
    "content-type": "~1.0.1",
    "cookie": "0.1.3",
    "cookie-signature": "1.0.6",
    "debug": "~2.2.0",
    "depd": "~1.0.1",
    "escape-html": "1.0.2",
    "etag": "~1.7.0",
    "finalhandler": "0.4.0",
    "fresh": "0.3.0",
    "merge-descriptors": "1.0.0",
    "methods": "~1.1.1",
    "on-finished": "~2.3.0",
    "parseurl": "~1.3.0",
    "path-to-regexp": "0.1.7",
    "proxy-addr": "~1.0.8",
    "qs": "4.0.0",
    "range-parser": "~1.0.2",
    "send": "0.13.0",
    "serve-static": "~1.10.0",
    "type-is": "~1.6.6",
    "utils-merge": "1.0.0",
    "vary": "~1.0.1"
  },
  "devDependencies": {
    "after": "0.8.1",
    "ejs": "2.3.3",
    "istanbul": "0.3.17",
    "marked": "0.3.5",
    "mocha": "2.2.5",
    "should": "7.0.2",
    "supertest": "1.0.1",
    "body-parser": "~1.13.3",
    "connect-redis": "~2.4.1",
    "cookie-parser": "~1.3.5",
    "cookie-session": "~1.2.0",
    "express-session": "~1.11.3",
    "jade": "~1.11.0",
    "method-override": "~2.3.5",
    "morgan": "~1.6.1",
    "multiparty": "~4.1.2",
    "vhost": "~3.0.1"
  },
  "engines": {
    "node": ">= 0.10.0"
  },
  "files": [
    "LICENSE",
    "History.md",
    "Readme.md",
    "index.js",
    "lib/"
  ],
  "scripts":{"test":"mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/","test-ci":"istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/ test/acceptance/","test-cov":"istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/ test/acceptance/","test-tap":"mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/"},"gitHead":"ef7ad681b245fba023843ce94f6bcb8e275bbb8e","bugs":{"url":"https://github.com/strongloop/express/issues"},"_id":"express@4.13.3","_shasum":"ddb2f1fb4502bf33598d2b032b037960ca6c80a3","_from":"express@*","_npmVersion":"1.4.28","_npmUser":{"name":"dougwilson","email":"doug@somethingdoug.com"},"maintainers":[{"name":"tjholowaychuk","email":"tj@vision-media.ca"},{"name":"jongleberry","email":"jonathanrichardong@gmail.com"},{"name":"dougwilson","email":"doug@somethingdoug.com"},{"name":"rfeng","email":"enjoyjava@gmail.com"},{"name":"aredridel","email":"aredridel@dinhe.net"},{"name":"strongloop","email":"callback@strongloop.com"},{"name":"defunctzombie","email":"shtylman@gmail.com"}],"dist":{"shasum":"ddb2f1fb4502bf33598d2b032b037960ca6c80a3","tarball":"http://registry.npmjs.org/express/-/express-4.13.3.tgz"},"directories":{},"_resolved":"https://registry.npmjs.org/express/-/express-4.13.3.tgz","readme":"ERROR: No README data found!"}

作者:MIG无线合作开发部实习生marcozhguo

电子邮箱:446882229@qq.com

参考资料:《深入浅出Nodejs》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语
  • 一、Nodejs核心模块的原理
    • 1.1 JavaScript核心模块的编译过程
      • 1.2 C/C++核心模块的编译过程
        • 1.3 C/C++扩展模块
        • 二、模块调用栈
        • 三、包与NPM
          • 3.1 包结构
            • 3.2 包描述文件和NPM
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档