基础准备:
1.NodeJs是一个非阻塞IO,单线程的,运行在服务端的JavaScript平台,基于Google的V8引擎
2.NodeJs使用事件驱动模型,采用的是观察订阅模式,实现在event模块下
3.Nodejs有很多模块,采用npm管理包,比如node和mysql,redis对接,都需要用到相应的包
4 mysql是关系型数据库,我们在本地计算机的某个端口,起一个mysql服务,在node中监听这个端口
5.redis是是一个key-value的存储系统,存在内存里,断电存储消失,适合做登录,同样我们在本地计算机起一个redis服务
整体框架:
1.前端通过http-server的npm插件起一个前端http服务器,返回浏览器博客的前端页面,用jquery发送ajax请求 前端的请求包括有:GET,POST两种,POST用于更新博客内容,删除博客,用户登录,前端的服务器端口在8001
2.服务器同样用http的createServer方法起一个http服务器,端口在8000
3.用户的登录使用redis保存session,session和cookie验证用户是否登录
4.博客的保存使用mysql数据库
5.使用nodejs的http模块处理网络请求,使用fs模块处理保存日志
6.使用koa2框架重构,编写自定义的中间件
7.使用nginx代理
8.前端页面返回的数据以JSON对象表示,确定前端所需的数据格式和结构,不同路由返回的数据如何解析,数据格式是{data,errno}一个是数据
模块划分:
前端页面结构’
1.http请求处理模块:
使用nodejs原生的http模块来处理,首先在设置www.js为package.json中的npn run dev的入口文件
并使用nodemon实时监控文件的变化实现代码变更的热更新
const http = require('http')
const PORT = 8000
const serverHandle = require('../app')
const server = http.createServer(serverHandle)
server.listen(PORT)
后端起了一个http服务器,监听8000端口的http请求,我们在app.js中编写我们如何处理前端发过来
的http请求,使用注册回调的方式,在http.createServer(callback)中,我们传入一个callback,这个callback需要两个参数,一个是request,这两个都是对象,一个就是服务器返回给前端的response,http模块提供给处理http请求的的callback这两个参数
http.createServer((req,res) => {
//do something with req & res
})
前端来了一个http请求,它的目的就是从服务器返回它需要的数据,以JSON格式返回它需要的数据,分为Get和POST两种,GET请求负责请求数据,返回成功status为200。POST用于提交数据,fetch需要发送两次请求
serverHandle = (req,res) => {}如下
const serverHandle = (req, res) => {
// 记录 access log
access(`${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()}`)
// 设置返回格式 JSON
res.setHeader('Content-type', 'application/json')
// 获取 path
const url = req.url
req.path = url.split('?')[0]
// 解析 query
req.query = querystring.parse(url.split('?')[1])
// 解析 cookie
req.cookie = {}
const cookieStr = req.headers.cookie || '' // k1=v1;k2=v2;k3=v3
cookieStr.split(';').forEach(item => {
if (!item) {
return
}
const arr = item.split('=')
const key = arr[0].trim()
const val = arr[1].trim()
req.cookie[key] = val
})
// 解析 session (使用 redis)
let needSetCookie = false
let userId = req.cookie.userid
if (!userId) {
needSetCookie = true
userId = `${Date.now()}_${Math.random()}`
// 初始化 redis 中的 session 值
set(userId, {})
}
// 获取 session
req.sessionId = userId
get(req.sessionId).then(sessionData => {
if (sessionData == null) {
// 初始化 redis 中的 session 值
set(req.sessionId, {})
// 设置 session
req.session = {}
} else {
// 设置 session
req.session = sessionData
}
// console.log('req.session ', req.session)
// 处理 post data
return getPostData(req)
})
.then(postData => {
req.body = postData
// 处理 blog 路由
const blogResult = handleBlogRouter(req, res)
if (blogResult) {
blogResult.then(blogData => {
if (needSetCookie) {
res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
}
res.end(
JSON.stringify(blogData)
)
})
return
}
// 处理 user 路由
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.write("404 Not Found\n")
res.end()
})
}
module.exports = serverHandle
一个http请求来了,我们首先对req进行预处理,cookie在req.header中,解析它的header,二次包装req我们分如下几步看:
(1)从req.url中得到url
(2)从url中解析出请求的path和query,分别是url.split(“?”)的第一项和第二项,我们使用querystring包来把url中的key-value参数解析成参数对象,然后我们把它放到req.query中
(3)解析req中的cookie,req.headers.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();
)
一定要注意的就是,这里的解析处理来key-value一定要用trim处理才行
(4)解析session,session用作用户登录,我们解析完cookie后,就可以得到我们在cookie中放的userid,如果这个userId不存在,那我们为它自动生成一个userId :`${Date.now()_${Math.random()}},然后我们要把这个值存到session中去,这里需要一个与数据库的redis接口。然后我们解析userId,我们把userId赋值给req.sessionId,`调用redis数据库提供的get接口,对返回的异步Promise请求,如果是null,说明没有登陆过,我们直接set这个sessionId,初始化为空,如果有,那我们就设置这个返回的sessionData给req.session,这样我们就得到了一组req.sessionId和req.session,
(5)处理路由,路由返回都是Promise对象,子路由只用if判断室友路由paht匹配,不匹配不做处理,在servaerHandle中检测不同的路由的返回值,返回值存在就处理,并且res.send(JSON.stringify(data)),如果都没匹配上,我们在serverHandle的结尾返回404未命中路由,res.setStatus(404) res.write('404 not found') 以res.end()结束路由
2.用户登录模块:用户登录包括前端的登录页面,登录后使用location.href = ‘./admin.html’跳转到自己个人中心页面,在admin里面,给url拼接一个query :isadmin=1,表明这里是管理员面,我们在前端拼接url的时候,就默认了:
const $textKeyword = $('#text-keyword')
const $btnSearch = $('#btn-search')
const $tableContainer = $('#table-container')
// 拼接接口 url
let url = '/api/blog/list?isadmin=1' // 增加一个 isadmin=1 参数,使用登录者的用户名,后端也需要修改 !!!
const urlParams = getUrlParams()
if (urlParams.keyword) {
url += '&keyword=' + urlParams.keyword
}
// 加载数据
get(url).then(res => {
if (res.errno !== 0) {
alert('数据错误')
return
}
// 显示数据
const data = res.data || []
data.forEach(item => {
$tableContainer.append($(`
<tr>
<td>
<a href="/detail.html?id=${item.id}" target="_blank">${item.title}</a>
</td>
<td>
<a href="/edit.html?id=${item.id}">编辑</a>
</td>
<td>
<a data-id="${item.id}" class="item-del">删除</a>
</td>
</tr>
`))
})
})
在admin路由下,我们调用loginCheck函数来判断是否已经登录,接下来说一下用户登录的流程:
const loginCheck = (req) => {
if (!req.session.username) {
return Promise.resolve(
new ErrorModel('尚未登录')
)
}
}
if (req.query.isadmin) {
// 管理员界面
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 未登录
return loginCheckResult
}
// 强制查询自己的博客
author = req.session.username
}
首先用户如果进行了登录,首先经过serhandle的全局http请求处理,我们之前解析了cookie中是否有userId字段,首次登陆肯定没有userId这个字段,所以我们自动生成一个,并且把这个userId赋值给req.sessionId,后面我们会在redis设置sessionId-sessiondata的键值对。
然后进行一个redis查询,我们调用redis提供的get接口,以sessionId为查询的key,如果查询到了,就返回对应value,并且保存你到req.session中,也就是完成一个session的重读取。这个value应该是一个包括了username和realname的json对象。
第一次那自然是莫得这个sessionId对应的value,那返回为空,我们也初始化一个redis键值对。
然后匹配/login路由,在req.body中取得username和password,经过escape函数的编码,防止sql注入攻击,我们用username和password拼接一个查询myblog数据库中users表的sql语句
const login = (username, password) => {
username = escape(username)
// 生成加密密码
password = genPassword(password)
password = escape(password)
const sql = `
select username, realname from users where username=${username} and password=${password}
`
// console.log('sql is', sql)
return exec(sql).then(rows => {
return rows[0] || {}
})
}
返回rows[0]就包含了username和realname或者返回空对象
在user路由下,我们检测sql查询返回的数据中,是否有username,有的话说明登录成功了,我们把它们放到req.session.username和req.session.realname中,然后我们就有了req.session和req.sessionId,然后我们调用redis的set函数,把此次的登录信息保存到redis中,redis的特点就是不断电就保存在服务器的内存中,读取速度也快,比起localStorage,可以储存的多,用户多的时候,我们一般用redis做登录验证。
好了, 现在第一次登录了,我们已经完成了userId的自动生成, 在login的子路由中,我们调用login函数,login函数就负责解析req.body中的username和password并拼接sql语句进行查询,然后返回的username和realname赋值给sessiondata,调用redis的set方法完成sessionId和sessiondata的redis存储。
然后再serelhandle里,我们就要设置cookie了,我们调用:
res.setHeader('Set-Cookie',`userId'=userId,path='/',httpOnly,expires={getExpiresTime()}`)
然后这一次登录后,我们就成功的在redis中保存了登录信息,又在cookie中设置了以这个用户生成的userId为session的key的信息。
然后接下里就很重要,login后,服务器会返回一个succeModel或者errormodel,前端通过判断服务器返回的data中errno来判断是否登录成功:
$('#btnLogin').click(() => {
const username = $('#textUsername').val()
const password = $('#textPassword').val()
const url = '/api/user/login'
const data = {
username,
password
}
post(url, data).then(res => {
if (res.errno === 0) {
// 登录成功
location.href = './admin.html'
} else {
// 登录失败
alert(res.message)
}
})
})
如果errno为0,登录就成功了,用location.href跳转到./admin.html页面,然后这就又要向服务器发送一个http请求,前端admin页面发送一个get(url)请求来请求博客的list列表,需要访问blog下的子路由
let url = '/api/blog/list?isadmin=1'
这个http请求也要走一遍serverHandle,那我们已经登录了,所以cookie中就又userId,所以这次检测userId的时候就是真了,就不需要自动生成userId啦。因为login后,我们在redis中一sessionId(唯一userId)和sessionData(包含username和realname两个字段)。然后我们调用get函数,这次就get到了!然后就得到了之前用这个userId设置的sessionId就可以查到这个用户的username和realname组成的sessiondata,而且也放到了req.session中
所以登录状态的检测只需要在blog路由下面写一个loginCheck函数,这个函数检测req.session.username是否存在,存在就登录,返回一个Promise对象。
const loginCheck = (req) => {
if (!req.session.username) {
return Promise.resolve(
new ErrorModel('尚未登录')
)
}
}
下面是blog路由。如果loginCheck有返回值(loginCheck我们写成只有错误才有返回值),就返回没登录,现在情况是登录了,那我们就把username赋值给author,用于sql查询语句的拼接,我们
const handleBlogRouter = (req, res) => {
const method = req.method // GET POST
const id = req.query.id
// 获取博客列表
if (method === 'GET' && req.path === '/api/blog/list') {
let author = req.query.author || ''
const keyword = req.query.keyword || ''
// const listData = getList(author, keyword)
// return new SuccessModel(listData)
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 data = getDetail(id)
// return new SuccessModel(data)
const result = getDetail(id)
return result.then(data => {
return new SuccessModel(data)
})
}
// 新建一篇博客
if (method === 'POST' && req.path === '/api/blog/new') {
// const data = newBlog(req.body)
// return new SuccessModel(data)
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 未登录
return loginCheckResult
}
req.body.author = 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 = updateBlog(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
const result = delBlog(id, author)
return result.then(val => {
if (val) {
return new SuccessModel()
} else {
return new ErrorModel('删除博客失败')
}
})
}
}
module.exports = handleBlogRouter
接下来是getList函数
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)
}
然后返回的就是执行了查询语句后的异步promise包装的博客列表数据。马上就要大功告成了!
在servelhandle里,,我们直接用then处理resolve出的blogData,并将其res.end返回给客户端
res.end(JSON.stringify(blogData))
const blogResult = handleBlogRouter(req, res)
if (blogResult) {
blogResult.then(blogData => {
if (needSetCookie) {
res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
}
res.end(
JSON.stringify(blogData)
)
})
return
}
然后我们在admin中终于tm的得到了这个数据,在前端,我们就可以用map函数渲染这个列表数据了:
get(url).then(res => {
if (res.errno !== 0) {
alert('数据错误')
return
}
// 显示数据
const data = res.data || []
data.forEach(item => {
$tableContainer.append($(`
<tr>
<td>
<a href="/detail.html?id=${item.id}" target="_blank">${item.title}</a>
</td>
<td>
<a href="/edit.html?id=${item.id}">编辑</a>
</td>
<td>
<a data-id="${item.id}" class="item-del">删除</a>
</td>
</tr>
`))
})
})
然后终于就实现了admin主页只看到属于自己的列表,自然在新建页面,编辑页面,我们进入这些子路由的时候,都先做一个loginCheck,如果返回了login失败的erroModel,路由直接return出这个ErrorModel,在serverHandle里,我们同样也把这个errormodel当数据返回。
if (method === 'POST' && req.path === '/api/blog/update') {
const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
// 未登录
return loginCheckResult
}
const result = updateBlog(id, req.body)
return result.then(val => {
if (val) {
return new SuccessModel()
} else {
return new ErrorModel('更新博客失败')
}
})
}
post(url, data).then(res => {
if (res.errno !== 0) {
alert('操作错误')
return
}
alert('更新成功')
location.href = '/admin.html'
})
前端POST请求后如果得到服务器返回的这个errorModel,那直接alert失误,否则alert成功,
删除和新建也是一样的。
3.讲讲mysql和redis相关的知识
在node中使用mysql,首先我们需要在服务器上起一个mysql服务器,默认起在3306端口,在node配置中,我们写好对应的config,导入mysql包,使用提供的createConnection方法建立node到mysql的连接
const mysql = require('mysql')
const { MYSQL_CONF } = require('../conf/db')
// 创建链接对象
const con = mysql.createConnection(MYSQL_CONF)
// 开始链接
con.connect()
// 统一执行 sql 的函数
function exec(sql) {
const promise = new Promise((resolve, reject) => {
con.query(sql, (err, result) => {
if (err) {
reject(err)
return
}
resolve(result)//将promise.value = result,promise.status = 'fulfiiled'
})
})
return promise
}
if (env === 'dev') {
// mysql
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: 'lpl951004',
port: '3306',
database: 'myblog'
}
// redis
REDIS_CONF = {
port: 6379,
host: '127.0.0.1'
}
}
redis也是一样,我们导入redis包,
const redis = require('redis')
const { REDIS_CONF } = require('../conf/db.js')
// 创建客户端
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
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) {
reject(err)
return
}
if (val == null) {
resolve(null)
return
}
try {
resolve(
JSON.parse(val)
)
} catch (ex) {
resolve(val)
}
})
})
return promise
}
module.exports = {
set,
get
}
4.讲一讲express框架和中间件的实现。
首先讲一下express,我们之前用的是原生node的http,fs,path,等包来实现的这个服务端的http服务器
现在我们用express重构一下项目。使用express的时候,我们需要用到和express相关的一些包来管理session,路由等等。
express框架有一个express实例,
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const session = require('express-session')
const RedisStore = require('connect-redis')(session)
// var indexRouter = require('./routes/index');
// var usersRouter = require('./routes/users');
const blogRouter = require('./routes/blog')
const userRouter = require('./routes/user')
var app = express();
// // view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'jade');
const ENV = process.env.NODE_ENV
if (ENV !== 'production') {
// 开发环境 / 测试环境
app.use(logger('dev'));
} else {
// 线上环境
const logFileName = path.join(__dirname, 'logs', 'access.log')
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a'
})
app.use(logger('combined', {
stream: writeStream
}));
}
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// app.use(express.static(path.join(__dirname, 'public')));
const redisClient = require('./db/redis')
const sessionStore = new RedisStore({
client: redisClient
})
app.use(session({
secret: 'WJiol#23123_',
cookie: {
// path: '/', // 默认配置
// httpOnly: true, // 默认配置
maxAge: 24 * 60 * 60 * 1000
},
store: sessionStore
}))
// app.use('/', indexRouter);
// app.use('/users', usersRouter);
app.use('/api/blog', blogRouter);
app.use('/api/user', userRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'dev' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
使用express的流程:
首先导入相关的包,实例化一个express对象,根据process.env.NODE_ENV来决定使用不同的环境下的配置
然后按顺序,注意要按顺序注册相关的中间件,比如第一个是前端返回的json形式的request,首先用express.json()进行解析
然后用express.urlencoded进行url的解析,然后使用cookieParser()进行cookie的解析,然后实例化我们的redis,使用的是express-session中间件,传入中间件和中间件所需要的参数对象。
接下来就是处理路由了,和我们之前用原生nodejs处理是一样的。不同的路由我们分发到不同的子路由处理,最后注册了Error中间件和错误处理的中间件。
我们讲一讲路由
var express = require('express');
var router = express.Router();
const { login } = require('../controller/user')
const { SuccessModel, ErrorModel } = require('../model/resModel')
router.post('/login', function(req, res, next) {
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
res.json(
new SuccessModel()
)
return
}
res.json(
new ErrorModel('登录失败')
)
})
});
module.exports = router;
实例化一个express.Router(),对这个router注册不同路由的处理回调,然后导出这个router
对于请求我们可以编写一个middleware,
const {ErrorModel} from './model/resModel'
module.exports = (req,res,next) => {
if(req.session.username) {
next();
return
}
res.json(
new ErrorModel('未登录')
)
}