express学习之博客项目

93 阅读5分钟

注:本文是对双越老师的express学习总结,无任何贩卖及收费行为,需要视频课程的请去慕课网自行购买Node.js 从零开发 web server 博客项目-慕课网实战 (imooc.com)

1.不使用框架

1.1 初始环境搭建

  1. 创建文件夹,npm init初始化npm环境
  2. 建立bin文件夹,写入www.js文件,即初次执行的文件
  3. 建立app.js
  4. npm i nodemon cross-env --save-dev
  5. 在scripts中加入 dev:cross-env NODE_ENV=dev nodemon ./bin/www.js,此时就可以通过npm run dev来运行了,而且是热启动
const http = require("http");
const serverHandle = require("./app");
const PORT = 8888;

const serve = http.createServer(serverHandle);
serve.listen(PORT);
  • app.js
const serverHandle = (req, res) => {
  //设置返回格式JSON
  res.setHeader("content-type", "application/json");
  const resData = {
    name: "双越",
  };

  res.end(JSON.stringify(resData));
};
module.exports = serverHandle;

image.png

为什么要把www和app拆分开呢,这是为了做个抽离,app主要是业务配置,www.js 是和server的技术有关系的,是一个基本的功能

1.2 开发接口之初始化路由

由于Node的 http 模块只对HTTP报文的头部进行了解析,然后触发 request 事件。如果请求中还带有内容部分(如 POST 请求,它具有报头和内容),内容部分需要用户自行接收和解析。

所以我们需要在app.js中对query参数及body参数进行处理

// 用于处理 post data
const getPostData = (req) => {
    const promise = new Promise((resolve, reject) => {
        if (req.method !== 'POST') {
            resolve({})
            return
        }
        if (req.headers['content-type'] !== 'application/json') {
            resolve({})
            return
        }
        let postData = ''
        req.on('data', chunk => {
            postData += chunk.toString()
        })
        req.on('end', () => {
            if (!postData) {
                resolve({})
                return
            }
            resolve(
                JSON.parse(postData)
            )
        })
    })
    return promise
}
const serverHandle = (req, res) => {
    // 设置返回格式 JSON
    res.setHeader('Content-type', 'application/json')
    
    // 获取 path
    const url = req.url
    req.path = url.split('?')[0]

    // 解析 query
    req.query = querystring.parse(url.split('?')[1])
    //解析body
    req.body = postData
}

至此,我们就解析完成了query参数和body参数,此时我们开始写增删改查,写之前我们要把路由和路由处理函数进行拆分

1) 新建src文件夹,新建src/router文件夹,src/router/blog.js,主要是写路由请求;新建controller文件夹,该文件夹中的函数主要是对路由进行处理

2) 写增删改查之前,我们先对返回的函数进行封装,新建src/model/resModel.js

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
}

至此,我们就可以正式的写增删改查了

const handleBlogRouter = (req, res) => {
    const method = req.method // GET POST
    const id = req.query.id

    // 获取博客列表
    if (method === 'GET' && req.path === '/api/blog/list') {
        let author = req.query.author || ''
        const keyword = req.query.keyword || ''
        // const listData = getList(author, keyword)
        // return new SuccessModel(listData)

        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 data = getDetail(id)
        // return new SuccessModel(data)
        const result = getDetail(id)
        return result.then(data => {
            return new SuccessModel(data)
        })
    }

    // 新建一篇博客
    if (method === 'POST' && req.path === '/api/blog/new') {
        // const data = newBlog(req.body)
        // return new SuccessModel(data)

        const loginCheckResult = loginCheck(req)
        if (loginCheckResult) {
            // 未登录
            return loginCheckResult
        }

        req.body.author = 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 = updateBlog(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
        const result = delBlog(id, author)
        return result.then(val => {
            if (val) {
                return new SuccessModel()
            } else {
                return new ErrorModel('删除博客失败')
            }
        })
    }
}

module.exports = handleBlogRouter

1.3 接入mysql

新建src/conf/db.js

const env = process.env.NODE_ENV  // 环境参数

// 配置
let MYSQL_CONF
let REDIS_CONF

if (env === 'dev') {
    // mysql
    MYSQL_CONF = {
        host: 'localhost',
        user: 'root',
        password: 'Mysql_2019',
        port: '3306',
        database: 'myblog'
    }

    // redis
    REDIS_CONF = {
        port: 6379,
        host: '127.0.0.1'
    }
}

if (env === 'production') {
    // mysql
    MYSQL_CONF = {
        host: 'localhost',
        user: 'root',
        password: 'Mysql_2019',
        port: '3306',
        database: 'myblog'
    }

    // redis
    REDIS_CONF = {
        port: 6379,
        host: '127.0.0.1'
    }
}

module.exports = {
    MYSQL_CONF,
    REDIS_CONF
}

新建src/db/mysql.js

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
}

