Node.js学习笔记

一、Node.js 介绍

1、介绍

Node.js 是一个能在服务器端运行 JavaScript **的开放源代码、跨平台的 JavaScript 运行环境**,Node.js 与传统服务器端的多线程不同,Node.js 是单线程(既是优点也是缺点,渲染页面快,但对于用户较多的场景有局限)

Node 采用 Google 开发的V8 引擎运行 js 代码,使用事件驱动、非阻塞和异步I/O模型等技术来提高性能,可优化应用程序的传输量和规模

Node 大部分基本模块都用 JavaScript 编写,在 Node 出现之前 JS 通常作为客户端程序设计语言使用,用 JS 写出的程序常在用户的浏览器上运行

目前 Node 已被 IBM、Microsoft、Yahoo!、Walmart、Groupon、SAP、LinkedIn、Rakuten、PayPal、Voxer 和 GoDaddy 等企业采用

2、Node 是对 ES 标准的一个实现

Node 是对 ES 标准的一个实现,Node 也是一个 JS 引擎,但 Node 仅仅对 ES 标准进行了实现,所以在 Node 中不包含 DOM 和 BOM

Node 中可使用所有内建对象:String、Number、Boolean、Math、Date、RegExp、Function、Object、Array,而 BOM 和 DOM 不能使用,但可以使用 console 也可以使用定时器(setTimeout()、setInterval())

3、Node 的服务器是单线程的

Node 可在后台编写服务器,Node 编写服务器都是单线程的服务器(Node 处理请求时是单线程,但是在后台拥有一个 I/O(对磁盘的读写操作)线程池)

而传统的服务器都是多线程的(即每进来一个请求,就创建一个线程去处理请求)

4、Node 用途

(1)Web 服务 API,如 REST

(2)实时多人游戏

(3)后端的 Web 服务,如跨域、服务器端的请求

(4)基于 Web 的应用

(5)多客户端的通信,如即时通信

二、CommonJS 规范

1、ECMAScript 标准的缺陷

(1)没有模块系统

(2)标准库较少

(3)没有标准接口

(4)缺乏管理系统,如下载各模块需要去相应官网下载不方便

2、模块化

若程序设计的规模达到一定程度,则必须对其进行模块化

模块化可以有多种形式,但至少应该提供能够将代码分割为多个源文件的机制

CommonJS 的模块功能可帮我们解决该问题

3、CommonJS

CommonJS 规范的提出主要是为了弥补当前 JavaScript 没有模块标准的缺陷

CommonJS 规范为 JS 指定了一个美好的愿景,希望 JS 能在任何地方运行

CommonJS 对模块的定义:模块引用、模块定义、模块标识(即模块的名字,是传递给 require() 方法的参数,必须是符合驼峰命名法的字符串或相对/绝对路径)

Node 中使用的是 CommonJS 规范

三、Node.js 基础

1、模块化

在 Node 中模块分为三类:底层由 C++ 编写的内建模块、Node 提供的核心模块(通过 npm 安装)、用户编写的模块(称为文件模块)

在 Node 中一个 js 文件就是一个模块

模块暴露

在 Node 中每个 js 文件中的 js 代码都是独立运行在一个函数中,变量和方法是局部变量/方法,而不是全局作用域,所以一个模块中的变量和函数在其他模块中无法访问

可通过 exports 向外部暴露变量和方法,只需将需要暴露给外部的变量或方法设置为 exports 的属性即可

exports.x = 'xxx' //或 module.exports.x = 'xxx'
exports.fn = function(){}

模块引入

(1)核心模块

对于由 node 引擎提供的核心模块,标识就是模块的名字,通过 require("模块名") 引入

node 在使用模块名字来引入时会首先在当前目录的 node_modules 中寻找是否含有该模块,若有则直接使用,若没有则去上一级目录的 node_modules 中寻找,若有则直接使用,若没有再去上一级目录的 node_modules 中寻找,直到找到为止,若直到找到磁盘的根目录依然没有则报错

