NodeJS基礎(chǔ)API搭建服務(wù)器詳細(xì)過(guò)程記錄
前言
在習(xí)慣了使用express框架,jade模板引擎等現(xiàn)成工具來(lái)寫代碼之后,很多人對(duì)于基本的NodeJS API會(huì)慢慢生疏。本文將以一個(gè)超小型web項(xiàng)目,來(lái)詳細(xì)介紹如何使用NodeJS基礎(chǔ)的http, fs, path, url等模塊提供的API來(lái)搭建一個(gè)簡(jiǎn)單的web服務(wù)器。當(dāng)做對(duì)NodeJS的一次復(fù)習(xí),也為初學(xué)NodeJS的開(kāi)發(fā)者提供一個(gè)參考。本文所搭建的項(xiàng)目將不會(huì)使用express等后端框架,僅使用最基礎(chǔ)的NodeJS API,按照MVC設(shè)計(jì)模式的思路進(jìn)行編碼和講解,交流意見(jiàn)。源代碼地址如下,建議下載源碼邊看博客邊對(duì)照源碼才能比較快理解整個(gè)過(guò)程。https://github.com/hongchh/node-example
項(xiàng)目介紹
有一個(gè)簡(jiǎn)單的食品店網(wǎng)站,它包括一個(gè)主頁(yè)index和一個(gè)詳情頁(yè)detail。主頁(yè)展示食品店的所有食品,包括食品圖片、名稱、價(jià)格3個(gè)信息,如下圖所示。

用戶點(diǎn)擊任何一項(xiàng)食品就會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的詳情頁(yè),包括食品圖片、名稱、價(jià)格和描述4個(gè)信息,如下圖所示。

