这里分出所有建立项目遇到的问题与难点。(配合之前发的<使用vue+node搭建图片上传下载的web端简单服务 juejin.cn/post/684490…>)
根据文档koa应该是中大型项目所使用的,个人玩使用express即可。不过,我还是想试试看。
后台部分
git地址 包含了路由,mysql增删改查。用户注册登录,图片上传下载,加密验证
- package.json(部分)
"dependencies": {
"@koa/cors": "^2.2.3", //koa跨域
"date-time": "^3.1.0", //获取当前日期
"jsonwebtoken": "^8.5.1", //JWT验证
"koa": "^2.7.0", //koa框架
"koa-bodyparser": "^4.2.1", //req.body一般在post请求中使用
"koa-handle-error": "0.0.5", //koa错误处理(开发角度上,应该暂时禁用。因为无法准确找到错误位置)
"koa-logger": "^3.2.1", //koa的路由日志,可自定义
"koa-multer": "^1.0.2", //koa表单文件上传处理使用
"koa-static": "^5.0.0", //静态文件访问
"koa2-router": "^1.1.2", //路由处理
"mysql": "^2.17.1", //连接mysql
"nanoid": "^2.0.3" //生成唯一id,入库使用
},
"devDependencies": {
"babel-eslint": "^10.0.2", //转译使用
"eslint": "^6.1.0",
"nodemon": "^1.19.1" //这个可能是在发布后使用,目前不知道他是和PM2一起用,还是单独的东西
},
程序入口
- 引入以上插件配置
const Koa = require('koa')
const handleError = require("koa-handle-error")
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser')
const cors = require('@koa/cors')
const app = new Koa();
const route = require('./route/allRouter')
const serve = require('koa-static');
// 这一块作为正式发布的错误处理部分,应该是类似邮件提示的东西
const onError = err => {
console.error(err)
}
const basePath = __dirname;
app.use(logger()) // 路由相关路径
.use(handleError(onError)) // 错误处理
// 这个multipart可能并没有用,因为文档中没有这一块东西。只是百度说可以解决文件上传的问题。后面可能用了其他方式解决了
.use(bodyParser({ multipart: true }))
// 跨域处理,这个可以自己配置
.use(cors())
// 上传图片的静态资源路径
.use(serve(__dirname + '/uploads'))
// 上传头像的静态资源路径
.use(serve(__dirname + '/portrait'))
.use(async (ctx, next) => {
// 将所有路由分部绑入当前主入口文件的路径
ctx.basePath = basePath;
await next();
})
// 所有自定义的路由放在最后,保证上面的中间件可以全部获取
.use(route)
// 监听3000端口
.listen(3000, '0.0.0.0', () => {
console.log('成功启动服务')
})
主程序用于注册整个后端的辅助功能。下面是路由部分,应该就是最主要的部分了
路由配置
和koa路由相关,我选择了koa2-router因为与express很像。npm上有一个比这个使用数量高了好几倍的路由插件(koa-joi-router),不过需要花些时间去学习这个。所以暂时先用这个了 我建立了一个allRouter.js用于管理所有的路由路径
// allRouter.js
// 所有路由管理
const Router = require('koa2-router');
const router = new Router();
const Login = require('./login')
const Image = require('./image')
const User = require('./user')
router.use('/',Login) // 登录注册相关路由
router.use('/image',Image) // 图片相关的路由
router.use('/user',User) //个人中心相关路由
module.exports = router; // 在主程序中,引入,使用app.use(router)方式导入这个文件
将不同功能的路由拆分,并放入不同的文件中。 .use是类似匹配主入口一样 每个路由文件,使用get/post/delete/put等restful风格接口进行详细路由匹配。 后面会有用法
使用mysql
数据从mysql获取,使用了npm上的mysql包
// 对于数据库操作比较薄弱,所以这里简单写一下
const mysql = require('mysql');
// 使用配置文件配置数据库
const { database } = require('../config.json')
const pool = mysql.createPool(
// 这里请使用自己的数据库地址,此地址为本地虚拟机默认设置
database
);
/**
*
* @param {*} params 将封装的sql语句直接导入
* 这里使用promise返回结果
*/
const dealSql = sql => {
return new Promise((resolve, reject) => {
pool.query(sql, function (error, results, fileds) {
if (error) reject(error);
resolve({ results, fileds });
});
})
}
module.exports = dealSql
config.json
{
"database": {
"host": "你的数据库地址",
"user": "root",
"password": "root",
"database": "test",
"connectionLimit": 10
}
}
使用promise封装后,使用可直接套用async和await将异步写法转为类似同步写法 后面会有用法
之后只会使用部分代码,而不是整个文件
注册信息与上次图片
// login.js
/** 上面还有其他的引入,具体可查看git地址 */
const nanoid = require('nanoid') // 入库的唯一值
// image上传解析问题,直接使用buffer方式解析失败
const multer = require('koa-multer')
let storage = multer.diskStorage({
destination:
path.resolve(__dirname, '..', 'portrait')
,
filename: function (req, file, cb) {
// 通过uuid生成几乎唯一的名字(应该可以增加年月日)
cb(null, [nanoid(), file.originalname].join('.'))
}
})
const upload = multer({ storage });
// 注册信息入库,与文件上传
router.post('/signup', upload.single('portrait'), async ctx => {
// 前端传入的值,使用es6的解构
let { account, nickname, pass, birthday, hobbies, sex, imageType } = ctx.req.body;
// 使用了koa-multer,上面的配置会自动保存图片在硬盘中,这里直接获取图片信息即可
let portrait = ctx.req.file.filename;
hobbies = hobbies == "undefined" ? null : hobbies.toString()
// 这里拼接sql语句
let sql = mysql.format('INSERT INTO `users` SET ?', [{ account, nickname, pass, birthday, hobbies, portrait, sex, imageType }])
// 这里的dealSql就是上面讲的mysql处理,这里就可以直接用.then处理了
let result = await dealSql(sql)
.then(
({ results }) => {
return { results }
}
)
.catch(
err => {
return { message: err.message }
}
)
ctx.body = result;
})
如果注册成功,前端会跳转至登录页面(前后端分离应该也有这层意思吧,路由的跳转也有前端控制了)
登录成功后,获取JWT
在后台验证完毕后,应该将需要的信息,返回给前端,但也需要将一些加密信息一同给前端。比如账号等等。因为我不怎么熟悉后端,所以只加密了账号。
// login.js
const { addToken } = require('../token/token') //使用token加密
router.post('/login', async ctx => {
let { account, pass } = ctx.request.body;
// 这里拼接sql语句
let sql = mysql.format('SELECT nickname,portrait FROM `users` where ? and ?', [{ account }, { pass }])
let result = await dealSql(sql)
.then(
({ results }) =>
results[0]
)
.catch(
err => {
return { message: err.message }
}
)
// 如果没有错误信息,那么添加token
if (!result.message) {
result.token = await addToken({ account }).then(data => data)
}
ctx.body = result;
})
上面引入的token.js
// token身份验证
const jwt = require('jsonwebtoken')
const { secret } = require('./secret.json') // 自定义的密钥(这里secret就是一个string)
/**
*
* @param {*} data 需要加密的数据
* @param {*} option 加密相关参数,参照jwt
*/
const addToken = (data, option = {}) => {
return new Promise((resolve, reject) => {
jwt.sign(data, secret, option, (err, token) => {
if (err) {
reject(err)
} else {
resolve(token)
}
})
})
}
/**
*
* @param {*} token 需要加密的数据
* @param {*} option 加密相关参数,参照jwt
*/
const checkToken = (token, option = {}) => {
return new Promise((resolve, reject) => {
token = token.split(' ')[1];
jwt.verify(token, secret, option, (err, decoded) => {
if (err) {
reject(err)
} else {
resolve(decoded)
}
})
})
}
module.exports = {
addToken,
checkToken
}
就两个方法,一个加密,一个检查
加密部分,需要设置一个过期时间的,我本地测试,设置时间嫌麻烦。正式开发,肯定需要的
解析上面的加密信息
在需要通过用户账号进行一些操作时。可将加密信息重新返回给后端(在前端请求头中添加['Authorization'],后面接上登录成功时返回的加密信息) 这个是user.js中,所有路由最头部的中间件,默认匹配进入/user的所有路由
router
// 路由中间件,所有和用户相关的都要经过token解密
.use(async (ctx, next) => {
let param = ctx.request.header.authorization;
// 解密token
ctx.token = await checkToken(param).then(data => data)
await next();
})
最后是一个下载
好像是因为谷歌浏览器和现在的策略。不让图片直接下载。需要返回字节码进行下载。我这里试过前端把图片放入canvas和后台获取blob进行下载。都成功了。但canvas在下载大图的时候不成功,目前不知道原因。
const send = require('koa-send') // 安装了koa-multer。这个就自带了。不需要再使用npm去安装
// 下载时使用
router.get("/downloads",async ctx=>{
let {filename} = ctx.query;
// 返回的就是blob
await send(ctx,filename,{
root: path.resolve(__dirname,'..','uploads')
})
})
前端获取到blob后,将blob转为url。加入一个a标签的href。再设置download属性。js模拟点击后,即可下载。