实战——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存在跨域不共享,所以就需要使用反向代理

反向代理说明

实战——NodeJs项目_第1张图片

其实就是在服务器访问web服务器和后端服务器的时候,先通过Nginx来作为中介,以localhost/index.html为例,Nginx会判断路径是哪个,如果是 /…的就把你导向web服务器,如果是请求接口的就导向nodejs后端服务器

自行下载安装后,在安装的文件中有个conf文件夹,我们需要对立面的nginx.conf文件进行配置

我们用VSCode直接打开文件就行,然后如下配置,上面有个port(端口)自己配置就行,不要是被使用的端口就可以。注意这里不是js中的对象,不要习惯性的用冒号来进行赋值,是用空格的

实战——NodeJs项目_第2张图片

可以在通过执行 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页面了

代码传送门~咻

你可能感兴趣的:(前端开发)