項(xiàng)目結(jié)構(gòu)
項(xiàng)目的文件結(jié)構(gòu)如下所示。
node-example |--data(存放項(xiàng)目數(shù)據(jù)的文件夾) |--detail.json(存放食品詳情數(shù)據(jù)) |--foods.json(存放首頁(yè)食品數(shù)據(jù)) |--model(提供訪問(wèn)和操作數(shù)據(jù)服務(wù)的數(shù)據(jù)模型) |--detail.js(詳情數(shù)據(jù)訪問(wèn)模塊) |--foods.js(食品數(shù)據(jù)訪問(wèn)模塊) |--public(存放css,js,圖片等靜態(tài)文件) |--css(存放css文件的文件夾) |--img(存放圖片的文件夾) |--js(存放js文件的文件夾) |--route(路由,控制器) |--api(處理普通請(qǐng)求的路由,或者叫控制器) |--static(處理靜態(tài)文件請(qǐng)求的路由,或者叫控制器) |--views(視圖,即用戶界面) |--index.html(主頁(yè)界面) |--detail.html(詳情頁(yè)面) |--server.js(服務(wù)器啟動(dòng)文件) |--package.json(項(xiàng)目包信息) |--README.md(項(xiàng)目信息以及啟動(dòng)方法描述)
本文只講解服務(wù)端編程,因此兩個(gè)簡(jiǎn)單界面的實(shí)現(xiàn)過(guò)程這里就不再啰嗦了。假設(shè)你已經(jīng)能夠自行完成前端的界面編程,下面開(kāi)始講解服務(wù)端編程。
編寫服務(wù)器
server.js中要完成服務(wù)器的創(chuàng)建和啟動(dòng),并將請(qǐng)求轉(zhuǎn)發(fā)給相應(yīng)的路由去處理。詳細(xì)代碼如下所示(假設(shè)我們已經(jīng)有了能夠正常工作的路由,這里采用Top-Down的思路,我們一層一層地往下寫,專注于解決每個(gè)層次的問(wèn)題)。代碼中使用正則表達(dá)式來(lái)判定客戶端request是否是在請(qǐng)求靜態(tài)文件,如果是,則交給專門處理靜態(tài)文件請(qǐng)求的路由static去處理,否則交給普通請(qǐng)求的路由器api去處理。普通請(qǐng)求根據(jù)它的HTTP方法來(lái)判斷使用get或者post。最后,設(shè)置服務(wù)器監(jiān)聽(tīng)3000端口,server.js的代碼就算完成了。
var http = require('http');
var url = require('url');
var api = require('./route/api');
var static = require('./route/static');
// 匹配靜態(tài)文件夾路徑的正則表達(dá)式,用于判定請(qǐng)求是否為靜態(tài)文件請(qǐng)求
var staticExp = /\/public\/(img|css|js)\/[a-z]*\.(jpg|png|gif|css|js)/;
http.createServer((req, res) => {
var pathname = url.parse(req.url).pathname;
if (staticExp.test(pathname)) {// 靜態(tài)文件請(qǐng)求交由static處理
static.get(__dirname + pathname, res);
} else if (req.method == 'POST') {// 處理普通post請(qǐng)求
api.post(req, res);
} else {// 處理普通get請(qǐng)求
api.get(req, res);
}
}).listen(3000);
console.log('[Server Info] Start server at http://localhost:3000/');
編寫路由
我從簡(jiǎn)單的開(kāi)始,先寫處理靜態(tài)文件請(qǐng)求的路由static。這個(gè)路由的邏輯很簡(jiǎn)單,只要客戶端想要請(qǐng)求某個(gè)靜態(tài)文件(css/js/圖片),就將被請(qǐng)求的文件發(fā)送給客戶端即可。代碼如下所示。有以下幾點(diǎn)需要注意的地方,首先,客戶端請(qǐng)求文件,需要判斷文件是否存在,如果存在才將其發(fā)送給客戶端,不存在則作其他處理(這里我暫時(shí)沒(méi)做其他處理)。其次,將文件響應(yīng)給客戶端的時(shí)候,需要設(shè)置好http報(bào)頭的MIME type,這樣文件發(fā)過(guò)去之后客戶端才能識(shí)別出文件類型從而正確使用。最后,像圖片、音頻等多媒體文件需要用二進(jìn)制的讀寫方式,所以在響應(yīng)圖片的時(shí)候記得加上“binary”。
var fs = require('fs');
var path = require('path');
var MIME = {};
MIME[".css"] = "text/css";
MIME[".js"] = "text/js";
MIME[".jpg"] = "image/jpeg";
MIME[".jpeg"] = "image/jpeg";
MIME[".png"] = "image/png";
MIME[".gif"] = "image/gif";
function get(pathname, res) {
if (fs.existsSync(pathname)) {
var extname = path.extname(pathname);
res.writeHead(200, {'Content-Type': MIME[extname]});
fs.readFile(pathname, (err, data) => {
if (err) {
console.log(err);
res.end();
} else {
if (isImage(extname)) {
res.end(data, "binary");// 二進(jìn)制文件需要加上binary
} else {
res.end(data.toString());
}
}
});
}
}
// 根據(jù)拓展名判斷是否為圖片
function isImage(extname) {
if (extname === '.jpg' || extname === '.jpeg' ||
extname === '.png' || extname === '.gif') {
return true;
}
return false;
}
// 提供給其他模塊使用的接口
module.exports = {
get: get
};
static寫完了,下面來(lái)繼續(xù)寫api。api需要根據(jù)請(qǐng)求的URL來(lái)響應(yīng)對(duì)應(yīng)的內(nèi)容。例如客戶端請(qǐng)求“/”,就響應(yīng)它網(wǎng)站的主頁(yè),請(qǐng)求“/detail?id=0”就響應(yīng)它id為0的食品的詳情頁(yè)面。如果客戶端請(qǐng)求了不存在的URL,則給回一個(gè)404響應(yīng),表示沒(méi)有找到。代碼如下所示。這里我分了兩個(gè)handler,本項(xiàng)目沒(méi)有post操作,所以只有g(shù)etHandler會(huì)使用到。給出postHanlder的目的是為了簡(jiǎn)單說(shuō)明如何寫處理客戶端post請(qǐng)求的路由。
以getHanlder[‘/']為例,當(dāng)客戶端請(qǐng)求“/”的時(shí)候,不是簡(jiǎn)單地把index.html響應(yīng)給服務(wù)器這么簡(jiǎn)單,想象一下,一家食品店,每天提供的菜式可能會(huì)有所不同,或者因?yàn)榧竟?jié)問(wèn)題而導(dǎo)致每個(gè)季節(jié)的特色菜都有所不同,所以我們網(wǎng)站主頁(yè)展示的菜式也可能隨之而變化。因此,我們需要根據(jù)數(shù)據(jù)庫(kù)中存儲(chǔ)的主頁(yè)數(shù)據(jù)來(lái)動(dòng)態(tài)渲染主頁(yè)的內(nèi)容。我把idnex.html寫成模板,為了不適用jade等模板引擎,我在html里面使用如同“{{foodMenu}}”這種形式的標(biāo)記,當(dāng)讀取完模板之后,利用簡(jiǎn)單的字符串操作將標(biāo)記替換成我們需要?jiǎng)討B(tài)渲染的內(nèi)容,即可實(shí)現(xiàn)動(dòng)態(tài)渲染HTML的目的。
靜態(tài)文件之外的其他路由,或者叫控制器(controller),一般都會(huì)包含業(yè)務(wù)邏輯,即業(yè)務(wù)邏輯一般是在這一層完成的。像上面的根據(jù)數(shù)據(jù)庫(kù)內(nèi)容動(dòng)態(tài)渲染出首頁(yè),或者你在其他場(chǎng)景下面會(huì)見(jiàn)到的如登錄注冊(cè)的數(shù)據(jù)檢驗(yàn),成功登錄之后將客戶端重定向到對(duì)應(yīng)的用戶界面等等業(yè)務(wù)邏輯都是在這一層實(shí)現(xiàn)。
var fs = require('fs');
var url = require('url');
var querystring = require('querystring');
var foods = require('../model/foods')();
var detail = require('../model/detail')();
var getHandler = {};
var postHandler = {};
// 處理對(duì)主頁(yè)的請(qǐng)求
getHandler['/'] = function(req, res) {
var foodMenu = "";
// 拼裝首頁(yè)數(shù)據(jù)
var food = foods.getAllFoods();
for (var i = 0; i < food.length; ++i) {
foodMenu += '<div class="food-card" id="' + food[i].id + '"><img src="';
foodMenu += food[i].image + '"><h1>' + food[i].name + '</h1><h2>' + food[i].price + '</h2></div>';
}
res.writeHead(200, {"Content-Type": "text/html"});
fs.readFile(__dirname + '/../views/index.html', (err, data) => {
if (err) {
console.log(err);
res.end();
} else {
// 動(dòng)態(tài)渲染模板
res.end(data.toString().replace('{{foodMenu}}', foodMenu));
}
});
};
// 處理對(duì)詳情頁(yè)面的請(qǐng)求
getHandler['/detail'] = function(req, res) {
var query = querystring.parse(url.parse(req.url).query);
var foodDetail = detail.getDetail(query.id);
res.writeHead(200, {"Content-Type": "text/html"});
fs.readFile(__dirname + '/../views/detail.html', (err, data) => {
// 動(dòng)態(tài)渲染模板
res.end(data.toString().replace('{{image}}', foodDetail.image)
.replace('{{name}}', foodDetail.name)
.replace('{{description}}', foodDetail.description)
.replace('{{price}}', foodDetail.price));
});
};
// 404響應(yīng),告知客戶端資源未找到
getHandler['/404'] = function(req, res) {
res.writeHead(404, {"Content-Type": "text/plain"});
res.end("404 Not Found");
};
// post請(qǐng)求的處理方法示例
postHandler['/'] = function(res, data) {
// do something
};
// get請(qǐng)求
function get(req, res) {
var reqUrl = url.parse(req.url);
if (typeof getHandler[reqUrl.pathname] === "function") {
getHandler[reqUrl.pathname](req, res);
} else {
getHandler["/404"](req, res);
}
}
// post請(qǐng)求(示例)
function post(req, res) {
var reqUrl = url.parse(req.url);
if (typeof postHandler[reqUrl.pathname] === "function") {
var postData = "";
req.on('data', (data) => {
postData += data;
});
req.on('end', () => {
postData = querystring.parse(postData);
postHandler[reqUrl.pathname](res, postData);
});
} else {
getHandler["/404"](req, res);
}
}
// 提供給其他模塊使用的接口
module.exports = {
get: get,
post: post
};
最后,講一下post方法的處理過(guò)程,雖然本項(xiàng)目中沒(méi)有使用到post。post方法跟get方法最主要的不同之處在于post方法除了發(fā)送http頭部信息之外還帶有客戶端提交的數(shù)據(jù)。在接收到post請(qǐng)求的時(shí)候,需要將數(shù)據(jù)讀取出來(lái),讀取數(shù)據(jù)的方式也挺簡(jiǎn)單,只要給request設(shè)置監(jiān)聽(tīng)器就行了。當(dāng)request對(duì)象收到數(shù)據(jù)的時(shí)候會(huì)觸發(fā)“data”事件,因此,給這個(gè)事件設(shè)置監(jiān)聽(tīng)器,讓它收到數(shù)據(jù)的時(shí)候就把數(shù)據(jù)保存起來(lái)。在接收完一個(gè)請(qǐng)求全部的post數(shù)據(jù)之后會(huì)觸發(fā)“end”事件,因此,給這個(gè)事件設(shè)置監(jiān)聽(tīng)器,使得在接收完全部數(shù)據(jù)之后才開(kāi)始對(duì)提交的數(shù)據(jù)進(jìn)行相關(guān)的操作。
編寫數(shù)據(jù)模型
先拿主頁(yè)來(lái)講吧。通過(guò)前面的截圖,我們可以知道,主頁(yè)上的數(shù)據(jù)包括展示菜品的圖片、名稱、價(jià)格,另外需要根據(jù)不同的菜品跳轉(zhuǎn)到對(duì)應(yīng)的詳情頁(yè),因此還需要一個(gè)id來(lái)用作標(biāo)識(shí)符。最后,可以得到如下的數(shù)據(jù)模型(下面的模型我使用json描述,你也可以采取其他辦法)。這個(gè)數(shù)據(jù)模型描述了主頁(yè)的數(shù)據(jù)模型,即首頁(yè)有很多個(gè)食品foods,用數(shù)組表示,每個(gè)數(shù)據(jù)元素代表一個(gè)食品。每個(gè)食品包括四項(xiàng)信息,id,image,name,price。id的值是一個(gè)數(shù)字,作為唯一標(biāo)識(shí)符。image是一個(gè)字符串,用來(lái)指明圖片地址。name的值是字符串,表示食品的名字,price的值是一個(gè)字符串,表示食品的價(jià)格。
{
"foods": [{
"id": "number",
"image": "string",
"name": "string",
"price": "string"
}]
}
設(shè)計(jì)好數(shù)據(jù)模型的目的是方便我們?cè)O(shè)計(jì)偽數(shù)據(jù),也方便我們對(duì)數(shù)據(jù)進(jìn)行操作,一般在開(kāi)始編程之前要做的事情就是設(shè)計(jì)好數(shù)據(jù)模型(數(shù)據(jù)結(jié)構(gòu)),這樣寫程序時(shí)候才會(huì)更加順利,很多接口才能規(guī)范下來(lái)。雖然我這里把model這一步放在了最后,但我這里model里面只是寫了數(shù)據(jù)訪問(wèn)模塊,不代表數(shù)據(jù)模型是最后才設(shè)計(jì)的,只是因?yàn)槲疫@里講解的思路是自定向下,剛好講到model就順帶提一提數(shù)據(jù)模型設(shè)計(jì)。
下面以foods.js為例來(lái)講解如何編寫model。代碼如下所示。這里由于沒(méi)有數(shù)據(jù)庫(kù)(涉及數(shù)據(jù)庫(kù)的話對(duì)于新手來(lái)說(shuō)比較麻煩,為了講清楚過(guò)程本文將不采用數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù)),我將所有數(shù)據(jù)使用json文件存儲(chǔ),例如foods.json中存儲(chǔ)了主頁(yè)的所有食品的數(shù)據(jù)。foods model將對(duì)外提供接口,用于支持訪問(wèn)主頁(yè)的食品數(shù)據(jù),修改食品數(shù)據(jù)等操作(數(shù)據(jù)庫(kù)常說(shuō)的增刪查改CRUD四個(gè)操作)。本項(xiàng)目只需要用到查詢所有視頻的操作,所以我這里簡(jiǎn)單實(shí)現(xiàn)了一個(gè)獲取所有食品的方法,另外附帶一個(gè)根據(jù)id獲取單個(gè)食品的方法(這個(gè)方法僅是示例,沒(méi)有用到)。
var fs = require('fs');
module.exports = function() {
// 讀取文件中的數(shù)據(jù),將其轉(zhuǎn)成一個(gè)對(duì)象方便使用
var data = JSON.parse(fs.readFileSync(__dirname + '/../data/foods.json'));
var foods = {
getAllFoods: getAllFoods,
getFood: getFood
};
// 獲取所有食品
function getAllFoods() {
return data.foods;
}
// 根據(jù)id獲取單個(gè)食品
function getFood(id) {
for (var i = 0; i < data.foods.length; ++i) {
if (data.foods[i].id == id)
return data.foods[i];
}
}
return foods;
};
model里面的模塊一般提供數(shù)據(jù)操作的服務(wù)供控制器使用,所以在這一層就主要關(guān)注實(shí)現(xiàn)數(shù)據(jù)CRUD操作即可,基本沒(méi)有什么業(yè)務(wù)邏輯了。
照著寫foods的思路,我們?cè)侔裠etail寫完,整個(gè)項(xiàng)目就完成了。是不是挺簡(jiǎn)單的。進(jìn)到項(xiàng)目目錄下面,使用node server.js啟動(dòng)服務(wù)器跑一跑吧。

