NodeJS基础API搭建服务器详细过程记录


Posted in NodeJs onApril 01, 2017

前言

在习惯了使用express框架,jade模板引擎等现成工具来写代码之后,很多人对于基本的NodeJS API会慢慢生疏。本文将以一个超小型web项目,来详细介绍如何使用NodeJS基础的http, fs, path, url等模块提供的API来搭建一个简单的web服务器。当做对NodeJS的一次复习,也为初学NodeJS的开发者提供一个参考。本文所搭建的项目将不会使用express等后端框架,仅使用最基础的NodeJS API,按照MVC设计模式的思路进行编码和讲解,交流意见。源代码地址如下,建议下载源码边看博客边对照源码才能比较快理解整个过程。https://github.com/hongchh/node-example

项目介绍

有一个简单的食品店网站,它包括一个主页index和一个详情页detail。主页展示食品店的所有食品,包括食品图片、名称、价格3个信息,如下图所示。

NodeJS基础API搭建服务器详细过程记录

用户点击任何一项食品就会跳转到对应的详情页,包括食品图片、名称、价格和描述4个信息,如下图所示。

NodeJS基础API搭建服务器详细过程记录

项目结构

项目的文件结构如下所示。

node-example
 |--data(存放项目数据的文件夹)
   |--detail.json(存放食品详情数据)
   |--foods.json(存放首页食品数据)
 |--model(提供访问和操作数据服务的数据模型)
   |--detail.js(详情数据访问模块)
   |--foods.js(食品数据访问模块)
 |--public(存放css,js,图片等静态文件)
   |--css(存放css文件的文件夹)
   |--img(存放图片的文件夹)
   |--js(存放js文件的文件夹)
 |--route(路由,控制器)
   |--api(处理普通请求的路由,或者叫控制器)
   |--static(处理静态文件请求的路由,或者叫控制器)
 |--views(视图,即用户界面)
   |--index.html(主页界面)
   |--detail.html(详情页面)
 |--server.js(服务器启动文件)
 |--package.json(项目包信息)
 |--README.md(项目信息以及启动方法描述)

本文只讲解服务端编程,因此两个简单界面的实现过程这里就不再??铝恕<偕枘阋丫?芄蛔孕型瓿汕岸说慕缑姹喑蹋?旅婵?冀步夥?穸吮喑獭?/p>

编写服务器

server.js中要完成服务器的创建和启动,并将请求转发给相应的路由去处理。详细代码如下所示(假设我们已经有了能够正常工作的路由,这里采用Top-Down的思路,我们一层一层地往下写,专注于解决每个层次的问题)。代码中使用正则表达式来判定客户端request是否是在请求静态文件,如果是,则交给专门处理静态文件请求的路由static去处理,否则交给普通请求的路由器api去处理。普通请求根据它的HTTP方法来判断使用get或者post。最后,设置服务器监听3000端口,server.js的代码就算完成了。

var http = require('http');
var url = require('url');
var api = require('./route/api');
var static = require('./route/static');

// 匹配静态文件夹路径的正则表达式,用于判定请求是否为静态文件请求
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”。

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");// 二进制文件需要加上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 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 = {};

// 处理对主页的请求
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 + '"><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 {
  // 动态渲染模板
  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', (err, 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") {
 getHandler[reqUrl.pathname](req, res);
 } else {
 getHandler["/404"](req, res);
 }
}

// 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);
  postHandler[reqUrl.pathname](res, postData);
 });
 } else {
 getHandler["/404"](req, res);
 }
}

// 提供给其他模块使用的接口
module.exports = {
 get: get,
 post: post
};

最后,讲一下post方法的处理过程,虽然本项目中没有使用到post。post方法跟get方法最主要的不同之处在于post方法除了发送http头部信息之外还带有客户端提交的数据。在接收到post请求的时候,需要将数据读取出来,读取数据的方式也挺简单,只要给request设置监听器就行了。当request对象收到数据的时候会触发“data”事件,因此,给这个事件设置监听器,让它收到数据的时候就把数据保存起来。在接收完一个请求全部的post数据之后会触发“end”事件,因此,给这个事件设置监听器,使得在接收完全部数据之后才开始对提交的数据进行相关的操作。

