實戰——NodeJs項目

NodeJs項目

功能描述

  1. 用戶登錄(基於cookie、session和redis實現登錄)
  2. 博客的創建、編輯、刪除、查詢(基於mysql實現數據的增刪改查)
  3. 通過Nginx的反向代理來解決跨域問題

總體使用的是MVC的設計模式來進行的

項目依賴

nodejs:使用原生nodejs提供的各種模塊

mysql:使用mysql包來實現nodejs讀取mysql數據庫中的數據

redis:使用redis來存儲session的數據

cross-env:項目運行的環境變量

默認已經會了上述的幾個包的使用

安裝需要

  1. mysql數據庫來存儲項目數據
  2. 需要安裝redis來處理session數據的存儲
  3. 需要安裝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) 作者,非空

項目結構設計

  1. 目錄結構
  • 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
  1. 數據配置及獲取

    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
      }
      
  2. 用戶登錄

    /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
    
  3. 博客管理

    /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
    
  4. 其他代碼

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

代碼傳送門~咻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章