从零跟着撸web server后台

前言

接触前端也有一段时间了,感觉自己总是卡在很简单的地方,学习也是漫无目的的。所以在这段时间我意识到自己一定要好好思考一下是否坚持下去,重新找到新的突破口。还是再三思索下买了慕课网上的一门课来学习一下,总的来说这次购买没有令我失望,非常感谢老师让我收获满满,能够有信心继续前端的道路。

课程干货

  • 接口:Node.js 处理 http 请求,搭建开发环境,处理路由,开发各个接口
  • 数据存储:MySQL 介绍,Node.js 连接MySQL,接口对接 MySQL
  • 登录:Cookie和Session,登录中间件开发,与前端页面联调(结合Nginx),使用 Redis 存储,Session
  • 安全:预防SQL、XSS攻击
  • 日志:Node.js 文件操作,stream 流,日志功能的开发,日志文件拆分,日志分析
  • Express框架:Express 介绍和使用,中间件实现原理,开发 API 接口,结合常用插件,实现登录与日志
  • Koa2 框架:Koa2 介绍和使用,中间件实现原理,开发API接口,结合常用插件,实现登录与日志
  • 线上部署:PM2 介绍和配置,PM2 多进程模型,进程守候及日志记录

总体架构

项目的总体架构如下在这里插入图片描述
课程使用了三种方法来实现,第一种是原生的node.js开发,我们可以学到很多原理性的东西;然后第二个是使用了express进行了项目的重构;最后是使用了koa2重构。我觉得使用koa2是最好的实现方法,其中用到了比较新的异步解决方案 async/await ,可以减少很多 promise的实现代码。所以我就以koa2作为项目的讲解。

项目开发过程

1. 安装koa脚手架 koa-generator
npm install koa-generator -g

使用 koa2 koa2-blog 命令生成一个项目。然后npm install 去下载依赖包,进入项目运行npm start 访问localhost:3000看是否安装成功

安装cross-env插件区分环境

npm install cross-env --save-dev

