功能介绍:用户通过登陆注册,可在物料市场上传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端功能拆解
- 入口:使用中间件和插件、连接数据库、监听端口
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);
- 登陆注册
- 注册
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;
- 上传
思路:从请求体中拿到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;
- 物料分类初始化
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);
}
})
- 生成文件链接
思路:返回服务器文件链接,从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',
}
}]);