资讯专栏INFORMATION COLUMN

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

TNFE / 713人阅读

摘要:模块系统为了让的文件可以相互调用,提供了一个简单的模块系统。但是,没有模块系统。包管理简称,是随同一起安装的包管理工具。输入命令,根据提示配置包的相关信息,生成相应的。以上所描述的模块载入机制均定义在模块之中。

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 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依赖 即开发依赖插件

卸载模块

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:Extraminiprogramgm-xcc-demogm-demo
ode_modules",
  "E:Extraminiprogramgm-xcc-demo
ode_modules",
  "E:Extraminiprogram
ode_modules",
  "E:Extra
ode_modules",
  "E:
ode_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对路径查找实现了缓存机制,否则每次判断路径都是同步阻塞式进行,会导致严重的性能消耗。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96932.html

相关文章

  • Node.js 开发指南 读书笔记

    摘要:为指定事件注册一个监听器,接受一个字符串和一个回调函数。发射事件,传递若干可选参数到事件监听器的参数表。为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。 1.Node.js 简介 Node.js 其实就是借助谷歌的 V8 引擎,将桌面端的 js 带到了服务器端,它的出现我将其归结为两点: V8 引擎的出色; js 异步 io 与事件驱动给服务器带来极高...

    CocoaChina 评论0 收藏0
  • 全栈开发自学路线

    摘要:前言这里筑梦师是一名正在努力学习的开发工程师目前致力于全栈方向的学习希望可以和大家一起交流技术共同进步用简书记录下自己的学习历程个人学习方法分享本文目录更新说明目录学习方法学习态度全栈开发学习路线很长知识拓展很长在这里收取很多人的建议以后决 前言 这里筑梦师,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,用简书记录下自己的学习历程...

    galaxy_robot 评论0 收藏0
  • 全栈开发自学路线

    摘要:前言这里筑梦师是一名正在努力学习的开发工程师目前致力于全栈方向的学习希望可以和大家一起交流技术共同进步用简书记录下自己的学习历程个人学习方法分享本文目录更新说明目录学习方法学习态度全栈开发学习路线很长知识拓展很长在这里收取很多人的建议以后决 前言 这里筑梦师,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,用简书记录下自己的学习历程...

    Scorpion 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<