Better

Ethan的博客,欢迎访问交流

博客搭建应用篇

上篇博客记录了博客搭建的基础,这里记录实际应用。

UI

网站基于Bootstrap3构建,这里需要注意Boostrap3Boostrap4基本不兼容,同时实现的响应的原理也有很大区别,观察两者源代码会发现Boostrap4使用了很多的flex布局,而Boostrap3实用的是@media查询。Boostrap3主要针对如下尺寸做了适配,自己也可以按照这几个尺寸结合Boostrap实现同步适配。

响应式设计:Bootstrap的栅格系统可以重复作用,类似于<div class="col-lg-8 col-md-10 col-sm-10 col-xs-10">使用,会自动根据屏幕大小进行适配!

  • 参考栅格系统的响应设计。
    • 超小屏幕 手机 (<768px) .col-xs-
    • 小屏幕 平板 (≥768px) .col-sm-
    • 中等屏幕 桌面显示器 (≥992px) .col-md-
    • 大屏幕 大桌面显示器 (≥1200px) .col-lg-
  • min-width
    • 768
    • 992
    • 1200
  • max-width
    • 767
    • 991
    • 1199

目录结构

  • models: 存放操作数据库的文件
  • public: 存放静态文件,如样式、图片等
  • routes: 存放路由文件
  • views: 存放模板文件
  • index.js: 程序主文件
  • package.json: 存储项目名、描述、作者、依赖等等信息

安装依赖

  • express: web 框架
  • express-session: session 中间件
  • connect-mongo: 将 session 存储于 mongodb,结合 express-session 使用
  • connect-flash: 页面通知提示的中间件,基于 session 实现
  • ejs: 模板
  • express-formidable: 接收表单及文件的上传中间件
  • config-lite: 读取配置文件
  • marked: markdown 解析
  • moment: 时间格式化
  • mongolass: mongodb 驱动
  • objectid-to-timestamp: 根据 ObjectId 生成时间戳
  • sha1: sha1 加密,用于密码加密
  • winston: 日志
  • express-winston: 基于 winston 的用于 express 的日志中间件

config-lite

config-lite 是一个轻量的读取配置文件的模块。config-lite 会根据环境变量(NODE_ENV)的不同从当前执行进程目录下的 config 目录加载不同的配置文件。如果不设置 NODE_ENV,则读取默认的 default 配置文件,如果设置了 NODE_ENV,则会合并指定的配置文件和 default 配置文件作为配置,config-lite 支持 .js、.json、.node、.yml、.yaml 后缀的文件。

如果程序以 NODE_ENV=test node app 启动,则 config-lite 会依次降级查找 config/test.js、config/test.json、config/test.node、config/test.yml、config/test.yaml 并合并 default 配置; 如果程序以 NODE_ENV=production node app 启动,则 config-lite 会依次降级查找 config/production.js、config/production.json、config/production.node、config/production.yml、config/production.yaml 并合并 default 配置。

会话session

cookie 与 session 的区别

  1. cookie 存储在浏览器(有大小限制),session 存储在服务端(没有大小限制)
  2. 通常 session 的实现是基于 cookie 的,即 session id 存储于 cookie 中

我们通过引入 express-session 中间件实现对会话的支持:

app.use(session(options))

session 中间件会在 req 上添加 session 对象,即 req.session 初始值为 {},当我们登录后设置 req.session.user = 用户信息,返回浏览器的头信息中会带上 set-cookie 将 session id 写到浏览器 cookie 中,那么该用户下次请求时,通过带上来的 cookie 中的 session id 我们就可以查找到该用户,并将用户信息保存到 req.session.user。

页面通知

connect-flash 是基于 session 实现的,它的原理很简单:设置初始值 req.session.flash={},通过 req.flash(name, value) 设置这个对象下的字段和值,通过 req.flash(name) 获取这个对象下的值,同时删除这个字段。

