总体使用的是MVC的设计模式来进行的
nodejs:使用原生nodejs提供的各种模块
mysql:使用mysql包来实现nodejs读取mysql数据库中的数据
redis:使用redis来存储session的数据
cross-env:项目运行的环境变量
默认已经会了上述的几个包的使用
这是一个比较小的项目,数据库中存储的数据只有两类,用户以及博客
用户表(users)
字段名 | 字段类型 | 是否主键 | 描述 |
---|---|---|---|
id | int | Primary Key | 自增,非空 |
username | varchar(20) | 用户名,非空 | |
password | varchar(20) | 密码,非空 | |
realname | varchar(10) | 真名,非空 |
博客表(blogs)
字段名 | 字段类型 | 是否主键 | 描述 |
---|---|---|---|
id | int | Primary Key | 自增,非空 |
title | varchar(50) | 标题,非空 | |
content | longtext | 博客内容,非空 | |
createtime | bigint | 创建时间,时间戳,非空 | |
author | varchar(20) | 作者,非空 |
node-blog
|----bin
|---- www.js
|----node_modules
|----src
|----config
|----db.js
|----controller
|----blog.js
|----user.js
|----db
|----mysql.js
|----redis.js
|----model
|----resModel.js
|----router
|----blog.js
|----user.js
|----app.js
|----package.json
数据配置及获取
db.js 数据库的配置文件(mysql和redis)
// 该项目是模拟实际的开发情形,因此我们需要根据不同的运行环境来进行区分不同的配置,当然在这里我们其实只有一种运行环境,那就是本地环境,但是我们写的需要规范
const env = process.env.NODE_ENV // 环境参数
let MYSQL_CONF
let REDIS_CONF
// 本地环境
if (env === 'dev') {
// mysql 配置
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: 'root',
database: 'myblog',
port: 3306
}
// redis 配置
REDIS_CONF = {
port: 6379,
host: '127.0.0.1'
}
}
// 线上环境
if (env === 'production') {
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: 'root',
database: 'myblog',
port: 3306
}
// redis 配置
REDIS_CONF = {
port: 6379,
host: '127.0.0.1'
}
}
module.exports = {
MYSQL_CONF,
REDIS_CONF
}
mysql
mysql.js数据库操作(Model层)
const mysql = require('mysql')
const { MYSQL_CONF } = require('../config/db')
const con = mysql.createConnection(MYSQL_CONF)
// 开始连接
con.connect()
// 统一执行sql的函数
// 可能会疑惑这里没有数据库的关闭操作,是不是不安全,因为我们这里是通过promise操作的,如果这里我们关闭了数据库,后面就无法获取数据,会报错
function exec(sql) {
const promise = new Promise((resolve, reject) => {
con.query(sql, (err, result) => {
if (err) return reject(err)
return resolve(result)
})
})
return promise
}
module.exports = {
exec
}
在实际开发中其实可以用class和单例模式结合的方式来进行控制,保证只有一个实例访问就行了
所谓class和单例模式结合就是:执行构造函数的时候进行判断,如果构造函数已经执行则不再执行
使用es6提供的static 来创建静态方法
redis
在redis中存储的数据是键值对的方式,
redis.js
const redis = require("redis")
const { REDIS_CONF } = require('../config/db')
const redisClient = redis.createClient(REDIS_CONF)
redisClient.on('error', err => {
console.error(err);
})
function set(key, val) {
if (typeof val === 'object') {
val = JSON.stringify(val)
}
redisClient.set(key, val, redis.print)
}
function get(key) {
const promise = new Promise((resolve, reject) => {
redisClient.get(key, (err, val) => {
if (err) return reject(err)
// console.log(val)
if (val == null) {
return resolve(null)
}
try {
resolve(JSON.parse(val))
} catch (error) {
resolve(val)
}
})
})
return promise
}
module.exports = {
set, get
}
用户登录
/controller/user.js(Controller层)
这部分就是根据用户名和密码通过sql语句去数据库中查询,返回响应数据
const { exec } = require('../db/mysql')
const login = (username, password) => {
const sql = `select username,realname from users where username='${username}' and password = ${password}`
return exec(sql).then(rows => {
// console.log(rows[0])
return rows[0] || {}
})
}
module.exports = {
login
}
/router/user.js (路由)
const { login } = require('../controller/user')
const { SuccessModel, ErrorModel } = require('../model/resModel')
const { set } = require('../db/redis')
const handleUserRouter = (req, res) => {
const method = req.method
// 登录
if (method === 'POST' && req.path === "/api/user/login") {
const { username, password } = req.body
const result = login(username, password)
return result.then(data => {
if (data.username) {
// 设置session
req.session.username = data.username
req.session.realname = data.realname
// 每次登陆成功后需要把用户信息存储到Redis中去,这样就算服务器重启也不会影响之前的登录信息,因为redis和后端服务器也是分离的
set(req.sessionId, req.session)
return new SuccessModel()
}
return new ErrorModel('用户登录失败')
})
}
}
module.exports = handleUserRouter
博客管理
/controller/blog.js (Controller层)
const { exec } = require('../db/mysql')
const { get } = require('../db/redis')
const getSession = (sessionId) => {
return get(sessionId).then(session => {
return JSON.parse(session) || {}
})
}
// 这里where 1 = 1 是一个取巧的操作,这个操作既不会影响我们获取的数据,同时也可以简单了我们后面拼接其他条件,不然的话还需要在今天是否要加where的判断
const getList = (author = '', keyword = '') => {
let sql = `select * from blogs where 1=1 `
if (author) {
sql += `and author='${author}' `
}
if (keyword) {
sql += `and title like '%${keyword}%' `
}
sql += `order by createtime desc`
// 返回一个promise
return exec(sql)
}
const getDetail = (id) => {
// 返回假数据
const sql = `select * from blogs where id = ${id}`
return exec(sql).then(rows => {
return rows[0]
})
}
const newBlog = (blogData = {}) => {
// blogData 是一个博客对象,包含title、 content 、author属性
const title = blogData.title
const content = blogData.content
const author = blogData.author
const createtime = Date.now()
const sql = `insert into blogs (title,content,createtime,author) values('${title}','${content}',${createtime},'${author}')`
return exec(sql).then(insertData => {
return { id: insertData.insertId }
})
}
const updataBlog = (id, blogData = {}) => {
// id 要更新博客的id
// blogdata 是一个博客对象,包含title content属性
const title = blogData.title
const content = blogData.content
const sql = `update blogs set title = '${title}' , content = '${content}' where id = ${id}`
return exec(sql).then(updateData => {
// console.log(updateData)
if (updateData.affectedRows > 0) {
return true
}
return false
})
}
const delBlog = (id, author) => {
// id 是删除博客的id
const sql = `delete from blogs where id = ${id} and author = '${author}'`
return exec(sql).then(deleteData => {
if (deleteData.affectedRows > 0) {
return true
}
return false
})
}
module.exports = {
getList,
getDetail,
newBlog,
updataBlog,
delBlog,
getSession
}
都是一些增删改查的操作,自己看吧
/router/blog.js (路由)
登录检查是为了保证用户只能对自己的blog进行修改删除增加
const {
getList,
getDetail,
newBlog,
updataBlog,
delBlog,
getSession
} = require('../controller/blog') // 解构赋值的方式直接取相应的方法
const { SuccessModel, ErrorModel } = require('../model/resModel')
// 统一的登录验证函数
// 去查看之前的登录状态,这里就简单判断了用户名是否存在
const loginCheck = (req) => {
if (!req.session.username) {
return Promise.resolve(new ErrorModel('尚未登录'))
}
}
const handleBlogRouter = (req, res) => {
const method = req.method
const id = req.query.id
// 获取博客列表
if (method === 'GET' && req.path === '/api/blog/list') {
let author = req.query.author || ''
const keyword = req.query.keyword || ''
// 这里的操作是为了让用登录后查看的是自己的列表在admin.html页面的时候
if (req.query.isadmin) {
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 如果有值表示未登录
return loginCheckResult
}
author = req.session.username
}
// 调用方法获取博客列表
const result = getList(author, keyword)
return result.then(listData => {
return new SuccessModel(listData)
})
}
// 获取博客详情
if (method === "GET" && req.path === '/api/blog/detail') {
const result = getDetail(id)
return result.then(data => {
return new SuccessModel(data)
})
}
// 新建一篇博客
if (method === "POST" && req.path === "/api/blog/new") {
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 如果有值表示未登录
return loginCheckResult
}
req.body.author = req.session.username
console.log(req.session.username)
const result = newBlog(req.body)
return result.then(data => {
return new SuccessModel(data)
})
}
// 更新一篇博客
if (method === "POST" && req.path === "/api/blog/update") {
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 如果有值表示未登录
return loginCheckResult
}
const result = updataBlog(id, req.body)
return result.then(val => {
if (val) {
return new SuccessModel()
} else {
return new ErrorModel('更新博客失败')
}
})
}
// 删除一篇博客
if (method === "POST" && req.path === "/api/blog/del") {
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 如果有值表示未登录
return loginCheckResult
}
const author = req.session.username
console.log(id, author)
const result = delBlog(id, author)
return result.then(val => {
if (val) {
return new SuccessModel()
} else {
return new ErrorModel('删除博客失败')
}
})
}
}
module.exports = handleBlogRouter
其他代码
app.js(这个才是真正的入口,www.js其实就是启动一下服务器)
const urlObj = require('url')
const handleBlogRouter = require("./src/router/blog")
const handleUserRouter = require("./src/router/user")
const { set, get } = require('./src/db/redis')
// 获取cookie的过期时间
const getCookieExpires = () => {
const d = new Date()
d.setTime(d.getTime() + (24 * 60 * 60 * 1000))
// console.log(d.toGMTString())
return d.toGMTString()
}
// 用于处理post data
const getPostData = (req) => {
const promise = new Promise((resolve, reject) => {
if (req.method !== "POST") {
return resolve({})
}
if (req.headers['content-type'] !== 'application/json') {
return resolve({})
}
let postData = ''
req.on('data', chunk => {
postData += chunk.toString()
})
req.on('end', () => {
// console.log(postData)
if (!postData) return resolve({})
return resolve(JSON.parse(postData))
})
})
return promise
}
// 设置返回格式 JSON
const serverHandle = (req, res) => {
res.setHeader('content-type', 'application/json')
req.path = urlObj.parse(req.url, true).pathname
// console.log(req.url) /api/blog/list?author=zhangsan&keyword=A
// 获取请求参数,增加true后会转换成一个对象
req.query = urlObj.parse(req.url, true).query
// 处理cookie
// 因为cookie是也是一些键值对的方式,但是是字符串的形式,因此需要做如下处理
req.cookie = {}
const cookieStr = req.headers.cookie || ''
cookieStr.split(';').forEach(item => {
if (!item) return
const arr = item.split('=')
const key = arr[0].trim()
const val = arr[1].trim()
// console.log(key, val)
req.cookie[key] = val
})
// 解析session
let needSetCookie = false
let userId = req.cookie.userid
req.sessionId = userId
// 登录状态的保持,每次进行路由前会去判断一下用户之前是否登录了(如果执行一些增删改的操作)
// 从redis中去获取数据,类似数据库的获取操作,因为这是一个异步的操作,因此我们就需要把后续的操作放到then里去保证我之前的数据已经获取了(用户信息)
get(req.sessionId).then(sessionData => {
if (sessionData == null) {
set(req.sessionId, {})
req.session = {}
}
else {
req.session = sessionData
}
// 处理post数据
return getPostData(req)
}).then(postData => {
req.body = postData
const blogResult = handleBlogRouter(req, res)
if (blogResult) {
blogResult.then(blogData => {
// 第一次请求的时候就把cookie设置了响应回去
if (needSetCookie) {
res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly;expires=${getCookieExpires()}`)
}
res.end(JSON.stringify(blogData))
})
return
}
const userResult = handleUserRouter(req, res)
if (userResult) {
userResult.then(userData => {
if (needSetCookie) {
res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly;expires=${getCookieExpires()}`)
}
res.end(JSON.stringify(userData))
})
return
}
// 未命中路由 返回404
res.writeHead(404, {
'content-type': 'text/plain'
})
res.end("404 Not Found\n")
})
}
module.exports = serverHandle
resModel.js
这个文件是为了设置响应数据的格式
class BaseModel {
/**
* 构造函数
* @param {Object} data 数据
* @param {string} message 信息
*/
constructor(data, message) {
if (typeof data === 'string') {
/*
做参数兼容,如果没有出入message,
那么直接把data赋给message
*/
[data, message] = [message, data]
}
if (data) this.data = data
if (message) this.message = message
}
}
class SuccessModel extends BaseModel {
constructor(data, message) {
super(data, message)
this.errno = 0
}
}
class ErrorModel extends BaseModel {
constructor(data, message) {
super(data, message)
this.errno = -1
}
}
module.exports = {
SuccessModel,
ErrorModel
}
www.js
创建服务器
const http = require('http')
const serverHandle = require('../app')
const PORT = 8000
const server = http.createServer(serverHandle)
server.listen(PORT)
package.json
{
"name": "node-blog",
"version": "1.0.0",
"description": "",
"main": "bin/www.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
//这里是配置的一些环境,本地环境
"dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
// 线上环境
"prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cross-env": "^5.2.0",
"mysql": "^2.17.1",
"redis": "^2.8.0"
}
}
Nginx介绍
为什么会需要反向代理呢?
因为我们现在运行的在两个不同的地址中
web服务器 http://localhost:8001
nodejs服务 http://localhost:8000
这就会导致一个问题那就是 “跨域”,当然处理跨域的方式有很多种,这里我们就通过使用Nginx的反向代理来实现
还有一个原因就是cookie存在跨域不共享,所以就需要使用反向代理
反向代理说明
其实就是在服务器访问web服务器和后端服务器的时候,先通过Nginx来作为中介,以localhost/index.html为例,Nginx会判断路径是哪个,如果是 /…的就把你导向web服务器,如果是请求接口的就导向nodejs后端服务器
自行下载安装后,在安装的文件中有个conf文件夹,我们需要对立面的nginx.conf文件进行配置
我们用VSCode直接打开文件就行,然后如下配置,上面有个port(端口)自己配置就行,不要是被使用的端口就可以。注意这里不是js中的对象,不要习惯性的用冒号来进行赋值,是用空格的
可以在通过执行 nginx -t 来测试配置的是否有问题,如果没报错就没问题了
然后直接输入nginx.exe 启动就行,不要关掉
因为这是nodejs的项目,所以HTML页面就不再这里进行贴了,大家自己简单的写一写就行,数据展示以及ajax数据的请求,相信这对于看这个的小伙伴来说是信手拈来的
总共就6个页面,每个页面都不超过100行的代码(反正我是这样的,怎么简单怎么来,再丑也是自己看的,主要是关注nodejs的功能)
index.html 用于展示所有博客信息
detail.html 用于展示博客详情
admin.html 用于用户自己管理博客
new.html 用于新增博客
edit.html 用于编辑博客
login.html 用于登录博客
说明
根据自己再Nginx中配置的端口,直接在浏览器中运行,我是配置了8080,因此就直接http://localhost:8080/index.html
运行的时候需要保持数据是联通的、Nginx是开启的、redis也是开启的,不然无法跑起来
我把项目里的node_modules删了,大家自己npm install一下就行,反正有package.json文件
nodejs的文件 在终端输入
意思是在本地运行,这个在package.json中进行配置了
npm run dev
也许我在这里的一些表述可能不够准确,如果有错误欢迎提出来,我会改的~~大家如果也尝试过这个后会发现用原生Nodejs来写项目好麻烦啊,so 后面会用express和Koa来重新写一遍这个项目
最后附上一下代码吧,想看的可以看,nodejs的基本上都已经贴了,也就HTML页面了
代码传送门~咻