基于Nodejs搭建博客后台

基础准备:

1.NodeJs是一个非阻塞IO,单线程的,运行在服务端的JavaScript平台,基于Google的V8引擎

2.NodeJs使用事件驱动模型,采用的是观察订阅模式,实现在event模块下

3.Nodejs有很多模块,采用npm管理包,比如node和mysql,redis对接,都需要用到相应的包

4 mysql是关系型数据库,我们在本地计算机的某个端口,起一个mysql服务,在node中监听这个端口

5.redis是是一个key-value的存储系统,存在内存里,断电存储消失,适合做登录,同样我们在本地计算机起一个redis服务

整体框架:

1.前端通过http-server的npm插件起一个前端http服务器,返回浏览器博客的前端页面,用jquery发送ajax请求 前端的请求包括有:GET,POST两种,POST用于更新博客内容,删除博客,用户登录,前端的服务器端口在8001

2.服务器同样用http的createServer方法起一个http服务器,端口在8000

3.用户的登录使用redis保存session,session和cookie验证用户是否登录

4.博客的保存使用mysql数据库

5.使用nodejs的http模块处理网络请求,使用fs模块处理保存日志

6.使用koa2框架重构,编写自定义的中间件

7.使用nginx代理

8.前端页面返回的数据以JSON对象表示,确定前端所需的数据格式和结构,不同路由返回的数据如何解析,数据格式是{data,errno}一个是数据

模块划分:

 

前端页面结构’

1.http请求处理模块:

使用nodejs原生的http模块来处理,首先在设置www.js为package.json中的npn run dev的入口文件

并使用nodemon实时监控文件的变化实现代码变更的热更新

const http = require('http')

const PORT = 8000
const serverHandle = require('../app')

const server = http.createServer(serverHandle)
server.listen(PORT)

  后端起了一个http服务器,监听8000端口的http请求,我们在app.js中编写我们如何处理前端发过来

的http请求,使用注册回调的方式,在http.createServer(callback)中,我们传入一个callback,这个callback需要两个参数,一个是request,这两个都是对象,一个就是服务器返回给前端的response,http模块提供给处理http请求的的callback这两个参数

http.createServer((req,res) => {
    //do something with req & res
})

前端来了一个http请求,它的目的就是从服务器返回它需要的数据,以JSON格式返回它需要的数据,分为Get和POST两种,GET请求负责请求数据,返回成功status为200。POST用于提交数据,fetch需要发送两次请求

serverHandle = (req,res) => {}如下

const serverHandle = (req, res) => {
    // 记录 access log
    access(`${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()}`)

    // 设置返回格式 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])

    // 解析 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
    })

    // 解析 session (使用 redis)
    let needSetCookie = false
    let userId = req.cookie.userid
    if (!userId) {
        needSetCookie = true
        userId = `${Date.now()}_${Math.random()}`
        // 初始化 redis 中的 session 值
        set(userId, {})
    }
    

    // 获取 session
    req.sessionId = userId
    get(req.sessionId).then(sessionData => {
        if (sessionData == null) {
            // 初始化 redis 中的 session 值
            set(req.sessionId, {})
            // 设置 session
            req.session = {}
        } else {
            // 设置 session
            req.session = sessionData
        }
        // console.log('req.session ', req.session)

        // 处理 post data
        return getPostData(req)
    })
    .then(postData => {
        req.body = postData

        // 处理 blog 路由
   
        const blogResult = handleBlogRouter(req, res)
        if (blogResult) {
            blogResult.then(blogData => {
                if (needSetCookie) {
                    res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
                }

                res.end(
                    JSON.stringify(blogData)
                )
            })
            return
        }
        
        // 处理 user 路由
   
        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.write("404 Not Found\n")
        res.end()
    })
}

module.exports = serverHandle

一个http请求来了,我们首先对req进行预处理,cookie在req.header中,解析它的header,二次包装req我们分如下几步看:

(1)从req.url中得到url

(2)从url中解析出请求的path和query,分别是url.split(“?”)的第一项和第二项,我们使用querystring包来把url中的key-value参数解析成参数对象,然后我们把它放到req.query中

