这里有新鲜出炉的 7 天学会 Node.js,程序狗速度看过来!
Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台, 用来方便地搭建快速的 易于扩展的网络应用 · Node.js 借助事件驱动, 非阻塞 I/O 模型变得轻量和高效, 非常适合 运行在分布式设备 的 数据密集型 的实时应用
本文将以一个超小型 web 项目,来详细介绍如何使用 NodeJS 基础的 http, fs, path, url 等模块提供的 API 来搭建一个简单的 web 服务器。具有很好的参考价值。下面跟着小编一起来看下吧
前言
在习惯了使用 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 个信息,如下图所示。
用户点击任何一项食品就会跳转到对应的详情页,包括食品图片、名称、价格和描述 4 个信息,如下图所示。
项目结构
项目的文件结构如下所示。
- 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(项目信息以及启动方法描述)
本文只讲解服务端编程,因此两个简单界面的实现过程这里就不再啰嗦了。假设你已经能够自行完成前端的界面编程,下面开始讲解服务端编程。
编写服务器
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 启动服务器跑一跑吧。
最后,看完整个项目,你大概可以发现整个编写过程,或者说每个模块的划分,都好像遵照某种特定的模式在进行,其实我是按照 MVC 的模式来编写这个项目的,最近在另外一门学课的学习中也经常用到 MVC,觉得还是挺不错的一种设计模式,有兴趣可以研究一下。当然,我不能说我写的代码完全符合 MVC 的规范,毕竟每个人的理解都可能有那么一些出入。本文仅供参考,欢迎交流建议,谢谢!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持 phperz!
来源: http://www.phperz.com/article/17/0619/332584.html