美文网首页node
nodejs深入学(3)模块机制

nodejs深入学(3)模块机制

作者: 白昔月 | 来源:发表于2017-12-12 17:45 被阅读991次

    前言

    js是从网页小脚本演变过来的,至今,前端的js库,也不像一个真正的模块。前端js经历了工具类库、组件库、前端框架和前端应用的变迁。但是,依然没有完成真正的模块化转变(也就是不断的聚类和抽象)。因为,js没有模块,因此,也就没办法完成真正的封装等工作,只能通过人为的命名空间来约束代码。

    前端js的发展之路

    这个第二章就是讲nodejs模块机制的。另外,由于原书的第二章编排的比较混乱,因此,我的笔记也做了结构上的调整。

    CommonJS规范

    CommonJS出现之前,js存在很多问题,没有模块系统、标准库较少、没有标准接口(例如数据库连接接口等)、缺乏包管理系统。

    CommonJS的美好愿景是希望js在任何地方都可以运行。也就是说,js不仅仅可以开发网页程序,还可以开发服务器端程序、命令行工具、桌面应用以及混合应用。CommonJS规范涵盖了模块、二进制、Buffer、字符集编码、IO流、进程环境、文件系统、套接字、单元测试、Web服务网关接口、包管理等。这种关系可以用下边的图来表示:

    w3c、CommonJS以及Node之间的关系

    node借助CommonJS的Modules规范,实现了一套非常易用的模块系统,接下来,我们就讲讲这个由npm管理的模块系统。

    CommonJS的模块规范

    CommonJS的模块规范包含三个部分:模块引用、模块定义和模块标识。

    模块引用

    const math = require('math');
    

    上边的代码展示了模块引用的方法,使用的是CommonJS规范中定义的require()方法。通过require引入一个模块API到当前的上下文中。

    模块定义

    对于引入的模块,CommonJS在上下文中提供了exports对象用于导出当前模块的方法或者变量,同时exports也是模块的唯一出入口,这个就类似于面向对象的封装特性了。在模块中,还存在一个module对象,它代表模块自身,也就是说,在node中,一个文件就是一个模块,这个module就代表了这个文件,而这个exports只是module的一个属性。

    // math.js
    exports.add = function () {
        var sum = 0,
            i = 0,
            args = arguments,
            l = args.length;
        while (i < l) {
            sum += args[i++];
        }
        return sum;
    };
        ....
        ....
    
    // program.js
    var math = require('math');
    exports.increment = function (val) {
        return math.add(val, 1);
    };
    
    模块定义

    模块标识

    模块标识其实就是传递给require()方法的参数,这个参数需要符合如下特点:

    1.建议使用小驼峰命名字符串
    2.以.或..的相对路径开头,或者直接使用绝对路径。当然,如果在同一个目录下,也可以直接引入。
    3.文件名不需要文件名后缀.js

    小结

    CommonJS构建的这套模块导出和引入机制使得用户完全不必考虑变量污染,命名空间等方案与之相比相形见绌。

    Node的模块实现

    node模块分类

    分类 说明 加载
    核心模块 Node程序自身提供的模块 在node源代码编译的过程中,就编译进了二进制执行文件,属于安装包的一部分。在node进程启动时,部分核心模块会被直接加载进内存,因此,这部分核心模块的引入不需要文件定位和编译执行,并且优先进行路径分析,所以核心模块加载速度最快。如果想要提高自己的node的加载速度,可以把自己的包,写入到安装包装中,使之变成核心模块。
    文件模块 用户自己编写的模块和网络上的第三方模块 文件模块是在运行时动态加载的,需要完整的路径分析、文件定位、编译执行的过程,加载速度比核心模块加载的速度要慢。

    node模块引入的步骤

    路径分析、文件定位和编译执行。

    模块加载

    node模块会优先从缓存加载,前端浏览器会缓存静态脚本文件以提高前端的访问速度,因此,node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同之处在于,浏览器缓存文件,node缓存的是编译和执行后的对象。不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这被称为第一优先级。当然,核心模块的缓存检查先于文件模块的缓存检查。

    优先从缓存加载,例如几个文件都用了const router = require('koa-router')();路由这个模块,那么可以在初始化node的时候就将这个模块加载到缓存中,这样,可以提高后续应用中模块的访问速度。

    路径分析和文件定位

    因为,标识符有几种形式:

    1.核心模块:如http、fs、path等。
    2..或者..开始的相对路径文件模块。
    3.以/开始的绝对路径文件模块。
    4.非路径形式的文件模块,例如自定义的connect模块。

    核心模块

    核心模块的优先级,仅次于缓存加载,它在node的源代码编辑过程中已经编译为二进制代码,加载速度最快。(注意:在自定义的标识符命名上,请不要和核心模块产生冲突)

    核心模块加载时第二快的。

    路径形式的文件模块

    以.、..和/开始的标识符,这里都被当做文件模块来处理,require会将这些路径转化成真实路径,并以真实路径作为索引,然后,将编译结果放入缓存,以加快二次加载。

    路径形式的文件模块加载时第三快的。

    自定义模块

    自定义模块可能是一个文件或者是一个包,因为既不是核心模块,又没有路径,因此加载速度是最慢的。为了更好的理解自定义模块,我们先来了解一下模块路径这个概念。

    模块路径

    console.log(module.paths);
    

    通过这个语句可以查看模块路径,得到的模块路径是一个路径数组。

    //linux
    [ '/home/jackson/research/node_modules',
    '/home/jackson/node_modules',
    '/home/node_modules',
    '/node_modules' ]
    
    //win
    [ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ]
    

    我们可以看出,这个模块路径的生成规则是:
    1.当前文件目录下的node_modules目录
    2.父目录下的的node_modules目录
    3.爷爷目录下的node_modules目录
    4.祖宗目录下的的node_modules目录,也就是向上一直找,直到根目录下的node_modules目录

    这个查找方式很像原型链或者作用域链。在加载过程中,node会逐步尝试模块路径中的路径,直到找到文件为止,那么如果路径越深,模块查找耗时就会越多,加载时间也就会越慢。

    文件定位

    文件扩展名分析、目录和包的处理。

    文件扩展名分析

    1.require中不需要包含扩展名,node会按照.js、.json、.node的次序补足扩展名。由于,在这个过程中,node会调用fs进行单线程阻塞,因此,可以为json和.node的文件加上扩展名,以此提高效率。

    另外,书中说的一句话:同步配合缓存,可以大幅度缓解node单线程中阻塞式调用的缺陷,我没有明白,准备问问作者去,或者以后慢慢体会。

    目录分析和包

    如果文件没有找到,但是查到了一个目录的话,此时会将目录当做包来处理。首先,会在这个目录下寻找package.json,通过JSON.parse()解析出包的描述对象,从中取出main属性制定的文件名进行定位。如果这些都没有,则会默认把index.js或者index.json或者index.node作为制定的加载文件。如果都没有,就会报错了。

    模块编译

    node中每个文件就是一个模块,他的定义如下:

    function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if (parent && parent.children) {
    parent.children.push(this);
    }
    this.filename = null;
    this.loaded = false;
    this.children = [];
    }
    

    编译过程会对不同的文件类型进行区分:
    1.js文件,通过fs模块同步读取后,并编译。
    2.node文件,这是用c/c++编写的扩展文件,通过dlopen()方法加载并编译。
    3.json文件,通过fs模块同步读取后,用JSON.parse()解析并返回结果。代码如下:

    // Native extension for .json
    Module._extensions['.json'] = function (module, filename) {
        var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
        try {
            module.exports = JSON.parse(stripBOM(content));
        } catch (err) {
            err.message = filename + ': ' + err.message;
            throw err;
        }
    };
    
    console.log(require.extensions);
    
    //{ '.js': [Function], '.json': [Function], '.node': [Function] }
    //通过require.extensions可以查看到已有的扩展加载方式
    
    

    4.其他文件,当做js文件处理。

    每一个编译成功的模块都会将文件路径作为索引缓存到Module._cache对象中,提高了二次引入的性能。

    js模块编译

    基于CommonJS模块规范,每一个模块文件都包含require、exports、module三个变量,同时,node API中还提供了__filename、__dirname这两个变量。这些,都是在编译过程中,由node进行的包装,并自动添加的,我们看一下编译后的样子:

    (function (exports, require, module, __filename, __dirname) {
    var math = require('math');
    exports.area = function (radius) {
    return Math.PI * radius * radius;
    };
    });
    

    这样,每个文件模块之间都有了作用域隔离,包装后,代码会通过vm原生模块(这里就是V8的原生模块)调用runInThisContext()方法执行(类似于eval),返回一个具体的function对象。这个对象,就可以被其他文件(也是模块)调用了,只不过调用只限于使用exports上的属性和方法。

    另外,由于exports对象是通过形式参数传递的,因此,直接改变赋值只会改变该形参,但不能改变作用域外的值,例如:

    var change = function (a) {
    a = 100;
    console.log(a); // => 100
    };
    var a = 10;
    change(a);
    console.log(a); // => 10
    

    在这种情况下,使用module.exports就可以了。

    私有方法的测试

    此处还需要了解另外一种引入,rewire,他会在编译代码的时候,为代码增加set和get方法,通过闭包将私有方法对外暴露。

    在模块中的没有用exports引用的都是私有方法,这部分的测试也很重要。我们可以使用rewire来进行私有模块的测试,也就是使用rewire引用模块

    //
    var limit = function (num) {
        return num < 0 ? 0 : num;
    };
    
    //测试用例
    it('limit should return success', function () {
        var lib = rewire('../lib/index.js');
        var litmit = lib.__get__('limit');
        litmit(10).should.be.equal(10);
    });
    

    rewire的模块引入和require一样,都会为原始文件增加参数:

    (function(exports, require, module, __filename, __dirname) {֖ })
    

    此外,他还会注入其他的代码:

    (function (exports, require, module, __filename, __dirname) {
        var method = function () { };
        exports.__set__ = function (name, value) {
            eval(name " = " value.toString());
        };
        exports.__get__ = function (name) {
            return eval(name);
        };
    });
    

    每一个被rewire引入的模块,都会有set()和get()方法,这个就是巧妙的利用了闭包的原理,在eval()执行时,实现了对模块内部局部变量的访问,从而可以将局部变量导出给测试用例进行调用执行。

    node模块编译(c/c++模块)

    通过process.dlopen()进行编译,但是,实际上.node是已经通过c/c++编译完成的文件,因此,这个编译过程只是将.node文件进行关联和加入缓存。后边,我们将会讲解如何自己编译c/c++文件,并得到.node文件。

    json模块编译

    这个是最直接,也是最简单的,node会直接将json在require的作用下解析为可以使用的字符串并关联到exports上,都做完后,还会进行缓存,提高再次调用的效率。因此,不必自己再次调用JSON.parse()方法了,去解析json了。

    核心模块

    node的核心模块是c/c++和js写的,其中c/c++文件源码保存在node项目的src下,js文件源码保存在node的lib下。

    js核心模块编译

    1.首先会将js模块文件编译为c/c++代码,然后才会编译c/c++文件。
    2.转存这些由js编译为的c/c++代码,这里node采用了v8附带的js2c.py工具,将内置的js代码(src/node.js和lib/*.js)转换为c++里的数组,生成node_natives.h头文件。我们看看代码:

    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", dgram_native, sizeof(dgram_native) - 1 },
        ...
    };
        }
    
    

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

    编译过程

    与普通js模块一样,核心模块也会经历包装的过程,将require、exports、module、__filename、__dirname等参数增加上,并完成作用域分离。但是,这些js代码是从内存加载而来的,也就是在process.binding('natives')取出,编译后会缓存在NativeModule._cache对象上。此处代码如下:

    function NativeModule(id) {
    this.filename = id + '.js';
    this.id = id;
    this.exports = {};
    this.loaded = false;
    }
    NativeModule._source = process.binding('natives');
    NativeModule._cache = {};
    

    c/c++核心模块编译过程

    在核心模块中,有些模块全部由c/c++编写,有些模块则由c/c++完成核心部分,其他部分则由js实现包装或向外导出,以满足性能需求,这也是node能够提高性能的一种常见方式。

    这些全部由c/c++编写的模块被称为内建模块,例如:buffer、crypto、evals、fs、os等

    内建模块的组织形式

    内建模块的结构定义如下:

    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成员:

    #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_extensions.h文件将这些散列的内建模块统一放入一个叫node_module_list的数组中,这些模块有:

    node_buffer
    node_crypto
    node_evals
    node_fs
    node_http_parser
    node_os
    node_zlib
    node_timer_wrap
    node_tcp_wrap
    node_udp_wrap
    node_pipe_wrap
    node_cares_wrap
    node_tty_wrap
    node_process_wrap
    node_fs_event_wrap
    node_signal_watcher

    这些内建模块的取出也十分简单,node提供了get_builtin_module()方法,从node_module_list数组中取出这些模块。

    内建模块的优势在于,c/c++的效率高于js,编译后直接变为二进制文件,进入缓存,直接调用。

    内建模块的导出

    文件模块依赖核心模块,核心模块依赖内建模块。我们看个图:

    依赖关系

    因此,文件模块不推荐调用内建模块,但是可以通过process.Binding()来加载内建模块(Binding()的实现正在src/node.cc中,当然,如果不是十分了解内建模块,请慎重使用process.binding()来之间调用内建模块)

    static Handle < Value > Binding(const Arguments& args) {
        HandleScope scope;
    Local < String > module = args[0] -> ToString();
    String:: Utf8Value module_v(module);
    node_module_struct * modp;
    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);
    }
    // Append a string to process.moduleLoadList
    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));
    if ((modp = get_builtin_module(* module_v)) != NULL) {
        exports = Object:: New();
        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_builtin_module()方法取出内建模块对象,接着执行register_func()填充到exports空对象上,最后,将exports对象按照模块名缓存,并返回给调用方。

    这个方法还可以导出其他内容,例如js核心文件被c/c++数组存储后,可以通过process.binding('natives')取出NativeModule._source

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

    该方法将通过js2c.py工具转换出的字符串数组取出,然后重新转换为普通字符串,已对js核心模块进行编译和执行。

    核心模块的引入流程

    看图就明白了


    核心模块的引入流程

    核心模块的编写

    前边说了这么多,其实就是为编写核心模块做准备的。当然,尽管我们没有参与核心模块编写的机会,但是,了解其原理,总是好的。

    我们给出一个简单的js版本模型,也就是hello world来看一下如何编写c/c++核心模块。

    exports.sayHello = function () {
    return 'Hello world!';
    };
    

    第一步:编写头文件和编写c/c++
    写一个node_hello.h并保存到node的src下

    #ifndef NODE_HELLO_H_
    #define NODE_HELLO_H_
    #include <v8.h>
    namespace node {
    // 预定义方法
    v8::Handle<v8::Value> SayHello(const v8::Arguments& args);
    }
    #endif
    

    第二步编写node_hello.cc并保存到node的src下

    #include < node.h >
    #include < node_hello.h >
    #include < v8.h >
        namespace node {
        using namespace v8;
        // 实现预定义的方法
        Handle < Value > SayHello(const Arguments& args) {
    HandleScope scope;
        return scope.Close(String:: New("Hello world!"));
    }
    // 给传入的目标对象添加sayHello方法
    void Init_Hello(Handle < Object > target) {
        target -> Set(String:: NewSymbol("sayHello"), FunctionTemplate:: New(SayHello) -> GetFunction());
    }
    }
    // 调用NODE_MODULE()将注册方法定义到内存中
    NODE_MODULE(node_hello, node:: Init_Hello)
    

    第三步:

    修改src/node_extensions.h,在NODE_EXT_LIST_END前,添加NODE_EXT_LIST_ITEM(node_hello),以此,将node_hello加入到node_module_list数组中。

    第四步:

    编译两份代码,变为可执行文件。

    第五步:

    修改node.gyp,并在target_name:node节点的sources中添加上新编写的两个文件。然后,从新编译整个node项目。

    编译安装后,就可以使用了。

    $ node
    > var hello = process.binding('hello');
    > hello.sayHello();
    'Hello world!'
    >
    

    c/c++扩展模块

    js的位运算是参考java实现的,但是,java的位运算是基于int的,js中只有double,因此,需要先将double转换为int,因此,效率不是很高。

    在应用中,会存在大量的位运算需求,包括转码、编码、解码等,此时,可以使用c/c++扩展模块来节省cpu资源。

    c/c++扩展模块属于node文件模块的一类,首先将c/c++编译为.node文件,然后通过process.dlopen()方法加载执行。(此处需要注意,不同平台由于编译器的差异,因此,编译的结果其实也不一样,inx下通过g++/gcc编译为的是.so,win下编译出的是.dll,node统一将其命名为*.node),我们来看下边这个图做的详细介绍:

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

    前提条件

    1.GYP项目生产工具,Generate Your Projects,哈哈哈,生成你的项目,may the force be with you....通过gyp工具,帮助生成各个平台下的项目文件,例如win下的.sln,mac下的文件等,另外,node自身编码其实就是通过gyp编译的,我们还可以找一个扩展工具node-gyp,安装如下:

    npm install -g node-gyp
    

    2.V8引擎c++库,v8是c++写的,实现js和c++相互调用
    3.libuv库,通过libuv调用底层功能,例如事件循环的epoll,还有文件操作等等
    4.node内部库,例如node::ObjectWrap类可以用于包装你自己写的自定义类,它可以帮助实现对象回收等工作。
    5.其他库,这些库存在于deps下,例如zlib、openssl、http_parser

    c/c++扩展模块的编写

    前边铺垫了这么多,终于要进行编写了,好激动哈哈哈哈。

    c/c++扩展模块,可以先编译,然后直接通过dlopen()动态加载,不需要跟随node一起编译。

    我们来看一下同样的hello world是如何加载的:

    exports.sayHello = function () {
    return 'Hello world!';
    };
    

    编写hello.cc,并存储到src下

    #include <node.h>
    #include <v8.h>
    using namespace v8;
    // 实现预定义的方法
    Handle<Value> SayHello(const Arguments& args) {
    HandleScope scope;
    return scope.Close(String::New("Hello world!"));
    }
    // 给传入的目标对象添加sayHello()方法
    void Init_Hello(Handle<Object> target) {
    target->Set(String::NewSymbol("sayHello"), FunctionTemplate::New(SayHello)->GetFunction());
    }
    // 调用NODE_MODULE()方法将注֩方法定义到内存中
    NODE_MODULE(hello, Init_Hello)
    

    然后,将方法挂载到target对象上,然后通过NODE_MODULE声明即可。

    然后就可以通过dlopen()动态加载了。

    c/c++扩展模块的编译

    在gyp的帮助下进行编译,先写*.gyp文件,然后调用node-gyp进行编译,这个文件被约定外binding.gyp

    {
        'targets': [
            {
                'target_name': 'hello',
                'sources': [
                    'src/hello.cc'
                ],
                'conditions': [
                    ['OS == "win"',
                        {
                            'libraries': ['-lnode.lib']
                        }
                    ]
                ]
            }
        ]
    }
    

    然后执行:

    node-gyp configure
    

    输出结果:

    gyp info it worked if it ends with ok
    gyp info using node-gyp@0.8.3
    gyp info using node@0.8.14 | darwin | x64
    gyp info spawn python
    gyp info spawn args [ '/usr/local/lib/node_modules/node-gyp/gyp/gyp',
    gyp info spawn args 'binding.gyp',
    gyp info spawn args '-f',
    gyp info spawn args 'make',
    gyp info spawn args '-I',
    gyp info spawn args '/Users/jacksontian/git/diveintonode/examples/02/addon/build/config.gypi',
    gyp info spawn args '-I',
    gyp info spawn args '/usr/local/lib/node_modules/node-gyp/addon.gypi',
    gyp info spawn args '-I',
    gyp info spawn args '/Users/jacksontian/.node-gyp/0.8.14/common.gypi',
    gyp info spawn args '-Dlibrary=shared_library',
    gyp info spawn args '-Dvisibility=default',
    gyp info spawn args '-Dnode_root_dir=/Users/jacksontian/.node-gyp/0.8.14',
    gyp info spawn args '-Dmodule_root_dir=/Users/jacksontian/git/diveintonode/examples/02/addon',
    gyp info spawn args '--depth=.',
    gyp info spawn args '--generator-output',
    gyp info spawn args 'build',
    gyp info spawn args '-Goutput_dir=.' ]
    gyp info ok
    

    node-gyp configure会在当前目录创建build目录,并生成相关的项目文件,*inx下build目录会有Makefile等文件,win下,会生成vcxproj等文件

    然后执行构建命令

    $ node-gyp build
    

    会输出

    gyp info it worked if it ends with ok
    gyp info using node-gyp@0.8.3
    gyp info using node@0.8.14 | darwin | x64
    gyp info spawn make
    gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
    CXX(target) Release/obj.target/hello/hello.o
    SOLINK_MODULE(target) Release/hello.node
    SOLINK_MODULE(target) Release/hello.node: Finished
    gyp info ok
    

    最终获得了build/Release/hello.node文件。

    c/c++扩展模块的加载

    直接使用require就可以了,这里node会调用process.dlopen()动态加载这个文件,然后使用即可。

    var hello = require('./build/Release/hello.node');
    //这里node会调用process.dlopen()动态加载这个文件:
    //Native extension for .node
    //Module._extensions['.node'] = process.dlopen;
    console.log(hello.sayHello());
    

    process.dlopen()的引入过程

    1.通过libuv库,调用uv_dlopen()打开动态链接库。
    2.通过libuv库,调用uv_dlsym()找到动态链接库中通过NODE_MODULE宏定义的方法地址。

    注意:libuv是一层封装,那么在*inx下调用的是dlfcn.hܿ头文件定义的dlopen()和dlsym(),在win下则是LoadLibraryExW()和GetProcAddress()

    process.dlopen()的引入过程

    由于编写模块时,通过NODE_MODULE将模块定义为node_module_struct结构,所以在获取函数地址之后,将它映射为node_module_struct几乎是无缝对接的。接下来的过程就是讲传入的exports对象作为实参运行,将c++中定义的方法挂载在exports对象上,这样就可以实现跟js文件模块一样的调用效果了。另外,因为,*.node是已经编译好的,因此,无需在加载后进行编译,这也提高了一些速度。

    模块调用栈

    模块调用栈,也就是各个模块之间的调用关系。

    模块之间的调用关系

    通过这个图,我们还可以看出js模块既可以是功能模块,也可是作为c/c++模块的包装。

    包与npm

    首先,我们来看一下弱类型的js是如何基于CommonJS实现包组织模块的:

    包组织模块示意图

    同时,CommonJS对于包的规范也很简单,只有包结构和包描述两部分。

    包结构

    符合commonjs规范的包是这样婶的:
    1.package.json,包描述文件
    2.bin,存放可执行二进制文件的目录,有时,开发的程序可能是一个命令行工具,因此,通过全局安装,那么就可以到bin下找到执行命令的工具了。
    3.lib,用于存放js代码的目录
    4.doc,用于存放文档的目录
    5.test,用于存放单元测试用例的代码的目录

    包描述

    包描述文件,就是一个package.json,它需要包含:
    1.name,包名,包名必须是唯一的。
    2.description,包简介
    3.version,版本号。通常为major.minor.revision的格式,版本号可以控制npm下载不同的版本,确认是开发版本还是测试版本等。
    4.keywords,帮助npm进行搜索。
    5.maintaners,维护者列表,格式是maintaners:[{name,email,web}]
    6.contributors,贡献者列表,格式是contributors:[{name,email,web}]
    7.bugs,反馈bugs的网址
    8.licenses,许可。
    9.repositories,代码托管地址
    10.dependencies,包依赖,通过这个依赖可以确认那些包需要被下载。
    11.homepage,可选,当前包的相关网页
    12.os,操作系统
    13.cpu,cpu
    14.engine,支持js的引擎,可以填写ejs、 flusspferd、 gpsee、 jsc、spidermonkey、 narwhal、 node和v8。
    15.builtin,标志当前包是否是内建在底层系统的标志组件
    16.directories,包目录说明
    17.implements,实现规范,标志当前包实现了哪些commonjs规范。
    18.scripts,脚本对象说明,说明安装、编译、测试、卸载

    "scripts": { "install": "install.js",
    "uninstall": "uninstall.js",
    "build": "build.js",
    "doc": "make-doc.js",
    "test": "test.js" }
    

    19.author,包作者
    20.main,require()会寻找main指定的程序入口,如果没写,则为index,并轮询查找index.js、index.node、index.json
    21.devDependencies,开发模式下的依赖。

    我们来看一下express的package.json文件:

    {
        "name": "express",
            "description": "Sinatra inspired web development framework",
                "version": "3.3.4",
                    "author": "TJ Holowaychuk <tj@vision-media.ca>",
                        "contributors": [
                            {
                                "name": "TJ Holowaychuk",
                                "email": "tj@vision-media.ca"
                            },
                            {
                                "name": "Aaron Heckmann",
                                "email": "aaron.heckmann+github@gmail.com"
                            },
                            {
                                "name": "Ciaran Jessup",
                                "email": "ciaranj@gmail.com"
                            },
                            {
                                "name": "Guillermo Rauch",
                                "email": "rauchg@gmail.com"
                            }
                        ],
                            "dependencies": {
            "connect": "2.8.4",
                "commander": "1.2.0",
                    "range-parser": "0.0.4",
                        "mkdirp": "0.3.5",
                            "cookie": "0.1.0",
                                "buffer-crc32": "0.2.1",
                                    "fresh": "0.1.0",
                                        "methods": "0.0.1",
                                            "send": "0.1.3",
                                                "cookie-signature": "1.0.1",
                                                    "debug": "*"
        },
        "devDependencies": {
            "ejs": "*",
                "mocha": "*",
                    "jade": "0.30.0",
                        "hjs": "*",
                            "stylus": "*",
                                "should": "*",
                                    "connect-redis": "*",
                                        "marked": "*",
                                            "supertest": "0.6.0"
        }
        "keywords": [
            "express",
            "framework",
            "sinatra",
            "web",
            "rest",
            "restful",
            "router",
            "app",
            "api"
        ],
            "repository": "git://github.com/visionmedia/express",
                "main": "index",
                    "bin": {
            "express": "./bin/express"
        },
        "scripts": {
            "prepublish": "npm prune",
                "test": "make test"
        },
        "engines": {
            "node": "*"
        }
    }
    

    npm常用功能

    1.查看帮助,例如npm help <command>
    2.安装依赖包,npm install,当然,还有全局安装,也就是-g,它根据package.json描述的bin字段进行配置,将实际脚本链接到与node可执行文件相同的路径下。这个目录可以通过path.resolve(process.execPath, '..', '..', 'lib', 'node_modules');推算。如果node可执行位置是/usr/local/bin/node,那么这个全局安装的模块就在/usr/local/lib/node_modules下。然后通过软链接的方式,链接到node可执行目录下。另外,还可以进行本地安装,只需要指明要安装的包的package.json所在的位置即可(位置可以使url、文件和文件夹)。还可以从非官方源安装,执行时需要增加后缀,--registry=http://registry.url,例如:npm install underscore -registry=http://registry.url如果使用过程几乎都采用镜像源安装,可以通过命令修改默认源:npm config set registry http://registry.url

    npm钩子命令

    钩子命令如下:

    "scripts": {
    "preinstall": "preinstall.js",
    "install": "install.js",
    "uninstall": "uninstall.js",
    "test": "test.js"
    }
    

    例如,npm test会自动指向test目录,并执行测试。此时,会调用package.json中的测试命令,以此进行测试。

    包发布

    笔者自己也写过包,并发布,对,这个笔者不是朴灵,是我,是我,是我白昔月。名字为票市通对接云之讯短信接口,地址是:https://www.npmjs.com/package/bimartmessage,大家可以看看

    1.编写模块
    2.初始化package.js,可以用npm init快速进行
    3.注册包仓库账户,npm adduser
    4.上传包,npm publish <folder>
    5.安装包,npm install
    6.管理包权限,npm owner,通过这个命令可以添加、删除、查看帮助写包的人。

    npm owner ls <package name>
    npm owner add <user> <package name>
    npm owner rm <user> <package name>
    

    分析包

    使用 npm ls分析包。


    使用 npm ls分析包

    局域npm

    这个可以看附录D,不过,我不一定写那部分笔记,如果写了,再补充吧,先看看局域npm的结构:


    混合使用官方仓库和局域仓库的示意图

    另外,企业内部使用局域npm,可以保证企业内部的开发协助,杜绝企业内部的程序大量的复制粘贴,造成代码的不可维护。通过企业内部的npm对代码、对包进行统一管理,从而提高项目的维护和使用效率。

    npm潜在问题

    安全问题,不用使用来路不明的包。如果大企业的话,一定要经过安全部门的认证才可以使用。
    排查npm潜在问题的步骤如下:
    1.具备良好的测试
    2.具备良好的文档,readme、api等
    3.具备良好的测试覆盖率
    4.具备良好的编码规范
    5.其他各种手段......

    前后端共用模块

    前端的瓶颈在于带宽和浏览器兼容(需要网络加载资源),后端的瓶颈在于cpu和内存使用。因此,commonjs给出了AMD规范,Asynchronous Module Definition,也就是异步模块定义。另外,阿里的玉伯还提出了CMD规范。

    AMD规范

    AMD规范定义模块:define(id?, dependencies?, factory);
    id和依赖是可选的,factory是实际的代码,我们看个例子:

    define(function () {
        var exports = {};
        exports.sayHello = function () {
            alert('Hello from module: ' + module.id);
        };
        return exports;
    });
    

    通过define包装,进行作用域隔离,避免污染全局变量或者全局命名空间。同时,结果通过返回方式导出。(node是require()加载导出)

    CMD规范

    我们来比较一下AMD和CMD

    先看AMD

    //依赖,也就是node中的require,需要在定义时引入,不是动态的。
    define(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {};
    });
    

    再看CMD

    define(factory);
    //然后在需要依赖时,CMD动态引入
    define(function(require, exports, module) {
    // The module code goes here
    //依赖通过require, exports, module传递给模块,通过require()可以随时动态引入需要的依赖
    })
    

    兼容多种模块规范

    为了让同一个模块可以运行在前后端,开发者需要将类库封装在一个闭包内(这个闭包可以贮存在内存中,供反复使用),下面写一个代码,兼容node、AMD、CMD和常见浏览器环境。(应用方面,比如计算钱的时候,就需要这样的前后端统一的处理方式,还有就是日期等都是有需要的)

    ; (function (name, definition) {
        // 检测上下文环境是否为AMD或者 CMD
        var hasDefine = typeof define === 'function',
            // 检查上下文环境是否为Node
            hasExports = typeof module !== 'undefined' && module.exports;
        if (hasDefine) {
            // AMD环境或者 CMD环境
            define(definition);
        } else if (hasExports) {
            // 定义为普通Node模块
            module.exports = definition();
        } else {
            // 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
            this[name] = definition();
        }
    })('hello', function () {
        var hello = function () { };
        return hello;
    });
    

    相关文章

      网友评论

        本文标题:nodejs深入学(3)模块机制

        本文链接:https://www.haomeiwen.com/subject/gnoyixtx.html