纯干货|Node.js快速入门指南与实践 快来围观

前言

Node.js知识点虽然不多,但是想要通篇的看完并快速上手还是需要一些时间的,在这个要求效率的时代,快速的掌握、了解一门新的技术也是衡量个人能力一项的标准。


所以,这篇文章不会将Node.js中所有的知识点都梳理一遍,而是会根据个人在工作中的经验将一些常用Node.js模块的API做一些梳理并会向大家推荐一些常用的第三方模块。希望能对你有所帮助。

个人总结 Node.js现状

笔者上班的地方在十三朝古都,2019年换了两份Node.js后端开发的工作。在找工作面试的过程中发现大多数公司都是在用Node.js构建Web前端框架,只有极少数的公司在使用Node.js做后端开发,而这些公司中95%都是创业型的公司。我想这只能说明一点Node.js开发成本低、容易上手。


有段时间朋友推荐我去北上广深,说那块Node.js工作还不错,奈何本人实属不愿离不开三秦大地!😂

惯例

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()

process.nextTick() 方法将callback添加到当前执行栈最后,在当前栈执行完后立即执行callback。

 console.log('开始');
 process.nextTick(() => {
   console.log('下一个时间点的回调');
 });
 console.log('调度');
 //output:
 //开始
 //调度
 //下一个时间点的回调

5、常用属性

 process.pid //返回进程的PID
 process.env //返回包含用户环境对象

6、标准流对象

  1. process.stdin 标准输入流
  2. process.stdout 标准输出流
  3. 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个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

核心模块部分在Node源代码编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉。

2、模块加载过程

  1. 优先从缓存加载
  2. 路径分析
  3. 文件定位
  4. 模块编译

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>>

Node.js中文网

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值