利用Express框架建立博客

项目文件夹结构

建立项目所需文件夹

  • public 静态资源
    样式文件、脚本文件、图片文件
  • model 数据库操作
    创建集合的文件和连接数据库的文件
  • route 路由
    路由文件
  • views 模板
    模板文件
  • config
    不同运行环境下的应用配置信息抽离到单独的文件中
  • middleware
    登录拦截

route 文件夹:(模块化路由)

route/home 文件夹

route/home/article.js
route/home/comment.js
route/home/index.js

route/admin 文件夹

route/admin/article.js
route/admin/article-add.js
route/admin/article-edit.js
route/admin/login.js
route/admin/loginPage.js
route/admin/logout.js
route/admin/user-edit.js
route/admin/user-edit-fn.js
route/admin/user-modify.js
route/admin/userPage.js

route 文件夹下的文件

route/home.js
描述:博客展示页面路由

//引用express框架
const express = require('express');
//创建博客展示页面路由(二级路由)
const home = express.Router();

将回调函数放入route文件夹下的home文件夹中

//博客前台首页的展示页面+路由优化
home.get('/', require('./home/index'));
//将路由对象作为模块成员进行导出
module.exports = home;

route/admin.js
描述:博客管理页面路由

//引用express框架
const express = require('express');
//创建博客展示页面路由
const admin = express.Router();
//将路由对象作为模块成员进行导出
module.exports = admin;

回调函数在route文件夹下的admin文件夹中

//渲染登录页面
admin.get('/login', require('./admin/loginPage'));

:res.render()方法中第一个参数要传递模板路径,默认情况下要传递绝对路径。但考虑到渲染模板有很多所以需要在app.js中进行配置---(标注1)

//实现登入功能
admin.post('/login', require('./admin/login'));
//创建用户列表路由
admin.get('/user', require('./admin/userPage'));
//实现退出功能
admin.get('/logout', require('./admin/logout'));
//创建用户编辑页面路由
admin.get('/user-edit', require('./admin/user-edit'));
//创建实现用户添加功能路由
admin.post('/user-edit', require('./admin/user-edit-fn'));
//创建用户信息修改功能路由
admin.post('/user-modify', require('./admin/user-modify'));
//删除用户功能路由
admin.get('/delete', require('./admin/user-delete'));
//文章列表页面路由
admin.get('/article', require('./admin/article'));
//文章编辑页面路由
admin.get('/article-edit', require('./admin/article-edit'));
//实现文章添加功能的路由
admin.post('/article-add', require('./admin/article-add'));

views 文件夹:(模板文件)

views/home 文件夹

views/home/common文件夹

views/home/common/header.art
views/home/common/layout.art

home 文件夹下的文件:

views/home/article.art
views/home/default.art

首页文章实现简介

//html标签替换为空 /对字符串进行截取 /原文输出
{{@$value.content.replace(/<[^>]+>/g,'').substr(0,90) + '...'}}

views/admin 文件夹

views/admin/common文件夹(模板的公共部分)

子模版的相对路径是相对于当前文件的(由模板引擎解析)

views/admin/common/aside.art(侧边栏)
views/admin/common/header.art(头部)
views/admin/common/layout.art(HTML骨架)

views/admin 文件夹下的文件:
模板内部外链资源文件问题(外链资源是由浏览器解析的):
外链资源即模板内部外链的css\js\img文件。需要注意的是模板文件中的外链资源的相对路径是相对与浏览器地址栏中的请求路径的,如:http://localhost/admin/login 地址,浏览器认为login为路径下的文件,/admin为请求路径,所以模板中的相对路径是相对与/admin的。
为保证外链资源能够正常被访问,应该使用绝对路径。由于在app.js中开放了静态资源(public文件夹),所以使用绝对路径访问的是public文件夹下的资源,
如:

"css/base.css"相对路径前加上了/admin/

views/admin/login.art
为登录表单设置请求地址及请求方式