(2)文件模块

文件模块是由用户自己创建的模块,通过 require(文件路径) 函数引入外部模块,若使用相对路径必须以 ... 开头

使用 require 引入模块后,该函数会返回一个对象,这个对象代表的是引入的模块

模块中的变量和方法是局部的

console.log(global.变量) 的结果是 undefined 可证明这一说法

模块中的代码是包装在一个函数中执行

在模块中执行 console.log(arguments) 输出有结果说明模块是相当于一个函数

或 console.log(arguments.callee) 输出 [Function] 也说明模块是相当于一个函数

注:arguments.callee 这个属性保存的是当前执行的函数对象

node 在执行模块中的代码时,会首先在代码最顶部添加如下代码

function (exports, require, module, __filename__, __dirname__){}

实际上模块中的代码都是包装在一个函数中执行的,并且在函数执行时同时传递进 5 个实参:

exports 该对象用来将变量或函数暴露到外部
require 是个函数,用来引入外部模块
module 代表的是当前模块本身,exports 就是 module 的属性,既可以使用 exports 导出,也可以使用 module.exports 导出
__filename__ 当前模块的完整路径
__dirname__ 当前模块所在文件夹的完整路径

exports 和 module.exports 对比

二者的效果基本相同

但通过 exports 只能使用 export.xxx = xxx 的方式来向外暴露内部变量,注意不能使用 exports = {}

而 module.exports 既可通过 module.export.xxx = xxx 的形式,也可以直接赋值 module.exports = {} 来向外暴露内部变量,且可一次性暴露多个变量或方法

//方式一
module.exports.name = 'xxx'; //或 exports.name = 'xxx';
module.exports.age = 12;
module.exports.sayName = function(){}
//方式二
module.exports = {
    name: 'xxx',
    age: 12,
    sayNamge:function(){}
}

2、全局对象 global

在 node 中有一个全局对象 global,它的作用和网页中的 window 类似

在全局中创建的变量都会作为 global 的属性保存

在全局中创建的函数都会作为 global 的方法保存

3、包 package

CommonJS 的包规范允许将一组相关的模块组合到一起,形成一组完整的工具

CommonJS 的包规范由包结构包描述文件两部分组成,包实际上就是一个压缩文件,解压后还原为目录,该目录包含如下文件或子目录

package.json  描述文件,只有该文件在包目录中是必须的
bin  可执行二进制文件,非必须
lib  js 代码,非必须
doc  文档,非必须
test  单元测试,非必须

包结构:用于组织包中的各种文件

包描述文件:描述包的非代码相关信息,以供外部读取分析,它是一个 JSON 格式的文件 package.json,文件中不能写注释,位于包的根目录下,是包的重要组成部分,其中字段包括 name、description、version、keyword、maintainers、contributors、bugs、licenses、repositories、dependencies、homepage、os、cpu、engine、builtin、directories、implements、scripts、author、bin、main、devDependencies

4、npm

CommonJS 包规范是理论,npm(Node Package Manager)是其中一种实践

对于 Node 而言,npm 帮助其完成了第三方模块的发布、安装和依赖等

借助 npm,Node 与第三方模块之间形成了很好的一个生态系统

npm 相关命令

npm -v 查看版本

npm version 查看所有模块的版本

npm 帮助说明

npm search 包名 搜索模块包

npm install 包名npm i 包名 在当前目录安装包

npm install 包名 -g 全局模式安装包(全局安装的包一般都是一些工具)

npm install 包名 --save 安装包并添加到依赖中

npm install 下载当前项目所依赖的包

npm remove 包名npm r 包名 删除一个模块

npm install 文件路径 从本地安装

npm install -g cnpm --registry=https://registry.npm.taobao.org 安装 cnpm

npm install 包名 --registry=地址 从镜像源安装

npm config set registry 地址 设置镜像源