以下是博客的增删改查

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 = xss(blogData.title)
    // console.log('title is', 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}');
    `

    return exec(sql).then(insertData => {
        // console.log('insertData is ', insertData)
        return {
            id: insertData.insertId
        }
    })
}

const updateBlog = (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}
    `

    return exec(sql).then(updateData => {
        // console.log('updateData is ', 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(delData => {
        // console.log('delData is ', delData)
        if (delData.affectedRows > 0) {
            return true
        }
        return false
    })
}

1.4 登录模块

  • 什么是cookie,cookie就是存储在浏览器的一段字符串,最大5kb,且跨域不共享
  • 每次发起http请求,就会将请求域的cookie一起发给server
  • server可以修改cookie并返回给浏览器
  • redis是一个key-value的数据库,速度更快,一般可以把缓存的数据存储在redis中,redis适用于读写比较快,但是不稳定,价格高;mysql稳定,读写慢,价格低

cookie的介绍

js可以操作cookie,浏览器查看cookie,server也可以操作cookie

    // 解析 cookie
    req.cookie = {}
    const cookieStr = req.headers.cookie || ''  // k1=v1;k2=v2;k3=v3
    cookieStr.split(';').forEach(item => {
        if (!item) {
            return
        }
        const arr = item.split('=')
        const key = arr[0].trim()
        const val = arr[1].trim()
        req.cookie[key] = val
    })

当解析了cookie之后,我们就可以做简单的登录验证了

比如:如果cookie中有username,我们就证明他有登录权限

  // 登录验证的测试
    if (method === 'GET' && req.path === '/api/user/login-test') {
        if (req.session.username) {
            return Promise.resolve(
                new SuccessModel({
        
                })
            )
        }
        return Promise.resolve(
            new ErrorModel('尚未登录')
        )
    }

后端如何操作cookie呢?基本步骤如下:

  • 后端先校验账号密码是否正确,如果正确,则可以res.setHeader('Set-Cookie',username=${xxx})
  • 此时前端可以在响应头中拿到cookie,然后可以存在本地并且设置cookie,下次再访问就可以携带cookie了,这就有点像token的处理

session的介绍

上边用cookie来进行验证,会存在问题,会暴露字段,我们可以在cookie存储userid,server端对应username

redis的介绍

首先安装redis

const redis = require('redis')

!(async function () {

    // 创建客户端
    const redisClient = redis.createClient(6379, '127.0.0.1')

    // 连接
    await redisClient.connect()
        .then(() => console.log('redis connect success!'))
        .catch(console.error)

    // set
    await redisClient.set('myname', 'zhangsan123')

    // get
    const myname = await redisClient.get('myname')
    console.log('myname', myname)

    // 退出
    redisClient.quit()
})()

使用redis

新建src/db/redis.js

const redis = require('redis')
const { REDIS_CONF } = require('../conf/db.js')

// 创建客户端
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)

// 连接数据库,启动之后立刻执行
!(async function () {
    await redisClient.connect()
        .then(() => console.log('redis connect success!'))
        .catch(console.error)
})()

// set
async function set(key, val) {
    let objVal
    if (typeof val === 'object') {
        objVal = JSON.stringify(val)
    } else {
        objVal = val
    }
    await redisClient.set(key, objVal)
}

// get
async function get(key) {
    try {
        let val = await redisClient.get(key)

        if (val == null) return val

        try {
            val = JSON.parse(val) // 尝试转换为 JS 对象
        } catch (err) { }

        return val
    } catch (err) {
        throw err
    }
}

module.exports = { set, get } 

1.5 日志