nodejs深入学(3)模块机制

前言

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;
});

推荐阅读更多精彩内容

  • 1 Node.js模块的实现 之前在网上查阅了许多介绍Node.js的文章,可惜对于Node.js的模块机制大都着...
    zlx_2017阅读 861评论 0 1
  • 1 Node.js模块的实现# 之前在网上查阅了许多介绍Node.js的文章,可惜对于Node.js的模块机制大都...
    七寸知架构阅读 1,840评论 1 50
  • 之前 ECMAScript 的问题: 没有模块系统,标准库较少(如文件系统等缺失API),没有标准接口,无包管理系...
    Air_cc阅读 207评论 0 0
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,313评论 2 41
  • 再没去成都前,就心心念念一定要去九寨沟。无奈从成都去九寨沟路途遥远,想去的心又一直作祟。最后决定报了一个四天的旅行...
    脱俗儿阅读 513评论 0 0
  • (孩儿脸-风雨惊声)台风袭穗城,风来啸啸声。楼高风吹急,怪声惊童心。抱着心安乐,奶饮慢慢尝。任它风再吹,倚窗雨打窗。
    甘朝武阅读 104评论 0 0
  • 校园里树上挂的果沒有长熟过的。中国厕所永运弥漫着一股酒香。我写文章与刻石牌,拜佛求神一样都是想永存长久,想灵魂不死。
    石岩_d707阅读 70评论 0 0
  • 来大理,一月有余。在这苍山洱海间,初来乍到,我不觉新奇,年深月久,可能也不会厌倦。曾经年少,向往远方。二十五岁之后...
    独孤久野阅读 387评论 10 8