5、文件系统

Buffer 缓冲区

Buffer 是传输数据时保存数据的缓冲区

Buffer 的结构和数组很像,它的元素为 16 进制的两位数,操作的方法也和数组类似

实际上一个元素就表示内存中的一个字节

Buffer 是 Node.js 中扩充的对象,使用 Buffer 不需要引入模块,直接使用即可

数组中不能存储二进制文件,而 Buffer 就是专门用来存储二进制数据,在 Buffer 中存储的都是二进制数据,但是显示时都是以 16 进制的形式显示,传输的时候都是二进制,buffer 中每个元素的范围是 00 - ff(即 00000000 - 11111111),buffer 中的英文一个元素占用内存的一个字节,一个汉字占用 3 个字节(传输数据的最小单位是字节(8 bit))

Buffer 中的内存不是通过 JavaScript 分配的,而是在底层通过 C++ 申请的,也就是可以直接通过 Buffer 来创建内存中的空间

相关方法

Buffer.from(字符串) 将一个字符串转换为 buffer

Buffer.alloc(大小) 创建一个指定大小的 Buffer

Buffer.allocUnsafe(大小) 创建一个指定大小的 Buffer,但可能含有敏感数据

Buffer实例对象.toString() 将缓冲区中的数据转换为字符串

//将一个字符串保存到 buffer 中
var str = 'hello';
var buf = Buffer.from(str);
console.log(buf);  //输出 <Buffer 48 65 6c 6c 6f>(计算机中所有的二进制都会以十六进制显示)
console.log(buf.length); //输出 5,表示占用内存的大小占用 5 个字节
console.log(str.length); //输出 5,表示字符串的长度

var str = 'hello 你好';
var buf = Buffer.from(str);
console.log(buf.length); //输出 12,即 6 + 2 * 3,表示占用 12 个字节
console.log(str.length); //输出 8

//通过构造函数创建一个指定大小的 buffer,不推荐使用
var buf2 = new Buffer(10);  //10 个字节的 buffer

//通过类方法 Buffer.alloc 创建一个指定大小的 buffer,在分配内存时清空之前的数据
var buf3 = Buffer.alloc(10);  //10 个字节的 buffer

//通过类方法 Buffer.allocUnsafe 创建一个指定大小的 buffer,但 buffer 中可能含有敏感数据,在分配内存时不清空之前的数据
var buf4 = Buffer.allocUnsafe(10);  //10 个字节的 buffer

注意 Buffer 的构造函数都不推荐使用

Buffer 的大小一旦确定则不能修改,Buffer 实际上是对底层内存的直接操作,一般会给 Buffer 分配一段连续内存空间,当给 Buffer 中超出范围的索引赋值时该位置内存可能已被其他程序占用,则需要分配其他与前面不连续的内存空间会降低效率,所以为避免这种情况 Buffer 的大小确定后不能修改

可以通过索引操作 Buffer 中的元素

只要数字在控制台或页面中输出一定是十进制,若要输出非十进制只需 .toString(进制)

fs(文件系统)

在 Node 中,与文件系统的交互很重要,服务器的本质是将本地文件发送给远程客户端

Node 通过 fs 模块来和文件系统交互对系统中的文件进行操作,该模块提供了一些标准文件访问 API 来打开、读取、写入文件以及与其交互

fs 模块是 Node 中已经继承好了,不需要再使用 npm 下载,直接引入即可

加载 fs 模块 const fs = require("fs");

fs 模块中所有的操作都有同步和异步两种形式可供选择

同步文件系统会阻塞程序的执行,即除非操作完毕,否则不会向下执行代码

异步文件系统不会阻塞程序的执行,而是在操作完成时通过回调函数将结果返回

文件写入

(1)同步文件写入

fs.openSync(path, flags[,mode]) 打开文件,返回一个文件的描述符作为结果,可通过该描述符来对文件进行各种操作

