前言
js是从网页小脚本演变过来的,至今,前端的js库,也不像一个真正的模块。前端js经历了工具类库、组件库、前端框架和前端应用的变迁。但是,依然没有完成真正的模块化转变(也就是不断的聚类和抽象)。因为,js没有模块,因此,也就没办法完成真正的封装等工作,只能通过人为的命名空间来约束代码。
这个第二章就是讲nodejs模块机制的。另外,由于原书的第二章编排的比较混乱,因此,我的笔记也做了结构上的调整。
CommonJS规范
CommonJS出现之前,js存在很多问题,没有模块系统、标准库较少、没有标准接口(例如数据库连接接口等)、缺乏包管理系统。
CommonJS的美好愿景是希望js在任何地方都可以运行。也就是说,js不仅仅可以开发网页程序,还可以开发服务器端程序、命令行工具、桌面应用以及混合应用。CommonJS规范涵盖了模块、二进制、Buffer、字符集编码、IO流、进程环境、文件系统、套接字、单元测试、Web服务网关接口、包管理等。这种关系可以用下边的图来表示:
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()
由于编写模块时,通过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
这个可以看附录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;
});