Node.js高级编程读书笔记 - 4 构建Web应用程序

Outline

5 构建Web应用程序

  • 5.1 构建和使用HTTP中间件
  • 5.2 用Express.js创建Web应用程序
  • 5.3 使用Socket.IO创建通用的实时Web应用程序

5 构建Web应用程序

5.1 构建和使用HTTP中间件

5.1.1 Web开发的常见任务:

(1) HTTP服务器负责的任务
解析请求URL、维护会话关联、持久化会话数据、解析Cookie等。

(2) 业务程序可以参与的任务
检查和修改请求和响应,一些Web框架正是包装了请求和响应的传递链以方面业务程序的编码工作。

5.1.2 Connect HTTP中间件框架

文中将请求-响应循环进行包装的组件成为中间件,不过从后文的表述看是类似于职责链(chain of responsibility)的模式应用,正如Java Servlet中的Filter,Struts中的Interceptor,Netty中的ChannelPipeline等。该模式有一个地方需要注意:Be act well in the round, 即在这条链中各组件需要表现出良好的行为。

Connect框架对中间件组件定义个一个模型,同时提供了一个引擎来运行中间件组件。

安装:

npm install connect

5.1.3 构建自定义的HTTP中间件

app.js:入口

/**
the app using `connect` startup,
REF https://github.com/senchalabs/connect
*/
var connect = require("connect");
var port = 8888;

//console.log("connect="+connect);
var app = connect();

// setup middlewares
// var helloWorld = require("./helloworld");
var replyText = require("./reply_text");
var headerHandler = require("./header_handler");
var log = require("./request_logger");
var simulateError = require("./simulate_error");
var errorHandler = require("./error_handler");

app.use(simulateError());
app.use(log(__dirname+"/logs"));//current woring directory
app.use(headerHandler("Powered-by", "Node.js"));
//app.use("/", helloWorld);
app.use("/", replyText("Hello World!"));
app.use(errorHandler());// put the error handler as last in the middleware chain

// start http server
var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

helloworld.js

/**
a middleware component used for `connect`: output hello world
*/
function helloWorld(request, response){
  response.end("Hello World");
}

module.exports = helloWorld;

reply_text.js

/**
a middleware a middleware component used for `connect`: reply text
*/
function replyText(text){
  return function(request, response){
    response.end(text);
  }
}

module.exports = replyText;

header_handler.js

/**
a middleware component used for `connect`: handle respone header
*/
function headerHandler(name, value){
  return function(request, response, next){
    response.setHeader(name, value);

    next();//past to next middleware
  }
}

module.exports = headerHandler;

request_logger.js

/**
a middleware component used for `connect`: log requests
*/

var fs = require("fs");
var path = require("path");
var util = require("util");

function log(directory){
  // make sure directory exist
  var isExists =fs.existsSync(directory);
  if(!isExists){
    fs.mkdirSync(directory);
  }

  return function(request, response, next){
    var nowDate = new Date();
    var nowDateString = nowDate.getFullYear() + "-"+(nowDate.getMonth() + 1) + "-" + nowDate.getDate();
    var fileName = path.join(directory, nowDateString + ".txt");
    var fileWriteStream = fs.createWriteStream(fileName, {flags: "a+"});
    fileWriteStream.write(nowDate.toLocaleTimeString()+ ": " + request.method + " " + request.url+ "\n"+
      util.inspect(request.headers)+ "\n");
    //request.pipe(fileWriteStream);
    fileWriteStream.close();

    next(); // pass to next middleware
  }
}

module.exports = log;

simulate_error.js

/**
a middleware component used for `connect`: simulate generate error in the middleware chain
*/

function simulateError(){
  return function(request, response, next) {
    next(new Error("This is an error"));
  }
}

module.exports = simulateError;

error_handler.js

/**
a middleware component used for `connect`: handler all the errors in the middleware chain
*/