-path 要打开文件的路径
-flags 打开文件要做的操作类型,如 r(只读)、w(可写)
-mode 设置文件的操作权限,一般不传

fs.writeSync(fd, string[,position[,encoding]]) 向文件写入内容

-fd 文件的描述符,需要传递要写入的文件的描述符
-string 要写入的内容
-position 写入的起始索引位置
-encoding 写入的编码,默认 utf-8

fs.closeSync(fd) 关闭文件

var fs = require("fs");
var fd = fs.openSync('hello.txt','w',2);
fs.writeSync(fd,'xxxx');
fs.closeSync(fd);

(2)异步文件写入

fs.open(path, flags[, mode], callback) 打开文件,没有返回值,异步调用的方法结果都是通过回调函数的参数返回的

回调函数有两个参数:
    err 错误对象,若没有错误则为 null
    fd 文件的描述符

fs.write(fd, string[, position[, encoding]], callback) 异步写入一个文件

fs.close(fd, callback) 关闭文件

var fs = require("fs");
fs.open('hello.txt','w', function(err, fd){
    if(!err){
        console.log(fd);
        fs.write(fd,"异步写入的内容",function(err){
            if(!err){
                console.log("写入成功");
            }
            fs.close(fd, function(err){
                if(!err){
                    console.log("文件已关闭");
                }
            })
        })
    }else{
        conosle.log(err);
    }
});
fs.writeSync(fd,'xxxx');
fs.closeSync(fd);

相比同步,异步代码较为繁琐但执行效率更高,且同步中没有对错误进行处理,若遇到一个错误则整个程序就结束

(3)简单文件写入

异步:fs.writeFile(file, data[, options], callback)

同步:fs.writeFileSync(file, data[, options])

-file 要操作的文件的路径
-data 要写入的数据
-options 选项对象,可对写入进行一些设置,包括 encoding(默认是 'utf8')、mode(权限,默认是 0o666)、flag(默认是 'w')
-callback 当写入完成后执行的函数

var fs = require("fs");
fs.writeFile('hello.txt','写入的内容', {flag:'w'}, function(err){
    if(!err){
        console.log('写入成功');
    }
});

通过上述方式写入文件无需手动打开关闭文件

(4)流式文件写入

上述文件写入方式是要将写入内容在内存准备好一次性全写入,内容较多时内存占用多,且速度慢,因此同步、异步、简单文件写入都不适合大文件的写入,性能较差,容易导致内存溢出

fs.createWriteStream(path[, options]) 创建一个可写流

-path 文件路径
-options 配置的参数

可写流.write(写入的内容) 通过可写流像文件中输出内容,只要流还在可分多次写入

可写流.end() 关闭流

可通过监听流的 open 和 close 事件来监听流的打开和关闭

可写流.on(事件字符串,function(){}) on 绑定的是长期事件

可写流.once(事件字符串,function(){}) once 是绑定一个一次性事件,该事件将会在触发一次后自动失效

var fs = require("fs");
var ws = fs.createWriteStream('hello.txt');
ws.once('open', function(){
    console.log('流打开了')
});
ws.once('close', function(){
    console.log('流关闭了')
});
ws.write("写入的内容1");
ws.write("写入的内容2");
ws.write("写入的内容3");

ws.end();
//若使用 ws.close(); 则只会写入“写入的内容1”,因为内容还没写完就把流输出端关闭,而 ws.end(); 可理解为把流的输入端关闭,内容只要进入流中都会写入

文件读取

(1)同步文件读取

和同步文件写入差不多

(2)异步文件读取

和异步文件写入差不多

(3)简单文件读取

异步:fs.readFile(path[, options], callback)

同步:fs.readFileSync(path[, options])

-path 要读取的文件的路径
-options 读取的选项
-callback 异步中回调函数,通过回调函数将读取到的内容返回,回调函数参数有 err 和 data
    err 错误对象
    data 读取到的数据,会返回一个 Buffer,因为文件可能为文本、图片、音频等,返回 Buffer 通用性更高

