@Bios
2018-12-10T08:38:59.000000Z
字数 16049
阅读 1366
Vue express
这是我毕业项目,从0到1,前后台独立开发完成。功能不多,在此记录,温故而知新!项目github地址:https://github.com/FinGet/Exam ,博客地址:https://finget.github.io/
更新记录:2018-4-9,md5加密
window下安装mongodb,需要参考的可以移步我的博客中:win10安装mongodb
本次项目使用的是express4 + vue2+ + elementUI1+ + mongodb3.4+
先看项目文件目录结构:

我页面用的vue所以
server/views和server/public都没有用
vue init webpack exam
npm i -g express-generator// 在项目文件根目录下express server
由于前后台都是写在一个项目中的,我就将server下的package.json和vue下的package.json合并了

npm i axios --save
首先axios不支持vue.use()式声明
// 在main.js中如下声明使用import axios from 'axios';Vue.prototype.$axios=axios;// 那么在其他vue组件中就可以this.$axios调用使用
npm i element-ui --save
import ElementUI from 'element-ui' // 加载ElementUIimport 'element-ui/lib/theme-default/index.css'Vue.use(ElementUI) // 全局使用elementUI
npm i vue-lazyload --save
// main.jsimport VueLazyLoad from 'vue-lazyload'Vue.use(VueLazyLoad, { // 全局使用图片懒加载loading: 'static/loading-svg/loading-bars.svg', // 图片还没加载时的svg图片try: 1 // default 1})
使用懒加载:
<img width="300" height="53" v-lazy="logoSrc" alt="">logoSrc:require('../common/img/logo.png')// 不能写成:<img width="300" height="53" v-lazy="../common/img/logo.png" alt="">
npm i mongoose --save
就不一一列举所有的插件了(没有用vuex)
// commonFun.js//获取sessionStoragefunction getSessionStorage(key, format) {var data;if (sessionStorage.getItem(key)) {if (format == 'json') {data = JSON.parse(sessionStorage.getItem(key));} else {data = sessionStorage.getItem(key);}} else {data = false}return data;}//写入sessionStoragefunction setSessionStorage(key, content, format) {var data;if (format == 'json') {data = JSON.stringify(content);} else {data = content;}sessionStorage.setItem(key, data);}export var mySessionStorage = {get: getSessionStorage,set: setSessionStorage}
全局挂载
// main.jsimport * as commonFun from './common/js/commonFun.js'Vue.prototype.$mySessionStorage = commonFun.mySessionStorage;
在页面中使用
this.$mySessionStorage.set(key,content,format);this.$mySessionStorage.get(key);
// main.js// 登录判断router.beforeEach((to, from, next) => {var userdata = getUserData();if (to.path != '/managelogin'&&to.name!='404'&&to.path != '/'&&to.path != "/frontregister"&&to.path!='/manageregister') { // 判断是否登录if(!userdata.userName){ElementUI.Message.error('抱歉,您还没有登录!');if(to.path.indexOf('front')>0){router.push({path:'/'});} else {router.push({path:'/managelogin'});}} else {next();}}else {next();}})
绑定面包屑要根据实际情况来定,但是
this.$router.currentRoute.matched是最主要的
<template><div class="bread"><el-breadcrumb separator="/"><el-breadcrumb-item v-for="(item, index) in breadData" :key="item.id" :to="{ name: item.meta.breadName=='管理系统'?'Index':item.name }">{{item.meta.breadName}}</el-breadcrumb-item></el-breadcrumb></div></template><script type="text/ecmascript-6">export default {data() {return {breadData:[]}},watch: {$route () {this.initBreadData();}},methods:{//面包屑initBreadData(){this.breadData=this.$router.currentRoute.matched;// console.log(this.breadData)}},created(){this.initBreadData();}}</script>
路由部分:

根据实际情况来,不能套用,要看你的路由怎么写的
this.$router.currentRoute.path
:default-active="activeIndex"
// conponents/sidebar.vue//初始化列表active状态...methods:{initActiveIndex(){// var str =this.$router.currentRoute.path;this.activeIndex=this.$router.currentRoute.path;// console.log(str)}},watch:{'$route':'initActiveIndex'},created(){this.initActiveIndex();}...
要想请求到后台数据,这一步是必须的
配置代理之后,localhost:8088/api/* -> localhost:3000/api/*
config/index.jsproxyTable: {// proxy all requests starting with /api to jsonplaceholder'/api': {target: 'http://127.0.0.1:3000/api', // 端口号根据后台设置来,默认是3000changeOrigin: true,pathRewrite: {'^/api': '' // 若target中没有/api、这里又为空,则404;}}},
<div v-if="dialogForm.type!='judgement'&&dialogForm.type!='Q&A'"><el-form-item v-for="(item,index) in dialogForm.surveyQuestionOptionList":key="item.key":label="'选项'+(index+1) +':'":prop="'surveyQuestionOptionList.' + index + '.optionContent'":rules="{required:true, message:'选项不能为空', trigger:'blur'}">// 最重要的是prop 一定要带上`.optionContent`,也就是你绑定值的key<el-input placeholder="请输入选项" class="dialog_input" v-model="item.optionContent"></el-input><i class="el-icon-delete delete-icon" @click="deleteDlalogOption(index)"></i></el-form-item><el-button type="primary" size="small" class="marginB10" @click="addDialogOption">添加选项</el-button></div>
goToExam(id){// params传参只能用name引入this.$router.push({name:'ForntExam',params:{id:id}});}
<div class="single"><h4>单选题(只有一个正确答案)</h4><ul><li class="marginB10" v-for="(item,index) in singleQuestions" :key="item.id"><p class="question-title">{{index+1}} 、{{item.name}}()</p><span class="option"v-if="item.type!='judgement'&&item.type!='Q&A'"itemv-for="(item1,index1) in item.selection" :key="item1.id"><el-radio v-model="item.sanswer" :label="options[index1]" :key="index1">{{options[index1]}}、{{item1}}</el-radio></span></li></ul></div>
init(){if(this.id == '' || !this.id ){this.$router.push({path:'forntexamindex'});return} else {this.$axios.get('/api/getExamInfo',{params:{id: this.id}}).then(response => {let res = response.data;if(res.status == '0') {for(let key in this.paperData) {this.paperData[key] = res.result[key];}res.result._questions.forEach(item => {if(item.type=='single'){item.sanswer = ''; // 重要的在这 给他新增一个属性,用来存答案this.singleQuestions.push(item);} else if(item.type == 'multi'){item.sanswer = []; // 多选题this.multiQuestions.push(item);} else if(item.type == 'Q&A') {item.sanswer = '';this.QAQuestions.push(item);} else if(item.type == 'judgement'){item.sanswer = '';this.judgeQuestions.push(item);}})}}).catch(err => {this.$message.error(err);})}}
在server根目录下新建db.js
// db.jsvar mongoose = require('mongoose');var dbUrl = 'mongodb://127.0.0.1:27017/examSystem';var db = mongoose.connect(dbUrl);db.connection.on('error',function(error) {console.log('数据库链接失败:'+ error);});db.connection.on('connected',function() {console.log('数据库链接成功!');});db.connection.on('disconnected',function() {console.log('Mongoose connection disconnected');});module.exports = db;
// server/app.js// 链接数据库require('./db');
需要express-session 和 cookie-parser插件
// app.js// 加载解析session的中间件// session 的 store 有四个常用选项:1)内存 2)cookie 3)缓存 4)数据库// 数据库 session。除非你很熟悉这一块,知道自己要什么,否则还是老老实实用缓存吧 需要用到(connect-mongo插件 line 7)// app.use(sessionParser({ 会在数据库中新建一个session集合存储session// secret: 'express',// store: new mongoStore({// url:'mongodb://127.0.0.1:27017/examSystem',// collection:'session'// })// }));// 默认使用内存来存 session,对于开发调试来说很方便app.use(sessionParser({secret: '12345', // 建议使用 128 个字符的随机字符串name: 'userInfo',cookie: { maxAge: 1800000 }, // 时间可以长点resave:true,rolling:true,saveUninitialized:false}));
默认的使用方式:
// appi.jsvar index = require('./routes/index');app.use('/', index);
// routes/indexvar express = require('express');var router = express.Router();/* GET home page. */router.get('/', function(req, res, next) {res.render('index', { title: 'Express' });});module.exports = router;
我之前做的一个电子商城采用的这种方式:github地址
我的项目中:
// app.jsvar indexs = require('./routes/index');var routes = require('./routes/routes');indexs(app);routes(app);
// routes/index.jsmodule.exports = function(app) {app.get('/api', (req, res) => {res.render('index', {title: 'Express'});})}
两种方式有什么不同:
- 如果你有多个路由文件 (例如goods.js,index.js,users.js……),你都需要去app.js中引入
// app.jsvar index = require('./routes/index');var users = require('./routes/users');var goods = require('./routes/goods');app.use('/', index);app.use('/users', users);app.use('/goods', goods);
在前台请求的时候:
// goods.js....router.get("/list", function (req, res, next) {...}
// xxx.vue...this.$axios.get('/goods/list').then()... // 不能忘了加上goods,也就是你在app.js中定义的一级路由...
如果没看懂,可以去GitHub上看一下实际代码,有助于理解
route.js就搞定了
// route.jsvar Teacher = require('../controllers/teacher'),Student = require('../controllers/student');module.exports = function(app) {/*----------------------教师用户----------------------*/app.post('/api/register',Teacher.register);// 用户登录app.post('/api/login', Teacher.signup);// 登出app.post("/api/logout", Teacher.signout);// 获取用户信息app.post('/api/getUserInfo',Teacher.getUserInfo);// 修改用户信息app.post('/api/updateUser', Teacher.updateUser);// 获取试卷(分页、模糊查询)app.get('/api/mypapers', Teacher.getPapers);// 保存试卷app.post('/api/savePaper', Teacher.savePaper);// 发布试卷app.post('/api/publishPaper', Teacher.publishPaper);// 删除试卷app.post('/api/deletePaper', Teacher.deletePaper);// 查找试卷app.post('/api/findPaper', Teacher.findPaper);// 修改试题app.post('/api/updateQuestion', Teacher.updateQuestion);// 修改试卷app.post('/api/updatePaper', Teacher.updatePaper);// 获取所有的考试app.get('/api/getAllExams',Teacher.getAllExams);// 获取已考试的试卷app.get('/api/getExams',Teacher.getExams);// 获取学生考试成绩app.get('/api/getScores', Teacher.getScores);// 批阅试卷app.get('/api/getCheckPapers', Teacher.getCheckPapers);// 打分提交app.get('/api/submitScore', Teacher.submitScore);/*----------------------学生用户----------------------*/// 学生注册app.post('/api/studentregister',Student.register);// 学生登录app.post('/api/studentlogin', Student.signup);// 学生登出app.post('/api/studentlogout', Student.signout);// 修改信息app.post('/api/updateStudent', Student.updateStudent);// 获取考试记录app.get('/api/getexamlogs', Student.getExamLogs);// 获取个人信息app.get('/api/studentinfo', Student.getInfo);// 获取考试信息app.get('/api/getExamsPaper',Student.getExams);// 获取试卷信息app.get('/api/getExamInfo',Student.getExamInfo);// 提交考试信息app.post('/api/submitExam',Student.submitExam);}
可以看到,我将每个路由的方法都是提取出去的,这样可以避免这个文件不会有太多的代码,可读性降低,将代码分离开来,也有助于维护

在使用的时候:
// xxx.vue...this.$axios.get('/api/getexamlogs').then()......
我这次用mongodb,主要是因为可以用js来操作,对我来说比较简单,mysql我不会用。在实际开发过程中发现,考试系统各个表(集合)都是需要关联,mongodb这种非关系型数据库,做起来反而麻烦了不少。在此将一些数据库增删改查的方法回顾一下。
如果对mongodb,mongoose没有基础的了解,建议看一看mongoose深入浅出 ,mongoose基础操作
// controllers/student.jsconst Student = require('../model/student');var mongoose = require('mongoose');var Schema = mongoose.Schema;var student = new Student({userId: 12001, // 学号userName: '张三', // 用户名passWord: '123321', // 密码grade: 3, // 年级 1~6 分别代表一年级到六年级class: 3, // 班级exams:[{ // 参加的考试_paper:Schema.Types.ObjectId("5a40a4ef485a584d44764ff1"), // 这个是_id,在mongodb自动生成的,从数据库复制过来,初始化一个学生,应该是没有参加考试的score:100,date: new Date(),answers: []}]})// 保存student.save((err,doc) => {console.log(err);});
exports.register = function (req,res) {let userInfo = req.body.userInfo; // req.body 获取post方式传递的参数Student.findOne(userInfo,(err,doc) => {if(err) {...} else {if(doc) {res.json({status:'2',msg: '用户已存在'})} else {userInfo.exams = [];// userInfo 是个对象,包含了用户相关的信息Student.create(userInfo,(err1,doc1) => {if(err1) {...}else {if(doc1) {...} else {...}}})}}})};
如下图是我的student集合:
在该集合中,学生参加过的考试记录,存在exams数组中,当想实现分页查询几条数据的时候,需要用到$slice
$slice:[start,size]第一个参数表示,数组开始的下标,第二个表示截取的数量
在后台接收到前台传递的pageSize和pageNumber时,需要计算出当前需要截取的下标,即let skip = (pageNumber-1)*pageSize
exports.getExamLogs = function (req, res){let userName =req.session.userName;let name = req.param('name');// 通过req.param()取到的值都是字符串,而limit()需要一个数字作为参数let pageSize = parseInt(req.param('pageSize'));let pageNumber = parseInt(req.param('pageNumber'));let skip = (pageNumber-1)*pageSize; // 跳过几条let reg = new RegExp(name,'i'); // 在nodejs中,必须要使用RegExp,来构建正则表达式对象。Student.findOne({"userName":userName},{"exams":{$slice:[skip,pageSize]}}).populate({path:'exams._paper',match:{name: reg}}).exec((err,doc) => {if (err) {...} else {if (doc) {res.json({status: '0',msg:'success',result:doc,count: doc.exams.length?doc.exams.length:0})} else {...}}})};

每个试卷都是独立的文档,通过他们的名称
name实现模糊查询
// 获取考试信息exports.getExams = function (req,res) {let userName =req.session.userName;let name = req.param('name');// 通过req.param()取到的值都是字符串,而limit()需要一个数字作为参数let pageSize = parseInt(req.param('pageSize'));let pageNumber = parseInt(req.param('pageNumber'));let skip = (pageNumber-1)*pageSize; // 跳过几条let reg = new RegExp(name,'i'); // 在nodejs中,必须要使用RegExp,来构建正则表达式对象。Student.findOne({"userName":userName},(err,doc)=>{if(err) {res.json({status: '1',msg: err.message})} else {if(doc) {// 关键在这里Paper.find({startTime:{$exists:true},name:reg}).skip(skip).limit(pageSize).populate({path:'_questions'}).exec((err1,doc1)=>{....})};

先通过
populate查询除关联文档,在模糊分页查询
exports.getPapers = function (req, res) {// console.log(req.session.userName);let name = req.param('name'),// 通过req.param()取到的值都是字符串,而limit()需要一个数字作为参数pageSize = parseInt(req.param('pageSize')),pageNumber = parseInt(req.param('pageNumber')),userName = req.session.userName;let skip = (pageNumber-1)*pageSize; // 跳过几条let reg = new RegExp(name,'i'); // 在nodejs中,必须要使用RegExp,来构建正则表达式对象。let params = {name: reg};Teacher.findOne({'userName':userName}).populate({path:'_papers',match:{name: reg},options:{skip:skip,limit:pageSize}}).exec((err, doc) => {....})};
mongodb本来就是非关系型的数据库,但是有很多时候不同的集合直接是需要关联的,这是就用到了mongoose提供的populate
直接看图,不同集合直接的关联,用的就是_id,比如下图中,学生参加的考试,关联了试卷,试卷里面又关联了题目

怎么查询呢:
Student.findOne({}).populate({path:'exams._paper'}).exec(....)
更多的可以看看我项目中的实际代码都在server/controllers下面
在系统中,教师可以增加试卷,这个时候我就不知道该怎么保存前台传过来的数据。数据中既有试卷的信息,也有很多题目。题目都属于该试卷,改试卷又属于当前登录系统的老师(即创建试卷的老师)。
怎么才能让试卷、教师、问题关联起来啊,ref存的是_id,然而这些新增的数据,是保存之后才有_id的。
exports.savePaper = function (req, res) {let paperForm = req.body.paperForm;let userName = req.session.userName;if(paperForm == {}){res.json({status:'5',msg: '数据不能为空'})}// 第一步查找当前登录的教师Teacher.findOne({"userName": userName}, (err,doc)=>{if (err) {...} else {if (doc) {let paperData = {name:paperForm.name,totalPoints:paperForm.totalPoints,time:paperForm.time,_teacher: doc._id, // 这里就可以拿到教师的_id_questions: [],examnum:0}// 第二步创建试卷Paper.create(paperData,function (err1,doc1) {if (err1) {...} else {if (doc1) {doc._papers.push(doc1._id); // 教师中添加该试卷的_iddoc.save(); // 很重要 不save则没有数据// 第三步 创建问题paperForm._questions.forEach(item => {item._papers = [];item._papers.push(doc1._id); // 试卷中存入试卷的_id,因为此时已经创建了试卷,所以可以拿到_iditem._teacher = doc._id; // 试卷中存入教师的_id})Question.create(paperForm._questions,function (err2,doc2) {if (err2) {...} else {if (doc2) {doc2.forEach(item => {doc1._questions.push(item._id); // 当问题创建成功,则在试卷中存入问题的_id})doc1.save();res.json({status:'0',msg: 'success'})} else {...}}})} else {...}}})}else {...}}})};
删除某一个试卷,既要删除教师中对应的试卷_id,也要删除问题中对应的试卷_id
// 删除试卷exports.deletePaper = function (req, res) {let id = req.body.id;let userName = req.session.userName;// 第一步 删除教师中的_id _papers是一个数组,所以用到了`$pull`Teacher.update({"userName":userName},{'$pull':{'_papers':{$in:id}}}, (err,doc)=>{if (err) {res.json({status:'1',msg: err.message})} else {if (doc) {// 第二步 删除试卷 即 移除一个文档Paper.remove({"_id":{$in:id}},function (err1,doc1){if(err1) {res.json({status:'1',msg: err1.message})} else {if (doc1) {// 第三步 updateMany删除多个问题中的_id 这里并没有删除试卷中包含的问题,是为了以后题库做准备Question.updateMany({'_papers':{$in:id}},{'$pull':{'_papers':{$in:id}}},function (err2,doc2) {if(err2){...} else {if (doc2){...}}})} else {...}}})} else {...}}})};
// 修改试卷-修改试卷exports.updatePaper = function (req,res) {let userName = req.session.userName;let params = req.body.params;let paperParams = { // 试卷需要更新的字段name: params.name,totalPoints: params.totalPoints,time: params.time}let updateQuestion = []; // 需要更新的题目let addQuestion = []; // 需要新增的题目params._questions.forEach(item => {if(item._id) { // 通过判断是否有_id区分已有的或者是新增的updateQuestion.push(item);} else {addQuestion.push(item);}})Teacher.findOne({'userName':userName},(err,doc)=>{if (err) {...} else {if (doc) {Paper.findOneAndUpdate({"_id":params._id},paperParams,(err1,doc1) => {if(err1) {...}else {if(doc1){updateQuestion.forEach((item,index)=>{ // 循环更新题目,好像很傻的方法,可能有更好的办法Question.update({"_id":item._id},item,(err2,doc2)=>{if(err2){res.json({status:'1',msg: err2.message})}else {if(doc2){if(index == (updateQuestion.length-1)){if (addQuestion.length>0){addQuestion.forEach(item => {item._papers = [];item._papers.push(doc1._id);item._teacher = doc._id;})// 创建新增题目Question.create(addQuestion,(err3,doc3) => {if(err3) {...} else {if(doc3) {doc3.forEach(item => {doc1._questions.push(item._id); // 还要将新增的题目关联到试卷当中})doc1.save(); // 很重要 不save则没有数据res.json({status:'0',msg: 'success'})// .......................判断太长省略........................})};
// 打分提交exports.submitScore = function (req, res) {let name = req.param('userName'),date = req.param('date'),score = req.param('score') - 0,userName = req.session.userName;Teacher.findOne({'userName':userName},(err,doc) => {if(err) {...} else {if(doc) {Student.update({"userName":name,"exams.date":date},{$set:{"exams.$.score":score,"exams.$.isSure":true}},(err1, doc1) => {if(err1) {...} else {if(doc1) {...} else {...}}})} else {...}}})};
//student.jsconst crypto = require('crypto');let mdHash = function(data){// hash 的定义要写在这个方法内,不然会报错Digest already called ****const hash = crypto.createHash('md5');return hash.update(data).digest('hex');}// 使用//注册exports.register = function (req,res) {let userInfo = req.body.userInfo;获取到前台传过来的密码,先加密再存储userInfo.passWord = mdHash(userInfo.passWord);...