Node.js 實(shí)戰(zhàn)_1 Node 基礎(chǔ)

Node 基礎(chǔ)

JavaScript 是編程語(yǔ)言,而 Node.js 是執(zhí)行環(huán)境。

Node.js 是一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行環(huán)境(runtime)。

Node.js 特性:事件驅(qū)動(dòng)、異步 API、非阻塞 I/O。

Node.js 是專為數(shù)據(jù)密集型實(shí)時(shí)程序(DITR)設(shè)計(jì)的。

Node.js 通過(guò)事件輪詢(event loop)來(lái)實(shí)現(xiàn)非阻塞I/O網(wǎng)絡(luò)的調(diào)用,而事件輪詢是單向運(yùn)行的先入先出隊(duì)列。

ES2015

ECMAScript 2015 是 ECMAScript 標(biāo)準(zhǔn)的第6個(gè)版本,所以有時(shí)候也被稱為 ES6,一般簡(jiǎn)寫(xiě)為 ES2015。

Node 支持的 ES2015 特性匯總

ES5 之前需要用 prototype 對(duì)象來(lái)創(chuàng)建類似于類的結(jié)構(gòu):

function User() {
  // 構(gòu)造器
}

User.prototype.method = function () {
  // 方法
}

ES6 版本支持類:

class User {
  constructor() {}
  method() {}
}

const 和 let 解決作用域問(wèn)題

在 ES5 中,所有的變量都是用 var 創(chuàng)建的。

應(yīng)該用 const 還是 let?

在決定是用 const 還是用 let 時(shí),幾乎都可以用 const。因?yàn)槟愕拇蟛糠执a都是在用你自己的類實(shí)例、對(duì)象常量或不會(huì)變的值,所以大部分情況下都可以用 const。即便是有可修改屬性的對(duì)象,也是可以用 const 聲明的,因?yàn)?const 的意思是引用是只讀的,而不是值是不可變的。

原生的 promise 和生成器支持

生成器能把異步 I/O 變成同步編程風(fēng)格。

模版字符串