最后,看完整個(gè)項(xiàng)目,你大概可以發(fā)現(xiàn)整個(gè)編寫過(guò)程,或者說(shuō)每個(gè)模塊的劃分,都好像遵照某種特定的模式在進(jìn)行,其實(shí)我是按照MVC的模式來(lái)編寫這個(gè)項(xiàng)目的,最近在另外一門學(xué)課的學(xué)習(xí)中也經(jīng)常用到MVC,覺(jué)得還是挺不錯(cuò)的一種設(shè)計(jì)模式,有興趣可以研究一下。當(dāng)然,我不能說(shuō)我寫的代碼完全符合MVC的規(guī)范,畢竟每個(gè)人的理解都可能有那么一些出入。本文僅供參考,歡迎交流建議,謝謝!
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,同時(shí)也希望多多支持腳本之家!
- node.js中使用q.js實(shí)現(xiàn)api的promise化
- 實(shí)現(xiàn)一個(gè)完整的Node.js RESTful API的示例
- 詳解本地Node.js服務(wù)器作為api服務(wù)器的解決辦法
- node.js實(shí)現(xiàn)微信JS-API封裝接口的示例代碼
- 淺析Node.js 中 Stream API 的使用
- Node.js原生api搭建web服務(wù)器的方法步驟
- 30分鐘用Node.js構(gòu)建一個(gè)API服務(wù)器的步驟詳解
- 零基礎(chǔ)之Node.js搭建API服務(wù)器的詳解
- NodeJS仿WebApi路由示例
- 深入分析node.js的異步API和其局限性
- 淺析Node在構(gòu)建超媒體API中的作用
- Node.js API詳解之 tty功能與用法實(shí)例分析
相關(guān)文章
基于html5和nodejs相結(jié)合實(shí)現(xiàn)websocket即使通訊
HTML5 擁有許多引人注目的新特性,如 Canvas、本地存儲(chǔ)、多媒體編程接口、WebSocket 等等。雖然現(xiàn)在大家把它捧的很火的樣子,但是個(gè)人認(rèn)為它還需要其他平臺(tái)的支持才能真正的"火起來(lái)"2015-11-11
Node中文件斷點(diǎn)續(xù)傳原理和方法總結(jié)
在之前做過(guò)一個(gè)小項(xiàng)目,涉及到了文件上傳,在大文件上面使用了斷點(diǎn)續(xù)傳,降低了服務(wù)器方面的壓力,現(xiàn)在小編把Node中文件斷點(diǎn)續(xù)傳原理和方法總結(jié)分享給大家,感興趣的朋友一起看看吧2022-01-01
nodejs實(shí)現(xiàn)套接字服務(wù)功能詳解
這篇文章主要介紹了nodejs實(shí)現(xiàn)套接字服務(wù)功能,簡(jiǎn)單描述了套接字的概念、功能,并結(jié)合實(shí)例形式分析了nodejs使用socket對(duì)象創(chuàng)建及使用套接字進(jìn)行數(shù)據(jù)傳輸相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2018-06-06