(3)解析req中的cookie,req.headers.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();
    )

一定要注意的就是,这里的解析处理来key-value一定要用trim处理才行

(4)解析session,session用作用户登录,我们解析完cookie后,就可以得到我们在cookie中放的userid,如果这个userId不存在,那我们为它自动生成一个userId :`${Date.now()_${Math.random()}},然后我们要把这个值存到session中去,这里需要一个与数据库的redis接口。然后我们解析userId,我们把userId赋值给req.sessionId,`调用redis数据库提供的get接口,对返回的异步Promise请求,如果是null,说明没有登陆过,我们直接set这个sessionId,初始化为空,如果有,那我们就设置这个返回的sessionData给req.session,这样我们就得到了一组req.sessionId和req.session,

(5)处理路由,路由返回都是Promise对象,子路由只用if判断室友路由paht匹配,不匹配不做处理,在servaerHandle中检测不同的路由的返回值,返回值存在就处理,并且res.send(JSON.stringify(data)),如果都没匹配上,我们在serverHandle的结尾返回404未命中路由,res.setStatus(404)  res.write('404 not found')  以res.end()结束路由

 

2.用户登录模块:用户登录包括前端的登录页面,登录后使用location.href = ‘./admin.html’跳转到自己个人中心页面,在admin里面,给url拼接一个query :isadmin=1,表明这里是管理员面,我们在前端拼接url的时候,就默认了:

 const $textKeyword = $('#text-keyword')
        const $btnSearch = $('#btn-search')
        const $tableContainer = $('#table-container')

        // 拼接接口 url
        let url = '/api/blog/list?isadmin=1'  // 增加一个 isadmin=1 参数,使用登录者的用户名,后端也需要修改 !!!
        const urlParams = getUrlParams()
        if (urlParams.keyword) {
            url += '&keyword=' + urlParams.keyword
        }

        // 加载数据
        get(url).then(res => {
            if (res.errno !== 0) {
                alert('数据错误')
                return
            }

            // 显示数据
            const data = res.data || []
            data.forEach(item => {
                $tableContainer.append($(`
                    <tr>
                        <td>
                            <a href="/detail.html?id=${item.id}" target="_blank">${item.title}</a>
                        </td>
                        <td>
                            <a href="/edit.html?id=${item.id}">编辑</a>
                        </td>
                        <td>
                            <a data-id="${item.id}" class="item-del">删除</a>
                        </td>
                    </tr>
                `))
            })
        })

在admin路由下,我们调用loginCheck函数来判断是否已经登录,接下来说一下用户登录的流程:

const loginCheck = (req) => {
    if (!req.session.username) {
        return Promise.resolve(
            new ErrorModel('尚未登录')
        )
    }
}


 if (req.query.isadmin) {
            // 管理员界面
            const loginCheckResult = loginCheck(req)
            if (loginCheckResult) {
                // 未登录
                return loginCheckResult
            }
            // 强制查询自己的博客
            author = req.session.username
        }

首先用户如果进行了登录,首先经过serhandle的全局http请求处理,我们之前解析了cookie中是否有userId字段,首次登陆肯定没有userId这个字段,所以我们自动生成一个,并且把这个userId赋值给req.sessionId,后面我们会在redis设置sessionId-sessiondata的键值对。

然后进行一个redis查询,我们调用redis提供的get接口,以sessionId为查询的key,如果查询到了,就返回对应value,并且保存你到req.session中,也就是完成一个session的重读取。这个value应该是一个包括了username和realname的json对象。

第一次那自然是莫得这个sessionId对应的value,那返回为空,我们也初始化一个redis键值对。

然后匹配/login路由,在req.body中取得username和password,经过escape函数的编码,防止sql注入攻击,我们用username和password拼接一个查询myblog数据库中users表的sql语句

const login = (username, password) => {
    username = escape(username)
    
    // 生成加密密码
    password = genPassword(password)
    password = escape(password)

    const sql = `
        select username, realname from users where username=${username} and password=${password}
    `
    // console.log('sql is', sql)
    return exec(sql).then(rows => {
        return rows[0] || {}
    })
}