为表单项设置name属性如:

注:要阻止表单默认提交行为


// 在public/admin/js/common.js中
function serializeToJson(form) {
    var result = {};
    //.serializeArray()是JQury提供的方法 获取到表单中用户输入的内容
    //[{name: "email", value: "uwhdb@gv"},{name: "password", value: "1223"}]返回内容是一个数组
    var f = form.serializeArray();
    f.forEach(function(item) {
        result[item.name] = item.value;
    });
    return result;
}
views/admin/user.art
views/admin/user-edit.art
views/admin/error.art
views/admin/article.art
views/admin/article-edit.art

model 文件夹:

model/connect.js
连接数据库

//引入mongoose第三方模块
const mongoose = require('mongoose');
//连接数据库
mongoose.connect('mongodb://localhost/blog', { useNewUrlParser: true, useUnifiedTopology: true }).then(() => console.log('数据库连接成功')).catch(() => console.log('数据库连接失败'));
//替换连接数据库
//在为数据库添加账号后需要使用账号和密码
//27017端口可以默认不写
mongoose.connect('mongodb://Emrof:123456@localhost:27017/blog', { useNewUrlParser: true, useUnifiedTopology: true }).then(() => console.log('数据库连接成功')).catch(() => console.log('数据库连接失败'));

在development.json中

{
    "db": {
        "user": "Emrof",
        "pwd": "123456",
        "host": "localhost",
        "port": "27017",
        "name": "blog"
    }
}
//替换连接数据库
//config
mongoose.connect(`mongodb://${config.get('db.user')}:${config.get('db.pwd')}@${config.get('db.host')}:${config.get('db.port')}/${config.get('db.name')}`, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => console.log('数据库连接成功')).catch(() => console.log('数据库连接失败'));

model/user.js

//引入mongoose第三方模块
const mongoose = require('mongoose');
//创建用户集合规则
const userSchema = new mongoose.Schema({});
//创建用户集合
const User = mongoose.model('User', userSchema);
//ES6中如果对象的键和值名称一样 可以省略值
module.exports = {User,validateUser};

model/article.js

//引入mongoose第三方模块
const mongoose = require('mongoose');
//创建文章集合规则
const articleSchema = new mongoose.Schema({})
//根据规则创建集合
const Article = mongoose.model('Article', articleSchema);
//将集合规则作为模块成员进行导出
module.exports = {Article}

model/comment.js

//引入mongoose第三方模块
const mongoose = require('mongoose');
const commentSchema = new mongoose.Schema({})
//创建评论集合
const Comment = mongoose.model('Comment', commentSchema);
//将模块集合构造函数作为模块成员进行导出
module.exports = {Comment}

根目录下的文件
package.json文件
可以通过初始化项目描述文件得到

//命令行中输入
npm init -y

app.js文件
描述:项目入口文件
app.js文件需要进行的操作:

//引用express框架
const express = require('express');
//创建网站服务器
const app = express();
//监听端口
app.listen(80);
console.log('网站服务器启动成功');

配置路由

//导入路由模块
const home = require('./route/home');
const admin = require('./route/admin');
//为路由匹配请求路径
app.use('/home', home);
app.use('/admin', admin);

配置静态资源

//引入path系统模块
const path = require('path');
//开放静态资源文件
app.use(express.static(path.join(__dirname, 'public')));

模板配置--对本文中标注1进行解释

