Node.js介绍首先,我们先从名字说起。在网上查资料的时候,会发现关于node的写法多种多样。哪个最标准?在官方网站之后,该项目被称为“Node”或“Node.js”。简单来说,Node就是运行在服务器端的JavaScript。JavaScript是一种脚本语言(能用于编程并直接执行源代码的语言就是脚本语言),所有的脚本语言都需要解析器才能运行。对于html写的js,一般都是由浏览器解析执行。对于独立执行的js代码,需要Node解析器来解析执行。每个解析器都是一个运行环境,它不仅可以让js定义各种数据结构,进行各种计算,还可以让js使用运行环境提供的内置对象和方法来做一些事情。比如js在浏览器中运行的目的是为了操作DOM,浏览器提供了document等内置对象。js运行在node中的目的是操作磁盘文件或搭建HTTP服务器,node相应地提供了fs、http等内置对象。Node不是一个js应用,而是一个js运行环境。当你看到Node.js这个名字时,你可能会误以为它是一个JavaScript应用程序。实际上,node使用C++语言封装了GoogleV8引擎,是一个JavaScript运行环境。V8引擎执行JavaScript的速度非常快,性能非常好。Node是一个服务器端的JavaScript平台,可以让开发者快速创建网络应用,并使用JavaScript进行前后端编程,让开发者可以更专注于系统设计并保持其一致性。//快速构建服务器consthttp=require('http')http.createServer((req,res)=>{res.writeHead(200,{'Content-Type':'text/plain'})res.end('helloWorld!')}).listen(8088)$nodehelloWorld.jsNode采用事件驱动,异步编程node的设计思想以事件驱动为核心,它提供的大部分API都是事件-基于和异步。开发者需要根据自己的业务逻辑注册相应的回调函数,这些回调函数是异步执行的。这意味着虽然这些函数在代码结构中看起来是顺序注册的,但它们并不依赖于它们出现的先后顺序,而是等待相应的事件触发。在服务器开发中,并发请求处理是一个大问题,阻塞函数会导致资源浪费和时间延迟。通过事件注册和异步函数,开发者可以充分利用系统资源,无阻塞和等待地执行代码,有限的资源可以用于其他任务。Node以单进程单线程的方式运行,这与JavaScript的运行方式是一致的。事件驱动机制是通过节点内部单线程高效维护事件循环队列来实现的。没有多线程资源占用和上下文切换,这意味着面对大规模的http请求,node以事件驱动的方式处理一切。由此我们是否可以推测,这样的设计会导致负载的压力集中在CPU(事件循环处理?)而不是内存上。淘宝共享数据平台团队对node的性能测试:物理机配置:RHEL5.2,CPU2.2GHz,内存4GNode.js应用场景:MemCache代理,每次取100字节数据连接池大小:50并发用户:100测试结果(socketmode):内存(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没有标准接口。没有WebServer、数据库等统一接口。JavaScript没有包管理系统。无法自动加载和安装依赖项。要实现模块化编程,首先需要解决的问题是命名冲突和文件依赖。CommonJS规范有了CommonJS规范的出现,它的目标是构建一个JavaScript的生态系统,包括Web服务器、桌面、命令行工具和浏览器。CommonJS制定了一些规范来解决这些问题,node就是这些规范的一个实现。Node本身实现了require方法作为其引入模块的方法,npm也实现了依赖管理、基于CommonJS定义的包规范自动安装模块等功能。NodeNative模块中模块的分类Native模块是NodeAPI提供的核心模块(如:os、http、fs、buffer、path等模块)。原生模块在节点源码编译时编译成二进制可执行文件,加载速度最快。consthttp=require('http');文件模块是一个动态加载的模块,动态加载的模块主要由nativemodule模块实现和完成。启动时已经加载了native模块,需要调用模块的require方法加载file模块。首先定义一个文件模块,以计算圆的面积和周长的两种方法为例:constPI=Math.PI;exports.area=(r)=>{returnPI*r*r;};exports.circumference=(r)=>{return2*PI*r;};将这个文件保存为circle.js,新建一个app.js文件,写入如下代码://调用文件模块必须指定路径,否则会报错constcircle=require('./circle.js');console.log('半径为4的圆的面积为'+circle.area(4));require这个文件后,定义在exports对象中上面的方法可以随意调用。包管理NodePackagedModules,简称NPM,是随node一起安装的包管理工具。Node本身提供了一些基础的API模块,但是这些基础模块很难满足开发者的需求。Node需要使用NPM来管理一些开发者自己开发的模块,并提供给其他开发者使用。NPM建立了一个节点生态系统,节点开发者和用户可以在这里相互交流。当你需要下载第三方包时,首先要知道有哪些包可用。npmjs.com提供了一个可以按包名搜索的平台。一旦你知道包名,你就可以使用命令来安装它。npm-v//测试是否安装成功。npm常用命令行代码:npminstallmoduleNamesnpminstallmoduleNames-g//全局安装npminstallmoduleNames@2.0.0//具体版本的安装依赖npminstallmoduleNames--save//--save即可简写为-S//会在package.json的dependencies属性下添加moduleNames依赖,即生产依赖插件npminstallmoduleNames--save-dev//--save-dev可以简写为-D//会在package.json的devDependencies属性下添加moduleNames依赖,即Development依赖插件uninstallmodulenpmuninstallmoduleNamesupdatemodulenpmupdatemoduleNamessearchmodulenpmsearchmoduleNamesswitchtemplatewarehousesource:npmconfigsetregistryhttps://registry.npm.taobao.org/npmconfiggetregistry//执行验证是否切换成功在npm服务器上发布自己的包第一次使用npm发布自己的包需要在npmjs注册一个账号.com。也可以使用命令npmadduser,提示输入账号、密码和邮箱,然后提示创建成功('LoggedinasUsernameonhttps://registry.npmjs.org/.').输入npminit命令,根据提示配置包信息,生成对应的package.json。npm命令运行时,会读取当前目录下的package.json文件,并说明该文件是通过npmpublish发送的。包的名称和版本就是你项目中package.json的名称和远景。此处注意:名称不能与现有包的名称相同。名称不能有大写字母/空格/下划线。不想在npm上发布的代码文件写入.gitignore或.npmignore并上传。更新包的命令和发布包的命令是一样的,但是不要忘记每次更新都要修改包的版本。模块初始化模块中的JavaScript代码仅在模块首次使用时执行一次,并在执行期间初始化模块的导出对象。之后,缓存的导出对象被重用。其中,nativemodules定义在lib目录下,而filemodules则不确定。模块加载优先级模块加载优先级:缓存模块>原生模块>文件模块>从文件加载require方法虽然很简单,但是内部加载非常复杂,其加载优先级也不一样。如下图所示:模块加载策略从原生模块加载原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名后首先检查模块是否在本机模块列表中。native模块也有缓存区,也是先从缓存区加载。如果缓存区还没有加载,则调用native模块的loading方法加载执行。从文件加载实际上,文件模块中有三种类型的模块,通过后缀来区分。Node会根据后缀名来决定加载方式。.js通过fs模块同步读取js文件并编译执行。.node是一个用c/c++编写的插件。通过dlopen方法加载。.json读取文件,调用JSON.parse解析加载。当文件模块缓存不存在且不是原生模块时,node会解析require方法传入的参数,从文件系统中加载实际文件。加载文件模块的工作主要由nativemodule模块实现完成,在启动时已经加载,流程直接调用runMain静态方法。Module.runMain=function(){Module._load(process.argv[1],null,true);};_load静态方法解析文件名后执行varmodule=newModule(id,parent);并根据文件路径缓存当前模块对象,根据文件名加载模块实例对象。模块.load(文件名);以.js后缀的文件为例,node在编译js文件的过程中实际完成的步骤是将js文件的内容从头到尾包裹起来。比如刚才的app.js打包后是这样的:半径为4的圆是'+circle.area(4));});这段代码上下文清晰,不污染全局,返回特定的函数对象。最后将模块对象、模块、文件名、目录名的exports和require方法作为实参导入并执行。这就是为什么require没有在app.js文件中定义,但是这个方法存在。在这个主文件中,可以通过require方法导入其他模块。其实这个require方法其实就是调用了load方法。加载、编译和缓存模块后,load方法返回模块的导出对象。这就是为什么只有在circle.js文件中的exports对象上定义的方法才能被外部调用。上面介绍的模块加载机制定义在module模块中。文件模块加载时的路径分析require方法接受以下参数:http、fs、path等,原生模块。./mod或../mod,文件模块的相对路径。/pathtomodule/mod,文件模块的绝对路径。mod,一个不是本机模块的文件模块。在进入路径搜索之前,有必要在模块路径节点中描述以下概念。对于每一个加载的文件模块,在创建模块对象时,模块都会有一个paths属性,其值是根据当前文件的路径计算的。例子:我们创建一个类似modulepath.js的文件,它的内容是:console.log(module.paths);执行nodemodulepath.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']可以看出生成模块路径的规则是:从当前文件目录搜索node_modules目录;然后依次进入父目录,搜索父目录的node_modules目录;迭代到根目录下的node_modules目录。另外还有一个全局模块路径,就是当前节点执行文件的相对目录(../../lib/node)。如果环境变量中设置了HOME目录和NODE_PATH目录,则整个路径还包括NODE_PATH和HOME目录下的.node_libraries和.node_modules。它的最终值大致如下:[NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node]简单的说,如果你需要一个绝对路径的文件,就不会遍历每个在搜索node_modules目录时,这是最快的。其余过程如下:从模块路径数组中取出第一个目录作为查找参考。直接从目录中查找文件,存在则结束查找。如果不存在,则进行下一步搜索。尝试添加.js、.json、.node后缀进行搜索,如果有文件则结束。如果不存在,则转到下一个。尝试将require参数作为包进行搜索,读取目录下的package.json文件,得到main参数指定的文件。尝试查找文件,如果存在则结束搜索。如果不存在,则进行第三次查找。如果继续失败,则取出模块路径数组中的下一个目录作为参考查找,循环1~5步。如果继续失败,则循环步骤1到6,直到模块路径中的最后一个值。如果仍然失败,则抛出异常。整个搜索过程与JavaScript原型链的搜索和作用域的搜索非常相似。不同的是node对路径搜索实现了缓存机制,否则每条判断路径都会被同步阻塞,会导致严重的性能消耗。
