实战——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)

字段名字段类型是否主键描述
idintPrimary Key自增,非空
usernamevarchar(20)用户名,非空
passwordvarchar(20)密码,非空
realnamevarchar(10)真名,非空

博客表(blogs)

字段名字段类型是否主键描述
idintPrimary Key自增,非空
titlevarchar(50)标题,非空
contentlongtext博客内容,非空
createtimebigint创建时间,时间戳,非空
authorvarchar(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页面了

代码传送门~咻

  • 32
    点赞
  • 195
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值