编写数据模型

先拿主页来讲吧。通过前面的截图,我们可以知道,主页上的数据包括展示菜品的图片、名称、价格,另外需要根据不同的菜品跳转到对应的详情页,因此还需要一个id来用作标识符。最后,可以得到如下的数据模型(下面的模型我使用json描述,你也可以采取其他办法)。这个数据模型描述了主页的数据模型,即首页有很多个食品foods,用数组表示,每个数据元素代表一个食品。每个食品包括四项信息,id,image,name,price。id的值是一个数字,作为唯一标识符。image是一个字符串,用来指明图片地址。name的值是字符串,表示食品的名字,price的值是一个字符串,表示食品的价格。

{
 "foods": [{
 "id": "number",
 "image": "string",
 "name": "string",
 "price": "string"
 }]
}

设计好数据模型的目的是方便我们设计伪数据,也方便我们对数据进行操作,一般在开始编程之前要做的事情就是设计好数据模型(数据结构),这样写程序时候才会更加顺利,很多接口才能规范下来。虽然我这里把model这一步放在了最后,但我这里model里面只是写了数据访问模块,不代表数据模型是最后才设计的,只是因为我这里讲解的思路是自定向下,刚好讲到model就顺带提一提数据模型设计。

下面以foods.js为例来讲解如何编写model。代码如下所示。这里由于没有数据库(涉及数据库的话对于新手来说比较麻烦,为了讲清楚过程本文将不采用数据库存储数据),我将所有数据使用json文件存储,例如foods.json中存储了主页的所有食品的数据。foods model将对外提供接口,用于支持访问主页的食品数据,修改食品数据等操作(数据库常说的增删查改CRUD四个操作)。本项目只需要用到查询所有视频的操作,所以我这里简单实现了一个获取所有食品的方法,另外附带一个根据id获取单个食品的方法(这个方法仅是示例,没有用到)。

var fs = require('fs');

