0%

从零开始用 Express + MongoDB 搭建图片分享社区(一)

@mRcfps

查看代码

初始化项目结构

主要

此教程属于Node.js 后端工程师学习路线的一部分,点击可查看全部内容。

首先我们创建项目目录,并初始化 npm:

$ mkdir Instagrammy && cd Instagrammy
$ npm init

添加 express 的项目依赖:

$ npm install express

最终生成的 package.json 文件如下:

package.json查看完整代码
{
"name": "instagrammy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
}
}

然后编写服务器的入口文件 server.js,内容如下:

server.js查看完整代码
const express = require('express');

app = express();
app.set('port', process.env.PORT || 3000);

app.get('/', function(req, res) {
res.send('Hello World!');
});

app.listen(app.get('port'), function() {
console.log(`Server is running on http://localhost:${app.get('port')}`);
});

通过 node server.js 运行 server.js 文件,然后在浏览器中访问 http://localhost:3000,便可以看到服务器的返回的 Hello World:

配置中间件

Express 本身是一个非常简洁的 web 框架,但是通过中间件这一设计模式,能够实现非常丰富的功能。一个 Express 中间件本质上是一个函数:

function someMiddleware(req, res, next) {}

req 参数是一个 express.Request 对象,封装了用户请求;res 参数则是一个 express.Response 对象,封装了即将返回给用户的响应;next 则是在执行完所有逻辑后用于触发下一个中间件的函数。

添加中间件的代码则非常简单:

app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

中间件 A、B、C 将会在处理每次请求时按顺序执行(这也意味着中间件的顺序是非常重要的)。接下来我们将添加以下基础中间件(也是几乎所有应用都会用到的中间件):

  • morgan:用于记录日志的中间件,对于开发调试和生产监控都很有用;
  • bodyParser:用于解析客户端请求的中间件,包括 HTML 表单和 JSON 请求;
  • methodOverride:为老的浏览器提供 REST 请求的兼容性支持;
  • cookieParser:用于收发 cookie;
  • errorHandler:用于在发生错误时打印调用栈,仅在开发时使用
  • handlebars:用于渲染用户界面的模板引擎,会在后面细讲。

我们通过 npm 安装这些中间件:

$ npm install express-handlebars body-parser cookie-parser morgan method-override errorhandler

创建 server 目录,在其中添加 configure.js 模块,用于配置所有的中间件:

server/configure.js查看完整代码
const path = require('path');
const exphbs = require('express-handlebars');
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const methodOverride = require('method-override');
const errorHandler = require('errorhandler');

module.exports = function(app) {
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(methodOverride());
app.use(cookieParser('secret-value'));
app.use('/public/', express.static(path.join(__dirname, '../public')));

if (app.get('env') === 'development') {
app.use(errorHandler());
}

return app;
};

值得一提的是,除了上面提到的中间件,我们还用到了 express 自带的静态资源中间件 express.static,用于向客户端发送图片、CSS等静态文件。最后,我们通过获取 env 变量来判断是否处于开发环境,如果是的话就添加 errorHandler 以便于调试代码。

在 server.js 中调用刚才用于配置中间件的代码:

server.js查看完整代码
const express = require('express');
[tuture-add]const configure = require('./server/configure');

app = express();
[tuture-add]app = configure(app);
app.set('port', process.env.PORT || 3000);