然后在package.json文件里面设置好环境的启动方式

 "scripts": {
    "start": "node bin/www",
    "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
    "prd": "cross-env NODE_ENV=production pm2 start pm2.conf.json",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
2. 初始化路由

根据框架提供的思路和方式,我们创建了 user 和 blog 分别处理不同的路由(只负责路由处理,不涉及数据的处理,降低耦合性)

const router = require('koa-router')()
//统一的处理接口数据函数
const { getList, getDetail, newBlog, updateBlog, delBlog } = require('../controller/blog')
const { SuccessModel, ErrorModel } = require('../model/resModel')//成功和失败的模型
const loginCheck = require('../middleware/loginCheck')//是否登陆的中间件
router.prefix('/api/blog')//路由前缀
router.get('/list', async function (ctx, next) {
    let author = ctx.query.author || ''
    const keyword = ctx.query.keyword || ''
    if (ctx.query.isadmin) {
        if (ctx.session.username == null) {
            ctx.body = new ErrorModel('未登录')
            return
        }
        // 强制查询自己的博客
        author = ctx.session.username
    }
    const listData = await getList(author, keyword)
    ctx.body = new SuccessModel(listData)
})

router.get('/detail', async function (ctx, next) {
    const data = await getDetail(ctx.query.id)
    ctx.body = new SuccessModel(data)
})

router.post('/new', loginCheck, async function (ctx, next) {
    const body = ctx.request.body
    body.author = ctx.session.username
    const data = await newBlog(body)
    ctx.body = new SuccessModel(data)
})

router.post('/update', loginCheck, async function (ctx, next) {
    const val = await updateBlog(ctx.query.id, ctx.request.body)
    if (val) {
        ctx.body = new SuccessModel()
    } else {
        ctx.body = new ErrorModel('更新博客失败')
    }
})

router.post('/del', loginCheck, async function (ctx, next) {
    const author = ctx.session.username
    const val = await delBlog(ctx.query.id, author)
    if (val) {
        ctx.body = new SuccessModel()
    } else {
        ctx.body = new ErrorModel('删除博客失败')
    }
})
module.exports = router
3. 新建模型

首先解释一下SuccessModel和ErrorModel , 作用主要是供全局返回信息使用的。如登陆成功或者失败返回的标志位及提示信息

class BaseModel{
    constructor(data,message){
        if(typeof data === 'string'){
            this.message = data
            data = null
            message = null
        }
        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
}
4. 中间件开发

登陆校验的中间件 loginCheck
其实就是检查 session 中是否存有用户名的信息(信息在登陆成功时写进 session)

const { ErrorModel } = require('../model/resModel')

module.exports = async (ctx, next) => {
    if (ctx.session.username) {
        await next()
        return
    }
    ctx.body = new ErrorModel('未登录')
}
5. 控制层开发

接下来就讲一下控制层的实现,主要是连接数据库的sql语句来实现数据的交互

const xss = require('xss')
const { exec } = require('../db/mysql')

const getList = async (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;`

    return await exec(sql)
}
const getDetail = async (id) => {
    const sql = `select * from blogs where id='${id}'`
    const rows = await exec(sql)
    return rows[0]
}
const newBlog = async (blogData = {}) => {
    // blogData 是一个博客对象,包含 title content author 属性
    const title = xss(blogData.title)
    const content = xss(blogData.content)
    const author = blogData.author
    const createTime = Date.now()

    const sql = `
        insert into blogs (title, content, createtime, author)
        values ('${title}', '${content}', ${createTime}, '${author}');
    `
    const insertData = await exec(sql)
    return {
        id: insertData.insertId
    }
}
const updateBlog = async (id, blogData = {}) => {
    // id 就是要更新博客的 id
    // blogData 是一个博客对象,包含 title content 属性

    const title = xss(blogData.title)
    const content = xss(blogData.content)
    const sql = `
        update blogs set title='${title}', content='${content}' where id=${id}
    `
    const updateData = await exec(sql)
    if (updateData.affectedRows > 0) {
        return true
    }
    return false
}
const delBlog = async (id, author) => {
    // id 就是要删除博客的 id
    const sql = `delete from blogs where id='${id}' and author='${author}';`
    const delData = await exec(sql)
    if (delData.affectedRows > 0) {
        return true
    }
    return false
}
module.exports = {
    getList,
    getDetail,
    newBlog,
    updateBlog,
    delBlog
}
5. 连接数据库

说明一下 exec函数其实就是数据库查询的函数

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);
        })
    })
    return promise
}
module.exports = {
    exec,
    escape:mysql.escape
}

连接 mysql 的具体配置,分为生产环境和开发环境

const env = process.env.NODE_ENV  //获取环境变量
let MYSQL_CONF
if (env === 'dev') {
    MYSQL_CONF = {
        host: 'localhost',
        user: 'root',
        port: 3306,
        password: 'password',
        database: 'myblog'
    }
}
if (env === 'production') {
    MYSQL_CONF = {
        host: 'localhost',
        user: 'root',
        port: 3306,
        password: 'password',
        database: 'myblog'
    }
}
module.exports = {
    MYSQL_CONF
}

项目的WEB安全

1. sql 注入
   const sql = `select username,realname from users where username=${username} andpassword=${password};`

首先我们看一下 sql 的数据库查询,要是用户名输入 zhangsan' --就会把后面的密码注释了(也就是说不用密码就可以登陆)
解决方式是使用 mysql:escape进行转义

2. xss攻击

比如新建博客的时候可能会输入 <script>alert(1)</script>的形式,就会造成xss攻击,解决方式就是转义字符(如<,>等基本可以避免攻击)

const newBlog = async (blogData = {}) => {
    // blogData 是一个博客对象,包含 title content author 属性
    const title = xss(blogData.title)
    const content = xss(blogData.content)
    const author = blogData.author
    const createTime = Date.now()

    const sql = `
        insert into blogs (title, content, createtime, author)
        values ('${title}', '${content}', ${createTime}, '${author}');
    `
    const insertData = await exec(sql)
    return {
        id: insertData.insertId
    }
}

项目的日志

利用node的文件流,创建一个写文件流,并把它记录到本地文件当中

// logger
app.use(async (ctx, next) => {
	const start = new Date()
	await next()
	const ms = new Date() - start
	console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

const ENV = process.env.NODE_ENV
//记录日志
if (ENV !== 'production') {
  app.use(morgan('dev'));
} else {
  const logFileName = path.resolve(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a'
  })
  app.use(morgan('combined', {
    stream: writeStream
  }));
}

中间件原理实现

const http = require('http')

// 组合中间件
function compose(middlewareList) {
  return function (ctx) {
    // 中间件调用
    function dispatch(i) {
      const fn = middlewareList[i]
      try {
        return Promise.resolve(fn(ctx, dispatch.bind(null, i+1)))
      } catch(err) {
        return Promise.reject(err)
      }
    }
    return dispatch(0)
  }
}

class LikeKoa2 {
  constructor() {
    this.middlewareList = []
  }

  use(fn) {
    this.middlewareList.push(fn)
    return this
  }

  createCtx(req, res) {
    const ctx = {
      req,
      res
    }
    return ctx
  }

  handleRequest(ctx, fn) {
    return fn(ctx)
  }

  callback() {
    const fn = compose(this.middlewareList)
    return (req, res) => {
      const ctx = this.createCtx(req, res)
      return this.handleRequest(ctx, fn)
    }
  }

  lsiten(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
}
module.exports = {
  LikeKoa2
}

上线与配置

1. pm2 介绍
  • 进程守护,如果系统崩溃则会自动重启 node app.js和 nodemon app.js
  • 启动多进程,充分利用cpu和内存
  • 自带日志记录功能
2. pm2 配置与多进程
  • 优点: 充分利用多核cpu优势。

  • 缺点: 多进程之间,内存无法共享;解决方法是多进程访问redis,实现数共享

{
    "apps": {
        "name": "web-server",
        "script": "./bin/www.js",
        "watch": true, // 进行实时监听
        "ignore_watch": [
            "node_modules",
            "logs"
        ],
        "instances": 4, // 进程数,根据系统核数
        "error_file": "logs/error.log",
        "out_file": "logs/access.log",
        "log_date_format": "YYYY-MM-DD HH:mm:ss"
    }
}

总结

通过这一次的学习,让我懂得了如何进行后台方面的开发。学会了使用node进行数据库连接,后台接口的开发以及路由分配等基础而又重要的知识点。而且重要的是我学会了如何去设计代码,能够以一种简介明了的风格展现在我面前,考虑到生产环境和开发环境之间的配置,项目虽小但功能都全。希望自己在学习之后能够开发一个更加完善的项目,以及把前端部分的页面实现出来。

ps:笔者最近开通了微信公众号,大家可以关注一下哦,谢谢大家的支持!
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值