使用Node.js+expressjs从零开始搭建博客系列之二:认识布局模板和登录中间件

一、后台管理系统布局模板

1、后台应用设置

因为后台可以认为是一个单独的应用,在express中可以启用子应用这个概念。在app.js中添加:

  1. const backend = express();
  2. //设置后台应用的模板引擎
  3. backend.set('view engine', 'html');
  4. //设置后台模板的位置
  5. backend.set('views', path.join(__dirname, 'views', 'console'));
  6. //设置后台模板布局模板及碎片(组件)模板位置
  7. backend.engine('html', handlebars({extname:'.html', layoutsDir : 'views/console/layouts', partialsDir:'views/console/partials'}));
  8. //使用中间件指定到backend应用(即当请求为console时,为后台应用)
  9. app.use('/console', backend);
  10. //添加后台的路由
  11. router(app, backend);
2、路由设置:

把libs/router.js中修改为:

  1. const index = require('../routes/index.js');
  2. const consoleIndex = require('../routes/console/index.js');
  3. module.exports = function(app, backend){
  4. //前台的路由分发
  5. app.get('/', index);
  6. //后台的路由分发
  7. backend.get('/', consoleIndex);
  8. };
3、后台首页控制器:

在文件routes/console/index.js中写入:

  1. const index = {};
  2. //定义index模块的index方法
  3. index.index = function(req, res){
  4. res.render('index/index', {title:'控制台'});
  5. };
  6. module.exports = index;

注:

  • 因为在入口文件中指定了后台模板的文件位置,所以在赋值给模板的时候不用带console,所以当前模板为views/console/index/index.html
  • module.exports表示导出的模块
    4、碎片模板(组件模板):

一些公共模板,如公共头部和公共底部等,很多模板引擎中可以使用include包含。

views/console/partials/head.html,放公共的css文件

  1. <meta charset="utf-8">
  2. <link href="/static/css/bootstrap/boostrap.css" rel="stylesheet">
  3. <link href="/static/css/layout.css">

views/console/partials/foot.html,放公共的js文件

  1. <script type="/static/js/jquery.min.js"></script>
  2. <script type="/static/js/bootstrap.min.js"></script>
5、布局模板