function errorHandler(){
  return function(error, request, response, next){
    if(error){
      response.writeHead(500, {"Content-Type": "text/html"});
      response.end("Server encounters an error: <p><pre>"+ error.stack+"</pre></p>");
    } else{
      next();
    }
  }
}

module.exports = errorHandler;

5.1.4 使用Connect的内置中间件

morgan(logger)

/**
builtin middleware: logger, now named morgan,
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/morgan
$ npm install morgan
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains

var morgan = require('morgan');
app.use(morgan("tiny"));// create middleware morgan

app.use(function(request, response){
  response.end("Hello World!!!");
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

serve-static

/**
builtin middleware: serve-static
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/serve-static
$ npm install serve-static
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains

var serveStatic = require('serve-static')
app.use(serveStatic(__dirname+"/static"));// create middleware morgan

app.use(function(request, response){
  response.end("Hello World!!!");
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

errorhandler

/**
builtin middleware: errorhandler
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/errorhandler
$ npm install errorhandler
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains

// simualate an error
app.use(function(request, response, next){
  next(new Error("an error."));
});

var errorhandler = require('errorhandler');
app.use(errorhandler());// create middleware errorhandler

app.use(function(request, response){
  response.end("Hello World!!!");
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

qs: currently not as a middleware

/**
builtin middleware: logger, now named morgan,
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/qs
$ npm install qs
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains
app.use(function(request, response){
  var Qs = require('qs');// currently not as a middleware
  console.log(Qs.parse(request.url));
  response.end(JSON.stringify(Qs.parse(request.url)));
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

body-parser

/**
builtin middleware: logger, now named morgan,
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/body-parser
$ npm install body-parser

WARN:
body-parser deprecated bodyParser: use individual json/urlencoded middlewares parse_body.js:18:9
body-parser deprecated undefined extended: provide extended option ../node_modules/body-parser/index.js:105:29
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains

var bodyParser = require('body-parser');
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse "Content-Type": application/json
app.use(bodyParser.json());

app.use(function(request, response){
  response.setHeader('Content-Type', 'text/plain');
  response.end(JSON.stringify(request.body));//access the body
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

cookie-parser

/**
builtin middleware: logger, now named morgan,
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/cookie-parser
$ npm install cookie-parser

test with:
$ curl http://localhost:8888 --cookie "a=1;b=2",
Postman blocks header Cookie: https://www.getpostman.com/docs/requests
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains
var cookieParser = require('cookie-parser');
app.use(cookieParser());

app.use(function(request, response){
  response.setHeader('Content-Type', 'text/plain');
  //console.log(require('util').inspect(request.cookies, { depth: null }));
  response.end(JSON.stringify(request.cookies));//access the cookies
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

express-session

/**
builtin middleware: logger, now named morgan,
REF:
  https://github.com/senchalabs/connect#readme,
  https://www.npmjs.com/package/express-session
$ npm install express-session
*/

var connect = require("connect");

var app = connect();
var port = 8888;

// set up middleware chains
var cookieParser = require('cookie-parser');
app.use(cookieParser());
var session = require('express-session');
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true,
  cookie: {maxAge: 24 * 60 *60 * 3600}//expiration settings
}));

app.use(function(request, response){
  var Qs = require('qs');// currently not as a middleware

  var queries = Qs.parse(request.url);
  for(var name in queries){
    request.session[name] = queries[name];//access the session values
  }

  response.end(require("util").format(request.session)+"\n");
});

var server = require("http").createServer(app);
server.listen(port, function(){
  console.log("listen on: "+port);
});

5.2 用Express.js创建Web应用程序

5.2.1 基本概念

ExpressConnect中间件引擎的基础上(4.x版本的Express不再依赖于Connect),处理一般性的Web开发常见任务,包括:将请求映射到业务代码(通过定义路由表)、结果页面渲染等。

(1) 中间件的进一步说明

Express 2.x/3.x/4.x中关于内建中间件的变化较大,除serve-static等中间件之外,4.x版的已经将很多常见的中间件移出了内建中间件,作为第三方中间件。有关Express中间件的详细内容参见Using middleware

