Node.js随手笔记(一):node简介与模块系统

Node.js简介

首先从名字提及,网上查阅资料的时候会发现关于node的写法五花八门,到底哪种写法最标准呢?遵循官方网站的说法,一直将项目称之为“Node”或者“Node.js”。html

简单来讲,Node就是运行在服务器端的JavaScript。

JavaScript是一门脚本语言(能够用来编程的而且直接执行源代码的语言,就是脚本语言),脚本语言都须要一个解析器才能运行。对于写在html中的js,一般是由浏览器去解析执行。对于独立执行的js代码,则须要Node这个解析器解析执行。前端

每一种解析器就是一个运行环境,不但容许js定义各类数据结构,进行各类计算,还容许js使用运行环境提供的内置对象和方法作一些事情。例如运行在浏览器中的js的用途是操做DOM,浏览器提供了document之类的内置对象。而运行在node中的js的用途是操做磁盘文件或搭建HTTP服务器,node就相应提供了fs、http等内置对象。node

Node不是js应用,而是js的运行环境。

看到Node.js这个名字,可能会误觉得这是一个JavaScript应用,事实上,node采用c++语言对Google V8引擎进行了封装,是一个JavaScript运行环境。V8引擎执行JavaScript的速度很是快,性能也很是好。node是一个让开发者能够快速建立网络应用的服务端JavaScript平台,同时运用JavaScript进行前端与后端编程,从而开发者能够更专一于系统的设计以及保持其一致性。c++

// 快速构建服务器
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都是基于事件的、异步的风格。开发者须要根据本身的业务逻辑注册相应的回调函数,这些回调函数都是异步执行的。这意味着虽然在代码结构中,这些函数看似是依次注册的,可是它们并不依赖自身出现的顺序,而是等待相应的事件触发。git

在服务器开发中,并发的请求处理是个大问题,阻塞式的函数会致使资源浪费和时间延迟。经过事件注册、异步函数,开发者能够充分利用系统资源,执行代码无须阻塞等待,有限的资源能够用于其余的任务。web

Node以单进程、单线程模式运行

这点和JavaScript的运行方式一致,事件驱动机制是node经过内部单线程高效率地维护事件循环队列来实现的,没有多线程的资源占用和上下文切换,这意味着面对大规模的http请求,node凭借事件驱动搞定一切。由此咱们是否能够推测这样的设计会致使负载的压力集中在CPU(事件循环处理?)而不是内存。淘宝共享数据平台团队对node的性能测试:数据库

  • 物理机配置:RHEL 5.二、CPU 2.2GHz、内存4G
  • Node.js应用场景:MemCache代理,每次取100字节数据
  • 链接池大小:50
  • 并发用户数:100
  • 测试结果(socket模式):内存(30M)、QPS(16700)、CPU(95%)

眼见为实,虽然看不太懂这些测试数据,可是最终测试结果是:它的性能让人信服。npm

Node.js模块系统

为了让Node.js的文件能够相互调用,Node.js提供了一个简单的模块系统。模块系统是Node组织管理代码的利器也是调用第三方代码的途径。

模块是Node应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件多是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。编程

理想状况下,开发者只须要实现核心的业务逻辑,其余均可以加载别人已经写好的模块。可是,json

  • 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的经常使用命令行代码:
  1. npm install moduleNames

    npm install moduleNames -g  // 全局安装
    
    npm install moduleNames@2.0.0   // 安装特定版本依赖
    
    npm install moduleNames --save  // --save 可简写为 -S
    // 会在package.json的dependencies属性下添加moduleNames依赖 即生产依赖插件
    
    npm install moduleNames --save-dev  // --save-dev 可简写为 -D
    // 会在package.json的devDependencies属性下添加moduleNames依赖 即开发依赖插件
  2. 卸载模块

    npm uninstall moduleNames
  3. 更新模块

    npm update moduleNames
  4. 搜索模块

    npm search moduleNames
  5. 切换模板仓库源:

    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方法很简单,可是内部的加载倒是十分复杂的
,其加载优先级也各自不一样。以下图示:

image

模块加载策略

从原生模块加载

原生模块的优先级仅次于文件模块缓存的优先级。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 ]

image

简单说就是,若是require绝对路径的文件,查找时不会去遍历每个node_modules目录,其速度最快。其他流程以下:

  1. 从module path 数组中取出第一个目录做为查找基准。
  2. 直接从目录中查找该文件,若是存在,则结束查找。若是不存在,则进行下一条查找。
  3. 尝试添加.js、.json、.node后缀后查找,若是存在文件,则结束。若是不存在,则进行下一条。
  4. 尝试将require的参数做为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件。
  5. 尝试查找该文件,若是存在,则结束查找。若是不存在则进行第3条查找。
  6. 若是继续失败,则取出module path数组中的下一个目录做为基准查找,循环第1至5个步骤。
  7. 若是继续失败,循环第1至6个步骤,直到module path中的最后一个值。
  8. 若是仍然失败,则抛出异常。

整个查找过程十分相似JavaScript原型链的查找和做用域的查找。不一样的是node对路径查找实现了缓存机制,不然每次判断路径都是同步阻塞式进行,会致使严重的性能消耗。

相关文章
相关标签/搜索