app.get('/', function(req, res) {
// ...

搭建路由和控制器

现在我们已经配置好了基础的中间件,但是只有主页(URL 为 /)可以访问。接下来我们将实现以下路由:

  • GET /:网站主页
  • GET /images/image_id:展示单张图片
  • POST /images:上传图片
  • POST /images/image_id/like:点赞图片
  • POST /images/image_id/comment:评论图片

虽然 Express 的项目结构没有固定的套路,但是我们将采用经典的 MVC 模式(即 Model View Controller)来搭建我们的项目。Model 定义了数据模型,View 定义了用户界面,而 Controller 则定义了相应的业务逻辑。

首先创建 controllers 目录,在其中创建 home.js 文件,并定义 index 控制器如下:

controllers/home.js查看完整代码
module.exports = {
index: function(req, res) {
res.send('The home:index controller');
},
};

每个控制器实际上都是一个 Express 中间件(只不过不需要 next 函数,因为是最后一个中间件)。这里我们暂时用 res.send 发一条文字来代表这个 controller 已经实现。

再在 controllers 目录下创建 image.js,实现与图片处理相关的控制器:

controllers/image.js查看完整代码
module.exports = {
index: function(req, res) {
res.send('The image:index controller ' + req.params.image_id);
},
create: function(req, res) {
res.send('The image:create POST controller');
},
like: function(req, res) {
res.send('The image:like POST controller');
},
comment: function(req, res) {
res.send('The image:comment POST controller');
},
};

然后在 server 目录下创建路由模块 routes.js,建立从 URL 到控制器之间的映射:

server/routes.js查看完整代码
const express = require('express');

const router = express.Router();
const home = require('../controllers/home');
const image = require('../controllers/image');

module.exports = function(app) {
router.get('/', home.index);
router.get('/images/:image_id', image.index);
router.post('/images', image.create);
router.post('/images/:image_id/like', image.like);
router.post('/images/:image_id/comment', image.comment);
app.use(router);
};

这里我们用到了 Express 自带的路由类 Router,可以很方便地定义路由,并且 Router 本身也是一个中间件,可以直接通过 app.use 进行配置。

接着在 server/configure.js 模块中调用路由模块。

server/configure.js查看完整代码
// ...
const methodOverride = require('method-override');
const errorHandler = require('errorhandler');

[tuture-add]const routes = require('./routes');
[tuture-add]
module.exports = function(app) {
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended: true }));
// ...
app.use(errorHandler());
}

[tuture-add] routes(app);
return app;
};

最后我们去掉 server.js 中原来的首页路由。

server.js查看完整代码
// ...
app = configure(app);
app.set('port', process.env.PORT || 3000);

[tuture-del]app.get('/', function(req, res) {
[tuture-del] res.send('Hello World!');
[tuture-del]});
[tuture-del]
app.listen(app.get('port'), function() {
console.log(`Server is running on http://localhost:${app.get('port')}`);
});

到了这一步,我们运行服务器,打开浏览器访问 http://localhost:3000,可以看到 The home:index controller 的信息;访问 http://localhost:3000/test123,则是 The image:index controller test123。进一步,我们还可以通过 Postman 或者 curl 等工具测试 POST 方法的 controller 是否可用。下面以 curl 为例测试 POST /images

$ curl -X POST localhost:3000/images
The image:create POST controller

一切顺利!到这里可以泡杯茶好好犒劳一下自己了~

配置 handlebars 模板引擎

这一步,我们将开始实现 MVC 中的 V,即 View,用户界面。

首页的效果如下图所示:

图片详情的效果如下图所示:

尽管如今前后端分离已经是大势所趋,但是通过模板引擎在服务器端渲染页面也是有用武之地的,特别是快速地开发一些简单的应用。在模板引擎中,HandlebarsPug 当属其中的佼佼者。由于 Handlebars 和普通的 HTML 文档几乎完全一致,容易上手,因此这篇教程中我们选用 Handlebars,并且选用 Bootstrap 样式。

与普通的 HTML 文档相比,模板最大的特点即在于提供了数据的接入。例如 handlebars,可以在双花括号 {{}} 之间填写任何数据,当服务器渲染页面时只需传入相应的数据即可渲染成对应的内容。除此之外,handlebars 还支持条件语法、循环语法和模板嵌套等高级功能,下面将详细描述。

我们创建一个 views 目录,用于存放所有的模板代码。views 目录的结构如下所示:

views
├── image.handlebars
├── index.handlebars
├── layouts
│   └── main.handlebars
└── partials
├── comments.handlebars
├── popular.handlebars
└── stats.handlebars

其中 image.handlebars 和 index.handlebars 是页面模板,layouts/main.handlebars 则是整个网站的布局模板(所有页面共享),partials 目录则用于存放页面之间共享的组件模板,例如评论、数据等等。

首先完成布局模板 layouts/main.handlebars:

views/layouts/main.handlebars查看完整代码
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Instagrammy</title>
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
<div class="page-header">
<div class="container">
<div class="col-md-6">
<h1><a href="/">Instagrammy</a></h1>
</div>
</div>
</div>

<div class="container">
<div class="row">
<div class="col-sm-8">{{{body}}}</div>
</div>
</div>
</body>

</html>

main.handlebars 本身是一个完整的 HTML 文档,包括 headbody 部分。在 head 部分,我们定义了网站的一些元数据,还加入了 Bootstrap 的 CDN 链接;在 body 部分,包含两个容器:网站头部(header)和每个页面的自定义内容(即{{{body}}} )。

接下来编写 index.handlebars,即主页内容。这里我们暂时先写上一个大标题:

views/index.handlebars查看完整代码
<h1>Index Page</h1>

还有 image.handlebars,即图片详情页面内容:

views/image.handlebars查看完整代码
<h1>Image Page</h1>

index.handlebars 和 image.handlebars 的内容将替换布局模板中的 {{{body}}} 部分。在用户访问某个页面时,页面内容 = 布局模板+页面模板

模板写好之后,我们修改控制器相应的代码,通过 res.render 函数渲染模板:

controllers/home.js查看完整代码
module.exports = {
index: function(req, res) {
[tuture-del] res.send('The home:index controller');
[tuture-add] res.render('index');
},
};

render 函数接受一个字符串参数,即页面模板的名称。例如 index.handlebars 的名称即为 index

同样地,我们修改 image 控制器的代码:

controllers/image.js查看完整代码
module.exports = {
index: function(req, res) {
[tuture-del] res.send('The image:index controller ' + req.params.image_id);
[tuture-add] res.render('image');
},
create: function(req, res) {
res.send('The image:create POST controller');
// ...

最后,不要忘记在 server/configure.js 模块中配置 handlebars 中间件:

server/configure.js查看完整代码
// ...
const routes = require('./routes');

module.exports = function(app) {
[tuture-add] app.engine('handlebars', exphbs());
[tuture-add] app.set('view engine', 'handlebars');
[tuture-add]
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// ...

大功告成!现在运行服务器,分别访问主页(http://localhost:3000)和图片详情页面(http://localhost:3000/images/test),可以看到完整的页面(虽然没有数据),包括所有页面共享的头部和每个页面的自定义内容。

完善界面代码

在上面一步的基础上,我们将继续完善模板代码。

首先是在 index.handlebars 中添加上传图片的表单和展示最新图片的容器。

views/index.handlebars查看完整代码
[tuture-del]<h1>Index Page</h1>
[tuture-add]<div class="panel panel-primary">
[tuture-add] <div class="panel-heading">
[tuture-add] <h3 class="panel-title">
[tuture-add] 上传图片
[tuture-add] </h3>
[tuture-add] </div>
[tuture-add] <form action="/images" method="post" enctype="multipart/form-data">
[tuture-add] <div class="panel-body form-horizontal">
[tuture-add] <div class="form-group col-md-12">
[tuture-add] <label for="file" class="col-sm-2 control-label">浏览:</label>
[tuture-add] <div class="col-md-10">
[tuture-add] <input type="file" class="form-control" name="file" id="file">
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="form-group col-md-12">
[tuture-add] <label for="title" class="col-md-2 control-label">标题:</label>
[tuture-add] <div class="col-md-10">
[tuture-add] <input type="text" class="form-control" name="title">
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="form-group col-md-12">
[tuture-add] <label for="description" class="col-md-2 control-label">描述:</label>
[tuture-add] <div class="col-md-10">
[tuture-add] <textarea name="description" rows="2" class="form-control"></textarea>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="form-group col-md-12">
[tuture-add] <div class="col-md-12 text-right">
[tuture-add] <button type="submit" id="login-btn" class="btn btn-success">
[tuture-add] <i class="fa fa-cloud-upload"> 上传图片</i>
[tuture-add] </button>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </form>
[tuture-add]</div>
[tuture-add]
[tuture-add]<div class="panel panel-default">
[tuture-add] <div class="panel-heading">
[tuture-add] <h3 class="panel-title">
[tuture-add] 最新图片
[tuture-add] </h3>
[tuture-add] </div>
[tuture-add] <div class="panel-body">
[tuture-add] {{#each images}}
[tuture-add] <div class="col-md-4 text-center" style="padding-bottom: 1em;">
[tuture-add] <a href="/images/{{uniqueId}}">
[tuture-add] <img src="/public/upload/{{filename}}" alt="{{title}}"
[tuture-add] style="width: 175px; height: 175px;" class="img-thumbnail">
[tuture-add] </a>
[tuture-add] </div>
[tuture-add] {{/each}}
[tuture-add] </div>
[tuture-add]</div>

在展示最新图片时,我们用到了 handlebars 提供的循环语法(第 46 行到 53 行)。对于传入模板的数据对象 images 进行遍历,每个循环中可以访问单个 image 的全部属性,例如 uniqueId 等等。

接着完善 image.handlebars 的内容,包括展示图片的详细内容、发表评论的表单和展示所有评论的容器。

views/image.handlebars查看完整代码
[tuture-del]<h1>Image Page</h1>
[tuture-add]<div class="panel panel-primary">
[tuture-add] <div class="panel-heading">
[tuture-add] <h2 class="panel-title">{{image.title}}</h2>
[tuture-add] </div>
[tuture-add] <div class="panel-body">
[tuture-add] <p>{{image.description}}</p>
[tuture-add] <div class="col-md-12 text-center">
[tuture-add] <img src="/public/upload/{{image.filename}}" alt="{{image.title}}"
[tuture-add] class="thumbnail">
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="panel-footer">
[tuture-add] <div class="row">
[tuture-add] <div class="col-md-8">
[tuture-add] <button class="btn btn-success" id="btn-like" data-id="{{image.uniqueId}}">
[tuture-add] <i class="fa fa-heart"> 点赞</i>
[tuture-add] </button>
[tuture-add] <strong class="likes-count">{{image.likes}}</strong> &nbsp; - &nbsp;
[tuture-add] <i class="fa fa-eye"></i>
[tuture-add] <strong>{{image.views}}</strong>
[tuture-add] &nbsp; - &nbsp; 发表于: <em class="text-muted">{{image.timestamp}}</em>
[tuture-add] </div>
[tuture-add] <div class="col-md-4 text-right">
[tuture-add] <button class="btn btn-danger" id="btn-delete" data-id="{{image.uniqueId}}">
[tuture-add] <i class="fa fa-times"></i>
[tuture-add] </button>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add]</div>
[tuture-add]
[tuture-add]<div class="panel panel-default">
[tuture-add] <div class="panel-heading">
[tuture-add] <div class="row">
[tuture-add] <div class="col-md-8">
[tuture-add] <strong class="panel-title">评论</strong>
[tuture-add] </div>
[tuture-add] <div class="col-md-4 text-right">
[tuture-add] <button class="btn btn-default btn-sm" id="btn-comment" data-id="{{image.uniqueId}}">
[tuture-add] <i class="fa fa-comments-o"> 发表评论...</i>
[tuture-add] </button>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="panel-body">
[tuture-add] <blockquote id="post-comment">
[tuture-add] <div class="row">
[tuture-add] <form action="/images/{{image.uniqueId}}/comment" method="post">
[tuture-add] <div class="form-group col-sm-12">
[tuture-add] <label for="name" class="col-sm-2 control-label">昵称:</label>
[tuture-add] <div class="col-sm-10"><input type="text" class="form-control" name="name"></div>
[tuture-add] </div>
[tuture-add] <div class="form-group col-sm-12">
[tuture-add] <label for="email" class="col-sm-2 control-label">Email:</label>
[tuture-add] <div class="col-sm-10"><input type="text" class="form-control" name="email"></div>
[tuture-add] </div>
[tuture-add] <div class="form-group col-sm-12">
[tuture-add] <label for="comment" class="col-sm-2 control-label">评论:</label>
[tuture-add] <div class="col-sm-10">
[tuture-add] <textarea name="comment" class="form-control" rows="2"></textarea>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] <div class="form-group col-sm-12">
[tuture-add] <div class="col-sm-12 text-right">
[tuture-add] <button class="btn btn-success" id="comment-btn" type="button">
[tuture-add] <i class="fa fa-comment"></i> 发表
[tuture-add] </button>
[tuture-add] </div>
[tuture-add] </div>
[tuture-add] </form>
[tuture-add] </div>
[tuture-add] </blockquote>
[tuture-add] <ul class="media-list">
[tuture-add] {{#each comments}}
[tuture-add] <li class="media">
[tuture-add] <a href="#" class="pull-left">
[tuture-add] <img src="http://www.gravatar.com/avatar/{{gravatar}}?d=monsterid&s=45"
[tuture-add] class="media-object img-circle">
[tuture-add] </a>
[tuture-add] <div class="media-body">
[tuture-add] {{comment}}
[tuture-add] <br/>
[tuture-add] <strong class="media-heading">{{name}}</strong>
[tuture-add] <small class="text-muted">{{timestamp}}</small>
[tuture-add] </div>
[tuture-add] </li>
[tuture-add] {{/each}}
[tuture-add] </ul>
[tuture-add] </div>
[tuture-add]</div>

在展示所有评论的代码中,我们同样用到了 handlebars 的循环语法,非常方便。

然后,我们将分别实现网站右边栏中的统计数据、最受欢迎图片和最新评论组件。首先是统计数据组件模板:

views/partials/stats.handlebars查看完整代码
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">统计数据</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-4 text-left">图片:</div>
<div class="col-md-8 text-right">{{sidebar.stats.images}}</div>
</div>
<div class="row">
<div class="col-md-4 text-left">评论:</div>
<div class="col-md-8 text-right">{{sidebar.stats.comments}}</div>
</div>
<div class="row">
<div class="col-md-4 text-left">浏览:</div>
<div class="col-md-8 text-right">{{sidebar.stats.views}}</div>
</div>
<div class="row">
<div class="col-md-4 text-left">点赞:</div>
<div class="col-md-8 text-right">{{sidebar.stats.likes}}</div>
</div>
</div>
</div>

可以看出组件模板和页面模板并没有什么不同,都是一些 HTML 代码再加上数据接口。

再分别实现最受欢迎图片组件(popular.handlebars)和最新评论组件(comments.handlebars)。

views/partials/popular.handlebars查看完整代码
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">最受欢迎</h3>
</div>
<div class="panel-body">
{{#each sidebar.popular}}
<div class="col-md-4 text-center" style="padding-bottom: .5em;">
<a href="/images/{{uniqueId}}">
<img src="/public/upload/{{filename}}" style="width: 75px; height: 75px;"
class="img-thumbnail">
</a>
</div>
{{/each}}
</div>
</div>
views/partials/comments.handlebars查看完整代码
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">最新评论</h3>
</div>
<div class="panel-body">
<ul class="media-list">
{{#each sidebar.comments}}
<li class="media">
<a href="/images/{{image.uniqueId}}" class="pull-left">
<img src="/public/upload/{{image.filename}}" class="media-object"
height="45" width="45">
</a>
<div class="media-body">
{{comment}}<br/>
<strong class="media-heading">{{name}}</strong>
<small class="text-muted">{{timestamp}}</small>
</div>
</li>
{{/each}}
</ul>
</div>
</div>

最后,我们在布局模板 layouts/main.handlebars 中加入所有组件模板,加入模板的语法为 {{> component this}}。除此之外,由于我们用到了一些小图标,所以加上 font-awesome 的链接。

views/layouts/main.handlebars查看完整代码
...
<meta charset="UTF-8">
<title>Instagrammy</title>
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
[tuture-add] <link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet">
</head>

<body>
...
<div class="container">
<div class="row">
<div class="col-sm-8">{{{body}}}</div>
[tuture-add] <div class="col-sm-4">
[tuture-add] {{> stats this}}
[tuture-add] {{> popular this}}
[tuture-add] {{> comments this}}
[tuture-add] </div>
</div>
</div>
</body>
...

在实际开发中,我们可能经常需要调整页面代码。在修改并保存模板后,只需刷新浏览器即可看到界面的变化(但是如果修改服务器代码则需要重新运行服务器)。

将数据传入模板视图

如果没有数据传入,那么模板相应的数据部分将全都是空白。在这一步中,我们将用一些假数据来演示如何从控制器将数据传入模板视图。

首先在 home 控制器中构造一个 viewModel 对象,并在 render 函数中作为第二参数传入。可以看到 viewModel 对象与模板中的数据接口是完全一致的。

controllers/home.js查看完整代码
module.exports = {
index: function(req, res) {
[tuture-del] res.render('index');
[tuture-add] const viewModel = {
[tuture-add] images: [
[tuture-add] {
[tuture-add] uniqueId: 1,
[tuture-add] title: '示例图片1',
[tuture-add] description: '',
[tuture-add] filename: 'sample1.jpg',
[tuture-add] views: 0,
[tuture-add] likes: 0,
[tuture-add] timestamp: Date.now(),
[tuture-add] },
[tuture-add] {
[tuture-add] uniqueId: 2,
[tuture-add] title: '示例图片2',
[tuture-add] description: '',
[tuture-add] filename: 'sample2.jpg',
[tuture-add] views: 0,
[tuture-add] likes: 0,
[tuture-add] timestamp: Date.now(),
[tuture-add] },
[tuture-add] {
[tuture-add] uniqueId: 3,
[tuture-add] title: '示例图片3',
[tuture-add] description: '',
[tuture-add] filename: 'sample3.jpg',
[tuture-add] views: 0,
[tuture-add] likes: 0,
[tuture-add] timestamp: Date.now(),
[tuture-add] },
[tuture-add] ],
[tuture-add] };
[tuture-add] res.render('index', viewModel);
},
};

同理实现 image 控制器。

controllers/image.js查看完整代码
module.exports = {
index: function(req, res) {
[tuture-del] res.render('image');
[tuture-add] const viewModel = {
[tuture-add] image: {
[tuture-add] uniqueId: 1,
[tuture-add] title: '示例图片1',
[tuture-add] description: '这是张测试图片',
[tuture-add] filename: 'sample1.jpg',
[tuture-add] views: 0,
[tuture-add] likes: 0,
[tuture-add] timestamp: Date.now(),
[tuture-add] },
[tuture-add] comments: [
[tuture-add] {
[tuture-add] image_id: 1,
[tuture-add] email: 'test@testing.com',
[tuture-add] name: 'Test Tester',
[tuture-add] comment: 'Test 1',
[tuture-add] timestamp: Date.now(),
[tuture-add] },
[tuture-add] {
[tuture-add] image_id: 1,
[tuture-add] email: 'test@testing.com',
[tuture-add] name: 'Test Tester',
[tuture-add] comment: 'Test 2',
[tuture-add] timestamp: Date.now(),
[tuture-add] },
[tuture-add] ],
[tuture-add] };
[tuture-add] res.render('image', viewModel);
},
create: function(req, res) {
res.send('The image:create POST controller');
// ...

在传入数据时,我们可以自定义一些 helper 函数在模板中使用。例如 timestamp 时间戳,Date.now() 返回的是一串数字,显然用户体验很不友好,因此我们需要将其转换为方便用户阅读的时间,例如“几秒前”“两小时前”。这里我们选用 JavaScript 最流行的处理时间的库 moment.js,并通过 npm 安装:

$ npm install moment

然后在 server/configure.js 中配置 handlebars 的 helper 函数 timeago

server/configure.js查看完整代码
// ...
const morgan = require('morgan');
const methodOverride = require('method-override');
const errorHandler = require('errorhandler');
[tuture-add]const moment = require('moment');

const routes = require('./routes');

module.exports = function(app) {
[tuture-del] app.engine('handlebars', exphbs());
[tuture-add] // 定义 moment 全局语言
[tuture-add] moment.locale('zh-cn');
[tuture-add]
[tuture-add] app.engine(
[tuture-add] 'handlebars',
[tuture-add] exphbs.create({
[tuture-add] helpers: {
[tuture-add] timeago: function(timestamp) {
[tuture-add] return moment(timestamp).startOf('minute').fromNow();
[tuture-add] },
[tuture-add] },
[tuture-add] }).engine,
[tuture-add] );
app.set('view engine', 'handlebars');

app.use(morgan('dev'));
// ...

timeago 函数能够在模板中使用,将原始的 UNIX 时间戳转换为易于理解的中文时间戳。

接着在相应用到时间戳的地方加入 timeago 函数:

views/image.handlebars查看完整代码
...
<strong class="likes-count">{{image.likes}}</strong> &nbsp; - &nbsp;
<i class="fa fa-eye"></i>
<strong>{{image.views}}</strong>
[tuture-del] &nbsp; - &nbsp; 发表于: <em class="text-muted">{{image.timestamp}}</em>
[tuture-add] &nbsp; - &nbsp; 发表于: <em class="text-muted">{{timeago image.timestamp}}</em>
</div>
<div class="col-md-4 text-right">
<button class="btn btn-danger" id="btn-delete" data-id="{{image.uniqueId}}">
...
{{comment}}
<br/>
<strong class="media-heading">{{name}}</strong>
[tuture-del] <small class="text-muted">{{timestamp}}</small>
[tuture-add] <small class="text-muted">{{timeago timestamp}}</small>
</div>
</li>
{{/each}}
...
views/partials/comments.handlebars查看完整代码
...
<div class="media-body">
{{comment}}<br/>
<strong class="media-heading">{{name}}</strong>
[tuture-del] <small class="text-muted">{{timestamp}}</small>
[tuture-add] <small class="text-muted">{{timeago timestamp}}</small>
</div>
</li>
{{/each}}
...

至此,本系列教程的第一部分就已经完成了,MVC 我们实现了 V(视图)和 C (控制器)。在后续教程中,我们将接入 MongoDB 数据库用于网站数据的存取,并进一步实现图片上传、点赞和删除,以及添加评论等功能。

主要

此教程属于Node.js 后端工程师学习路线的一部分,点击可查看全部内容。

图雀社区 微信公众号

扫一扫关注上方公众号,拉学习群和答疑解惑

  • 本文作者: 图雀社区
  • 本文链接: https://tuture.co/2019/10/16/a0531f0/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!