var fs = require("fs");
fs.readFile('xxx.jpg', function(err, data){
    if(!err){
        //将 data 写入文件中
        fs.writeFile('xxx1.jpg', data, function(err){
            console.log("文件写入成功");
        })
    }
});

(4)流式文件读取

流式文件读取也适用于一些比较大的文件,可以分多次将文件读取到内存中

使用方式基本和流式文件写入相似

若要读取一个可读流中的数据,必须要为可读流绑定一个 data 事件,data 事件绑定完毕会自动开始读取数据,读完会自动关闭

可读流.pipe(可写流) 可将可读流中的内容直接输出到可写流中

例子:读取文件内容写入新文件

var fs = require("fs");
var rs = fs.createReadStream('xxx.mp3');
var ws = fs.createWriteStream('xx1.mp3');
rs.once('open', function(){
    console.log('可读流打开了')
});
rs.once('close', function(){
    console.log('可读流关闭了');
    //数据读取完毕,关闭可写流
    ws.end();
});
ws.once('open', function(){
    console.log('可写流打开了')
});
ws.once('close', function(){
    console.log('可写流关闭了')
});
rs.on("data", function(data){
    console.log(data.length);  //每次读取的长度
    //将读取到的数据写入可写流中
    ws.write(data);
});

更简便的写法,使用 pipe()

var fs = require("fs");
var rs = fs.createReadStream('xxx.mp3');
var ws = fs.createWriteStream('xx1.mp3');
rs.pipe(ws);

fs 模块的其他方法

(1)验证路径是否存在

fs.exists(path, callback)(被废弃,使用 fs.stat() 或 fs.access() 代替)

fs.existsSync(path) 返回布尔值

(2)获取文件状态

fs.stat(path, callback),返回一个对象,该对象中保存了当前对象状态的相关信息,回调有两个参数(err, stats),第一个是错误,第二个是 fs.Stats 对象

fs.Stats 对象中有属性/方法:

size 文件的大小
isFile() 是否是一个文件
isDirectory() 是否是一个文件夹(目录)

fs.statSync(path)

fs.stat('xx.mp3', function(err, stat){
    console.log(stat);
    console.log(stat.isDirectory());
})

(3)删除文件

fs.unlink(path, callback)

fs.unlinkSync(path)

(4)读取目录的目录结构

fs.readdir(path[, options], callback)

fs.readdirSync(path[, options]) 读取一个目录的目录结构,回调函数参数一个是错误 err,一个是字符串数组 files,files 中每个元素就是一个文件夹或文件的名字

fs.readdir('.',function(err, files){
    if(!err){
        console.log(files);
    }
})

(5)截断文件

fs.truncate(path, len, callback)

fs.truncateSync(path, len)

将文件修改为指定字节长度大小,其中一个字母占一个字节,一个汉字占 3 个字节

(6)建立目录

fs.mkdir(path[, mode], callback)

fs.mkdirSync(path[, mode])

(7)删除目录

fs.rmdir(path, callback)

fs.rmdirSync(path)

(8)重命名/剪切文件和目录

fs.rename(oldPath, newPath, callback)

fs.renameSync(oldPath, newPath)

(9)监视文件更改写入

fs.watchFile(filename[, options], listener)

-filename 要监视的文件的名字
-options 配置选项,可传参数 persistent(默认是 true)、interval(监视的间隔时间默认是 5007 ms)
-listener 回调函数,当文件发生变化时,回调函数会执行,回调函数中有两个参数
    curr 当前文件的状态,是 fs.Stats 对象
    prev 修改前文件的状态,是 fs.Stats 对象

fs.watchFile('hello.txt', {interval: 1000}, function(curr, prev){
    console.log("修改前文件大小:" + prev.size);
    console.log("修改后文件大小:" + curr.size);
});