express-session、connect-mongo 和 connect-flash 的区别与联系

  • express-session: 会话(session)支持中间件
  • connect-mongo: 将 session 存储于 mongodb,需结合 express-session 使用,我们也可以将 session 存储于 redis,如 connect-redis
  • connect-flash: 基于 session 实现的用于通知功能的中间件,需结合 express-session 使用

权限控制

把用户状态的检查封装成一个中间件,在每个需要权限控制的路由加载该中间件,即可实现页面的权限控制。

创建

module.exports = {
  checkLogin: function checkLogin(req, res, next) {
    if (!req.session.user) {
      req.flash('error', '未登录'); 
      return res.redirect('/signin');
    }
    next();
  },

  checkNotLogin: function checkNotLogin(req, res, next) {
    if (req.session.user) {
      req.flash('error', '已登录'); 
      return res.redirect('back');//返回之前的页面
    }
    next();
  }
};

加载与使用

var checkLogin = require('../middlewares/check').checkLogin;
router.post('/', checkLogin, function(req, res, next) {
  res.send(req.flash());
});

app.locals & res.locals

在调用 res.render 的时候,express 合并(merge)了 3 处的结果后传入要渲染的模板,优先级:res.render 传入的对象> res.locals 对象 > app.locals 对象。

app.locals 和 res.locals 几乎没有区别,都用来渲染模板,使用上的区别在于:app.locals 上通常挂载常量信息(如博客名、描述、作者信息),res.locals 上通常挂载变量信息,即每次请求可能的值都不一样(如请求者信息,res.locals.user = req.session.user)。

用处

一些与业务关系不紧密,每次都会用到的全局变量,使用上述变量达到减少代码冗余的目的。

// 设置模板全局常量
app.locals.blog = {
  title: pkg.name,
  description: pkg.description
};
// 添加模板必需的三个变量
app.use(function (req, res, next) {
  res.locals.user = req.session.user;
  res.locals.success = req.flash('success').toString();
  res.locals.error = req.flash('error').toString();
  next();
});

文件上传

// 处理表单及文件上传的中间件
app.use(require('express-formidable')({
    uploadDir: path.join(__dirname, 'public/img'),// 上传文件目录
    keepExtensions: true// 保留后缀
}));

日志

记录正常请求日志的中间件要放到 routes(app) 之前,记录错误请求日志的中间件要放到 routes(app) 之后。

正常请求的日志

app.use(expressWinston.logger({
    transports: [
        new (winston.transports.Console)({
            json: true,
            colorize: true
        }),
        new winston.transports.File({
            filename: 'logs/success.log'
        })
    ]
}));

错误请求的日志

app.use(expressWinston.errorLogger({
    transports: [
        new winston.transports.Console({
            json: true,
            colorize: true
        }),
        new winston.transports.File({
            filename: 'logs/error.log'
        })
    ]
}));

mongolass

连接

var config = require('config-lite')(__dirname);
var Mongolass = require('mongolass');
var mongolass = new Mongolass();
mongolass.connect(config.mongodb);

扩展对比

  • node-mongodb-native
  • Mongoose
  • Mongolass

基本用法

  • Model.create(dataObj).exec()
  • Model.findOne(dataObj).exec()
  • Model.find(dataObj).exec()
  • Model.sort(dataObj).exec();1表示升序,-1表示降序
  • Model.update(paramObj1,paramObj2),参数1条件约束,参数2表示修改值。
    • 参数二可为:{$inc:{pv:1}}自增
    • 参数二:$set:{}修改
  • Model.remove(dataObj).exec()
  • Model.count(dataObj).exec();

_id & ObjectId

MongoDB中存储的文档必须有一个"_id"键。这个键的值可以是任何类型的,默认是个ObjectId对象。

在一个集合里面,每个集合都有唯一的"_id"值,来确保集合里面每个文档都能被唯一标识。

ObjectId是"_id"的默认类型。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。

这是MongoDB采用ObjectId,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个 服务器上同步自动增加主键值既费力还费时MongoDB从一开始就设计用来作为分布式数据库,处理多个节 点是一个核心要求。

Plugin注册

