node开发物料市场总结

本文介绍了利用Koa2框架构建后端服务,实现用户登录注册、组件上传与预览、权限验证等功能。技术栈包括MongoDB、JWT、中间件等,通过模块化组织代码,实现接口路由、数据库操作和文件上传。同时,文章详细阐述了数据库模型设计、文件处理、登录注册逻辑以及Token的生成和校验过程。
摘要由CSDN通过智能技术生成

功能介绍:用户通过登陆注册,可在物料市场上传elementUI组件,其他用户可预览效果和代码,达到共享组件的目的。

技术栈

框架:koa2

数据库:MongoDB

中间件、插件

  • token验证:basic-auth、jsonwebtoken

  • 处理post请求:koa-body

  • 处理路由:koa-router

  • 解决跨域:koa2-cors

  • 设置module别名:module-alias

  • 数据库操作:mongoose

  • 校验器:validator

  • 给文件生成唯一id:uuid

项目结构

注:有后缀名的为文件,否则为文件夹

– config.js 存放端口号,数据库地址,token配置等

– document 项目文档

    db.drawio 数据表设计

– server 服务端代码

    codes 错误信息文案

    controllers 处理接口内部逻辑

    dbs 数据库操作

    middlewares 自定义中间件

    models schema构造器

    routers 路由定义

    utils 工具、通用方法

    app.js 入口文件

– init 初始化数据

    data 初始化的数据

    initData.js 初始化脚本

– public 向外暴露的文件夹

    document 文档,可放api文档

    uploads 上传的文件

server端启动

package.json配置,配置启动命令和别名

"scripts": {
    "start_server": "nodemon ./server/app.js",
    "start": "npm run start_server"
 },

"_moduleAliases": {
    "@config": "config",
    "@models": "server/models",
    "@dbs": "server/dbs",
    "@routers": "server/routers",
    "@controllers": "server/controllers",
    "@utils": "server/utils",
    "@codes": "server/codes",
    "@middlewares": "server/middlewares"
 } 

前端调用接口会走到router中,在routers/index.js中定义所有的接口路由

const router = require('koa-router')(); // 引入koa-router,不要忘记最后的括号

const user = require('@routers/user');
const material = require('@routers/material');
const category = require('@routers/category');

router.use('/api/user', user.routes(), user.allowedMethods())
    .use('/api/material', material.routes(), material.allowedMethods())
    .use('/api/category', category.routes(), category.allowedMethods());

module.exports = router;

router/user.js中定义模块的接口路由,可在定义时加入登陆验证的中间件,验证通过后调用controller去处理参数

const router = require('koa-router')();
const userController = require('@controllers/userController');
const Auth = require('@middlewares/auth');

const routers = router.get(`/getUsers`, new Auth().check, userController.getUsers)
    .post(`/register`, userController.createUser)
    .post(`/login`, userController.login)
    .get(`/getMyMaterial`, new Auth().check, userController.getMyMaterial)

module.exports = routers;

在models中定义数据库集合字段

  • schema:用于定义集合collection中文档document的结构

  • model:根据schema编译出的构造器,可以实例化出文档对象document,就可以对文档进行增删改查的操作了

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
    name: {
        type: String,
        required: true
    },
    pwd: {
        type: String,
        required: true
    },
})
// User为collection的名字,会自动转化为users
module.exports = mongoose.model('User', UserSchema);

server端功能拆解

  1. 入口:使用中间件和插件、连接数据库、监听端口
require('module-alias/register'); // 引入module-alias插件,可在package.json中加入_moduleAliases来配置别名
const Koa = require('koa');
const mongoose = require('mongoose');
const koaBody = require('koa-body');
const cors = require('koa2-cors');
const koaStatic = require('koa-static');
const path = require('path');
const routers = require('@routers/index'); // 引入routers的入口文件,别名配置过的
const config = require('@config');

const app = new Koa();

// 解析body中的数据
app.use(koaBody({
    // 支持form-data格式
    multipart: true,
    formidable: {
        // 保留文件扩展名
        keepExtensions: true,
        // 设置上传文件大小最大限制,默认2M
        maxFileSize: 10 * 1024 * 1024
    }
}));

// 生成静态服务,文件夹下的文件可以通过http来访问
app.use(koaStatic(path.join(config.root, 'public')));

// 连接MongoDB
mongoose.connect(config.dbs, {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

// 初始化路由中间件
app.use(routers.routes()).use(routers.allowedMethods());

// 支持跨域
app.use(cors({
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization', 'Date'],
    maxAge: 100,
    credentials: true,
    allowMethods: ['GET', 'POST', 'OPTIONS'],
    allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Custom-Header', 'anonymous'],
}));

app.listen(config.port);
  1. 登陆注册
  • 注册
async createUser(ctx) {
    const body = ctx.request.body;
    // 自己实现的一个校验参数的方法,模拟了前端常用的校验表单的rules
    const bodyValidator = validator.paramsValidate(body, {
        name: [{
            require: true,
            msg: userCode.ERROR_NAME_EMPTY
        }, {
            min: 2,
            max: 10,
            msg: userCode.ERROR_NAME_LENGTH
        }],
        pwd: [{
            require: true,
            msg: userCode.ERROR_PWD_EMPTY
        }, {
            min: 6,
            max: 16,
            msg: userCode.ERROR_PWD_LENGTH
        }],
    })
    if (bodyValidator.result) {
        // 简单的校验后查name是否存在过
        let userInfos = await dbs.getUserInfo({
            name: ctx.request.body.name
        });
        if (userInfos.length > 0) {
            ctx.body = resultDec.fail(userCode.FAIL_USER_NAME_IS_EXIST);
            return;
        }
        // 插入数据,并返回插入的数据
        let result = await dbs.createUser(ctx.request.body);
        ctx.body = resultDec.success(result);
    } else {
        ctx.body = resultDec.fail(bodyValidator.msg);
    }
}
  • 登陆
async login(ctx) {
    const body = ctx.request.body;
    let userInfos = await dbs.getUserInfo({
        name: body.name,
        pwd: body.pwd
    });
    // 校验账号密码有效性
    // TODO 后续增加密码加密
    if (userInfos.length) {
        // 校验成功后返回一个token
        const token = auth.generateToken(userInfos[0]._id);
        ctx.body = resultDec.success({
            token
        });
    } else {
        ctx.body = resultDec.fail(userCode.FAIL_USER_NAME_OR_PASSWORD_ERROR);
    }
},
  • token生成和校验

        生成token、获取userId方法:在utils/auth.js中

const jwt = require('jsonwebtoken');
const basicAuth = require('basic-auth');
const config = require('@config');

/**
 * @description 生成token令牌
 * @param uid 用户id
 * @param scope 权限等级数字
 * @returns token
 */
const generateToken = (uid, scope) => {
    const {
        secretKey,
        expiresIn
    } = config.security;
    const token = jwt.sign({
            uid,
            scope
        },
        secretKey, {
            expiresIn
        });

    return token;
}

const getUserId = (ctx) => {
    const userToken = basicAuth(ctx.req);
    const decode = jwt.verify(userToken.name, config.security.secretKey);
    return decode.uid;
}

module.exports = {
    generateToken,
    getUserId
}

        校验 token :middlewares/auth.js中间件,在router里用

const basicAuth = require('basic-auth');
const jwt = require('jsonwebtoken');
const config = require('@config');
const {
    resultDec
} = require('@utils/index');
const userCode = require('@codes/user');

class Auth {
    constructor(level) {
        this.level = level || 1;
    }

    get check() {
        return async (ctx, next) => {
            const userToken = basicAuth(ctx.req);
            let decode = {};
            // 如果没有拿到token或token校验失败就返回401状态码和错误信息
            if (!userToken || !userToken.name) {
                ctx.status = 401;
                ctx.body = resultDec.fail(userCode.TOKEN_ERROR);
                return;
            }

            try {
                // 校验token,前端在请求头中加上Authorization:`Basic ${window.btoa(${token}:)}`
                decode = jwt.verify(userToken.name, config.security.secretKey);
            } catch (err) {
                ctx.status = 401;
                ctx.body = resultDec.fail(userCode.TOKEN_ERROR);
                return;
            }

            ctx.auth = {
                uid: decode.uid,
                scope: decode.scope
            }

            // 认证通过
            await next();
        }
    }
}

module.exports = Auth;

  1. 上传

    思路:从请求体中拿到files(ctx.request.files[‘字段名’]),用uuid生成唯一文件名,判断当前用户是否存在过自己的物料文件夹(fs.existsSync),如果没有则先创建以${userId}为文件夹名的文件夹(fs.mkdirSync),接着保存文件到本地服务器

// utils/file.js
const fs = require('fs');
const uuid = require('uuid');
const config = require('@config');

const fileUtil = {
    saveMaterials: (file, userId) => {
        if (file) {
            // 物料文件夹materials/userId/uuid.后缀名
            const tempArr = file.name.split('.');
            tempArr.shift();
            const suffix = '.' + tempArr.join('.');
            const fileName = `${uuid.v1()}${suffix}`;
            const filePath = `${config.root}/public/uploads/materials/${userId}`;
            try {
                if (!fs.existsSync(filePath)) {
                    fs.mkdirSync(filePath);
                }
                fileUtil.saveFile(file, filePath + `/${fileName}`);
            } catch (err) {
                console.error('报错啦', err);
            }
            return fileName;
        }
    },
    // TODO 将文件保存到云上
    saveFile: (file, path) => {
        // 创建可读流
        const reader = fs.createReadStream(file.path);
        let filePath = path || `${config.root}/public/uploads/${file.name}`;
        // 创建可写流
        const upStream = fs.createWriteStream(filePath);
        // 可读流通过管道写入可写流
        reader.pipe(upStream);
    }
}

module.exports = fileUtil;
  1. 物料分类初始化
const mongoose = require('mongoose');
const categoryModel = require('../server/models/categoryModel');
const categoryList = require('./data/category');
const config = require('../config');

mongoose.connect(config.dbs, {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

categoryModel.remove();

categoryModel.insertMany(categoryList, (err) => {
    if (err) {
        console.error('插入数据失败', err);
    }
})
  1. 生成文件链接

    思路:返回服务器文件链接,从ctx.request.headers中获取当前访问域名,拼接文件路径,通过koa-static中间件暴露目录下的所有文件;如果需要返回文件中的内容,可以通过fs.readFileSync(localPath, ‘utf8’)获取到文件内容作为字符串

数据库操作记录

  • 过滤掉从数据库拿出来的多余的数据
JSON.parse(JSON.stringify(data));
  • 模糊查询
// find传入的查询条件
name: {
    $regex: params.name,
    $options: '$i'
}
  • 查询文档中的tags数组等于参数的tag
tags: {
    $elemMatch: {
        $eq: params.tag
    }
}
  • 获取分页数据
Model.find(query).skip((pageNo - 1) * pageSize).limit(pageSize)
  • 获取集合下的文档个数
const total = await Model.countDocuments(query);
  • 查询集合中指定id的数据
let data = {};
try {
     data = await Model.findById(params.id);
} catch (error) {
     data = {};
}
return data;

  • 整合两个集合的内容,比如查找某分类下的所有物料
const data = await CategoryModel.aggregate([{
      $match: params
    }, {
      $lookup: {
      // 在materials集合中找到和categories中code匹配的数据作为一个materialList字段
           from: 'materials',
           localField: 'code',
           foreignField: 'category',
           as: 'materialList',
       }
}]);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值