(2) 路由

Express 4.x可以使用expressexpress.Router对象用于定义路由映射。前者的常用的调用方式是express.METHOD(path, [callback...], callback),这里Method是HTTP动词;另外还有一种链式的调用方法:app.route('/book').get(function(req, res) {...).post(...).put(...),其含义是对同一URL可以响应不同的HTTP动词。后者用于定义模块化的路由映射 ,Router`实例是一个完整的中间件和路由系统,故被成为mini-app。有关路由的详细内容参见Routing

(3) 模板引擎

这里使用Express默认的模板引擎jade,有Java Web开发背景的可以从Velocity、FreeMaker等模板语言中获得一定的启示,一种用于视图渲染的DSL语言而已,详细内容参见Using template engines with Express

(4) 错误处理

这里使用express generator生成的默认错误处理机制,有关错误处理的详细内容参见Error handling

5.2.2 环境搭建

安装:

npm install -g express

搭建初始化应用程序:

# DO NOT USE THIS!!!
$ sudo apt-get install node-express
# USE THIS
$ sudo npm install -g express-generator
/usr/bin/express -> /usr/lib/node_modules/express-generator/bin/express
$ express --version

$ express my_app

install dependencies:
 $ cd my_app && npm install

run the app:
 $ DEBUG=my_app:* npm start

application directory layout:

.
└── my_app
    ├── app.js //应用设置
    ├── bin
        │   └── www //启动脚本
        ├── package.json //scripts元素定义了启动脚本
    ├── public
    │   ├── images
    │   ├── javascripts
    │   └── stylesheets
    │       └── style.css
    ├── routes
    │   ├── index.js
    │   └── users.js
    └── views
            ├── error.jade
            ├── index.jade
            └── layout.jade

8 directories, 9 files

5.2.3 应用程序

这里实现的功能与书中基本一致,仅存在Express版本上的区别,另外因一般都是自说明的代码,结合注释和文档应该可以看明白。

(1) package.json

增加项目

"express-session": "1.11.3",
"connect-flash": "*",
"method-override": "*"

express-session模块用于支持会话,connect-flash用于支持redirect时传递参数,method-override模块用于方法重写以支持DELETE等HTTP方法。

(2) app.js

    var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var expressSession = require("express-session");
var methodOverride = require('method-override');//for delete requests

var route_index = require('./routes/index');
var route_users = require('./routes/users');
var route_session = require('./routes/session');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
var secretString = "a secret string";// cookie and session
app.use(cookieParser(secretString));
app.use(expressSession({secret: secretString, maxAge: 3600000}));
app.use(express.static(path.join(__dirname, 'public')));
app.use(methodOverride("_method"));

// wrap session in req and res
//REF: http://thenitai.com/2013/11/25/how-to-access-sessions-in-jade-template/
app.use(function(req,res,next){
  res.locals.session = req.session;
  next();
});

// redirect with parameters
// REF: http://*.com/questions/12442716/res-redirectback-with-parameters
var flash = require('connect-flash');
app.use(flash());

app.use('/', route_index);
app.use('/users', route_users);
app.use('/session', route_session);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found!');
  err.status = 404;
  next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});

module.exports = app;

(3) routers

.
├── index.js
├── middleware
│   ├── load_user.js
│   ├── not_logged_in.js
│   └── restrict_user_to_self.js
├── session.js
└── users.js

index.js

not changed

users.js

/**
routers for user module,
prefix: /users
*/

var express = require('express');
var router = express.Router();

// middlewares
var loadUser = require(("./middleware/load_user"));
var restrictOperationToSelf = require("./middleware/restrict_user_to_self");
var notLoggedIn = require(("./middleware/not_logged_in"));

// mock data
var users = require("../data/users");// users.json

// /* GET users listing. */
// router.get('/', function(req, res, next) {
// });
//   res.send('respond with a resource');

//1 GET
router.get("/", function(request, response){
  response.render("users/index", {title: "Users", users: users});
});

// 4 GET /new - notLoggedIn
// WARN: should place before GET /:name
router.get("/new", notLoggedIn, function(request, response){
  response.render("users/new", {title: "New User"});
});

//2  GET /:name - loadUser
router.get("/:name", loadUser, function(request, response, next){
  // var user = users[request.params.name];//parse request parameters
  // if(user){
  //   response.render("users/profile", {title: "User profile", user: user});
  // } else{
  //   next();//pass control to middleware engine
  // }
  response.render("users/profile", {title: "User profile", user: request.user});
});

//3 DELETE /:name - loadUser and restrictOperationToSelf
router.delete("/:name", loadUser, restrictOperationToSelf, function(request, response, next){
  console.log(request.method);//~
  // var username = request.params.name;
  // if(users[username]){
  //   delete users[username];
  //   response.redirect("");//redirect
  // } else{
  //   next();
  // }
  delete users[request.user.username];

  // clean session
  request.session.destroy(); // destroy session
  response.redirect("/users");//redirect
});

//5 POST  - notLoggedIn
router.post("/", notLoggedIn, function(request, response){
  var username = request.body.username;// parse request body
  if(users[username]){// already exist
    response.send("Conflict", 409);
  } else{
    users[username] = request.body;
    response.redirect("/");
  }
});

module.exports = router;

session.js

/**
routers for user module,
prefix: /session
*/
var express = require('express');
var router = express.Router();

// middlewares
var loadUser = require(("./middleware/load_user"));
var restrictOperationToSelf = require("./middleware/restrict_user_to_self");
var notLoggedIn = require(("./middleware/not_logged_in"));

// mock data
var users = require("../data/users");

// set local variables: session for all responses
// router.dynamicHelpers({
//   session: function(request, response){
//     return request.session;
//   }
// });

//1 GET /new  - notLoggedIn
router.get("/new", notLoggedIn, function(request, response){
  var flashValue = request.flash('info');
  if(flashValue[0]){
    flashValue = flashValue[0];
  }
  response.render("session/new", {title: "Login", messages: flashValue});
});

//2 POST  - notLoggedIn
router.post("/", notLoggedIn, function(request, response){
  var username = request.body.username;

  if(users[username] && users[username].password === request.body.password){
    request.session.user = users[username];// put 'user' in session
    response.redirect("/users");
  } else{
    request.flash('info', 'Nonexisted User!');   // use connect-flash
    response.redirect("/session/new");
  }
});

//3 DELETE
router.delete("/", function(request, response, next){
  request.session.destroy(); // destroy session
  response.redirect("/users");
});

module.exports = router;

not_logged_in.js

/**
middleware: check sessions
*/
 function notLoggedIn(request, response, next){

  if(inWhiteList(request)){// whiltlists
    next();
  } else{
    if(request.session.user){// should not in a session
      response.send("Unauthorized", 401);
    } else{
      next();
    }
  }
}

/**
while lists
*/
function inWhiteList(request){
  // var url = reuest.url.toString();
  // if(url.indexOf("users") === -1 && url.indexOf("session") === -1){
  //   return true;
  // }
  return false;
}

module.exports = notLoggedIn;

load_user.js

/**
middleware:  load all users
*/
// mock data
var users = require("../../data/users");

function loadUser(request, response, next){
  var user = users[request.params.name];
  request.user = user;

  if(!user){
    response.send("Not Found!", 404);
  } else{
    next();
  }
}

module.exports = loadUser;

restrict_user_to_self.js

/**
middleware:  restrict delete operations to himself's data
*/

function restrictOperationToSelf(request, response, next){
  if(!request.session.user || request.session.user.username !== request.user.username){
    response.send("Unauthorized", 401);
  } else{
    next();
  }
}

module.exports = restrictOperationToSelf;

(4) views

用到了模板继承,见Template inheritance,其他的属于常规用法。

.
├── error.jade
├── index.jade
├── layout.jade
├── session
│   ├── new.jade
│   └── user.jade
└── users
    ├── index.jade
    ├── new.jade
    └── profile.jade

layout.jade

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    include ./session/user.jade
    block content

users/index.jade

extends ../layout

block content
  h1 Users

  p
    a(href="/users/new") Create new profile

  ul
    - for(var username in users) {
      li
    a(href="/users/"+encodeURIComponent(username))=users[username].name
    -};

users/new.jade

extends ../layout

block content
  h1=title

  form(method="POST", action="/users")
    p
      label(for="username") Username<br/>
      input#username(name="username")

    p
      label(for="name") Name<br/>
      input#name(name="name")

    p
      label(for="password") Password<br/>
      input#password(type="password", name="password")

    p
      label(for="bio") Bio<br/>
      textarea#bio(name="bio")

    p
      input(type="submit", value="Create")

users/profile.jade

extends ../layout

block content
  h1=user.name

  h2 Bio
  p=user.bio

  //- REF https://github.com/expressjs/method-override?_ga=1.68221208.881268083.1437241733
  form(action="/users/"+encodeURIComponent(user.username)+"?_method=DELETE", method="POST")
    input(type="submit", value="DELETE")

session/new.jade

h1=title

p
  a(href="/") Home

h2=messages

form(method="POST", action="/session")
  p
    label(for="username") User name: <br/>
    input#username(name="username")
  p
    label(for="password") Password: <br/>
    input#password(type="password", name="password")
  p
    input(type="submit", value="Log in")

session/user.jade

if(session.user)

  p
    span Hello&nbsp;
    span=session.user.username
    span !

  p
    form(method="POST", action="/session?_method=DELETE")
      input(type="submit", value="Log out")

else

  p
    a(href="/session/new") Login
    span &nbsp;or&nbsp;
    a(href="/users/new") Register

(4) data

测试用数据测试用数据data/users.json

{
"frank": {
"username": "frank",
"name": "Frank Sinatra",
"bio": "Singer",
"password": "frank"
},
"jobim": {
"username": "jobim",
"name": "Antonio Carlos Jobim",
"bio": "Composer",
"password": "jobim"
},
"fred": {
"username": "fred",
"name": "Fred Astaire",
"bio": "Dancer and Actor",
"password": "fred"
}
}

5.3 使用Socket.IO创建通用的实时Web应用程序

Node.js中创建WebSocket事实上的标准是Socket.IO,这里不会对WebSocket协议做过多介绍,可以将其视为HTTP的升级版本,提供了服务端和客户端浏览之间的全双工通信机制。

安装

npm install socket.io

5.3.1 Socket.IO HelloWorld

simple_server.js

/**
Socket.IO simple Server
*/
var fs = require("fs");
var httpServer = require("http").createServer(function(request, response){
  fs.readFile(__dirname+"/index.html", function(error, data){
    if(error){
      response.writeHead(500);
      response.end("Error serving index.html.");
    } else{
      response.writeHead(200);
      response.end(data);
    }
  });
});
var io = require("socket.io")(httpServer);
var port = 4001;
httpServer.listen(port, function(){
  console.log("listen on: " + port);
});

io.on("connection", function(socket){
  socket.emit('news', { hello: 'world' });
  socket.on("my-event", function(data){
    console.log(data);
  })
});

index.html - client

<!doctype html>
<html>
  <head>
    <script src="http://localhost:4001/socket.io/socket.io.js"></script>
    <script>
      var socket = io.connect("http://localhost:4001");
      socket.on('news', function (data) {
    console.log(data);
    socket.emit('my-event', { my: 'data' });
      });
    </script>
  </head>
  <body></body>
</html>

5.3.2 Chat application

Get started中的示例代码。

上一篇:输入DStream之基础数据源以及基于HDFS的实时wordcount程序


下一篇:关于权限的数据库设计