个人健身系统从零开始(Node.js开发)

Description

截止今日8月3日,个人健身管理系统第一版本已经开发好了,本地数据已经上传成功。来几张图欣赏一下。 pic1 pic2 pic3 pic4 pic5 pic6 pic7

正式简介

该管理系统是由我写完node.js爬虫后第一个练手的项目,起因是因为被人嘲讽瘦弱于是奋起锻炼,然后跟«囚徒健身»对上了眼,因此打算写一个能够管理自己健身的一个应用,正好又打算写一个管理系统,因此我就顺势而动了。

准备工作

因为打算现在网上各种教程都比较零零碎碎,好像正式的详细的写一个从思路到成果的教程没几个,为了以后回忆方便以及方便后来人,我打算将工具,当时的想法等等一并写进来。

环境准备

  • Mac一台
  • Node.js环境 mongo数据库环境(这个要一直开着,按照完成后运行命令mongod)
  • npm
  • express最新版本4.13.1(我用supervisor运行)
  • 能上国外网站的环境(我之前是红杏,但是它出问题的情况太多,因此我又买了枫叶主机的shadowssocks包年套餐)
  • 前端模板 bootstrap 以及二次开发模板,因为我比较注重功能的实现,界面设计不怎么在行,因此我选择了sublime和Metronic这两套模板(注意:这些模板在http://themeforest.net/有购买,海淘步骤比较繁琐,不过一个Paypal可以解决)
  • 高质量图片搜集工具zoommy(现在是4美金,作者常年在线=0=),这是搜集网上高质量以及无版权的图片,用起来舒心又放心,一张好的背景图效果加成很多!
  • monghub 以及robomongo (可视化的mongo数据库工具,前者界面看起来不错,后者功能很完全,我前期是用monghub,后期调试用robomongo)

思路整理

首先我一向是先设计界面,然后再设计数据库,最后开始功能的开发,因为我觉得只要设计稿有了,功能的思路就会很清晰,然后功能的开发一般不会偏离到哪里去-0-,一开始设计的是主页和注册登录页面以及用户管理页面。这类顺便提一句,开发是以node.js为核心,因此最好先去熟悉一下Node.js。推荐书籍«Node.js实战»«Node.js开发指南»,顺便把官网和国内学习node.js的基础文章都扫一遍就差不多了,具体文章我就不给出了,一搜一大把。

开发流程

界面设计

界面设计我是用hbuilder为编辑器,然后直接套用模板的一些界面,因为我是用express4.x版本开发,因此我选择了Jade为Html模板。ejs我当初用在爬虫上感觉不是很方便,jade的资料本来想共享的-0-但是印象笔记关闭了共享功能,这次项目开发我积累了不少资料,需要的同学可以发邮件给我,我用印象笔记的邮箱共享分享给你。jade的写法很简洁,但是我们是抄模板,如果抄一个比较复杂的组件可能就比较麻烦,本来想自己写一个转换工具的,但是后来发现有人写了。html2jadehttp://html2jade.org/,这个工具有个很漂亮的实现,以后再去研究。然后背景图用zoommy搜索了一些健身相关的图片。静态页面也差不多了。

动态效果开发

有一个管理每日健身的选项需要勾选的时候添加删除线,然后标记已完成。这个功能直接用jq来实现,只是动态的添加class而已。 还有一个是管理页面的菜单选择,只要把其他选项先hide,然后选择哪个show哪个就行了。具体代码都可以参考github上的。 还有几个动态效果是模板自带的,只要自己把该加载的js加载就行了。后面我会提到什么时候用Layout模板的加载,什么时候自己写加载。

路由

node.js的路由写法分三种,然后express自己建立的脚手架一开始就帮助写好了几个,因此只需要按照它的样子继续写所需要的就行了。我们分为4个路由,登录,注册,首页,用户管理。先来看看我们的加载。

var express = require('express');  
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session');
var connect = require('connect');
var multiparty = require('multiparty');
var http = require('http');
var util = require('util');
var fs = require('fs');
var SessionStore = require("session-mongoose")(connect);
var store = new SessionStore({
url:"mongodb://localhost/session",
  interval: 120000
});

因为用户管理系统需要session,而且我用mongoose来处理数据,因此我采用网上一种session的写法,用session-mongoose这个模块。图片上传用到了connetc这个模块以及后面几个常规模块

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
app.get('/logout', routes);
app.get('/linshui', routes);
app.post('/addnewtrain', routes);
app.get('/deletetrain/:id', routes);
app.get('/show/:id',routes);
app.use('/user', users);
app.post('/addusertrain', users);
app.get('/deletemytrain/:week/:train', users);
app.use('/signin', sign_in);
app.get('/signin/:name/:pass',sign_in);
app.use('/signup', sign_up);
app.get('/signup/:regname/:regpass/:regemail',sign_up);

app.use(function(req, res, next) {
  res.locals.user = req.session.user || null;
    next();
}); 路由写法experss得先写use,如果遇到特殊情况比如post或者get,都要写到对应use的下面。如:

app.use('/', routes);
app.get('/logout', routes);
app.get('/linshui', routes);
app.post('/addnewtrain', routes);
app.get('/deletetrain/:id', routes);

其中/linshui我写在默认的routes里面,这里面包含一些列管理后台中用到的路由 之后来看数据库这块。

var mongoose = require("mongoose");

// 连接字符串格式为mongodb://主机/数据库名
mongoose.connect('mongodb://localhost/gym');

// 数据库连接后,可以对open和error事件指定监听函数。
var db = mongoose.connection;
db.on('error', console.error);
db.once('open', function() {
    console.log('连接成功')
	    //在这里创建你的模式和模型
});

var Schema1 = mongoose.Schema;
var gymuserSchema = new Schema1({
    name : String,
    pass : String,
    email : String,
    avatar : String,
    mytrain:[]
});


var User = mongoose.model('User', gymuserSchema);
//倒出模型
module.exports = User 每个用户都有一个mytrain的数组,用来存放他们的健身项目。当初查文档的时候看到这种写法也是惊呆了,搭配上Mongoose的方便操作,数据库的ISUD四种操作都有官方文档的支持。数据库不设置密码,用户权限验证操作直接在route里面写。

功能实现

注册登录

来看这段代码

var express = require('express');
var router = express.Router();
//数据操作对象
var User = require('../database/user');
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('signup');
});
router.get('/signup', function(req, res, next) {
res.render('signup');
});
router.get('/signup/:regname/:regpass/:regemail', function(req, res,next) {
var regname = req.params.regname;
var regpass = req.params.regpass;
var regemail = req.params.regemail;
console.log(regname + regpass + "email:" + regemail);

User.findOne({ name: regname }, function(err, content) {
	console.log(content);
	if(content!=null){
		res.end("用户名已存在!");
    }else{
		var newuser = new User(User);
		newuser.name = regname;
		newuser.pass = regpass;
		newuser.email = regemail;
		console.log(newuser.name + newuser.pass + "email:" + newuser.email);
		newuser.save(function(err) {
			if (err) {
				console.log('保存失败');
			}
			  req.session.user = regname;
			  console.log(regname);
			  console.log('数据保存成功');
				
				res.write("done");
				res.end();
			});
}
});
});
router.get('/signup.html', function(req, res, next) {
res.render('signup');
});
module.exports = router;

