純乾貨|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中文網

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章