用反引號(hào)(`)定義模版字符串

// ES5 中,字符串常量不支持插值,也不支持跨行。
// 舊的方法
var a = 1;
console.log('一共有 ' + a + ' 個(gè)雞蛋!');

// 模版字符串語(yǔ)法
var a = 1;
console.log(`一共有 ${a} 個(gè)雞蛋!`);

箭頭函數(shù)

舊版語(yǔ)法:

const http = require('http');
const port = 8080;

const server = http.createServer(function (req, res) {
  res.end('Hello, world.');
});

server.listen(port, function () {
  console.log('Server listening on: http://localhost:%s', port);
});

箭頭函數(shù)語(yǔ)法:

const http = require('http');
const port = 8080;

const server = http.createServer((req, res) => {
  res.end('Hello, world.');
});

server.listen(port, () => {
  console.log('Server listening on: http://localhost:%s', port);
});

Node.js 架構(gòu)

Node.js 的軟件棧
Node.js 架構(gòu)

說(shuō)明:

  • libuv 是提供快速、跨平臺(tái)、非阻塞 I/O 的本地庫(kù);
  • V8 負(fù)責(zé) JavaScript 代碼的解釋和執(zhí)行,它可以將 JavaScript 直接編譯為機(jī)器碼;
  • C++ 綁定層可以將 libuv 和 V8 結(jié)合起來(lái)。

Node 的使用場(chǎng)景

Node 程序主要可以分成三種類型:Web 應(yīng)用程序、命令行工具、和后臺(tái)程序、桌面程序。

Node 的使用場(chǎng)景
From @itying.com

Node 功能的組織及重用

Node 模塊打包代碼是為了重用,但它們不會(huì)改變?nèi)肿饔糜颉?/p>

Node 的模塊系統(tǒng)避免了對(duì)全局作用域的污染,從而也就避免了命名沖突,并簡(jiǎn)化了代碼的重用。

模塊即可以是一個(gè)文件,也可以是包含一個(gè)或多個(gè)文件的目錄(默認(rèn)模塊入口為 index.js 文件)。

currency.js

自定義模塊,在該文件中添加兩個(gè)貨幣轉(zhuǎn)換函數(shù):
方法:通過(guò)設(shè)定 exports 對(duì)象的屬性來(lái)指明要暴露的函數(shù)或變量。

// 私有變量,外界無(wú)法訪問(wèn)到。
var candaianDollar = 0.91;

function roundTwoDecimals(amount) {
    return Math.round(amount * 100) / 100;
}

// canadianToUS() 函數(shù)設(shè)定在 exports 模塊中,所以引入這個(gè)模塊的代碼可以使用它。
exports.canadianToUS = function (canadian) {
    return roundTwoDecimals(canadian * candaianDollar);
}

// USToCanadian() 函數(shù)也設(shè)定在 exports 模塊中。
exports.USToCanadian = function (us) {
    return roundTwoDecimals(us / candaianDollar);
}

test-currency.js

引入模塊

// require() 函數(shù)是一個(gè)同步I/O函數(shù),一般在文件頂端引入。
// 相對(duì)路徑 ./ 表示當(dāng)前同一目錄下。
const currency = require('./currency');

// 使用 currency 模塊的 canadianToUS() 函數(shù)
console.log(currency.canadianToUS(50));

// 使用 currency 模塊的 USToCanadian() 函數(shù)
console.log(currency.USToCanadian(30));

用 module.exports 微調(diào)模塊的創(chuàng)建

  • 如果只需要從模塊中得到一個(gè)函數(shù),那么從 require 中返回一個(gè)函數(shù)的代碼比返回一個(gè)對(duì)象的代碼更優(yōu)雅。

  • 不能用任何其他對(duì)象、函數(shù)或者變量給 exports 賦值。

    我的理解:可以把函數(shù)或者對(duì)象設(shè)置為 exports 的屬性(exports.function = {...}),但是不能把函數(shù)或者對(duì)象賦值(exports = function)給 exports。

  • module.exports 可以對(duì)外提供單個(gè)變量、函數(shù)或者對(duì)象。

  • 如果你創(chuàng)建了一個(gè)既有 exports 又有 module.exports 的模塊,那它會(huì)返回 module.exports,而 exports 會(huì)被忽略。

exports 與 module.exports

最終在程序里導(dǎo)出的是module.exports。exports只是對(duì)module.exports的一個(gè)全局引用,最初被定義為一個(gè)可以添加屬性的空對(duì)象。所以exports.myFunc只是module.exports.myFunc的簡(jiǎn)寫(xiě)。

所以,如果把exports設(shè)定為別的,就打破了module.exports和exports之間的引用關(guān)系。

  • 根據(jù)需要使用 exportsmodule.exports 可以將功能組織成模塊,規(guī)避掉程序腳本一直增長(zhǎng)產(chǎn)生的弊端。

用 node_modules 重用模塊

Node 查找模塊的步驟
  • 用環(huán)境變量 NODE_PATH 可以改變 Node 模塊的默認(rèn)路徑。

注意事項(xiàng)

  1. 如果模塊是目錄,在模塊目錄中定義模塊的文件必須被命名為 index.js,除非你在這個(gè)目錄下一個(gè)叫package.json 的文件里特別指明。
  2. Node 能把模塊作為對(duì)象緩存起來(lái)。Node 加載模塊的順序:緩存模塊>核心模塊>當(dāng)前目錄模塊>node_modules模塊。
  3. 有些常用的 Node 核心模塊在 Node 初始化時(shí)就被加載緩存起來(lái)了,所以加載速度相對(duì)也會(huì)更快。

異步編程技術(shù)

Node 中兩種響應(yīng)邏輯管理方式:回調(diào)和事件監(jiān)聽(tīng)。

  • 回調(diào):適用于一次性異步邏輯。

  • 事件發(fā)射器:把異步邏輯跟一個(gè)概念實(shí)體關(guān)聯(lián)起來(lái),可以通過(guò)監(jiān)聽(tīng)器輕松管理。

  • 流程控制:可以管理異步任務(wù)的執(zhí)行順序,串行執(zhí)行或者并行執(zhí)行。

1. 用回調(diào)處理一次性事件

回調(diào)是一個(gè)函數(shù),它被當(dāng)做參數(shù)傳給異步函數(shù),它描述了異步操作完成之后要做什么。

HTML示例

title.json

JSON 文件會(huì)被格式化成一個(gè)包含文章標(biāo)題的字符串?dāng)?shù)組。

[
    "Kazakhstan is a huge country...what goes on there?",
    "This weather is making me craaazy",
    "My neighbor sort of howls at night"
]

template.html

HTML 模版文件,% 會(huì)被替換為博客文章的標(biāo)題。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>Latest Posts</h1>
    <ul><li>%</li></ul>
</html>

blog_recent.js

功能如下:

  • 異步獲取存放在 JSON 文件中的文章的標(biāo)題;
  • 異步獲取簡(jiǎn)單的 HTML 模版;
  • 把標(biāo)題組裝在 HTML 模版中;
  • 把 HTML 頁(yè)面發(fā)送給用戶。
// 獲取 JSON 文件中的標(biāo)題,并渲染 Web 頁(yè)面
'use strict';

const http = require('http');
const fs = require('fs');
const path = require('path');

// 創(chuàng)建 HTTP 服務(wù)器,并用回調(diào)定義響應(yīng)邏輯
http.createServer(function (req, res) { 
    if (req.url == '/') {
        // 讀取 JSON 文件并用回調(diào)定義如何處理其中的內(nèi)容
        // fs.readFile() 直接讀取文件的相對(duì)路徑會(huì)報(bào)錯(cuò),這里用 path 拼接。
        fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
            // 如果出錯(cuò),輸入錯(cuò)誤日志,并給客戶端返回錯(cuò)誤
            if (err) { 
                console.error(err);
                res.end('Server Error: Read JSON File Error');
            }else {
                // 從 JSON 文本中解析數(shù)據(jù)
                var titles = JSON.parse(data.toString());

                // 讀取 HTML 模版,并在加載完成后使用回調(diào)
                fs.readFile(path.join(__dirname, './template.html'), function (err, data) {
                    if (err) {
                        console.error(err);
                        res.end('Server Error: Read html Error');
                    }else {
                        var tmp1 = data.toString();

                        // 組裝 HTML 頁(yè)面以顯示博客標(biāo)題
                        var html = tmp1.replace('%', titles.join('</li><li>'));
                        res.writeHead(200, {'Content-Type': 'text/html'});
                        // 將 HTML 頁(yè)面發(fā)送給用戶
                        res.end(html);
                    }
                });
            }
        });
    }
}).listen(8000, '127.0.0.1');

這個(gè)示例嵌入了三層回調(diào):

http.createServer(function (req, res) { 
    fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
        fs.readFile(path.join(__dirname, './template.html'), function (err, data) {
        });
    });
});

優(yōu)化方式:創(chuàng)建中間函數(shù)以減少嵌套(也可以理解為:將代碼模塊化)。

就是把步驟中的單一功能抽象為單獨(dú)的中間函數(shù)。

...

優(yōu)化方式:通過(guò)盡早返回減少嵌套。

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    getTitles(res);
}).listen(8000, "127.0.0.1");

function getTitles(res) {
    fs.readFile(path.join(__dirname, './titles.json'), function (err, data) {
        // 不再創(chuàng)建 else 分支,而是直接return,因?yàn)槿绻鲥e(cuò)的話,也沒(méi)有必要繼續(xù)執(zhí)行這個(gè)函數(shù)了。
        if (err) return hadError(err, res);
        getTemplate(JSON.parse(data.toString()), res);
    });
}

function getTemplate(titles, res) {
    fs.readFile(path.join(__dirname, './template.html'), function (err, data) {
        if (err) return hadError(err, res);
        formatHtml(titles, data.toString(), res);
    });
}

function formatHtml(titles, tmp1, res) {
    var html = tmp1.replace('%', titles.join('</li><li>'));
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end(html);
}

function hadError(err, res) {
    console.error(err);
    res.end('Server Error');
}

Node 的異步回調(diào)慣例

Node 中的大多數(shù)內(nèi)置模塊在使用回調(diào)時(shí)都會(huì)帶兩個(gè)參數(shù):第一個(gè)用來(lái)放可能會(huì)發(fā)生的錯(cuò)誤,第二個(gè)放結(jié)果。錯(cuò)誤參數(shù)經(jīng)常被縮寫(xiě)為 err。

下面是這個(gè)常用的函數(shù)簽名的典型示例:

var fs = require('fs');
fs.readFile('./titles.json', function (err, data) {
    if (err) throw err;
    // do something with data if no error has occurred
})

2. 用事件發(fā)射器處理重復(fù)性事件

事件發(fā)射器會(huì)觸發(fā)事件,并且在那些事件被觸發(fā)時(shí)能處理它們。

事件是通過(guò)監(jiān)聽(tīng)器進(jìn)行處理的。

監(jiān)聽(tīng)器是跟事件相關(guān)聯(lián)的、當(dāng)有事件出現(xiàn)時(shí)就會(huì)被觸發(fā)的回調(diào)函數(shù)。

const net = require('net');
var server = net.createServer(function (socket) {
    // 當(dāng)有客戶端連接上來(lái)時(shí),它就會(huì)創(chuàng)建一個(gè)socket。
    // 用 on 方法添加監(jiān)聽(tīng)器響應(yīng) data 事件。
    socket.on('data', function (data) {
        socket.write(data);
    });

    // once 方法,data 事件只是在第一次會(huì)被處理
    socket.once('data', function (data) {
        socket.write(data);
    })
}).listen(8888);

用 Node 內(nèi)置的事件模版創(chuàng)建自己的事件發(fā)射器:

// 定義一個(gè)channel事件發(fā)射器,帶有一個(gè)監(jiān)聽(tīng)器,可以向加入頻道的人做出響應(yīng)。
var EventEmitter = require('events').EventEmitter;
var channel = new EventEmitter();
// 用on(或者用比較長(zhǎng)的addListener)方法給事件發(fā)射器添加了監(jiān)聽(tīng)器:
channel.on('join', function () {
    console.log('Welcome!');
})

// 用emit函數(shù)發(fā)射這個(gè)事件
channel.emit('join');

錯(cuò)誤處理

創(chuàng)建發(fā)出 error 類型事件的事件發(fā)射器,而不是直接拋出錯(cuò)誤。

// 創(chuàng)建一個(gè)錯(cuò)誤監(jiān)聽(tīng)器,將被發(fā)出的錯(cuò)誤輸出到控制臺(tái)中:
const events = require('events');
var myEmitter = new events.EventEmitter();
myEmitter.on('error', function (err) {
    console.log('ERROR: ' + err.message);
});
myEmitter.emit('error', new Error('Something is wrong.'));

異步開(kāi)發(fā)的難題

創(chuàng)建異步程序時(shí),必須密切關(guān)注程序的執(zhí)行流程、程序的執(zhí)行狀態(tài)...

// 示例:作用域是如何導(dǎo)致 bug 出現(xiàn)的:
function asyncFunction(callback) {
  // 200ms 后,執(zhí)行回調(diào)函數(shù)
  setTimeout(callback, 200);
}
let color = 'blue';
asyncFunction(() => {
  console.log(`The color is ${color}`);
});
color = 'green';

// 輸出結(jié)果:
// [Running] node "/Users/andy/Desktop/node/test_async.js"
// The color is green

用匿名函數(shù)保留全局變量的值:

// JavaScript 編程技巧:用閉包控制程序的狀態(tài)
function asyncFunction(callbback) {
    setTimeout(callbback, 200);
}
var conlor = 'blue';

// 將 color 的值傳給匿名函數(shù)
// color 變成了匿名函數(shù)的參數(shù),也就是這個(gè)匿名函數(shù)內(nèi)部的本地變量,
// 當(dāng)匿名函數(shù)外面的color值發(fā)生變化時(shí),本地版的color不會(huì)受影響。
(function(color) {
    asyncFunction(function() {
        console.log('The color is' + color);
    })
})(color);

color = 'green';

MDN web docs: JavaScript 閉包

異步邏輯的順序化

流程控制:讓一組異步任務(wù)按照順序執(zhí)行。串行/并行。

需要一個(gè)接著一個(gè)做的任務(wù)叫做串行任務(wù)。

不需要一個(gè)接著一個(gè)做的任務(wù)叫做并行任務(wù)。

串行任務(wù)與并行任務(wù)

實(shí)現(xiàn)串行化流程控制

使用回調(diào)讓幾個(gè)異步任務(wù)順序執(zhí)行:

setTimeout(function() {
    console.log('1');
    setTimeout(function() {
        console.log('2');
        setTimeout(function() {
            console.log('3');
        }, 100); // 任務(wù)3,花費(fèi) 0.1 秒
    }, 500); // 任務(wù)2,花費(fèi) 0.5 秒
}, 1000); // 任務(wù)1,花費(fèi) 1 秒

第三方模塊:Nimble:這個(gè)模塊官網(wǎng)上顯示7年沒(méi)更新了??????,而且現(xiàn)在流行用 Promise 或者 async 來(lái)實(shí)現(xiàn)。

// 《Node.js 實(shí)戰(zhàn)(第二版)》Async 示例:
const async = require('async');

// 給 Async 一個(gè)函數(shù)數(shù)組,讓它一個(gè)接一個(gè)地執(zhí)行
async.series([
  callback => {
    setTimeout(() => {
      console.log('I execute first.');
      callback();
    }, 1000);
  },
  callback => {
    setTimeout(() => {
      console.log('I execute next.');
      callback();
    }, 500);
  },
  callback => {
    setTimeout(() => {
      console.log('I execute last.');
      callback();
    }, 100);
  },
]);

// 執(zhí)行結(jié)果:
// [Running] node "/Users/andy/Desktop/node/test_async.js"
// I execute first.
// I execute next.
// I execute last.

串行化流程控制的工作機(jī)制:

為了用串行化流程控制讓幾個(gè)異步任務(wù)按順序執(zhí)行,需要先把這些任務(wù)按預(yù)期的執(zhí)行順序放到一個(gè)數(shù)組中。

這個(gè)數(shù)組將起到隊(duì)列的作用:完成一個(gè)任務(wù)后按順序從數(shù)組中取出下一個(gè)。

Demo示例:實(shí)現(xiàn)串行化流程控制。

實(shí)現(xiàn)并行化流程控制

為了讓異步任務(wù)并行執(zhí)行,仍然是要把任務(wù)放到數(shù)組中,但任務(wù)的存放順序無(wú)關(guān)緊要。每個(gè)任務(wù)都應(yīng)該調(diào)用處理器函數(shù)增加已完成任務(wù)的計(jì)數(shù)值。當(dāng)所有任務(wù)都完成后,處理器函數(shù)應(yīng)該執(zhí)行后續(xù)的邏輯。

'use strict';

const async = require('async');
const exec = require('child_process').exec;

// 輔助函數(shù):下載指定版本的 Node.js 源碼
function downloadNodeVersion(version, destination, callback) {
  const url = `http://nodejs.org/dist/v${version}/node-v${version}.tar.gz`;
  const filepath = `${destination}/${version}.tgz`;
  exec(`curl ${url} > ${filepath}`, callback);
}

