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 基本概念
Express在Connect中间件引擎的基础上(4.x版本的Express不再依赖于Connect),处理一般性的Web开发常见任务,包括:将请求映射到业务代码(通过定义路由表)、结果页面渲染等。
(1) 中间件的进一步说明
Express 2.x/3.x/4.x中关于内建中间件的变化较大,除serve-static等中间件之外,4.x版的已经将很多常见的中间件移出了内建中间件,作为第三方中间件。有关Express中间件的详细内容参见Using middleware。
(2) 路由
Express 4.x可以使用express
或express.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
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 or
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中的示例代码。