//当渲染后缀为art的模板时 所使用的模板引擎是什么 以便使用res.render()方法
app.engine('art', require('express-art-template'));
//告诉express框架模板所在位置
app.set('views', path.join(__dirname, 'views'));
//告诉express框架模板的默认后缀
app.set('view engine', 'art');
//数据库连接
require('./model/connect');
//引入body-parser模块 用来处理post请求参数
const bodyParser = require('body-parser');
//处理post请求参数
//extended: false表示使用系统模块querystring解析 而不是其他第三方模块
app.use(bodyParser.urlencoded({ extended: false }));
//导入express-session模块
const session = require('express-session');
//配置session secret的值可以自定义
//saveUninitialized false 没有登录的情况下不保存cookie
// cookie: {maxAge: 24 * 60 * 60 * 1000} 设置cookie的过期时间
app.use(session({secret: 'secret key',saveUninitialized: false,cookie: {maxAge: 24 * 60 * 60 * 1000}}));
//拦截请求 判断用户登录状态
//use的第一个参数/admin是指以/admin开头的路由
app.use('/admin', require('./middleware/loginGuard'));

由于在向模板中使用dateformat所以要在导入dateformat之前导入art-template

//导入art-template模板引擎
const template = require('art-template');
//使用dateformat第三方模块对日期进行处理
const dateFormat = require('dateformat');
//向模板内部导入dateFormat变量
template.defaults.imports.dateFormat = dateFormat;
//导入morgan第三方模块
const morgan = require('morgan');
//process.env 获取系统环境变量 返回值是对象
if (process.env.NODE_ENV == 'development') {
    //当前是开发环境
    console.log('当前是开发环境');
    //在开发环境中 将客户端发送到服务器端的请求信息打印到控制台中
    app.use(morgan('dev'));
} else {
    console.log('当前是生产环境');
    // 当前是生产环境
}
//导入config模块
const config = require('config');
//读取配置信息title
//在开发环境中读取development.json中的title
//在生产环境中读取production.json中的title
//若development.json和production.json中不存在title则读取default.json
console.log(config.get('title'));

项目包含的知识点

密码加密 bcrypt

哈希加密是单程加密方式:1234 => abcd
在加密的密码中加入随机字符串可以增加密码被破解的难度。

// 导入bcrypt模块
const bcrypt = require('bcrypt');
// 生成随机字符串 gen => generate 生成 salt 盐
let salt = await bcrypt.genSalt(10);
// 使用随机字符串对密码进行加密
let pass = await bcrypt.hash('明文密码', salt);
// 密码比对
let isEqual = await bcrypt.compare('明文密码', '加密密码');

bcrypt依赖的其他环境

  1. python 2.x
    安装好后添加环境变量 也就是安装路径
    image.png
  2. node-gyp
    npm install -g node-gyp
  3. windows-build-tools
    npm install --global --production windows-build-tools
  4. bcrypt
    npm install bcrypt

cookie与session

网站应用是基于http协议,即请求与相应模型的应用。特点是:在完成一次客户端和服务器端的请求与响应后,客户端与服务端便没有联系了(http协议的无状态性)
cookie:浏览器在电脑硬盘中开辟的一块空间,主要供服务器端存储数据。

  • cookie中的数据是以域名的形式进行区分的。
  • cookie中的数据是有过期时间的,超过时间数据会被浏览器自动删除。如果没有设置过期时间,浏览器在关闭的时候就会删除。
  • cookie中的数据会随着请求被自动发送到服务器端。

image.png

session:实际上就是一个对象,存储在服务器端的内存中,在session对象中也可以存储多条数据,每一条数据都有一个sessionid做为唯一标识。

image.png

在node.js中需要借助express-session实现session功能。

const session = require('express-session');
app.use(session({ secret: 'secret key' }));
//为请求对象下面添加session属性
//secret key密钥值可以自定义

Joi

JavaScript对象的规则描述语言和验证器。

const Joi = require('joi');
const schema = {
username: Joi.string().alphanum().min(3).max(30).required().error(new Error(‘错误信息’)),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
access_token: [Joi.string(), Joi.number()],
birthyear: Joi.number().integer().min(1900).max(2013),
email: Joi.string().email()
};
Joi.validate({ username: 'abc', birthyear: 1994 }, schema);

数据分页

当数据库中的数据非常多时,数据需要分批次显示,这时就需要用到数据分页功能。
分页功能核心要素:

  1. 当前页,用户通过点击上一页或者下一页或者页码产生,客户端通过get参数方式传递到服务器端
  2. 总页数,根据总页数判断当前页是否为最后一页,根据判断结果做响应操作
