前言
在习惯了使用express框架,jade模板引擎等现成工具来写代码之后,很多人对于基本的NodeJS API会慢慢生疏。本文将以一个超小型web项目,来详细介绍如何使用NodeJS基础的http,fs,path,url等模块提供的API来搭建一个简单的web服务器。当做对NodeJS的一次复习,也为初学NodeJS的开发者提供一个参考。本文所搭建的项目将不会使用express等后端框架,仅使用最基础的NodeJS API,按照MVC设计模式的思路进行编码和讲解,交流意见。源代码地址如下,建议下载源码边看博客边对照源码才能比较快理解整个过程。nofollow" href="https://github.com/hongchh/node-example">https://github.com/hongchh/node-example
项目介绍
有一个简单的食品店网站,它包括一个主页index和一个详情页detail。主页展示食品店的所有食品,包括食品图片、名称、价格3个信息,如下图所示。
用户点击任何一项食品就会跳转到对应的详情页,包括食品图片、名称、价格和描述4个信息,如下图所示。
项目结构
项目的文件结构如下所示。
本文只讲解服务端编程,因此两个简单界面的实现过程这里就不再啰嗦了。假设你已经能够自行完成前端的界面编程,下面开始讲解服务端编程。
编写服务器
server.js中要完成服务器的创建和启动,并将请求转发给相应的路由去处理。详细代码如下所示(假设我们已经有了能够正常工作的路由,这里采用Top-Down的思路,我们一层一层地往下写,专注于解决每个层次的问题)。代码中使用正则表达式来判定客户端request是否是在请求静态文件,如果是,则交给专门处理静态文件请求的路由static去处理,否则交给普通请求的路由器api去处理。普通请求根据它的HTTP方法来判断使用get或者post。最后,设置服务器监听3000端口,server.js的代码就算完成了。
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)) {// 静态文件请求交由static处理
static.get(__dirname + pathname,res);
} else if (req.method == 'POST') {// 处理普通post请求
api.post(req,res);
} else {// 处理普通get请求
api.get(req,res);
}
}).listen(3000);
console.log('[Server Info] Start server at http://localhost:3000/');
编写路由
我从简单的开始,先写处理静态文件请求的路由static。这个路由的逻辑很简单,只要客户端想要请求某个静态文件(css/js/图片),就将被请求的文件发送给客户端即可。代码如下所示。有以下几点需要注意的地方,首先,客户端请求文件,需要判断文件是否存在,如果存在才将其发送给客户端,不存在则作其他处理(这里我暂时没做其他处理)。其次,将文件响应给客户端的时候,需要设置好http报头的MIME type,这样文件发过去之后客户端才能识别出文件类型从而正确使用。最后,像图片、音频等多媒体文件需要用二进制的读写方式,所以在响应图片的时候记得加上“binary”。
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");// 二进制文件需要加上binary
} else {
res.end(data.toString());
}
}
});
}
}
// 根据拓展名判断是否为图片
function isImage(extname) {
if (extname === '.jpg' || extname === '.jpeg' ||
extname === '.png' || extname === '.gif') {
return true;
}
return false;
}
// 提供给其他模块使用的接口
module.exports = {
get: get
};
static写完了,下面来继续写api。api需要根据请求的URL来响应对应的内容。例如客户端请求“/”,就响应它网站的主页,请求“/detail?id=0”就响应它id为0的食品的详情页面。如果客户端请求了不存在的URL,则给回一个404响应,表示没有找到。代码如下所示。这里我分了两个handler,本项目没有post操作,所以只有getHandler会使用到。给出postHanlder的目的是为了简单说明如何写处理客户端post请求的路由。
以getHanlder[‘/']为例,当客户端请求“/”的时候,不是简单地把index.html响应给服务器这么简单,想象一下,一家食品店,每天提供的菜式可能会有所不同,或者因为季节问题而导致每个季节的特色菜都有所不同,所以我们网站主页展示的菜式也可能随之而变化。因此,我们需要根据数据库中存储的主页数据来动态渲染主页的内容。我把idnex.html写成模板,为了不适用jade等模板引擎,我在html里面使用如同“{{foodMenu}}”这种形式的标记,当读取完模板之后,利用简单的字符串操作将标记替换成我们需要动态渲染的内容,即可实现动态渲染HTML的目的。
静态文件之外的其他路由,或者叫控制器(controller),一般都会包含业务逻辑,即业务逻辑一般是在这一层完成的。像上面的根据数据库内容动态渲染出首页,或者你在其他场景下面会见到的如登录注册的数据检验,成功登录之后将客户端重定向到对应的用户界面等等业务逻辑都是在这一层实现。
var postHandler = {};
// 处理对主页的请求
getHandler['/'] = function(req,res) {
var foodMenu = "";
// 拼装首页数据
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 + '">
' + food[i].name + '
' + food[i].price + '
}
res.writeHead(200,{"Content-Type": "text/html"});
fs.readFile(__dirname + '/../views/index.html',data) => {
if (err) {
console.log(err);
res.end();
} else {
// 动态渲染模板
res.end(data.toString().replace('{{foodMenu}}',foodMenu));
}
});
};
// 处理对详情页面的请求
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',data) => {
// 动态渲染模板
res.end(data.toString().replace('{{image}}',foodDetail.image)
.replace('{{name}}',foodDetail.name)
.replace('{{description}}',foodDetail.description)
.replace('{{price}}',foodDetail.price));
});
};
// 404响应,告知客户端资源未找到
getHandler['/404'] = function(req,res) {
res.writeHead(404,{"Content-Type": "text/plain"});
res.end("404 Not Found");
};
// post请求的处理方法示例
postHandler['/'] = function(res,data) {
// do something
};
// get请求
function get(req,res) {
var reqUrl = url.parse(req.url);
if (typeof getHandler[reqUrl.pathname] === "function") {
getHandlerreqUrl.pathname;
} else {
getHandler"/404";
}
}
// post请求(示例)
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);
postHandlerreqUrl.pathname;
});
} else {
getHandler"/404";
}
}
// 提供给其他模块使用的接口
module.exports = {
get: get,post: post
};