前言
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() 方法將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觀察者。
【事件循環詳解 👉這裏👈】