总页数:Math.ceil(总数据条数 / 每页显示数据条数)
limit(2) // limit 限制查询数量  传入每页显示的数据数量
skip(1) // skip 跳过多少条数据  传入显示数据的开始位置
数据开始查询位置=(当前页-1)* 每页显示的数据条数
let page = req.query.page || 1;
//每一页显示的数据条数
let pagesize = 10;
//查询用户数据的总数 用户集合User下有countDocuments()方法
let count = await User.countDocuments({});
//总页数 向上取整
let total = Math.ceil(count / pagesize);
//页码对应的开始位置
let start = (page - 1) * pagesize;
// 将用户信息从数据库中查询出来
//渲染用户列表模板 
res.render('admin/user', {
    users: users,
    page: page,
    total: total
});

另一种分页方式
mongoose-sex-page 第三方模块

const pagination = require('mongoose-sex-page');
pagination(集合构造函数).page(1).size(20).display(8).exec();
//page()当前页
//size()每页显示的数据条数
//display()要显示的页码数量
//exec()向数据库发出查询请求

formidable

作用:解析表单,支持get请求参数,post请求参数、文件上传。

 // 引入formidable模块
 const formidable = require('formidable');
 // 创建表单解析对象
 const form = new formidable.IncomingForm();
 // 设置文件上传路径
 form.uploadDir = "/my/dir";
 // 是否保留表单上传文件的扩展名
 form.keepExtensions = ture;
 // 对表单进行解析
 form.parse(req, (err, fields, files) => {
// fields 存储普通请求参数
// files 存储上传的文件信息
 });

文件读取 FileReader

    var file = document.querySelector('#file');
    var preview = document.querySelector('#preview');
    //当用户选择完文件以后
    file.onchange = function() {
        //创建文件读取对象
        var reader = new FileReader();
        //用户选择的文件列表 this.files
        // this.files[0] 在一组用户中选择第一个文件
        //读取文件  .readAsDataURL()是异步方法
        reader.readAsDataURL(this.files[0]);
        //监听onload
        reader.onload = function() {
            //将文件读取的结果显示在页面中
            preview.src = reader.result;
        }
    }

mongoDB数据库添加账号

  1. 以系统管理员的方式运行powershell
  2. 连接数据库
    mongo
  3. 查看数据库
    show dbs
  4. 切换到admin数据库
    use admin
  5. 创建超级管理员账户
    db.createUser()
    如:db.createUser({user:'root',pwd:'root123',roles:['root']})
  6. 切换到blog数据
    use blog
  7. 创建普通账号
    db.createUser()
    如:db.createUser({user:'Emrof',pwd:'123456',roles:['readWrite']})
    退出exit
  8. 卸载mongodb服务
    停止服务 net stop mongodb
    mongod --remove
  9. 创建mongodb服务
mongod --logpath=
"C:\Program Files\MongoDB\Server\4.4\log\mongod.log" 
--dbpath=
"C:\Program Files\MongoDB\Server\4.4\data" --install –-auth
//–-auth 表示验证,输入后mongoDB必须在输入账号和密码后才能登录
  1. 启动mongodb服务
    net start mongodb
  2. 在项目中使用账号连接数据库
    mongoose.connect('mongodb://user:pass@localhost:port/database')

开发环境与生产环境

什么是开发环境与生产环境

环境,就是指项目运行的地方,当项目处于开发阶段,项目运行在开发人员的电脑上,项目所处的环境就是开发环境。当项目开发完成以后,要将项目放到真实的网站服务器电脑中运行,项目所处的环境就是生产环境。

为什么要区分开发环境与生产环境

因为在不同的环境中,项目的配置是不一样的,需要在项目代码中判断当前项目运行的环境,根据不同的环境应用不同的项目配置。

如何区分开发环境与生产环境