返回rows[0]就包含了username和realname或者返回空对象

在user路由下,我们检测sql查询返回的数据中,是否有username,有的话说明登录成功了,我们把它们放到req.session.username和req.session.realname中,然后我们就有了req.session和req.sessionId,然后我们调用redis的set函数,把此次的登录信息保存到redis中,redis的特点就是不断电就保存在服务器的内存中,读取速度也快,比起localStorage,可以储存的多,用户多的时候,我们一般用redis做登录验证。

好了, 现在第一次登录了,我们已经完成了userId的自动生成, 在login的子路由中,我们调用login函数,login函数就负责解析req.body中的username和password并拼接sql语句进行查询,然后返回的username和realname赋值给sessiondata,调用redis的set方法完成sessionId和sessiondata的redis存储。

然后再serelhandle里,我们就要设置cookie了,我们调用:

res.setHeader('Set-Cookie',`userId'=userId,path='/',httpOnly,expires={getExpiresTime()}`)

然后这一次登录后,我们就成功的在redis中保存了登录信息,又在cookie中设置了以这个用户生成的userId为session的key的信息。

然后接下里就很重要,login后,服务器会返回一个succeModel或者errormodel,前端通过判断服务器返回的data中errno来判断是否登录成功:

$('#btnLogin').click(() => {
            const username = $('#textUsername').val()
            const password = $('#textPassword').val()
            const url = '/api/user/login'
            const data = {
                username,
                password
            }
            post(url, data).then(res => {
                if (res.errno === 0) {
                    // 登录成功
                    location.href = './admin.html'
                } else {
                    // 登录失败
                    alert(res.message)
                }
            })
        })

如果errno为0,登录就成功了,用location.href跳转到./admin.html页面,然后这就又要向服务器发送一个http请求,前端admin页面发送一个get(url)请求来请求博客的list列表,需要访问blog下的子路由

  let url = '/api/blog/list?isadmin=1'

这个http请求也要走一遍serverHandle,那我们已经登录了,所以cookie中就又userId,所以这次检测userId的时候就是真了,就不需要自动生成userId啦。因为login后,我们在redis中一sessionId(唯一userId)和sessionData(包含username和realname两个字段)。然后我们调用get函数,这次就get到了!然后就得到了之前用这个userId设置的sessionId就可以查到这个用户的username和realname组成的sessiondata,而且也放到了req.session中

所以登录状态的检测只需要在blog路由下面写一个loginCheck函数,这个函数检测req.session.username是否存在,存在就登录,返回一个Promise对象。

const loginCheck = (req) => {
    if (!req.session.username) {
        return Promise.resolve(
            new ErrorModel('尚未登录')
        )
    }
}

下面是blog路由。如果loginCheck有返回值(loginCheck我们写成只有错误才有返回值),就返回没登录,现在情况是登录了,那我们就把username赋值给author,用于sql查询语句的拼接,我们

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

接下来是getList函数

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)
}

然后返回的就是执行了查询语句后的异步promise包装的博客列表数据。马上就要大功告成了!

在servelhandle里,,我们直接用then处理resolve出的blogData,并将其res.end返回给客户端

res.end(JSON.stringify(blogData))

 const blogResult = handleBlogRouter(req, res)
        if (blogResult) {
            blogResult.then(blogData => {
                if (needSetCookie) {
                    res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
                }

                res.end(
                    JSON.stringify(blogData)
                )
            })
            return
        }

然后我们在admin中终于tm的得到了这个数据,在前端,我们就可以用map函数渲染这个列表数据了:

 get(url).then(res => {
            if (res.errno !== 0) {
                alert('数据错误')
                return
            }

            // 显示数据
            const data = res.data || []
            data.forEach(item => {
                $tableContainer.append($(`
                    <tr>
                        <td>
                            <a href="/detail.html?id=${item.id}" target="_blank">${item.title}</a>
                        </td>
                        <td>
                            <a href="/edit.html?id=${item.id}">编辑</a>
                        </td>
                        <td>
                            <a data-id="${item.id}" class="item-del">删除</a>
                        </td>
                    </tr>
                `))
            })
        })