// 串行執(zhí)行兩個(gè)任務(wù):
// 任務(wù)一:并行下載兩個(gè)版本的源碼;
// 任務(wù)二:將下載好的版本歸檔到一個(gè)新文件中。
// 用串行化流程控制保證在文件下載完成之前不會(huì)做歸檔處理。
async.series([
  callback => {
    // 并行下載
    async.parallel([
      callback => {
        console.log('Downloading Node V4.4.7...');
        downloadNodeVersion('4.4.7', '/tmp', callback);
      },
      callback => {
        console.log('Downloading Node v6.3.0');
        downloadNodeVersion('6.3.0', '/tmp', callback);
      },
    ], callback);
  },
  callback => {
    console.log('Creating archive of download files ...');
    exec(
      'tar cvf node_distros.tar /tmp/4.4.7.tgz /tmp/6.3.0.tgz',
      err => {
        if (err) throw err;
        console.log('All down!');
        callback();
      }
    );
  },
], (err, results) => {
  if (err) throw err;
  console.log(results);
});
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • topics: 1.The Node.js philosophy 2.The reactor pattern 3....
    宮若石閱讀 1,238評(píng)論 0 1
  • Node.js第一天 1. 初識(shí)Node.js 1.1 Node.js是什么 Node.js? is a Java...
    再見(jiàn)天才閱讀 4,898評(píng)論 1 24
  • # 模塊機(jī)制 node采用模塊化結(jié)構(gòu),按照CommonJS規(guī)范定義和使用模塊,模塊與文件是一一對(duì)應(yīng)關(guān)系,即加載一個(gè)...
    RichRand閱讀 2,736評(píng)論 0 3
  • Node.js是目前非?;馃岬募夹g(shù),但是它的誕生經(jīng)歷卻很奇特。 眾所周知,在Netscape設(shè)計(jì)出JavaScri...
    w_zhuan閱讀 3,732評(píng)論 2 41
  • 工作當(dāng)中經(jīng)常會(huì)有會(huì)議,會(huì)議的主持人要提前的做許多的準(zhǔn)備工作,要能夠很好的把握好對(duì)話,因?yàn)闀?huì)議主要是通過(guò)語(yǔ)言來(lái)傳遞信...
    向陽(yáng)的石頭閱讀 385評(píng)論 0 0

友情鏈接更多精彩內(nèi)容