基礎準備:
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('未登錄')
)
}