mongolass提供了丰富的api,同时我们可以通过plugin扩充mongolass的api。Plugin可以直接注册在mongolass上,也可以注册在model上,区别很容易理解,能使用的作用域不同。

例如:根据_id生成时间戳,然后格式化成想要的格式,为对象添加此属性。

var moment = require('moment');
var objectIdToTimestamp = require('objectid-to-timestamp');
// 根据 id 生成创建时间 created_at
mongolass.plugin('addCreatedAt', {
  afterFind: function (results) {
    results.forEach(function (item) {
      item.created_at = moment(objectIdToTimestamp(item._id)).format('YYYY-MM-DD HH:mm');
    });
    return results;
  },
  afterFindOne: function (result) {
    if (result) {
      result.created_at = moment(objectIdToTimestamp(result._id)).format('YYYY-MM-DD HH:mm');
    }
    return result;
  }
});

此时Model可以链式调用此方法。

Model

生成设置scheme并导出

exports.User = mongolass.model('User', {
    name:{type:”string”},
    sex:{type:”string”,enum:[‘m’,’f’,’’]}
});

设置索引

exports.User.index({ name: 1 }, { unique: true }).exec();

外键管理

外键数据类型设计:比如文章的作者,由于mongodb可以根据_id来唯一标识。

author: { type: Mongolass.Types.ObjectId }

因为MongoDB是文档型数据库,所以它没有关系型数据库[joins], 为了解决这个问题,Mongoose封装了一个Population功能。使用Population可以实现在一个 document 中填充其他 collection(s) 的 document(s)。

populate用法:
Query.populate(path, [select], [model], [match], [options])
参数:
path
  类型:String或Object。
  String类型的时, 指定要填充的关联字段,要填充多个关联字段可以以空格分隔。
  Object类型的时,就是把 populate 的参数封装到一个对象里。当然也可以是个数组。
select
  类型:Object或String,可选,指定填充 document 中的哪些字段。
  Object类型的时,格式如:{name: 1, _id: 0},为0表示不填充,为1时表示填充。
  String类型的时,格式如:"name -_id",用空格分隔字段,在字段名前加上-表示不填充。详细语法介绍query-select
model
  类型:Model,可选,指定关联字段的 model,如果没有指定就会使用Schema的ref。
match
  类型:Object,可选,指定附加的查询条件。
options
类型:Object,可选,指定附加的其他查询选项,如排序以及条数限制等等。

级联删除

exec()执行返回一个promise对象,可使用then方法回调添加逻辑。

delPostById: function delPostById(postId, author) {
  return Post.remove({ author: author, _id: postId })
    .exec()
    .then(function (res) {
      // 文章删除后,再删除该文章下的所有留言
      if (res.result.ok && res.result.n > 0) {
        return CommentModel.delCommentsByPostId(postId);
      }
    });
}

分页

  • limit()方法接受一个数字参数,该参数指定从MongoDB中读取的记录条数。
  • skip方法同样接受一个数字参数作为跳过的记录条数。

error page & 404 page

通过 app.use 加载中间件,在中间件中通过 next 将请求传递到下一个中间件,next 可接受一个参数接收错误信息,如果使用了 next(error),则会返回错误而不会传递到下一个中间件。

定义一个错误处理中间件和其他普通中间件一样,不同的是使用的参数是4个而不是3个,第一个参数为错误对象。

// error page
app.use(function (err, req, res, next) {
  res.render('error', {
    error: err
  });
});

介绍了error的错误处理,那么如何处理404的页面呢,由于404比较特殊,表示没有路由匹配的页面。只需要在所有路由之后注册404中间件。

// 404 page
app.use(function (req, res) {
  if (!res.headersSent) {
    res.status(404).render('404');
  }
});

工具使用

postman

  • form-data:http请求中的multipart/form-data,它会将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。
  • x-www-form-urlencoded:application/x-www-from-urlencoded,会将表单内的数据转换为键值对,比如,name=Java&age = 23
  • row:可以上传任意格式的文本,可以上传text、json、xml、html等
  • binary:相当于Content-Type:application/octet-stream,从字面意思得知,只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件。


留言