NodeJs项目
功能描述
- 用户登录(基于cookie、session和redis实现登录)
- 博客的创建、编辑、删除、查询(基于mysql实现数据的增删改查)
- 通过Nginx的反向代理来解决跨域问题
总体使用的是MVC的设计模式来进行的
项目依赖
nodejs:使用原生nodejs提供的各种模块
mysql:使用mysql包来实现nodejs读取mysql数据库中的数据
redis:使用redis来存储session的数据
cross-env:项目运行的环境变量
默认已经会了上述的几个包的使用
安装需要
- mysql数据库来存储项目数据
- 需要安装redis来处理session数据的存储
- 需要安装Nginx来处理反向代理
项目开发
数据库设计
这是一个比较小的项目,数据库中存储的数据只有两类,用户以及博客
用户表(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) | 作者,非空 |
项目结构设计
- 目录结构
- www.js是服务器的创建
- app.js是服务器处理程序
- router文件夹是路由模块
- config是数据库配置模块(mysql和redis)
- db在这里就是MVC中的M,用于数据处理
- controller是MVC中的C,用户数据与视图的衔接处理
- model文件夹这里只是用于处理响应的数据,是数据模型
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反向代理
Nginx介绍
- 高性能的Web服务器
- 一般用于做静态服务器、负载均衡(我们暂时用不到)
- 反向代理(我们这里要用)
为什么会需要反向代理呢?
因为我们现在运行的在两个不同的地址中
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页面了