每一个route必须先在app.js里面注册,否则会报错。参数的传递是用jq里面get提交方式

    $.get("/signup/" + regname + "/" + regpass + "/" + regemail,function(data){       
        if(data == 'done')          
        {
           window.location.href="/signin";
        }else{
				    var $alert = $("<div class='alert alert-danger' > ").text(data);
				    $('.alerterror').append($alert);
          setTimeout(window.location.href="/signup",3000);
        }
    });   每个route处理数据都要记得调用end(),否则会一直等到服务器处理,最后超时报错。   登录模块类似,但是登录之后还有一句代码。
  
req.session.user = req.params.name;  这段是把session储存,之后在各个需要身份验证的地方都能方便的调用。其实身份验证还可以用cookie,在前端页面里面写验证也是可以的。

健身项目的管理

普通用户的操作

查看自己的每日健身项目。遍历自己的健身项目,并且查看标准。增加删除健身项目。 这些功能是基于已经后台添加了健身项目的前提下。因此我们需要先把数据库建立起来。 train.js包含了所需的基本内容

var mongoose = require("mongoose");

// 连接字符串格式为mongodb://主机/数据库名
mongoose.connect('mongodb://localhost/gym');

// 数据库连接后,可以对open和error事件指定监听函数。
var db = mongoose.connection;
db.on('error', console.error);
db.once('open', function() {
console.log('连接成功')
	//在这里创建你的模式和模型
});


/*
 * 训练单项包括:
 * 1、名称 2、具体描述 3、初级量标准 4 中级量标准 5、进阶量标准 6、演示图片
 * 
 */
var Schema2 = mongoose.Schema;
var trainSchema = new Schema2({
    name : String,
    content : String,
    junior_num : Number,
    junior_rate : Number,
    middle_num :Number,
    middle_rate : Number,
    advanced_num : Number,
    advanced_rate: Number,
    pic_array :[]
})

var Train = mongoose.model('Train', trainSchema);
//倒出模型
module.exports = Train

当初我把train和user写在一个文件里导出,但是出错,之后我就把两个数据schema分离并且分别导出。

之后我先完成了后台增加健身项目这个功能。 这其中就包括了图片上传,图片上传我现实在npmjshttps://www.npmjs.com/里面查找上传模块,测试了几个都不怎么理想。然后google了一下,好像是说express自带上传模块,后来经过测试,发现高版本的express已经不支持自带了,因此又查找,最后在多个模块测试后选择了multiparty这个模块。我选择在app.js写上传功能,因为我怕后面各个地方都需要用到,因此放在公共的地方。

app.post('/upload', function(req, res, next){  
 var form = new multiparty.Form({uploadDir: './public/images/'});
  //下载后处理
    form.parse(req, function(err, fields, files) {
      var filesTmp = JSON.stringify(files,null,2);
  
     if(err){
     console.log('parse error: ' + err);
      } else {
        console.log('parse files: ' + filesTmp);
        var inputFile = files.inputFile[0];
        var uploadedPath = inputFile.path;
        var dstPath = './public/files/' + inputFile.originalFilename;
		console.log("上传成功:" + uploadedPath);
        res.writeHead(200, {'content-type': 'text/plain;charset=utf-8'});
        res.end(uploadedPath);
  }
 });
});

