Node.js随手笔记(一):node简介与模块系统
Node.js简介
首先从名字说起,网上查阅资料的时候会发现关于node的写法五花八门,到底哪一种写法最标准呢?遵循官方网站的说法,一直将项目称之为“Node”或者“Node.js”。
简单来说,Node就是运行在服务器端的JavaScript。
JavaScript是一门脚本语言(可以用来编程的并且直接执行源代码的语言,就是脚本语言),脚本语言都需要一个解析器才能运行。对于写在html中的js,通常是由浏览器去解析执行。对于独立执行的js代码,则需要Node这个解析器解析执行。
每一种解析器就是一个运行环境,不但允许js定义各种数据结构,进行各种计算,还允许js使用运行环境提供的内置对象和方法做一些事情。例如运行在浏览器中的js的用途是操作DOM,浏览器提供了document之类的内置对象。而运行在node中的js的用途是操作磁盘文件或搭建HTTP服务器,node就相应提供了fs、http等内置对象。
Node不是js应用,而是js的运行环境。
看到Node.js这个名字,可能会误以为这是一个JavaScript应用,事实上,node采用c++语言对Google V8引擎进行了封装,是一个JavaScript运行环境。V8引擎执行JavaScript的速度非常快,性能也非常好。node是一个让开发者可以快速创建网络应用的服务端JavaScript平台,同时运用JavaScript进行前端与后端编程,从而开发者可以更专注于系统的设计以及保持其一致性。
// 快速构建服务器 const http = require('http') http.createServer((req,res)=>{ res.writeHead(200, {'Content-Type': 'text/plain'}) res.end('hello World!') }).listen(8088) $ node helloWorld.js
Node采用事件驱动、异步编程
node的设计思想以事件驱动为核心,它提供的绝大多数API都是基于事件的、异步的风格。开发者需要根据自己的业务逻辑注册相应的回调函数,这些回调函数都是异步执行的。这意味着虽然在代码结构中,这些函数看似是依次注册的,但是它们并不依赖自身出现的顺序,而是等待相应的事件触发。
在服务器开发中,并发的请求处理是个大问题,阻塞式的函数会导致资源浪费和时间延迟。通过事件注册、异步函数,开发者可以充分利用系统资源,执行代码无须阻塞等待,有限的资源可以用于其他的任务。
Node以单进程、单线程模式运行
这点和JavaScript的运行方式一致,事件驱动机制是node通过内部单线程高效率地维护事件循环队列来实现的,没有多线程的资源占用和上下文切换,这意味着面对大规模的http请求,node凭借事件驱动搞定一切。由此我们是否可以推测这样的设计会导致负载的压力集中在CPU(事件循环处理?)而不是内存。淘宝共享数据平台团队对node的性能测试:
- 物理机配置:RHEL 5.2、CPU 2.2GHz、内存4G
- Node.js应用场景:MemCache代理,每次取100字节数据
- 连接池大小:50
- 并发用户数:100
- 测试结果(socket模式):内存(30M)、QPS(16700)、CPU(95%)
眼见为实,虽然看不太懂这些测试数据,但是最终测试结果是:它的性能让人信服。
Node.js模块系统
为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。模块系统是Node组织管理代码的利器也是调用第三方代码的途径。
模块是Node应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。
理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,
- JavaScript没有模块系统。没有原生的支持密闭作用域或依赖管理。
- JavaScript没有标准库。除了一些核心库外,没有文件系统的API,没有IO流API等。
- JavaScript没有标准接口。没有如Web Server或者数据库的统一接口。
- JavaScript没有包管理系统。不能自动加载和安装依赖。
要想实现模块化编程首先需要解决的问题是,命名冲突以及文件依赖问题。
CommonJS规范
于是便有了CommonJS规范的出现,其目标是为了构建JavaScript在包括web服务器,桌面,命令行工具,以及浏览器方面的生态系统。CommonJS制定了解决这些问题的一些规范,而node就是这些规范的一种实现。node自身实现了require方法作为其引入模块的方法,同时npm也基于CommonJS定义的包规范,实现了依赖管理和模块自动安装等功能。
Node中模块分类
原生模块
原生模块即为Node API提供的核心模块(如:os、http、fs、buffer、path等模块),原生模块在node源代码编译的时候编译进了二进制执行文件,加载的速度最快。
const http = require('http');
文件模块
为动态加载模块,动态加载的模块主要由原生模块module来实现和完成。原生模块在启动时已经被加载,而文件模块需要通过调用module的require方法来实现加载。
首先定义一个文件模块,以计算圆形的面积和周长两个方法为例:
const PI = Math.PI; exports.area = (r) => { return PI * r * r; }; exports.circumference = (r) => { return 2 * PI * r; };
将这个文件存为circle.js,并新建一个app.js文件,并写入以下代码:
// 调用文件模块必须指定路径,否则会报错 const circle = require('./circle.js'); console.log( 'The area of a circle of radius 4 is ' + circle.area(4));
在require了这个文件之后,定义在exports对象上的方法便可以随意调用。
包管理
Node Packaged Modules 简称NPM,是随同node一起安装的包管理工具。Node本身提供了一些基本API模块,但是这些基本模块难以满足开发者需求。Node需要通过使用NPM来管理开发者自我研发的一些模块,并使其能够公用与其他的开发者。
NPM建立了一个node生态圈,node开发者和用户可以在里边互通有无。当你需要下载第三方包时,首先要知道有哪些包可用 npmjs.com 提供了可以根据包名来搜索的平台。知道包名后就可以使用命令去安装了。
npm -v // 测试是否安装成功。
npm的常用命令行代码:
npm install moduleNames
npm install moduleNames -g // 全局安装 npm install [email protected] // 安装特定版本依赖 npm install moduleNames --save // --save 可简写为 -S // 会在package.json的dependencies属性下添加moduleNames依赖 即生产依赖插件 npm install moduleNames --save-dev // --save-dev 可简写为 -D // 会在package.json的devDependencies属性下添加moduleNames依赖 即开发依赖插件
卸载模块
npm uninstall moduleNames
更新模块
npm update moduleNames
搜索模块
npm search moduleNames
切换模板仓库源:
npm config set registry https://registry.npm.taobao.org/ npm config get registry // 执行验证是否切换成功
在NPM服务器上发布自己的包
第一次使用NPM发布自己的包需要在 npmjs.com 注册一个账号。也可以使用命令 npm adduser,提示输入账号,密码和邮箱,然后将提示创建成功('Logged in as Username on https://registry.npmjs.org/.')。
输入npm init命令,根据提示配置包的相关信息,生成相应的package.json。npm命令运行时会读取当前目录的 package.json 文件和解释这个文件
通过npm publish发包,包的名称和版本就是你项目里package.json的name和vision。此处注意:
- name不能和已有包的名字重名。
- name不能有大写字母/空格/下划线。
- 不想发布到npm上的代码文件将它写入.gitignore或.npmignore中再上传。
- 更新包和发布包的命令一样,但是每次更新别忘记修改包的版本。
模块初始化
一个模块中的JavaScript代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。其中原生模块都被定义在lib这个目录下面,文件模块则不定性。
模块加载的优先级
模块加载的优先级:已经缓存模块 > 原生模块 > 文件模块 > 从文件加载
尽管require方法很简单,但是内部的加载却是十分复杂的
,其加载优先级也各自不同。如下图示:
模块加载策略
从原生模块加载
原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名之后,优先检查模块是否在原生模块列表中。
原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。
从文件加载
实际上,在文件模块中又分为三类模块,以后缀为区分,node会根据后缀名来决定加载方法。
- .js 通过fs模块同步读取js文件并编译执行。
- .node 通过c/c++进行编写的Addon。通过dlopen方法进行加载。
- .json 读取文件,调用JSON.parse解析加载。
当文件模块缓存中不存在,而且也不是原生模块的时候,node会解析require方法传入的参数,并从文件系统中加载实际的文件。
加载文件模块的工作主要有原生模块module来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法。
Module.runMain = function () { Module._load(process.argv[1], null, true); };
_load静态方法在分析文件名之后执行
var module = new Module(id, parent);
并根据文件路径缓存当前模块对象,该模块实例对象则根据文件名加载。
module.load(filename);
以.js后缀的文件为例,node在编译js文件的过程中实际完成的步骤是对js文件内容进行头尾包装。例如刚才的app.js,在包装之后变成这个样子:
(function (exports, require, module, __filename, __dirname) { var circle = require('./circle.js'); console.log('The area of a circle of radius 4 is ' + circle.area(4)); });
这段代码拥有明确的上下文,不污染全局,返回为一个具体的function对象。最后传入module对象的exports,require方法,module,文件名,目录名作为实参并执行。
这就是为什么require并有定义在app.js文件中,但是这个方法却存在的原因。在这个主文件中,可以通过require方法去引入其余的模块。而其实这个require方法实际调用的就是load方法。
load方法在载入、编译、缓存了module后,返回module的exports对象。这就是circle.js文件中只有定义在exports对象上的方法才能被外部调用的原因。
以上所描述的模块载入机制均定义在module模块之中。
文件模块加载过程中的路径分析
require方法接受以下几种参数的传递:
- http、fs、path等,原生模块。
- ./mod或../mod,相对路径的文件模块。
- /pathtomodule/mod, 绝对路径的文件模块。
- mod,非原生模块的文件模块。
在进入路径查找之前有必要描述以下module path这个node中的概念。对于每一个被加载的文件模块,创建这个模块对象的时候,这个模块便会有一个paths属性,它的值根据当前文件的路径计算得到。
例:
我们创建modulepath.js这样一个文件,其内容为:
console.log(module.paths);
执行node modulepath.js,将得到以下的输出结果:
[ '/Users/zhaoyunlong/Node/demo/node_modules', '/Users/zhaoyunlong/Node/node_modules', '/Users/zhaoyunlong/node_modules', '/Users/node_modules', '/node_modules' ]
Windows下:
[ 'E:\\Extra\\miniprogram\\gm-xcc-demo\\gm-demo\\node_modules', 'E:\\Extra\\miniprogram\\gm-xcc-demo\\node_modules', 'E:\\Extra\\miniprogram\\node_modules', 'E:\\Extra\\node_modules', 'E:\\node_modules' ]
可以看出module path的生成规则为:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录的node_modules目录;依次迭代,直到根目录下的node_modules目录。
除此之外还有一个全局module path,是当前node执行文件的相对目录(../../lib/node)。如果在环境变量中设置了HOME目录和NODE_PATH目录的话,整个路径还包含NODE_PATH和HOME目录下的.node_libraries与.node_modules。其最终值大致如下:
[ NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node ]
简单说就是,如果require绝对路径的文件,查找时不会去遍历每一个node_modules目录,其速度最快。其余流程如下:
- 从module path 数组中取出第一个目录作为查找基准。
- 直接从目录中查找该文件,如果存在,则结束查找。如果不存在,则进行下一条查找。
- 尝试添加.js、.json、.node后缀后查找,如果存在文件,则结束。如果不存在,则进行下一条。
- 尝试将require的参数作为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件。
- 尝试查找该文件,如果存在,则结束查找。如果不存在则进行第3条查找。
- 如果继续失败,则取出module path数组中的下一个目录作为基准查找,循环第1至5个步骤。
- 如果继续失败,循环第1至6个步骤,直到module path中的最后一个值。
- 如果仍然失败,则抛出异常。
整个查找过程十分类似JavaScript原型链的查找和作用域的查找。不同的是node对路径查找实现了缓存机制,否则每次判断路径都是同步阻塞式进行,会导致严重的性能消耗。