纯干货 | Node.js快速入门指南与实践
Posted dev-bear
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了纯干货 | Node.js快速入门指南与实践相关的知识,希望对你有一定的参考价值。
前言
Node.js知识点虽然不多,但是想要通篇的看完并快速上手还是需要一些时间的。而这篇文章主要总结工作中Node.js一些常用模块API的用法以及优秀的第三方模块,帮助你快速的上手。Good Luck??????
惯例
Node.js是基于Chrome V8引擎的javascript运行环境。
console - 控制台
1、控制台打印信息
console.log('日志信息');
console.warn('警告信息');
console.debug('调试信息');
console.error(new Error('错误信息'));
console.log('你好: %s,我是: %s', 'Node.js','developer'); //你好: Node.js,我是: developer
2、统计标签出现的次数
console.count('key'); //key:1
console.count('key'); //key:2
console.count('key'); //key:3
3、统计运行时长
console.time('100-elements');
for (let i = 0; i < 100; i++) {}
console.timeLog('100-elements', '完成第一个'); //100-elements: 0.092ms 完成第一个
for (let i = 0; i < 100; i++) {}
console.timeEnd('100-elements');//100-elements: 6.659ms
【chalk模块让你的日志多姿多彩】
const chalk = require('chalk');
console.log(chalk.blue('Hello Node.js!'));
console.log(chalk.red('Hello Node.js!'));
console.log(chalk.green('Hello Node.js!'));
assert - 断言
assert模块提供了一组简单的测试用于测试不变量。主要与mocha测试框架配合使用编写单元测试。
1、assert模块的API可以分为两种:
- 严格模式的API。
宽松模式的API。
只要有可能,请使用严格模式。否则,抽象的相等性比较可能会导致意外的结果。
const assert = require('assert');
const obj1 = { a: 1 };
const obj2 = { a: '1' }
assert.deepEqual(obj1, obj2);//相同,不是我们想要的结果
2、使用严格模式的方法:
- const assert = require(‘assert‘).strict;
- 使用严格模式的API(名称中包含Strict)
单元测试要尽可能的简单并且被测方法的结构一定是可以预料的。如果被测的方法不满足以上的要求你可能需要重构代码。虽然assert模块提供了很多的API,但是常用的只有以下三个。
- assert(value,[,message])
- assert.deepStrictEqual(actual, expected[, message])
- assert.strictEqual(actual, expected[, message])
assert还可以配合is-type-of模块还可以用来代替if语句。如果断言失败,会抛出AssertionError类型的错误。
const assert = require('assert').strict;
const is = require('is-type-of');
function plus(num1,num2){
assert(is.int(num1),'num1必须是整形数字');
assert(is.int(num2),'num2必须是整形数字');
...
}
【在编写测试时power-assert模块让断言失败的提示更详细】
const assert = require('power-assert');
const arr = [1,2,3]
describe('power-assert', () => {
it('should be equal', () => {
assert(arr[0] === 2);
});
});
path - 路径
path模块提供用于处理文件路径和目录路径的实用工具。path模块的默认操作因 Node.js 应用程序运行所在的操作系统而异。??戳这里??
1、获取文件名
path.basename('/foo/bar/baz/asdf/quux.html'); //quux.html
path.basename('/foo/bar/baz/asdf/quux.html','.html'); //quux.html
2、获取扩展名
path.extname('/foo/bar/baz/asdf/quux.html'); //.html
3、获取目录
path.dirname('/foo/bar/baz/asdf/quux.html'); //foo/bar/baz/asdf
4、拼接路径
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'); // /foo/bar/baz/asdf
5、解析路径
path.parse('/foo/bar/baz/asdf/quux.html');
//返回
//{root: '/',
// dir: '/foo/bar/baz/asdf',
// base: 'quux.html',
// ext: '.html',
// name: 'quux'}
6、绝对路径
path.resolve()会按照给定的路径序列从右到左进行处理,每个后续的 path 前置,直到构造出一个绝对路径。 例如,给定的路径片段序列:/foo、 /bar、 baz,调用 path.resolve(‘/foo‘, ‘/bar‘, ‘baz‘) 将返回 /bar/baz。如果在处理完所有给定的 path 片段之后还未生成绝对路径,则再加上当前工作目录。
path.resolve(); //返回当前工作目录的绝对路径
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果当前工作目录是 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'
7、判断是否为绝对路径
path.isAbsolute('/foo/bar'); //true
path.isAbsolute('qux/');// false
8、路径分段
'foo/bar/baz'.split(path.sep); //['foo', 'bar', 'baz']
fs - 文件系统
fs所有文件系统的操作都具有同步和异步的形式。
异步的形式总是将完成回调作为其最后一个参数。 传给完成回调的参数取决于具体方法,但第一个参数始终预留用于异常。 如果操作成功完成,则第一个参数将为 null 或 undefined。
1、创建文件夹
const dir = path.join(__dirname,'/my');
//异步
fs.mkdir(dir, { recursive: true }, (err) => {
if (err) throw err;
});
//同步
fs.mkdirSync(dir);
2、写文件
const file = path.join(__dirname,'/my/my.txt')
//异步
fs.writeFile(file,'Node.js','utf-8',(err)=>{
if (err) throw err;
});
//同步
fs.writeFileSync(file,'Node.js','utf-8');
3、读文件
//异步
fs.readFile(file,'utf-8',(err,data)=>{
if (err) throw err;
console.log(data);
})
//同步
fs.readFileSync(file,'utf-8')
4、判断文件/目录(递归的遍历文件夹会很有用)
const file = path.join(__dirname,'/my/my.txt');
const dir = path.join(__dirname,'/my/');
const stat_file = fs.statSync(file);
const stat_dir = fs.statSync(dir);
stat_file.isFile(); //true
stat_dir.isDirectory(); //true
5、判断路径是否存在
fs.existsSync(file); //true
6、读/写流
const file = path.join(__dirname,'/my/my.txt');
//写入
const ws = fs.createWriteStream(file,'utf8');
ws.write('我是Node.js开发者');
ws.end;
//读取
const rs = fs.createReadStream(file,'utf-8');
rs.on('data',data=>{
console.log(data); //我是Node.js开发者
});
7、递归遍历指定文件夹下所有文件
const getFiles = (directory, file_list = []) => {
const files = fs.readdirSync(directory);
files.forEach(file => {
var full_Path = path.join(directory, file);
const stat = fs.statSync(full_Path);
if (stat.isDirectory()) {
getFiles(full_Path, file_list);
} else {
file_list.push(full_Path);
}
});
return file_list;
}
//调用
const files = getFiles('文件夹目录');
【fs-extra模块让fs模块的功能更完善。】
【globby模块可以帮你过滤文件夹下指定的文件类型,在遍历目录时会很有用。】
了解更多fs 模块API请??戳这里??
http - HTTP
http模块封装了一个http服务器和http客户端。
1、创建http服务器
const http = require('http');
const server = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.write('Hello World');
response.end();
});
server.listen(3000, () => {
console.log('server is listening on 3000...');
});
2、创建http客户端
const http = require('http');
const req = http.request({hostname: 'localhost',port: 3000}, res => {
console.log(`状态码: ${res.statusCode}`);// 状态码200
res.on('data', data => {
console.log(`收到消: ${data}`);// 收到消息 Hello World
});
});
req.on('error', err => {
console.log(err.message);
});
req.end(); //必须调用end()
?使用 http.request() 时,必须始终调用 req.end() 来表示请求的结束,即使没有数据被写入请求主体。
【Koa/Express框架可以帮你快速创建一个http服务器。】??
process - 进程
process对象是一个全局变量,它提供有关当前 Node.js 进程的信息并对其进行控制。
1、获取工作目录
process.cwd();
2、退出进程
process.on('exit',code=>{
console.log(code);// 100
});
process.exit(100);
3、获取Node.js 进程的内存使用情况
process.memoryUsage();
//返回
//{
// rss: 19980288, //进程分配的物理内存大小
// heapTotal: 6537216, //V8内存总大小
// heapUsed: 4005104, //V8内存已用大小
// external: 8272 //V8管理的绑定到javascript的C++对象的内存使用情况
//}
扩展阅读: Node.js是一个基于V8引擎的javascript运行时,V8引擎在64位操作系统下可用的内存大约在1.4GB左右,32位操作系统可用的内存大约是0.7GB左右。
4、获取传入命令行参数
console.log(process.argv);
//启动node index.js arg1=1 arg2=2
process.nextTick() 方法将callback添加到当前执行栈最后,在当前栈执行完后立即执行callback。
console.log('开始');
process.nextTick(() => {
console.log('下一个时间点的回调');
});
console.log('调度');
//output:
//开始
//调度
//下一个时间点的回调
5、常用属性
process.pid //返回进程的PID
process.env //返回包含用户环境对象
6、标准流对象
- process.stdin 标准输入流
- process.stdout 标准输出流
process.stderr 标准错误流
console.log() 是由process.stdout实现。
console.error()是由process.srderr实现。
【使用cross-env模块来设置环境变量。】
child_process - 子进程
child_process 模块主要是来创建子进程。提供的方法包含同步和异步版本。
主要包含以下方法(以异步版本为主):
- child_process.spawn(command[, args][, options])
- child_process.execFile(file[, args][, options][, callback])
- childProcess.exec(command[, options][, callback])
childProcess.fork(modulePath[, args][, options])
1.fork()属于spawn()的衍生版本,主要用来运行指定的module。最大的特点就是父子进程自带IPC通信机制。
2.exec()和execFile()之间主要的区别在于exec()会衍生shell而execFile()不会,所以execFile()的效率更高。由于在window上启动.bat和.cmd文件必须要有终端,所以只能使用设置shell选项的exec()或spawn()来运行.bat或.cmd文件。
1、child_process.spawn() 启动子进程获取Node.js版本
const { spawn } = require('child_process');
const sp = spawn('node',['-v']);
sp.stdout.on('data',data=>{
console.log(data.toString());
});
sp.stderr.on('data', err => {
console.error(err); //v10.16.3
});
sp.on('close', (code) => {
console.log(`Code:${code}`);//Code:0
});
2、child_process.exec()启动进程执行命令
//child.js
console.log('i am child');
console.error('error');
//parent.js
const { exec } = require('child_process');
const sp = exec('node ./client.js');
sp.stdout.on('data',data=>{
console.log(`子进程输出:${data.toString()}`);//子进程输出:i am child
});
sp.stderr.on('data', err => {
console.error(`子进程报错:${err}`); //子进程报错:error
});
sp.on('close', (code) => {
console.log(`Code:${code}`);//Code:0
});
3、child_process.fork()启动子模块并于子模块通信
//parent.js
const { fork } = require('child_process');
const f = fork('./child.js');
f.on('message',msg=>{
console.log(`child message: ${msg}`);//child message: I am child
});
//向子进程发送消息
f.send('I am parent!');
//child.js
process.on('message', msg=> {
console.log('parent message:', msg);//parent message: I am parent!
process.exit();
});
//向父进程发送消息
process.send('I am child');
process.send()是一个同步方法,所以频繁的调用也会造成线程阻塞。
CPU密集型应用给Node带来的主要挑战是:由于Javascript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,是得后续I/O无法发起。但是适当的调整和分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用的发起,这样既可以享受到并行异步I/O的好处,又能充分利用CPU。
4、用child_process.fork()解决密集计算导致的线程阻塞
//child.js
process.on('message', msg => {
if(msg === 'start'){
for(let i = 0;i < 10000;i++){
for(let j = 0;j<100000;j++){}
}
process.send('Complete');//运行完成
process.exit();
}
});
//parent.js
const { fork } = require('child_process');
const f = fork('./client.js');
f.on('message', msg => {
console.log(msg);//Complete
});
//发送开始消息
f.send('start');
cluster - 集群
由于Node.js实例运行在单个线程上,为了充分的利用服务器资源cluster模块通过child_process.fork()启动多个工作进程来处理负载任务。
下面代码演示根据操作系统核数启动多个工作线程,在启动3s后会结束所有工作线程,最后工作线程会重新启动
const cluster = require('cluster');
const http = require('http');
//获取cpu核数
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 启动工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
for (const id in cluster.workers) {
//工作进程退出
cluster.workers[id].on('exit', (code, signal) => {
if (signal) {
console.log(`工作进程已被信号 ${signal} 杀死`);
} else if (code !== 0) {
console.log(`工作进程退出,退出码: ${code}`);
} else {
console.log('工作进程成功退出');
}
});
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid}关闭 (${signal || code}). 重启中...`);
// 重启工作进程
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello node.js');
}).listen(3000);
console.log(`工作进程 ${process.pid} 已启动`);
}
//3s后结束所有工作进程
setTimeout(() => {
for (const id in cluster.workers) {
cluster.workers[id].kill();
}
}, 3000);
cluster工作原理:cluster启动时,它会在内部启动TCP服务器,在cluster.fork()启动子进程时,讲这个TCP服务器端socket文件描述符发送给工作进程。如果进程是cluster.fork()复制出来的,那么它的环境变量里就存在NODE_UNIQUE_ID,如果工作进程中存在listen()网络端口的调用它将拿到该文件描述符,通过SO_REUSEADDR(允许在同一端口上启动同一服务器的多个实例)端口重用,从而实现多子进程共享端口。
【为企业级框架egg提供多进程能力的egg-cluster模块核心就是cluster模块】
Buffer - 缓冲器
Buffer是一个类似Array的对象,主要用来操作字节。Buffer所占用的内存不是通过V8分配的,属于堆外内存。
1、字符串转Buffer
const str = 'hello Node.js.'
Buffer.from(str,'utf8')
2、Buffer转字符串
const str = 'hello Node.js.'
const buf = Buffer.from(str,'utf8');
console.log(buf.toString('utf8'));// hello Node.js.
3、Buffer拼接
const str1 = 'hello Node.js.'
const str2 = 'I am development';
const buf1 = Buffer.from(str1,'utf8');
const buf2 = Buffer.from(str2,'utf8');
const length = buf1.length + buf2.length;
const newBuffer = Buffer.concat([buf1,buf2],length);
console.log(newBuffer.toString('utf8')); //hello Node.js.I am development
4、Buffer拷贝
const buf = Buffer.from('hello','utf8');
const buf2 = Buffer.alloc(5);
buf.copy(buf2);
console.log(buf2.toString('utf8'));//hello
5、Buffer填充
const buf = Buffer.alloc(10);
buf.fill('A');
console.log(buf.toString('utf8'));//AAAAAAAAAA
6、判断是否是Buffer
const buf = Buffer.alloc(10);
const obj = {};
console.log(Buffer.isBuffer(buf));//true
console.log(Buffer.isBuffer(obj));//false
7、字符串转十六进制
const buf = Buffer.from('Hello Node.js ','utf-8');
console.log(buf.toString('hex'));//48656c6c6f204e6f64652e6a7320
8、十六进制转字符串
const hex = '48656c6c6f204e6f64652e6a7320';
const buf= Buffer.from(hex,'hex');
console.log(buf.toString('utf8'));//Hello Node.js
querystring - 查询字符串
querystring模块主要用于解析和格式化URL查询字符串。
1、查询字符串转为对象
const querystring = require('querystring');
const obj = querystring.parse('foo=bar&abc=xyz&abc=123');
console.log(obj);//{ foo: 'bar', abc: [ 'xyz', '123' ] }
2、对象转为查询字符串
const obj = { foo: 'bar', abc: [ 'xyz', '123' ] };
console.log(querystring.stringify(obj));//foo=bar&abc=xyz&abc=123
3、查询字符串百分比编码
const str = querystring.escape('foo=bar&abc=xyz&abc=123');
console.log(str);//foo%3Dbar%26abc%3Dxyz%26abc%3D123
4、URL百分比编码字符解码
const str = querystring.unescape('foo%3Dbar%26abc%3Dxyz%26abc%3D123');
console.log(str);//foo=bar&abc=xyz&abc=123
module (模块)
Node.js模块系统中,中每一个文件都被视为一个独立的模块。Node.js中模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。
1、引入模块
const fs = require('fs');//加载系统模块
const myModule = require('./my.js');//加载同级目录下的my.js模块
const myModule = require('../test/my.js');//加载上级test目录下的my.js模块
const myModule = require('/user/test/my.js');//加载/user/test/下的my.js模块
引入模块,需要经历3个步骤:
- 路径分析
- 文件定位
- 编译执行
核心模块部分在Node源代码编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉。
2、模块加载过程
- 优先从缓存加载
- 路径分析
- 文件定位
- 模块编译
3、循环引入模块
当循环调用 require() 时,一个模块可能在未完成执行时被返回。
a.js
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');
b.js
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');
main.js
console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
当main.js加载a.js时,a.js又会加载b.js。此时,b.js会尝试加载a.js。为了防止无限循环,会返回一个a.js的exports对象的未完成的副本给b.js模块。然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。
4、获取模块所在的目录名
console.log(__dirname);
5、获取模块全名
console.log(__filename);
6、module.exports与exports
exports导出
exports.[property] = any;
exports = any;//不会导出
module.exports导出
module.export = any;
module.exports是真正的对外暴露的出口,而exports是一个默认被module.exports绑定的变量。如果module.exports没有任何的属性或方法(空对象),那么exports收集到的属性和方法都会赋值给module.exports。如果module.exports已经有一些方法或属性,那么exports收集的信息会被忽略。
示例:
b.js
exports.PI = 3.14;
module.exports = {
name:'Node.js'
}
main.js
const b = require('./b.js');
console.log(b.PI);//undefined
console.log(b.name);//Node.js
VM - 虚拟机
vm 模块提供了在 V8 虚拟机上下文中编译和运行代码的一系列 API。vm 模块不是一个安全的虚拟机。不要用它来运行不受信任的代码
eval()的替代方案
const vm = require('vm');
let x = 5;
const context = vm.createContext({});
const script = new vm.Script('x=6;value=5-2');
script.runInContext(context);
const { value } = context;
console.log(x); // 5
console.log(value); // 3
events - 事件触发器
events模块是Node.js实现事件驱动的核心模块,几乎所有常用的模块都继承了events模块。
1、事件的定义的触发
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
//注册事件
myEmitter.on('action',()=>{
console.log('触发事件');
});
myEmitter.emit('action');
注意触发事件与注册事件的顺序,如果在触发事件的调用在事件注册之前,事件不会被触发。
2、传递参数
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
//注册事件
myEmitter.on('action',(msg)=>{
console.log(`${msg} Node.js`);
});
//触发事件
myEmitter.emit('action','Hello');
eventEmitter.emit() 方法可以传任意数量的参数到事件callback函数
3、只触发一次
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
let m = 0;
//注册事件
myEmitter.once('action', () => {
console.log(++m);
});
//触发事件
myEmitter.emit('action'); //打印:1
myEmitter.emit('action'); //不打印
4、移除一个事件
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
function callback() {
console.log('事件被触发');
}
myEmitter.on('action', callback);
myEmitter.emit('action');
console.log(`listenser数量:${myEmitter.listenerCount('action')}`);
myEmitter.removeListener('action',callback);
myEmitter.emit('action');
console.log(`listenser数量:${myEmitter.listenerCount('action')}`);
timer - 定时器
timer模块暴露了一个全局的API,用于预定在将来某个时间段调用的函数。
setTimeout()和setInterval()与浏览其中的API是一致的,分别用于单次和多次定时执行任务。
1、process.nextTick()与setTimeout(fn,0)
setTimeout(()=>{
console.log('setTimeout');//后输出
},0);
process.nextTick(()=>{
console.log('process.nextTick'); //先输出
});
每次调用process.nextTick()方法,只会讲回调函数放入队列中,在下一轮Tick时取出执行。定时器采用红黑树的操作时间复杂度为0(lg(n)),nextTick()时间复杂度为0(1).相比之下process.nextTick()效率更高。
2、process.nextTick()与setImmediate()
setImmediate(() => {
console.log('setImmediate');//后输出
})
process.nextTick(() => {
console.log('process.nextTick'); //总是先输出
});
process.nextTick()中的回调函数执行的优先级要高于setImmediate()。主要原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。
【事件循环详解 ??这里??】
参考
以上是关于纯干货 | Node.js快速入门指南与实践的主要内容,如果未能解决你的问题,请参考以下文章
纯干货:FFT快速傅里叶变换的Python语言实现(源代码)