之后在后台Linshui.jade里面写上传页面

form(method='post', enctype='multipart/form-data', action='/upload',style="margin-top:15px")#frmUploadFile1
  .col-md-3.col-md-offset-3
    input(name='inputFile', type='file', multiple='mutiple').form-control
  .col-md-3
    input#imgurl1.alert.alert-success
  .col-md-2
    i.fa.fa-upload
  .btn.btn-primary.uploadMe1 上传

因为提交表单还需要上传后的地址,因此我在表单里面加了一个Input来放置上传后的url。上传图片我用jq的ajax提交。

$(".uploadMe1").click(function(){
	var formData = new FormData($("#frmUploadFile1")[0]);
	  $.ajax({
	    url: '/upload',
	    type: 'POST',
	    data: formData,
	    async: false,
	    cache: false,
	    contentType: false,
	    processData: false,
	    success: function(data){
	      if(data) {
	        alert("上传成功");
		      $("#imgurl1").val(data.slice(14,data.length));
	      } else {
	        alert("上传失败");
	      }

	    },
	    error: function(){
	      alert("与服务器通信发生错误");
	    }
	  });
	
	});   因为上传模块最后返回的是绝对地址,因此需要进行处理,因为返回的内容是固定的,因此直接slice分割一些就行了。

图片搞定后就是添加健身项目表单的提交。这里提一句,如果是get提交,那么在后台取是根据url里面的参数来取,因此写route的时候要把参数写对,不能多也不能少。get是以req.params.xxx来获取参数。post是以req.body.xxx来获取。如果参数不多,还是建议get来提交。当然如果为了安全性,Post提交当然更好。 提交添加健身项目表单 后台处理代码

router.post('/addnewtrain', function(req, res, next) {
   if(req.session.user == "admin"){

Train.findOne({ name: req.body.title }, function(err, content) {
	console.log(content);
	if(content!=null){
		res.end("用户名已存在!");
}else{
		var newtrain = new Train(Train);
		newtrain.name = req.body.title;
		newtrain.content = req.body.desc;
		newtrain.junior_num = req.body.junior_num;
		newtrain.junior_rate = req.body.junior_rate;
		newtrain.middle_num = req.body.middle_num;
		newtrain.middle_rate = req.body.middle_rate;
		newtrain.advanced_num = req.body.advanced_num;
		newtrain.advanced_rate= req.body.advanced_rate;
		if(req.body.img1){
			newtrain.pic_array.push(req.body.img1);
		}
		if(req.body.img2){
			newtrain.pic_array.push(req.body.img2);
		}
		if(req.body.img3){
			newtrain.pic_array.push(req.body.img3);
		}
		newtrain.save(function(err) {
			if (err) {
				console.log('保存失败');
			}
			  console.log('数据保存成功');
			  res.write("done");
				res.end();
			});
}
});
	
}else{
	res.redirect('/');
}
});

前台处理代码

 $(".addnewtrain").click(function(){
   var title = $("#traintitle").val();
   var desc = $("#traindesc").val();
   var junior_num = $("#trainjunior_num").val();
   var junior_rate = $("#trainjunior_rate").val();
   var middle_num = $("#trainmiddle_num").val();
   var middle_rate = $("#trainmiddle_rate").val();
   var advanced_num = $("#trainadvanced_num").val();
   var advanced_rate = $("#trainadvanced_rate").val();
   var img1 = $("#imgurl1").val();
   var img2 = $("#imgurl2").val();
   var img3 = $("#imgurl3").val();
   
   $.post("/addnewtrain",{
   
    title:title,
    。。。
  	},
  	  function(result){
      console.log(result);
      if(result=="done"){
      	  window.location.href="/linshui";
      }
    });
});

之后的用户添加健身项目就是直接从数据库里面获取。 这里是关键代码

Train.find(function(err,trains) {  
User.find({name: req.session.user
}, function(err,contents) {
if (err) {
console.log(err)
  return
}
var mytrains = contents[0].mytrain;
  res.render('user'{user:req.session.user,trains:trains,mytrains:mytrains,todaytrains:todaytrains,myalltrains:myalltrains});
});   删除健身项目代码,mongoose里面已经有很完美的处理方案,我们只需根据文档来操作就行了。

结尾

事实上还有几个功能还没加进来,比如用户资料,比如健身日记,比如每日签到,比如提交训练结果后根据锻炼时间长短分析提示应该进行下一个阶段的训练等等。不过对于我来说,这个系统已经够用了。而且今天写博客的中途去完成了今日的任务列表。人已经虚了=-=就先写到这里。后续功能将一步步完善,所有的代码我都放在了githubhttps://github.com/linshuizhaoying/gym 有什么建议还有问题可以email我,因为现在还没对博客添加评论插件,一切回复都用左下角的邮箱来。

Table of Contents