文件为views/console/layouts/layout.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>{{title}}</title>
  5. {{> head}}
  6. <link href="/static/css/compiled/index.css" rel="stylesheet">
  7. </head>
  8. <body>
  9. <div class="navbar">公共头部内容</div>
  10. <div class="sidebar">左侧导航</div>
  11. <div class="content">
  12. {{{body}}}
  13. </div>
  14. </body>
  15. </html>
  • {{> head}}表示包含head这个碎片文件(类似于其他模板中的include)
  • {{{body}}}表示具体模板继承中被替换的部分的内容,为啥是{{{呢,因为{{}}不能包含html,而{{{可以包含html

二、数据库操作封装

1、数据库操作需要依赖node的mysql模块:
  1. npm install mysql --save
2、使用node操作数据库的ORM模块Sequelize:(当然也可以不使用,直接使用mysql模块)
  1. npm install sequelize --save

sequelize官方文档地址:http://docs.sequelizejs.com/en/v3/

3、定义应用配置。

在libs/conf.js中定义配置

  1. module.exports = {
  2. database : {
  3. host : 'localhost',
  4. port : 3306,
  5. dbname : 'node_blog',
  6. user : 'shixinke',
  7. password : 'nodeblog',
  8. charset : 'utf8',
  9. pool : {
  10. max : 5,
  11. min : 0,
  12. idle : 10000
  13. }
  14. }
  15. };
4、建立自定义数据库操作基类db.js

文件为libs/db.js:

  1. const db = {};
  2. const Sequelize = require('sequelize');
  3. const conf = require('./conf.js').database;
  4. db.Sequelize = Sequelize;
  5. db.db = new Sequelize(conf.dbname, conf.user, conf.password, {host : conf.host, port : conf.port, pool : conf.pool});
  6. module.exports = db;

注:

  • db.Sequelize为Sequelize这个模块(后面定义模型的时候需要用到)
  • db.db为数据库操作对象

三、登录功能实现

1、定义用户模型:

数据库表结构:

  1. CREATE TABLE `blog_user` (
  2. `uid` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  3. `account` char(20) NOT NULL COMMENT '账号',
  4. `password` char(32) NOT NULL COMMENT '密码',
  5. `email` varchar(30) NOT NULL COMMENT '邮箱',
  6. `nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '昵称',
  7. `avatar` varchar(200) NOT NULL DEFAULT '' COMMENT '头像',
  8. `status` enum('ENABLE','DISABLED','UNCHECKED','LOCKED') NOT NULL DEFAULT 'UNCHECKED' COMMENT '状态',
  9. `last_login_time` timestamp NULL DEFAULT NULL COMMENT '上次登录时间',
  10. `last_login_ip` char(16) DEFAULT NULL,
  11. `create_time` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  12. PRIMARY KEY (`uid`),
  13. UNIQUE KEY `idx_account` (`account`),
  14. KEY `idx_nickname` (`nickname`),
  15. KEY `idx_email` (`email`)
  16. ) ENGINE=InnoDB AUTO_INCREMENT=200001 DEFAULT CHARSET=utf8 COMMENT='用户表';

文件为models/user.js

  1. const userModel = {};
  2. //加载数据库操作类
  3. const db = require('../libs/db.js');
  4. //定义表模型(要定义字段名及其数据类型)
  5. const User = db.db.define('user', {
  6. uid:db.Sequelize.INTEGER,
  7. account: db.Sequelize.STRING,
  8. password: db.Sequelize.STRING,
  9. nickname: db.Sequelize.STRING,
  10. email: db.Sequelize.STRING,
  11. avatar: db.Sequelize.STRING,
  12. status: db.Sequelize.STRING,
  13. last_login_time: db.Sequelize.DATE,
  14. last_login_ip: db.Sequelize.STRING,
  15. create_time: db.Sequelize.DATE
  16. }, {
  17. tableName:'blog_user',
  18. timestamps:true,
  19. createdAt : 'create_time',
  20. updateAt : false,
  21. deletedAt : false
  22. });
  23. userModel.checkLogin = async function(account, password){
  24. let result = await User.findOne({where:{account : account}});
  25. if (result && result.uid) {
  26. if (result.password != password) {
  27. return {error : '密码不正确', data : false};
  28. }
  29. if (result.status != 'ENABLED') {
  30. return {error : '该账号尚未启用', data : false};
  31. }
  32. return {error : null, data : result};
  33. } else {
  34. return {error : '该账号不存在', data : false};
  35. }
  36. };
  37. module.exports = userModel;

注:

  • sequelize.define()一般有三个参数,第一个参数为模型名,第二个参数为各字段定义及字段类型定义的对象,第三个参数是表相关的属性
  • tableNmae表示数据库表的名称
  • timestamps指定插入、更新和删除的时候是否记录操作时间
  • createdAt表示记录插入时间的字段名(如果不指定或不设置为false,默认为createdAt)
  • updatedAt表示修改插入的字段名(如果不指定或不设置为false,默认为updatedAt)
  • deletedAt表示删除插入的字段名(如果不指定或不设置为false,默认为deletedAt)

特别提醒:

  • async和await是node.js7的关键词,并且要启用才可以,在启动时加上:node —harmony-async-await app.js。
  • aysnc和await是成对出现的,在函数内部调用await则,函数必须声明为async
2、在入口文件中添加必要中间件
(1)添加bodyParser中间件

bodyParser用于解析客户端请求的body中的内容,内部使用JSON编码处理,url编码处理以及对于文件的上传处理.

安装body-parser

  1. npm install body-parser --save

在入口文件中启用body-parser中间件

  1. const bodyParser = require('body-parser');
  2. //解析json请求数据
  3. app.use(bodyParser.json());
  4. //解析urlencoded过的数据
  5. app.use(bodyParser.urlencoded({extened : true}));
(2)添加session中间件

安装express-session中间件

  1. npm install express-session

在入口文件中加入session

  1. const session = require('express-session');
  2. app.use(session({secret : 'shixinke', resave : true, saveUninitialized : false, cookie : {secure : false}}));
  • secret:表示用于cookie加密的字符串
  • resave是指每次请求都重新设置session cookie
  • saveUninitialized: 是指无论有没有session cookie,每次请求都设置个session cookie
  • cookie设置cookie相关的信息(secure:false表示不是https应用)
3、添加一些公共的函数

在文件libs/helper.js中:

(1)md5加密函数:
  1. const conf = require('./conf.js');
  2. const crypto = require('crypto');
  3. helper.md5 = function(str, saltKey){
  4. let salt;
  5. salt = saltKey ? saltKey : conf.security.salt;
  6. let md5sum = crypto.createHash('md5');
  7. md5sum.update(str+salt);
  8. str = md5sum.digest('hex');
  9. return str;
  10. };
(2)判断某个元素是否在数组中:
  1. helper.inArray = function(ele, arr){
  2. let isArr = false;
  3. if (arr && typeof arr == 'object' && arr.constructor === Array) {
  4. isArr = true;
  5. }
  6. if (!isArr) {
  7. return false;
  8. }
  9. let len = arr.length;
  10. for(let i=0;i < len; i++){
  11. if(ele == arr[i] ){
  12. return true;
  13. }
  14. }
  15. return false;
  16. };
4、在路由(控制器)中实现登录

文件为routes/console/login.js

  1. const login = {};
  2. const userModel = require('../../models/user.js');
  3. const helper = require('../../libs/helper.js');
  4. login.index = function(req, res){
  5. res.render('login/index', {title : '管理登录' layout : false});
  6. };
  7. login.checkLogin = async function(req, res){
  8. let result = {};
  9. //接收请求参数
  10. let account = req.body.account;
  11. let password = req.body.password;
  12. //简单判断参数的合法性
  13. if (!account || account == '') {
  14. res.json({code : 5001, message : '请输入账号'});
  15. }
  16. if (!password || password == '') {
  17. res.json({code : 5001, message : '请输入密码'});
  18. }
  19. password = helper.md5(password);
  20. try {
  21. result = await userModel.checkLogin(account, password);
  22. } catch (e) {
  23. result.error = '未找到数据';
  24. result.data = false;
  25. }
  26. if (result.error) {
  27. res.json({code : 5003, message : result.error});
  28. } else {
  29. let userInfo = {};
  30. userInfo = result.data.dataValues;
  31. //存入到session中
  32. req.session.user = userInfo;
  33. res.json(200, message : '登录成功', data:{url : '/console/index'}});
  34. }
  35. };
5、登录权限控制判断

如果在每个后台路由中判断是否登录的话,可能功能有点重复,现在可以使用中间件来实现,不过这种方式是否合理,还值得商榷。

(1)在配置中增加一个是不需要判断登录的路径,这些路径不用判断是事登录
  1. module.exports = {
  2. security : {
  3. withoutLogin : ['/login', '/register', '/login/checkLogin']
  4. }
  5. }
(2)在入口文件中判断中,加入登录权限控制
  1. backend.use(function(req, res, next){
  2. //获取session信息
  3. let userInfo = req.session.user;
  4. //判断session是否存在
  5. if ((!userInfo || !userInfo.uid) && !helper.inArray(req.path, conf.security.withoutLogin)) {
  6. res.redirect('/console/index');
  7. } else {
  8. next();
  9. }
  10. });
  11. app.use('/console', backend);

6、成果展示: