原文地址:codeburst.io/the-only-no…
做者:vick_onrails
摘要:這篇文章適合對Node一無所知或瞭解很少的初學者閱讀。全面但不深刻地講了包括http模塊、express、mongodb和RESTful API等知識點。javascript
若是你是前端開發工做者,那麼對你來講,基於NodeJS編寫web程序已經不是什麼新聞了。而無論是NodeJS仍是web程序都很是依賴JavaScript這門語言。html
首先,咱們要認識到一點:Node並非銀彈。也就是說,它不是全部項目的最佳解決方案。任何人均可以基於Node建立一個服務器,可是這須要你對編寫web程序的語言具備必定程序的有很深刻的理解。前端
最近,我從學習Node的過程當中發現了許多樂趣,同時我也意識到我已經掌握了必定的知識,應該分享出來,而且從社區得到反饋來提高本身。java
那麼就讓咱們開始吧。node
在Node.js出現以前,web應用每每基於客戶端/服務器模式,當客戶端向服務器請求資源時,服務器會響應這個請求而且返回相應的資源。服務器只會在接收到客戶端請求時纔會作出響應,同時會在響應結束後關閉與客戶端的鏈接。jquery
這種設計模式須要考慮到效率問題,由於每個請求都須要處理時間和資源。所以,服務器在每一次處理請求的資源後應該關閉這個鏈接,以便於響應其餘請求。git
若是同時有成千上萬個請求同時發往服務器,服務器會變成什麼樣子呢?當你問出這個問題時,你必定不想看到一個請求必須等待其餘請求被響應後才能輪到他的情形,由於這段延遲實在是太長了。github
想象一下,當你想要打開FaceBook,但由於在你以前已經有上千人向服務器發出過請求,因此你須要等待5分鐘才能看到內容。有沒有一種解決方案來同時處理成百上千個請求呢?所幸咱們有線程這個工具。web
線程是系統可以並行處理多任務所使用的方式。每個發給服務器的請求都會開啓一個新的線程,而每一個線程會獲取它運行代碼所須要的一切。ajax
這聽上去很奇怪?讓咱們來看看這個例子:
想象餐館裏只有一個廚師提供食物,當食物需求愈來愈多,事情也會變得愈來愈糟。在以前的全部訂單都被處理前,人們不得不等待很長時間。而咱們能想到的方法就是增長更多的服務員來解決這個問題,對吧?這樣可以同時應付更多的顧客。
每個線程都是一個新的服務員,而顧客就是瀏覽器。我想理解這一點對你來講並不困難。
可是這種系統有一個反作用,讓請求數達到必定數量時,過多的線程會佔用全部系統內存和資源。從新回到咱們的例子裏,僱傭愈來愈多的人來供應食物必然會提升人力成本和佔用更多的廚房空間。
固然,若是服務器在響應完客戶端的請求後馬上切斷鏈接並釋放全部資源,這對咱們來講天然是極好的。
多線程系統擅長於處理CPU密集型操做,由於這些操做須要處理大量的邏輯,並且計算這些邏輯會花費更多的時間。若是每個請求都會被一個新的線程處理,那麼主線程能夠被解放出來去處理一些重要的計算,這樣也能讓整個系統變得更快。
讓主線程沒必要忙於全部的運算操做是一種提升效率的好辦法,可是能不能在此之上更進一步呢?
想象一下咱們如今已經有了一個多線程服務器,運行於Ruby on rails環境。咱們須要它讀取文件而且發送給請求這個文件的瀏覽器。首先要知道的是Ruby並不會直接讀取文件,而是通知文件系統去讀取指定文件並返回它內容。顧名思義,文件系統就是計算機上一個專門用來存取文件的程序。
Ruby在向文件系統發出通知後會一直等待它完成讀取文件的操做,而不是轉頭去處理其餘任務。當文件系統處理任務完成後,Ruby纔會從新啓動去收集文件內容而且發送給瀏覽器。
這種方式很顯然會形成阻塞的狀況,而NodeJS的誕生就是爲了解決這個痛點。若是咱們使用Node來向文件系統發出通知,在文件系統去讀取文件的這段時間裏,Node會去處理其餘請求。而讀取文件的任務完成後,文件系統會通知Node去讀取資源而後將它返回給瀏覽器。事實上,這裏的內部實現都是依賴於Node的事件循環。
Node的核心就是JavaScript和事件循環。
簡單地說,事件循環就是一個等待事件而後在須要事件發生時去觸發它們的程序。此外還有一點很重要,就是Node和JavaScript同樣都是單線程的。
還記得咱們舉過的餐廳例子嗎?無論顧客數量有多少,Node開的餐廳裏永遠只有一個廚師烹飪食物。
與其餘語言不一樣,NodeJS不須要爲每個請求開啓一個新的線程,它會接收全部請求,而後將大部分任務委託給其餘的系統。Libuv
就是一個依賴於OS內核去高效處理這些任務的庫。當這些隱藏於幕後的工做者處理完委託給它們的事件後,它們會觸發綁定在這些事件上的回調函數去通知NodeJS。
這兒咱們接觸到了回調這個概念。回調理解起來並不困難,它是被其餘函數看成參數傳遞的函數,而且在某種特定狀況下會被調用。
NodeJS開發者們作的最多的就是編寫事件處理函數,而這些處理函數會在特定的NodeJS事件發生後被調用。
NodeJS雖然是單線程,但它比多線程系統要快得多。這是由於程序每每並非只有耗時巨長的數學運算和邏輯處理,大部分時間裏它們只是寫入文件、處理網絡請求或是向控制檯和外部設備申請權限。這些都是NodeJS擅長處理的問題:當NodeJS在處理這些事情時,它會迅速將這些事件委託給專門的系統,轉而去處理下一個事件。
若是你繼續深刻下去,你也許會意識到NodeJS並不擅長處理消耗CPU的操做。由於CPU密集型操做會佔用大量的主線程資源。對於單線程系統來講,最理想的狀況就是避免這些操做來釋放主線程去處理別的事情。
還有一個關鍵點是在JavaScript中,只有你寫的代碼不是併發執行的。也就是說,你的代碼每次只能處理一件事,而其餘工做者,好比文件系統能夠並行處理它們手頭的工做。
若是你還不能理解的話,能夠看看下面的例子:
好久之前有一個國王,他有一千個官員。國王寫了一個任務清單讓官員去作,清單很是很是很是長。有一個宰相,根據清單將任務委託給其餘全部官員。每完成一項任務他就將結果報告給國王,以後國王又會給他另外一份清單。由於在官員工做的時候,國王也在忙於寫其餘清單。
這個例子要講的是即便有不少官員在並行處理任務,國王每次也只能作一件事。這裏,國王就是你的代碼,而官員就是藏於NodeJS幕後的系統工做者。因此說,除了你的代碼,每件事都是並行發生的。
好了,讓咱們繼續這段NodeJS之旅吧。
用NodeJS寫一個web應用至關於編寫事件回調。讓咱們來看看下面的例子:
npm init
命令,一直回車直到你在文件夾根目錄下建立了一個package.json文件。//server.js
const http = require('http'),
server = http.createServer();
server.on('request',(request,response)=>{
response.writeHead(200,{'Content-Type':'text/plain'});
response.write('Hello world');
response.end();
});
server.listen(3000,()=>{
console.log('Node server created at port 3000');
});
複製代碼
node server.js
,你會看到下面的輸出:node server.js
//Node server started at port 3000
複製代碼
打開瀏覽器而且進入localhost:3000
,你應該可以看到一個Hello world
信息。
首先,咱們引入了http模塊。這個模塊提供了處理htpp操做的接口,咱們調用createServer()
方法來建立一個服務器。
以後,咱們爲request事件綁定了一個事件回調,傳遞給on方法的第二個參數。這個回調函數有2個參數對象,request表明接收到的請求,response表明響應的數據。
不只僅是處理request事件,咱們也可讓Node去作其餘事情。
//server.js
const http = require('http'),
server = http.createServer((request,response)=>{
response.writeHead(200,{'Content-Type':'text/plain'});
response.write('Hello world');
response.end();
});
server.listen(3000,()=>{
console.log('Node server created at port 3000');
});
複製代碼
在當面的代碼裏,咱們傳給createServer()一個回調函數,Node把它綁定在request事件上。這樣咱們只須要關心request和response對象了。
咱們使用response.writeHead()
來設置返回報文頭部字段,好比狀態碼和內容類型。而response.write()
是對web頁面進行寫入操做。最後使用response.end()
來結束這個響應。
最後,咱們告知服務器去監聽3000端口,這樣咱們能夠在本地開發時查看咱們web應用的一個demo。listen這個方法要求第二個參數是一個回調函數,服務器一啓動,這個回調函數就會被執行。
Node是一個單線程事件驅動的運行環境,也就是說,在Node裏,任何事都是對事件的響應。
前文的例子能夠改寫成下面這樣:
//server.js
const http = require('http'),
makeServer = function (request,response){
response.writeHead(200,{'Content-Type':'text/plain'});
response.write('Hello world');
response.end();
},
server = http.createServer(makeServer);
server.listen(3000,()=>{
console.log('Node server created at port 3000');
複製代碼
makeServer
是一個回調函數,因爲JavaScript把函數看成一等公民,因此他們能夠被傳給任何變量或是函數。若是你還不瞭解JavaScript,你應該花點時間去了解什麼是事件驅動程序。
當你開始編寫一些重要的JavaScript代碼時,你可能會遇到「回調地獄」。你的代碼變得難以閱讀由於大量的函數交織在一塊兒,錯綜複雜。這時你想要找到一種更先進、有效的方法來取代回調。看看Promise吧,Eric Elliott 寫了一篇文章來講解什麼是Promise,這是一個好的入門教程。
一個服務器會存儲大量的文件。當瀏覽器發送請求時,會告知服務器他們須要的文件,而服務器會將相應的文件返回給客戶端。這就叫作路由。
在NodeJS中,咱們須要手動定義本身的路由。這並不麻煩,看看下面這個基本的例子:
//server.js
const http = require('http'),
url = require('url'),
makeServer = function (request,response){
let path = url.parse(request.url).pathname;
console.log(path);
if(path === '/'){
response.writeHead(200,{'Content-Type':'text/plain'});
response.write('Hello world');
}
else if(path === '/about'){
response.writeHead(200,{'Content-Type':'text/plain'});
response.write('About page');
}
else if(path === '/blog'){
response.writeHead(200,{'Content-Type':'text/plain'});
response.write('Blog page');
}
else{
response.writeHead(404,{'Content-Type':'text/plain'});
response.write('Error page');
}
response.end();
},
server = http.createServer(makeServer);
server.listen(3000,()=>{
console.log('Node server created at port 3000');
});
複製代碼
粘貼這段代碼,輸入node server.js
命令來運行。在瀏覽器中打開localhost:3000
和localhost:3000/abou
,而後在試試打開localhost:3000/somethingelse
,是否是跳轉到了咱們的錯誤頁面?
雖然這樣知足了咱們啓動服務器的基本要求,可是要爲服務器上每個網頁都寫一遍代碼實在是太瘋狂了。事實上沒有人會這麼作,這個例子只是讓你瞭解路由是怎麼工做的。
若是你有注意到,咱們引入了url這個模塊,它能讓咱們處理url更加方便。
爲parse()方法傳入一個url字符串參數,這個方法會將url拆分紅protocol
、host
、path
和querystring
等部分。若是你不太瞭解這些單詞,能夠看看下面這張圖:
因此當咱們執行url.parse(request.url).pathname
語句時,咱們獲得一個url路徑名,或者是url自己。這些都是咱們用來進行路由請求的必要條件。不過這件事還有個更簡單的方法。
若是你以前作過功課,你必定據說過Express。這是一個用來構建web應用以及API的NodeJS框架,它也能夠用來編寫NodeJS應用。接着往下看,你會明白爲何我說它讓一切變得更簡單。
在你的終端或是命令行中,進入電腦的根目錄,輸入npm install express --save
來安裝Express模塊包。要在項目中使用Express,咱們須要引入它。
const express = require('express');
複製代碼
歡呼吧,生活將變得更美好。
如今,讓咱們用express進行基本的路由。
//server.js
const express = require('express'),
server = express();
server.set('port', process.env.PORT || 3000);
//Basic routes
server.get('/', (request,response)=>{
response.send('Home page');
});
server.get('/about',(request,response)=>{
response.send('About page');
});
//Express error handling middleware
server.use((request,response)=>{
response.type('text/plain');
response.status(505);
response.send('Error page');
});
//Binding to a port
server.listen(3000, ()=>{
console.log('Express server started at port 3000');
});
複製代碼
譯者注:這裏不是很理解爲何代碼中錯誤狀態碼是505。
如今的代碼是否是看上去更加清晰了?我相信你很容易就能理解它。
首先,當咱們引入express模塊後,獲得的是一個函數。調用這個函數後就能夠開始啓動咱們的服務器了。
接下來,咱們用server.set()
來設置監聽端口。而process.env.PORT
是程序運行時的環境所設置的。若是沒有這個設置,咱們默認它的值是3000.
而後,觀察上面的代碼,你會發現Express裏的路由都遵循一個格式:
server.VERB('route',callback);
複製代碼
這裏的VERB能夠是GET、POST等動做,而pathname是跟在域名後的字符串。同時,callback是咱們但願接收到一個請求後觸發的函數。
最後咱們再調用server.listen()
,還記得它的做用吧?
以上就是Node程序裏的路由,下面咱們來挖掘一下Node如何調用數據庫。
不少人喜歡用JavaScript來作全部事。恰好有一些數據庫知足這個需求,好比MongoDB、CouchDB等待。這些數據庫都是NoSQL數據庫。
一個NoSQL數據庫以鍵值對的形式做爲數據結構,它以文檔爲基礎,數據都不以表格形式保存。
咱們來能夠看看MongoDB這個NoSQL數據庫。若是你使用過MySQL、SQLserver等關係型數據庫,你應該熟悉數據庫、表格、行和列等概念。 MongoDB與他們相比並無特別大的區別,不過仍是來比較一下吧。
譯者注:這兒應該有個表格顯示MongoDB與MySQL的區別,可是原文裏沒有顯示。
爲了讓數據更加有組織性,在向MongoDB插入數據以前,咱們可使用Mongoose來檢查數據類型和爲文檔添加驗證規則。它看上去就像Mongo與Node之間的中介人。
因爲本文篇幅較長,爲了保證每一節都儘量的簡短,請你先閱讀官方的MongoDB安裝教程。
此外,Chris Sevilleja寫了一篇Easily Develop Node.js and MongoDB Apps with Mongoose,我認爲這是一篇適合入門的基礎教程。
API是應用程序向別的程序發送數據的通道。你有沒有登錄過某些須要你使用facebook帳號登陸的網頁?facebook將某些函數公開給這些網站使用,這些就是API。
一個RESTful API應該不以服務器/客戶端的狀態改變而改變。經過使用一個REST接口,不一樣的客戶端,即便它們的狀態各不相同,可是在訪問相同的REST終端時,應該作出同一種動做,而且接收到相同的數據。
API終端是API裏返回數據的一個函數。
編寫一個RESTful API涉及到使用JSON或是XML格式傳輸數據。讓咱們在NodeJS裏試試吧。咱們接下來會寫一個API,它會在客戶端經過AJAX發起請求後返回一個假的JSON數據。這不是一個理想的API,可是能幫助咱們理解在Node環境中它是怎麼工做的。
npm init
。這會建立一個收集依賴的文件;npm install --save express
來安裝express;server.js
,index.html
和users.js
;//users.js
module.exports.users = [
{
name: 'Mark',
age : 19,
occupation: 'Lawyer',
married : true,
children : ['John','Edson','ruby']
},
{
name: 'Richard',
age : 27,
occupation: 'Pilot',
married : false,
children : ['Abel']
},
{
name: 'Levine',
age : 34,
occupation: 'Singer',
married : false,
children : ['John','Promise']
},
{
name: 'Endurance',
age : 45,
occupation: 'Business man',
married : true,
children : ['Mary']
},
]
複製代碼
這是咱們傳給別的應用的數據,咱們導出這份數據讓全部程序均可以使用。也就是說,咱們將users這個數組保存在modules.exports
對象中。
//server.js
const express = require('express'),
server = express(),
users = require('./users');
//setting the port.
server.set('port', process.env.PORT || 3000);
//Adding routes
server.get('/',(request,response)=>{
response.sendFile(__dirname + '/index.html');
});
server.get('/users',(request,response)=>{
response.json(users);
});
//Binding to localhost://3000
server.listen(3000,()=>{
console.log('Express server started at port 3000');
});
複製代碼
咱們執行require('express')
語句而後使用express()
建立了一個服務變量。若是你仔細看,你還會發現咱們引入了別的東西,那就是users.js
。還記得咱們把數據放在哪了嗎?要想程序工做,它是必不可少的。
express有許多方法幫助咱們給瀏覽器傳輸特定類型的內容。response.sendFile()
會查找文件而且發送給服務器。咱們使用__dirname
來獲取服務器運行的根目錄路徑,而後咱們把字符串index.js
加在路徑後面保證咱們可以定位到正確的文件。
response.json()
向網頁發送JSON格式內容。咱們把要分享的users數組傳給它當參數。剩下的代碼我想你在以前的文章中已經很熟悉了。
//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home page</title>
</head>
<body>
<button>Get data</button>
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script type="text/javascript"> const btn = document.querySelector('button'); btn.addEventListener('click',getData); function getData(e){ $.ajax({ url : '/users', method : 'GET', success : function(data){ console.log(data); }, error: function(err){ console.log('Failed'); } }); } </script>
</body>
</html>
複製代碼
在文件夾根目錄中執行node server.js
,如今打開你的瀏覽器訪問localhost:3000
,按下按鈕而且打開你的瀏覽器控制檯。
在btn.addEventListent('click',getData);
這行代碼裏,getData經過AJAX發出一個GET請求,它使用了$.ajax({properties})
函數來設置url
,success
和error
等參數。
在實際生產環境中,你要作的不只僅是讀取JSON文件。你可能還想對數據進行增刪改查等操做。express框架會將這些操做與特定的http動詞綁定,好比POST、GET、PUT和DELETE等關鍵字。
要想深刻了解使用express如何編寫API,你能夠去閱讀Chris Sevilleja寫的Build a RESTful API with Express 4。
計算機網絡是計算機之間分享接收數據的鏈接。要在NodeJS中進行連網操做,咱們須要引入net
模塊。
const net = require('net');
複製代碼
在TCP中必須有兩個終端,一個終端與指定端口綁定,而另外一個則須要訪問這個指定端口。
若是你還有疑惑,能夠看看這個例子:
以你的手機爲例,一旦你買了一張sim卡,你就和sim的電話號碼綁定。當你的朋友想要打電話給你時,他們必須撥打這個號碼。這樣你就至關於一個TCP終端,而你的朋友是另外一個終端。
如今你明白了吧?
爲了更好地吸取這部分知識,咱們來寫一個程序,它可以監聽文件而且當文件被更改後會通知鏈接到它的客戶端。
node-network
;filewatcher.js
、subject.txt
和client.js
。把下面的代碼複製進filewatcher.js
。//filewatcher.js
const net = require('net'),
fs = require('fs'),
filename = process.argv[2],
server = net.createServer((connection)=>{
console.log('Subscriber connected');
connection.write(`watching ${filename} for changes`);
let watcher = fs.watch(filename,(err,data)=>{
connection.write(`${filename} has changed`);
});
connection.on('close',()=>{
console.log('Subscriber disconnected');
watcher.close();
});
});
server.listen(3000,()=>console.log('listening for subscribers'));
複製代碼
subject.txt
寫下下面一段話:Hello world, I'm gonna change 複製代碼
client.js
。const net = require('net');
let client = net.connect({port:3000});
client.on('data',(data)=>{
console.log(data.toString());
});
複製代碼
filename.js
,後面跟着咱們要監聽的文件名。//subject.txt會保存在filename變量中
node filewatcher.js subject.txt
//監聽訂閱者
複製代碼
在另外一個終端,也就是客戶端,咱們運行client.js
。 node client.js
如今,修改subject.txt
,而後看看客戶端的命令行,注意到多出了一條額外信息:
//subject.txt has changed.
複製代碼
網絡的一個主要的特徵就是許多客戶端均可以同時接入這個網絡。打開另外一個命令行窗口,輸入node client.js
來啓動另外一個客戶端,而後再修改subject.txt
文件。看看輸出了什麼?
若是你沒有理解,不要擔憂,讓咱們從新過一遍。
咱們的filewatcher.js
作了三件事:
net.createServer()
建立一個服務器並向許多客戶端發送信息。再來看一次filewatcher.js
。
//filewatcher.js
const net = require('net'),
fs = require('fs'),
filename = process.argv[2],
server = net.createServer((connection)=>{
console.log('Subscriber connected');
connection.write(`watching ${filename} for changes`);
let watcher = fs.watch(filename,(err,data)=>{
connection.write(`${filename} has changed`);
});
connection.on('close',()=>{
console.log('Subscriber disconnected');
watcher.close();
});
});
server.listen(3000,()=>console.log('listening for subscribers'));
複製代碼
咱們引入兩個模塊:fs和net來讀寫文件和執行網絡鏈接。你有注意到process.argv[2]
嗎?process是一個全局變量,提供NodeJS代碼運行的重要信息。argv[]
是一個參數數組,當咱們獲取argv[2]
時,但願獲得運行代碼的第三個參數。還記得在命令行中,咱們曾輸入文件名做爲第三個參數嗎?
node filewatcher.js subject.txt
複製代碼
此外,咱們還看到一些很是熟悉的代碼,好比net.createServer()
,這個函數會接收一個回調函數,它在客戶端鏈接到端口時觸發。這個回調函數只接收一個用來與客戶端交互的對象參數。
connection.write()
方法向任何鏈接到3000端口的客戶端發送數據。這樣,咱們的connetion
對象開始工做,通知客戶端有一個文件正在被監聽。
wactcher包含一個方法,它會在文件被修改後發送信息給客戶端。並且在客戶端斷開鏈接後,觸發了close事件,而後事件處理函數會向服務器發送信息讓它關閉watcher中止監聽。
//client.js
const net = require('net'),
client = net.connect({port:3000});
client.on('data',(data)=>{
console.log(data.toString());
});
複製代碼
client.js
很簡單,咱們引入net模塊而且調用connect方法去訪問3000端口,而後監聽每個data事件並打印出數據。
當咱們的filewatcher.js
每執行一次connection.write()
,咱們的客戶端就會觸發一次data事件。
以上只是網絡如何工做的一點皮毛。主要就是一個端點廣播信息時會觸發全部鏈接到這個端點的客戶端上的data事件。
若是你想要了解更多Node的網絡知識,能夠看看官方NodeJS的文檔:net模塊。你也許還須要閱讀Building a Tcp service using Node。
好了,這就是我要講的所有。若是你想要使用NodeJS來編寫web應用程序,你要知道的不只僅是編寫一個服務器和使用express進行路由。
下面是我推薦的一些書:
NodeJs the right way
Web Development With Node and Express
若是你還有什麼看法,能夠在下面發表評論。
譯者注:這個翻譯項目纔開始,之後會翻譯愈來愈多的做品。我會努力堅持的。
項目地址:github.com/WhiteYin/tr…