通过电脑操作系统中的系统环境变量区分当前是开发环境还是生产环境。
image.png

if (process.env.NODE_ENV == 'development') {
// 开发环境
} else {
// 生产环境
}

在开发环境中把客户端向服务器端发送的请求信息打印到控制台中
需要用到第三方模块morgan,morgan也是express的中间件函数

第三方模块config

作用:允许开发人员将不同运行环境下的应用配置信息抽离到单独的文件中,模块内部自动判断当前应用的运行环境,
并读取对应的配置信息,极大提供应用配置信息的维护成本,避免了当运行环境重复的多次切换时,手动到项目代码
中修改配置信息

使用步骤

  1. 使用npm install config命令下载模块
  2. 在项目的根目录下新建config文件夹
  3. 在config文件夹下面新建default.json、development.json、production.json文件
  4. 在项目中通过require方法,将模块进行导入
  5. 使用模块内部提供的get方法获取配置信息

将敏感配置信息存储在环境变量中
在config文件夹中建立custom-environment-variables.json文件
配置项属性的值填写系统环境变量的名字
项目运行时config模块查找系统环境变量,并读取其值作为当前配置项属于的值

image.png


零碎知识点

//根据app.js文件中const app = express();
//请求对象下面有一个app属性 也就是创建网站服务器的app
req.app
//把公共数据暴露到模板中
app.locals
//express框架下的页面重定向方法
res.redirect('');
//.redirect除了重定外 还做了res.end这步操作 所以要return
//不然命令行会报错
return res.redirect(`/admin/user-edit?message=邮箱地址已经被占用`);
//模板语法@表示原文输出
{{@$value._id}}
//express框架中的错误处理中间件
app.use((err, req, res, next) => {
    //将字符串对象转换为对象类型
    //JSON.parse()
    const result = JSON.parse(err);
    let params = [];
    for (let attr in result) {
        if (attr != 'path') {
            params.push(attr + '=' + result[attr]);
        }
    }
    // res.redirect(`${result.path}?message=${result.message}`)
    res.redirect(`${result.path}?${params.join('&')}`);
})
//触发错误处理中间件
try {
    //代码优化 请求验证处理代码放在了user.js中
    await validateUser(req.body);
} catch (err) {
    //验证没有通过
    //next()里只能传递一个字符串参数
    //JSON.stringify()将对象数据类型转换为字符串数据类型
    //代码优化 错误处理优化
    return next(JSON.stringify({ path: '/admin/user-edit', message: err.message }))
}
//减号有隐式类型转换 而加号为字符串拼接

如果表单要进行文件上传,表单的数据必须以二进制的数据上传

由于body-paser只能处理普通表单传递过来的参数,而不能处理二进制数据
使用要使用formidable 第三方模块(看项目包含的知识点)

字符串下有split字符串分割方法

//分隔符为public,返回值是一个数组,取数组的第二个值
files.cover.path.split('public')[1]

多集合联合查询

//根据id查询文章详细信息
let article = await Article.findOne({ _id: id }).populate('author');

以上方法遇到了问题,经查询,在此处获得了答案https://www.zhihu.com/questio...
所以采用了以下方法

//根据id查询文章详细信息
let article1 = await Article.findOne({ _id: id }).populate('author');
let article2 = JSON.stringify(article1);
let article = JSON.parse(article2);

日期格式的处理
使用了第三方模块 dateformat

{{dateFormat($value.publishDate,'yyyy-mm-dd')}}

项目所需的第三方模块

//命令行中输入
npm install express mongoose art-template express-art-template
在express中接收post请求参数需要用到第三方模块body-paeser
//命令行中输入
npm install body-parser
npm install -g node-gyp
npm install --global --production windows-build-tools
npm install bcrypt
npm install express-session
npm install joi
npm install formidable
npm install dateformat
npm install mongoose-sex-page
npm install morgan
npm install config

你可能感兴趣的:(前端node.js)