使用vue+node搭建图片上传下载的web端简单服务(后端部分主要功能)

733 阅读6分钟

这里分出所有建立项目遇到的问题与难点。(配合之前发的<使用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模拟点击后,即可下载。