Node-相册小项目(下)

将相册项目(中)继续升级,最终使用 Express 版本。

封装 render 渲染函数

  • 获取模板字符串中需要的数据
  • 获取模板字符串
    • fs.readFile('文件名');
  • 将模板字符串中用到的数据和模板字符串通过模板引擎整合到一起
    • _.template(模板字符串)({数据对象});
  • 发送响应
    • res.end(_.template(模板字符串)({数据对象}));

render(‘文件路径’, ‘数据对象’);
render(‘index’, ‘数据对象’);

综上所述,封装的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = res => {
res.render = (viewName, obj = {}) => {
fs.readFile(`${path.join(config.viewPath, viewName)}.html`, 'utf8', (err, data) => {
if (err) {
throw err;
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})
res.end(_.template(data)(obj));
});
};
};

handler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const fs = require('fs');
const path = require('path');
const _ = require('underscore');
const mime = require('mime');
const config = require('./config');
const qstring = require('querystring');
const formidable = require('formidable');
exports.showIndex = (req, res) => {
fs.readdir(config.uploadDir, (err, files) => {
if (err) {
throw err;
}
// 调用 render() 函数
res.render('index', {
albumNames: files
});
});
};
exports.showPublic = (req, res) => {
const url = decodeURI(req.url);
fs.readFile(`.${url}`, (err, data) => {
if (err) {
res.writeHead(404, 'Not Found');
res.end();
}
res.writeHead(200, {
'Content-Type': mime.lookup(req.url)
});
res.end(data);
});
};
exports.doAddAlbum = (req, res) => {
const albumName = req.query.albumName ? req.query.albumName.trim() : '';
if (/(\\|\/|\:|\*|\?|"|\<|\>|\|)|^$/.test(albumName)) {
return res.end('albumName param invalid error.');
}
fs.access(path.join(config.uploadDir, albumName), err => {
if (!err) {
return res.end('albumName already exists');
}
fs.mkdir(path.join(config.uploadDir, albumName), err => {
if (err) {
throw err;
}
res.redirect('/');
});
});
};
exports.showAlbum = (req, res) => {
const albumName = req.query.albumName ? req.query.albumName.trim() : '';
fs.access(path.join(config.uploadDir, albumName), err => {
if (err) {
return res.end('album not exists.');
}
fs.readdir(path.join(config.uploadDir, albumName), (err, files) => {
if (err) {
throw err;
}
res.render('album', {
albumName: albumName,
albumPaths: files.map(fileName => `/uploads/${albumName}/${fileName}`)
});
});
});
};
// 测试登录页
exports.showLogin = (req, res) => {
res.render('login', {
Name: 'Jack'
});
};
exports.doUpload = (req, res) => {
const albumName = req.query.albumName ? req.query.albumName : '';
fs.access(path.join(config.uploadDir, albumName), err => {
if (err) {
return res.end('album not exists');
}
const form = new formidable.IncomingForm();
form.uploadDir = path.join(config.uploadDir, albumName);
form.keepExtensions = true;
form.maxFieldsSize = 10 * 1024 * 1024;
form.parse(req, (err, fields, files) => {
if (err) {
return res.end('The default size is 10MB.');
}
res.redirect(`/album?albumName=${albumName}`);
});
});
};
exports.showRegister = (req, res) => {
fs.readFile(path.join(config.viewPath, 'register.html'), (err, data) => {
if (err) {
throw err;
}
res.end(data);
});
};
exports.doRegister = (req, res) => {
let buffers = [];
req.on('data', data => {
buffers.push(data);
});
req.on('end', () => {
const file = Buffer.concat(buffers);
fs.writeFile('./a', file, err => {
if (err) {
throw err;
}
console.log('writed success');
});
console.log(file.length);
// console.log(qstring.parse(body))
});
};

router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const url = require('url');
const handler = require('./handler');
const render = require('./common/render');
// http://www.baidu.com:80/a/b/c?key=value&key=value#xxx
// 协议://主机名:端口号:请求路径?查询字符串#内部定位
// 单页应用中就是利用 hash 来作为网站的请求标识路由的
module.exports = function (req, res) {
const urlObj = url.parse(req.url, true);
const pathname = urlObj.pathname;
const queryObj = urlObj.query;
const method = req.method.toLowerCase();
req.query = queryObj || {};
res.redirect = url => {
res.writeHead(302, {
'Location': encodeURI(url);
});
res.end();
};
// 只要调用了该方法,res 对象就拥有了一个成员叫做 render
// 以后只要是渲染模板,那就直接调用 res.render('视图名称', 数据对象)
render(res);
if (pathname === '/') {
handler.showIndex(req, res);
} else if (method === 'get' && (pathname.startsWith('/public/') || pathname.startsWith('/uploads/') || pathname.startsWith('/node_modules/'))) {
handler.showPublic(req, res);
} else if (method === 'get' && pathname === '/album/add') {
handler.doAddAlbum(req, res);
} else if (method === 'get' && pathname === '/album') {
handler.showAlbum(req, res);
} else if (method === 'post' && pathname === '/upload') {
handler.doUpload(req, res);
} else if (method === 'get' && pathname === '/register') {
handler.showRegister(req, res);
} else if (method === 'post' && pathname === '/register') {
handler.doRegister(req, res);
} else if (method === 'get' && pathname === '/login') {
handler.showLogin(req, res);
}
}

Express

Express 是一个基于 Node.js 开发的快速、开放、极简的 Web 开发框架,可以用来快速构建网站后台,使用 Express 可以让你更加专注于业务的处理。

  • Koa
  • Sails
  • ThinkJS

特性

  • Express框架建立在node.js内置的http模块上
    • 不对 Node.js 已有的特性进行二次抽象
    • 在 HTTP 模块之上扩展了 Web 应用所需的基本功能,例如:req.queryres.sendres.jsonres.render 等API
    • 原来的 HTTP 模块的 req.urlreq.methodres.writeres.end 等 API 依然存在
  • 轻量、API简单友好
  • 简单语义化的路由系统
  • 强大的中间件处理系统

Geting Started

  1. 安装
  2. hello world
  3. 路由
  4. 处理静态资源

安装:

1
$ npm install --save express

基本路由及基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
// 1. 调用 express 得到一个 app 实例对象
// 这里就好比是 http.createServer
// 得到的 app 就是 server
const app = express();
// 2. 添加路由,设置处理函数
// 当用户以 GET 请求 / 路径的时候,执行相应的回调处理函数
app.get('/', (req, res) => {
res.send('Index Page');;
});
// 当用户以 GET 请求 /login 路径的时候,执行相应的回调处理函数
app.get('/login', (req, res) => {
res.send('Login Page');
});
// 处理以 POST 请求 /upload
app.post('/upload', (req, res) {
res.send('Upload');
});
// 3. 绑定端口,启动服务器
app.listen(3000, () => {
console.log('server is listenning at port 3000.');
});

处理静态资源:

1
2
3
4
5
6
// 将 node_modules、public、uploads 目录开放给用户,可以直接通过绝对路径的形式访问该目录中的资源
// 第一个参数表示以什么标识开头,第二个参数调用 express.static('路径') 要暴力的目录的路径
// 注意第一个参数请求标识,所有请求标识都是以 / 开头的
app.use('/node_modules', express.static('./node_modules/'));
app.use('/public', express.static('./public/'));
app.use('/uploads', express.static('./uploads/'));

express 版的 相册

views 目录下

views 目录下 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我的相册 - </title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="public/css/main.css">
</head>
<body>
<div class="container-fluid">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">我的相册</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/">首页 <span class="sr-only">(current)</span></a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a href="" data-toggle="modal" data-target="#exampleModal" data-whatever="@mdo">新建相册</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<div class="container">
<div class="row">
<% albumNames.forEach(function(albumName){ %>
<div class="col-xs-6 col-md-3">
<a href="/album?albumName=<%= albumName %>" class="thumbnail">
<img src="public/img/icon.png" alt="">
</a>
<div class="caption">
<h3><%= albumName %></h3>
</div>
</div>
<% }) %>
</div>
</div>
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="/album/add" method="get">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="exampleModalLabel">新建相册</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="recipient-name" class="control-label">相册名称:</label>
<input type="text" class="form-control" name="albumName" placeholder="请输入相册名称">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button type="submit" class="btn btn-success">点击添加</button>
</div>
</form>
</div>
</div>
</div>
<script src="node_modules/jquery/dist/jquery.js"></script>
<script src="node_modules/bootstrap/dist/js/bootstrap.js"></script>
</body>
</html>

views 目录下 album.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我的相册 -</title>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="public/css/main.css">
</head>
<body>
<div class="container-fluid">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">我的相册</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/">首页 <span class="sr-only">(current)</span></a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="" data-toggle="modal" data-target="#exampleModal" data-whatever="@mdo">照片上传</a></li>
</ul>
</div>
</div>
</nav>
</div>
<div class="container">
<div class="row">
<% albumPaths.forEach(function (imgSrc) { %>
<div class="col-xs-6 col-md-3">
<div class="thumbnail">
<img src="<%= imgSrc %>" alt="">
</div>
</div>
<% }) %>
</div>
</div>
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="/upload?albumName=<%= albumName %>" method="post" enctype="multipart/form-data">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="exampleModalLabel">照片上传</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="recipient-name" class="control-label">请选择文件:</label>
<input type="file" class="form-control" name="pic">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button type="submit" class="btn btn-success">点击上传</button>
</div>
</form>
</div>
</div>
</div>
<script src="node_modules/jquery/dist/jquery.js"></script>
<script src="node_modules/bootstrap/dist/js/bootstrap.js"></script>
</body>
</html>

根目录

config.js

1
2
3
4
5
const path = require('path');
module.exports = {
uploadDir: path.join(__dirname, 'uploads'),
viewPath : path.join(__dirname, 'views');
};

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require('express');
const path = require('path');
const router = require('./router');
const config = require('./config');
const fs = require('fs');
// 1. 创建 app 实例对象
const app = express();
// 2. 处理公共请求
app
.use('/node_modules',express.static(path.join(__dirname, 'node_modules')))
.use('/public',express.static(path.join(__dirname, 'public')))
.use('/uploads',express.static(path.join(__dirname, 'uploads')));
// 使用 ejs 模板引擎,配置 html 后缀名
// 配置模板文件存放的路径,如果不设置,默认就是去当前目录下找 view 目录
app.set('views', config.viewPath);
app.engine('.html', require('ejs').__express);
app.set('view engine', 'html');
// 3. 加载路由
app.use(router);
// Express 全局处理错误中间件,把该中间件放到最后
// 在之前的任何中间件中,如果有错误发生,就调用 next ,将错误对象传递给 next
// 只要 next 有错误对象参数,该中间件就会被匹配到并执行
app.use((err, req, res, next) => {
fs.appendFile('./log.txt', err.message, err => {
if (err) {
console.log('写入日志失败');
}
// 有错误发生的时候,记录错误、通知网站管理员(发邮件、发短信)
console.log('记录错误日志成功');
res.render('500');
});
});
// 4. 启动监听
app.listen(3000, () => {
console.log('Server is running at port 3000.');
});

router.js

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express');
const handler = require('./handler');
// 1. 创建一个路由实例
const router = express.Router();
// 2. 给路由实例对象挂载路由
router
.get('/', handler.showIndex)
.get('/album/add', handler.doAddAlbum)
.get('/album', handler.showAlbum)
.post('/upload', handler.doUpload)
// 3. 暴露路由实例对象
module.exports = router;

handler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const fs = require('fs');
const path = require('path');
const config = require('./config');
const formidable = require('formidable');
exports.showIndex = (req, res, next) => {
// 1. 将所有的相册名称读取出来
// 2. 把相册名称数据和文件中的模板字符串编译替换
// 3. 发送给客户端浏览器
fs.readdir(config.uploadDir, (err, files) => {
if (err) {
//
return next(err);
}
res.render('index', {
albumNames: files
});
});
};
exports.doAddAlbum = (req, res, next) => {
const albumName = req.query.albumName ? req.query.albumName.trim() : '';
if (/(\\|\/|\:|\*|\?|"|\<|\>|\|)|^$/.test(albumName)) {
return res.end('albumName param invalid error.');
}
fs.access(path.join(config.uploadDir, albumName), err => {
if (!err) {
return res.end('albumName already exists');
}
fs.mkdir(path.join(config.uploadDir, albumName), err => {
if (err) {
return next(err);
}
res.redirect('/');
});
});
};
exports.showAlbum = (req, res, next) => {
const albumName = req.query.albumName ? req.query.albumName.trim() : '';
fs.access(path.join(config.uploadDir, albumName), err => {
if (err) {
return res.end('album not exists.');
}
fs.readdir(path.join(config.uploadDir, albumName), (err, files) => {
if (err) {
return next(err);
}
res.render('album', {
albumName: albumName,
albumPaths: files.map(fileName => `/uploads/${albumName}/${fileName}`)
});
});
});
};
exports.doUpload = (req, res, next) => {
const albumName = req.query.albumName ? req.query.albumName : '';
fs.access(path.join(config.uploadDir, albumName), err => {
if (err) {
return res.end('album not exists');
}
const form = new formidable.IncomingForm();
form.uploadDir = path.join(config.uploadDir, albumName);
form.keepExtensions = true;
form.maxFieldsSize = 10 * 1024 * 1024;
form.parse(req, (err, fields, files) => {
if (err) {
return res.end('The default size is 10MB.');
}
res.redirect(`/album?albumName=${albumName}`);
});
});
};

路由系统模块

Express 中提供了一种路由模块化的方式,具体使用形式就是可以将路由单独的放到一个模块中,然后通过 app.use 的形式加载路由系统。

中间件

如果把一个 http 处理过程比作是污水处理,中间件就像是一层层的过滤网(过滤器)。每个中间件在 http 处理过程中通过改写 request 或(和)response 的数据、状态,实现了特定的功能。

简单说,中间件(middleware)就是处理HTTP请求的函数。它最大的特点就是,一个中间件处理完,再传递给下一个中间件。App 实例在运行过程中,会调用一系列的中间件。

中间件的功能包括:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 终结请求-响应循环。
  • 调用堆栈中的下一个中间件。

中间件函数

每个中间件可以从App实例,接收三个参数,依次为:

  • request对象(代表HTTP请求)
  • response对象(代表HTTP回应)
  • next回调函数(代表下一个中间件)

每个中间件都可以对HTTP请求(request对象)进行加工,并且决定是否调用next方法,将request对象再传给下一个中间件。

1
2
3
4
5
6
function exampleMiddware(req, res, next) {
// handle req
// or end res
// or next()
// or next(err)
}

中间件常用 API

  • app.use(handler)
    • 任何请求方法、路径都会进入该中间件,然后执行里面的代码
  • app.use([path], handler)
    • 只有指定的请求路径,才会进入该中间件,然后执行里面的代码
  • app.get(path, handler)
    • 只有 get 请求,并且是指定的请求路径,才会执行该中间件
  • app.post(path, handler)
    • 只有 post 请求,并且是指定的请求路径,才会执行该中间件

常用第三方中间件

Express middleware

使用第三方中间件的步骤一般如下:

第一步 npm install --save 中间件名称

第二步:看文档,找到 example,然后 try-try-see

Express 中的错误处理

这是从框架、代码层面的解决。

程序运行异常的解决。

如何记录错误日志

API

express()

  • express() 类似于 http.createServer,得到一个 Server 实例对象
  • express.static(‘公共资源目录路径’) 处理静态资源
  • express.Router([options]):创建路由实例对象
    • 得到一个 router 实例
    • 给 router 挂载路由处理函数 get、post、
    • 然后就可以通过 app.use 的形式加载这个 router 实例

Application

  • app.locals
  • res.download(path [, filename] [, fn])
  • res.end([data] [, encoding])
  • res.json([body])
  • res.jsonp([body])
  • res.redirect([status,] path)
  • res.render(view [, locals] [, callback])
  • res.send([body])
  • res.sendFile(path [, options] [, fn])
  • res.sendStatus(statusCode)
  • res.set(field [, value])
  • res.status(code)
  • res.type(type)
    • ‘Content-Type’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require('express');
const app = express();
// app.locals 是一个对象,可以给它挂载数据
// 然后就可以在后续的任何处理函数中通过 req.app.locals 来使用了
// 作用:可以把多个处理函数中使用到的公共的资源挂载给 app.locals 属性
app.locals.name = 'hello';
app.get('/', (req, res) => {
console.log(req.app.locals.name)
});
app.get('/download', (req, res) => {
res.download('./README.md');
});
app.get('/json', (req, res) => {
res.json({
name: 'jack',
foo: 'bar'
});
});
app.listen(3000);

Request

  • router.all(path, [callback, …] callback)
  • router.METHOD(path, [callback, …] callback)
    • get
    • post
  • router.param([name,] callback)
  • router.route(path)
  • router.use([path], [function, …] function)

中间件插件 API

  • 安装插件
  • 加载插件
  • 使用插件 API

关于中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// app.use(中间件处理函数)
// app.use('请求路径', 请求处理函数)
// app.get('请求路径', 请求处理函数)
// app.post('请求路径', 请求处理函数)
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// 不限定于某个具体的 请求方法+请求路径
app.use('/a',express.static(path.join(__dirname, 'data/a')));
app.use('/b',express.static(path.join(__dirname, 'data/b')));
// 该中间件,任何请求都会进入该中间件
// 请求进入中间
// 记录所有的请求日志
app.use((req, res, next) => {
fs.appendFile('./log.txt', `${req.method} - ${req.url} - ${JSON.stringify(req.query)}
`, err => {
if (err) {
throw err;
}
// 代码执行到这里,说明日志记录成功,该中间件任务已完成,可以进入下一个中间件了
next();
});
});
// 该中间件可以处理任意的 /a 请求
// 无论是 get 请求 /a 还是 post 请求 /a 都会进入该中间件
// 只要请求路径不是 /a 就不会进入该中间件
app.use('/a', (req, res) => {
console.log('进入 /a 请求处理中间件了');
});
app.use((req, res, next) => {
console.log(222);
next();
});
app.get('/', (req, res) => {
res.send('index page');
});
// 处理 HTTP 具体方法和路径型中间件
// 一般在这里结束响应之后,就不需要调用下一个中间件了
app.get('/login', (req, res, next) => {
res.send('login page');
next();
});
app.use((req, res, next) => {
// 一个请求对应一个响应
// 响应结束,该处理流程中就不能再次发送响应数据了,否则报错
// res.send('aaa');
console.log('responsed end...');
next();
});
app.get('/login', (req, res, next) => {
console.log('aaa login');
next();
});
app.listen(3000);

express 支持使用字符串模式的路由路径

req.pramers 来取得路径的参数 如路径 article/:5, 是一个对象,里面可以获取到当前路径的参数

感谢您的支持!