然后终于就实现了admin主页只看到属于自己的列表,自然在新建页面,编辑页面,我们进入这些子路由的时候,都先做一个loginCheck,如果返回了login失败的erroModel,路由直接return出这个ErrorModel,在serverHandle里,我们同样也把这个errormodel当数据返回。

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('更新博客失败')
            }
        })
    }



post(url, data).then(res => {
                if (res.errno !== 0) {
                    alert('操作错误')
                    return
                }
                alert('更新成功')
                location.href = '/admin.html'
            })

前端POST请求后如果得到服务器返回的这个errorModel,那直接alert失误,否则alert成功,

删除和新建也是一样的。

3.讲讲mysql和redis相关的知识

在node中使用mysql,首先我们需要在服务器上起一个mysql服务器,默认起在3306端口,在node配置中,我们写好对应的config,导入mysql包,使用提供的createConnection方法建立node到mysql的连接

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)//将promise.value = result,promise.status = 'fulfiiled'
        })
    })
    return promise
}


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

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

redis也是一样,我们导入redis包,

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

// 创建客户端
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
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) {
                reject(err)
                return
            }
            if (val == null) {
                resolve(null)
                return
            }

            try {
                resolve(
                    JSON.parse(val)
                )
            } catch (ex) {
                resolve(val)
            }
        })
    })
    return promise
}

module.exports = {
    set,
    get
}

4.讲一讲express框架和中间件的实现。

首先讲一下express,我们之前用的是原生node的http,fs,path,等包来实现的这个服务端的http服务器

现在我们用express重构一下项目。使用express的时候,我们需要用到和express相关的一些包来管理session,路由等等。

express框架有一个express实例,

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const session = require('express-session')
const RedisStore = require('connect-redis')(session)

// var indexRouter = require('./routes/index');
// var usersRouter = require('./routes/users');
const blogRouter = require('./routes/blog')
const userRouter = require('./routes/user')

var app = express();

// // view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'jade');

const ENV = process.env.NODE_ENV
if (ENV !== 'production') {
  // 开发环境 / 测试环境
  app.use(logger('dev'));
} else {
  // 线上环境
  const logFileName = path.join(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a'
  })
  app.use(logger('combined', {
    stream: writeStream
  }));
}

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// app.use(express.static(path.join(__dirname, 'public')));

const redisClient = require('./db/redis')
const sessionStore = new RedisStore({
  client: redisClient
})
app.use(session({
  secret: 'WJiol#23123_',
  cookie: {
    // path: '/',   // 默认配置
    // httpOnly: true,  // 默认配置
    maxAge: 24 * 60 * 60 * 1000
  },
  store: sessionStore
}))

// app.use('/', indexRouter);
// app.use('/users', usersRouter);
app.use('/api/blog', blogRouter);
app.use('/api/user', userRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'dev' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

使用express的流程:

首先导入相关的包,实例化一个express对象,根据process.env.NODE_ENV来决定使用不同的环境下的配置

然后按顺序,注意要按顺序注册相关的中间件,比如第一个是前端返回的json形式的request,首先用express.json()进行解析

然后用express.urlencoded进行url的解析,然后使用cookieParser()进行cookie的解析,然后实例化我们的redis,使用的是express-session中间件,传入中间件和中间件所需要的参数对象。

接下来就是处理路由了,和我们之前用原生nodejs处理是一样的。不同的路由我们分发到不同的子路由处理,最后注册了Error中间件和错误处理的中间件。

我们讲一讲路由

var express = require('express');
var router = express.Router();
const { login } = require('../controller/user')
const { SuccessModel, ErrorModel } = require('../model/resModel')

router.post('/login', function(req, res, next) {
    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

            res.json(
                new SuccessModel()
            )
            return
        }
        res.json(
            new ErrorModel('登录失败')
        )
    })
});



module.exports = router;

实例化一个express.Router(),对这个router注册不同路由的处理回调,然后导出这个router

对于请求我们可以编写一个middleware,

const {ErrorModel} from './model/resModel'

module.exports = (req,res,next) => {
    if(req.session.username) {
        next();
        return
    }
    res.json(
    new ErrorModel('未登录')    
)
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值