NodeJs基础
什么是NodeJS
JS是脚本语言,脚本语言都需要一个解析器才能运行。对于写在HTML页面里的JS,浏览器充当了解析器的角色。而对于需要独立运行的JS,NodeJS就是一个解析器。
每一种解析器都是一个运行环境,会提供相应的内置对象和方法,比如运行在浏览器中的JS的用途是操作DOM,浏览器就提供了document之类的内置对象。而运行在NodeJS中的JS的用途是操作磁盘文件或搭建HTTP服务器,NodeJS就相应提供了fs、http等内置对象。
有啥用处
NodeJS的目的是为了实现高性能Web服务器,着重于事件机制和异步IO模型的优越性,而不是JS。简单可至使用命令交互模式调试JS代码片段,复杂可至编写工具提升工作效率。
如何安装
安装程序
到官网nodejs.org下载相应的安装包,Windows用户选择.msi后缀的安装文件, Mac用户选择.pkg后缀的安装文件。
编译安装
Linux系统下一般使用以下方式编译方式安装NodeJS:
- 确保系统下g++版本在4.6以上,python版本在2.6以上。
- 从nodejs.org下载tar.gz后缀的NodeJS最新版源代码包并解压到某个位置。
- 进入解压到的目录,使用以下命令编译和安装。
1 | $ ./configure |
如何运行
打开终端,键入node进入命令交互模式:
1 | $ node |
运行文件,首先编写hello.js文件,然后键入node + 文件名:
1 | $ node hello.js |
权限问题
在Linux系统下,使用NodeJS监听80或443端口提供HTTP(S)服务时需要root权限,有两种方式:
第一种方式是使用sudo命令运行NodeJS,通过以下命令运行的server.js中有权限使用80和443端口。一般推荐这种方式,可以保证仅为有需要的JS脚本提供root权限。
1 | $ sudo node server.js |
另一种方式是使用chmod +s命令让NodeJS总是以root权限运行,具体做法如下。因为这种方式让任何JS脚本都有了root权限,不太安全,因此在需要很考虑安全的系统下不推荐使用。
1 | $ sudo chown root /usr/local/bin/node |
模块
在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文件就是一个模块,而文件路径就是模块名。
在编写每个模块时,都有require、exports、module三个预先定义好的变量可供使用。
require
require 函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象。模块名可以是相对路径或者绝对路径,模块名中的.js扩展名可以省略。举个🌰:
1 | var foo1 = require('./foo'); |
exports
exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。举个🌰:
1 | exports.hello = function () { |
module
通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话。举个🌰:
1 | module.exports = function () { |
模块初始化
主模块
主模块负责调度组成整个程序的其它模块完成工作。例如下面的main.js:
1 | $ node main.js |
目录如下:
1 | - /home/user/hello/ |
count.js,该模块内部定义了一个私有变量i,并在exports对象导出了一个公有方法count。:
1 | var i = 0; |
main.js:
1 | var counter1 = require('./util/counter'); |
运行结果:
1 | $ node main.js |
此例说明counter.js并没有因为被require了两次而初始化两次,所有模块在执行过程中只初始化一次。
二进制模块
编译好的二进制模块除了文件扩展名是.node外,和JS模块的使用方式相同。
代码组织和部署
模块路径解析规则
require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。另外require函数还支持第三种形式的路径,写法类似于foo/bar,规则如下:
内置模块
不做路径解析,直接返回模块导出对象。如require(‘fs’;)。node_modules目录
例如某个模块的绝对路径是/home/user/hello.js,在该模块中使用require(‘foo/bar’)方式加载模块时,则NodeJS依次尝试使用以下路径(从当前目录逐步往上找)。
1 | /home/user/node_modules/foo/bar |
- NODE_PATH环境变量
与PATH环境变量类似, NodeJS 允许通过 NODE_PATH 环境变量来指定额外的模块搜索路径。 NODE_PATH 环境变量中可以多个目录路径,路径之间在 Linux 下用:分隔,在 Windows 下使用;分隔。例如定义 NODE_PATH 环境变量如下:
1 | NODE_PATH=/home/user/lib:/home/lib |
当使用require(‘foo/bar’)的方式加载模块时,则NodeJS依次尝试以下路径。
1 | /home/user/lib/foo/bar |
包
JS模块的基本单位是单个JS文件。我们可以把由多个子模块组成的大模块称做包,并把所有子模块放在同一个目录里。
组成一个包的目录结构:
1 | - /home/user/lib/ |
其中 cat 包含一个包,main.js 为主模块:
1 | var head = require('./head'); |
index.js
为了让包看起来更像模块,我们可以把模块命名为 index.js,那以下两条语句等价:
1 | var cat = require('/home/user/lib/cat'); |
package.json
如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个package.json文件,并在其中指定入口模块的路径。目录如下:
1 | - /home/user/lib/ |
package.json 内容:
1 | { |
可通过 require(‘/home/user/lib/cat’) 加载。
命令行程序
使用NodeJS编写的东西,要么是一个包,要么是一个命令行程序。
Linux (没实操,先跳过吧)
我们可以把JS文件当作shell脚本来运行,步骤如下:
- 使用#!注释来指定当前脚本使用的解析器,在node-echo.js文件顶部增加以下一行注释:
1 | #! /usr/bin/env node |
NodeJS会忽略掉位于JS模块首行的#!注释,不必担心这行注释是非法语句。
- 使用以下命令赋予node-echo.js文件执行权限:
1 | $ chmod +x /home/user/bin/node-echo.js |
- 在PATH环境变量中指定的某个目录下,例如在/usr/local/bin下边创建一个软链文件,文件名与我们希望使用的终端命令同名,命令如下:
1 | $ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo |
Windows
Windows下用 .cmd , 假设node-echo.js存放在C:\Users\user\bin目录,并且该目录已经添加到PATH环境变量里了。在该目录下新建一个名为node-echo.cmd的文件,内容如下:
1 | @node "C:\User\user\bin\node-echo.js" %* |
工程目录
1 | - /home/user/workspace/node-echo/ # 工程目录 |
NPM
NPM是随同NodeJS一起安装的包管理工具,常见使用场景如下:
- 允许用户从NPM服务器下载别人编写的三方包到本地使用。
- 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。
- 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。
下载三方包
到https://www.npmjs.com/搜索包名,然后终端打开终端下载安装包,命令如下:
1 | $ npm install argv |
下载指定版本的话,可以在包名后边加上@< version >,命令如下:
1 | $ npm install argv@0.0.1 |
可以在 package.json 申明三方包依赖:
1 | { |
这样可以直接使用 npm install 批量安装三方包,包括三方包依赖的包,目录如下:
1 | - project/ |
安装命令行程序
下载安装一个命令行程序的方法与三方包类似:
1 | $ npm install node-echo -g |
-g表示全局安装,并且NPM会自动创建好Linux系统下需要的软链文件或Windows系统下需要的.cmd文件。
1 | - /usr/local/ # Linux系统下 |
发布代码
- 终端运行 npm adduser 根据提示注册账号
- 接着编辑package.json文件,加入NPM必需的字段。接着上边node-echo的例子,package.json里必要的字段如下。
1 | { |
- 在package.json所在目录下运行npm publish发布代码了。
版本号
语义版本号分为X.Y.Z三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。
如果只是修复bug,需要更新Z位。
如果是新增了功能,但是向下兼容,需要更新Y位。
如果有大变动,向下不兼容,需要更新X位。
npm 常用命令
NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。
使用npm help
可查看某条命令的详细帮助,例如npm help install。 在package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。
使用npm update
可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。 使用npm update
-g可以把全局安装的对应命令行程序更新至最新版。 使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。
使用npm unpublish
@< version >可以撤销发布自己发布过的某个版本代码
文件操作
开门红
NodeJS提供了基本的文件操作API, 像文件拷贝这种高级功能就需要自己实现。
小文件拷贝
1 | var fs = require('fs'); |
process是一个全局变量,可通过process.argv获得命令行参数。第一值表示,node的目录(安装目录或者执行目录,我这样理解),第二个值表示执行的js文件,第三个是后面携带的参数。具体参考文档
大文件拷贝
1 | var fs = require('fs'); |
通过数据流的方式读一点写一点。
API走马观花
Buffer(数据块)
NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。除了可以读取文件得到Buffer的实例外,还能够直接构造,Buffer 类在 Node.js 中是一个全局变量。
1 | var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); // 直接构造 |
slice方法返回的Buffer的修改会作用于原Buffer,而js数组使用slice方法不会作用原数组。
1 | var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); |
因此要想不影响原来的Buffer,需要新建一个Buffer,通过.copy方法把原Buffer中的数据复制过去。
1 | var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); |
Stream(数据流)
NodeJS中通过各种Stream来提供对数据流的操作。
为数据来源创建一个只读数据流,doSomething函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。
1 | var rs = fs.createReadStream(src); |
Stream基于事件机制工作,所有Stream的实例都继承于NodeJS提供的EventEmitter。
实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。根据.write方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据drain事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。
1 | var rs = fs.createReadStream(src); |
NodeJS直接提供了.pipe方法来做这件事情,其内部实现方式与上边的代码类似。
File System(文件系统)
NodeJS通过fs内置模块提供对文件的操作。fs模块提供的API基本上可以分为以下三类:
- 文件属性读写。
其中常用的有fs.stat、fs.chmod、fs.chown等等。 - 文件内容读写。
其中常用的有fs.readFile、fs.readdir、fs.writeFile、fs.mkdir等等。 - 底层文件操作。
其中常用的有fs.open、fs.read、fs.write、fs.close等等。
上边提到的这些API都通过回调函数传递结果,例如:
1 | fs.readFile(pathname, function (err, data) { |
基本上所有fs模块API的回调参数都有两个。第一个参数在有错误发生时等于异常对象,第二个参数始终用于返回API方法执行结果。
fs模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作更方便时的情况。同步API除了方法名的末尾多了一个Sync之外,异常对象与执行结果的传递方式也有相应变化
Path(路径)
- path.normalize
将传入的路径转换为标准路径,具体讲的话,除了解析路径中的.与..外,还能去掉多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。
1 | var cache = {}; |
注意: 标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\/g, ‘/‘)再替换一下标准路径。
- path.join
将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。
1 | path.join('foo/', 'baz/', '../bar'); // => "foo/bar" |
- path.extname
当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。
1 | path.extname('foo/bar.js'); // => ".js" |
遍历目录
递归算法
通过不断缩小问题的规模来解决问题:
1 | function factorial(n) { |
陷阱: 使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。
遍历算法
目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。
1 | A |
同步遍历
1 | function travel(dir, callback) { |
假设有如下目录:
1 | - /home/user/ |
使用以下代码遍历该目录时,得到的输入如下:
1 | travel('/home/user', function (pathname) { |
异步遍历
如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理完全相同:
1 | function travel(dir, callback, finish) { |
文本编码
在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。
BOM的移除
BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符(”\uFEFF”),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:
1 | Bytes Encoding |
因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM。
以下代码实现了识别和去除UTF8 BOM的功能:
1 | function readText(pathname) { |
GBK转UTF8
GBK编码不在NodeJS自身支持范围内。因此,一般我们借助iconv-lite这个三方包来转换编码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。
1 | var iconv = require('iconv-lite'); |
单字节编码
即使一个文本文件中有中文等字符,如果我们需要处理的字符仅在ASCII0~128范围内,比如除了注释和字符串以外的JS代码,我们就可以统一使用单字节编码来读取文件,不用关心文件的实际编码是GBK还是UTF8。以下示例说明了这种方法。
1 | 1. GBK编码源文件内容: |
不管大于0xEF的单个字节在单字节编码下被解析成什么乱码字符,使用同样的单字节编码保存这些乱码字符时,背后对应的字节保持不变。
NodeJS中自带了一种binary编码可以用来实现这个方法:
1 | function replace(pathname) { |