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頁面了