module.exports = function() {
 // 读取文件中的数据,将其转成一个对象方便使用
 var data = JSON.parse(fs.readFileSync(__dirname + '/../data/foods.json'));
 var foods = {
 getAllFoods: getAllFoods,
 getFood: getFood
 };

 // 获取所有食品
 function getAllFoods() {
 return data.foods;
 }

 // 根据id获取单个食品
 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里面的模块一般提供数据操作的服务供控制器使用,所以在这一层就主要关注实现数据CRUD操作即可,基本没有什么业务逻辑了。

照着写foods的思路,我们再把detail写完,整个项目就完成了。是不是挺简单的。进到项目目录下面,使用node server.js启动服务器跑一跑吧。

NodeJS基础API搭建服务器详细过程记录

最后,看完整个项目,你大概可以发现整个编写过程,或者说每个模块的划分,都好像遵照某种特定的模式在进行,其实我是按照MVC的模式来编写这个项目的,最近在另外一门学课的学习中也经常用到MVC,觉得还是挺不错的一种设计模式,有兴趣可以研究一下。当然,我不能说我写的代码完全符合MVC的规范,毕竟每个人的理解都可能有那么一些出入。本文仅供参考,欢迎交流建议,谢谢!

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持三水点靠木!

NodeJs 相关文章推荐
golang、python、php、c++、c、java、Nodejs性能对比
Mar 12 NodeJs
nodejs的require模块(文件模块/核心模块)及路径介绍
Jan 14 NodeJs
nodejs 实现模拟form表单上传文件
Jul 14 NodeJs
Nodejs极简入门教程(二):定时器
Oct 25 NodeJs
nodejs调用cmd命令实现复制目录
May 04 NodeJs
基于nodejs+express(4.x+)实现文件上传功能
Nov 23 NodeJs
NodeJS爬虫实例之糗事百科
Dec 14 NodeJs
NodeJS父进程与子进程资源共享原理与实现方法
Mar 16 NodeJs
Nodejs实现多文件夹文件同步
Oct 17 NodeJs
nodejs基础之常用工具模块util用法分析
Dec 26 NodeJs
Nodejs异步流程框架async的方法
Jun 07 NodeJs
nodejs读取图片返回给浏览器显示
Jul 25 NodeJs
Ajax异步文件上传与NodeJS express服务端处理
Apr 01 #NodeJs
3分钟快速搭建nodejs本地服务器方法运行测试html/js
Apr 01 #NodeJs
nodejs使用express创建一个简单web应用
Mar 31 #NodeJs
nodejs实现邮件发送服务实例分享
Mar 29 #NodeJs
NodeJs测试框架Mocha的安装与使用
Mar 28 #NodeJs
NodeJS测试框架mocha入门教程
Mar 28 #NodeJs
nodejs模块nodemailer基本使用-邮件发送示例(支持附件)
Mar 28 #NodeJs
You might like
phpadmin如何导入导出大数据文件及php.ini参数修改
2013/02/18 PHP
is_uploaded_file函数引发的不能上传文件问题
2013/10/29 PHP
jquery ajax请求实例深入解析
2012/11/26 Javascript
js修改input的type属性问题探讨
2013/10/12 Javascript
动态加载jquery库的方法
2014/02/12 Javascript
Mvc提交表单的四种方法全程详解
2016/08/10 Javascript
关于jQuery.ajax()的jsonp碰上post详解
2017/07/02 jQuery
jQuery使用zTree插件实现可拖拽的树示例
2017/09/23 jQuery
微信小程序实现图片上传、删除和预览功能的方法
2017/12/18 Javascript
jQuery Validate插件ajax方式验证输入值的实例
2017/12/21 jQuery
js实现图片推拉门效果代码实例
2019/05/18 Javascript
小程序采集录音并上传到后台
2019/11/22 Javascript
Vue.js中使用Vuex实现组件数据共享案例
2020/07/31 Javascript
全局安装 Vue cli3 和 继续使用 Vue-cli2.x操作
2020/09/08 Javascript
[09:31]2016国际邀请赛中国区预选赛Yao赛后采访 答题送礼
2016/06/27 DOTA
Python实现检测服务器是否可以ping通的2种方法
2015/01/01 Python
简单介绍利用TK在Python下进行GUI编程的教程
2015/04/13 Python
python实现求两个字符串的最长公共子串方法
2018/07/20 Python
Python面向对象程序设计示例小结
2019/01/30 Python
Python里字典的基本用法(包括嵌套字典)
2019/02/27 Python
使用Python在Windows下获取USB PID&amp;VID的方法
2019/07/02 Python
Python 实现自动导入缺失的库
2019/10/29 Python
Python如何使用队列方式实现多线程爬虫
2020/05/12 Python
Python使用requests模块爬取百度翻译
2020/08/25 Python
python爬虫scrapy框架的梨视频案例解析
2021/02/20 Python
开发人员所需要知道的HTML5性能分析面面观
2012/07/05 HTML / CSS
HTML5实现自带进度条和滑块滑杆效果
2018/04/17 HTML / CSS
如何处理简单的PHP错误
2015/10/14 面试题
最新会计专业求职信范文
2014/01/28 职场文书
管理失职检讨书
2014/02/12 职场文书
高中教师评语大全
2014/04/25 职场文书
营销计划书范文
2015/01/17 职场文书
小学数学继续教育研修日志
2015/11/13 职场文书
2016圣诞节贺卡寄语
2015/12/07 职场文书
2016年学校禁毒宣传活动工作总结
2016/04/05 职场文书
python3实现无权最短路径的方法
2021/05/12 Python