前言
接触前端也有一段时间了,感觉自己总是卡在很简单的地方,学习也是漫无目的的。所以在这段时间我意识到自己一定要好好思考一下是否坚持下去,重新找到新的突破口。还是再三思索下买了慕课网上的一门课来学习一下,总的来说这次购买没有令我失望,非常感谢老师让我收获满满,能够有信心继续前端的道路。
课程干货
- 接口: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:笔者最近开通了微信公众号,大家可以关注一下哦,谢谢大家的支持!