博文已完成,版本号v1.0
范例网址已移除,请下载源码(下载后需要自行设置mysql)
附完整源代码下载链接(0积分下载):http://download.csdn.net/detail/qq20004604/9638269
v1.01
订正一个数据库查询重复内容的bug,在最后的models/blog.js里面
v1.02
订正一个加载博客时,无法显示”加载中“的bug;并且更改加载条件,改为调用定时器
v1.03
修复因为1.02版本而导致的登录、注册无法正常进行。解决办法是将主页的逻辑拆分出来,分拆为blog.js和index.js
【0】涉及到的框架、引擎、数据库:
① express 4.X
② jade
③ mysql 5.7.x
注:
①内容较长,我会尽力把整个框架、结构、顺序、思考方式说明白。
②基础是《node.js开发指南》这本书,作者:BYVoid。但他书的版本较老,很多东西现在已经无法应用,故进行更新,使用目前普遍的express 4.x版本(原书似乎是2.X版本),mysql(原书是mongodb),jade(原书是ejs)
【1】基本需求:
①有首页;
②支持注册;
③支持登录、登出;
④可以发表博客,发表的博客可以保存到数据库;
⑤可以查看博客,博客从数据库中读取,为了简化,这里设置为查看所有人的博客;
⑥查看博客时,初始查看的数量有限,但可以无限加载新的博客,直到查看完全部博客;
⑦一定程度上实现多语言(即可以切换显示的语言版本),但由于复杂度所限,因此只部分实现(但框架已建立好,可以通过继续完善代码实现整体的国际化);
⑧根据登录状态,对可以访问的页面进行控制(某些允许某些禁止)
【2】前后端交互的简单过程:
【3】关于客户端请求的几种情况(涉及到数据库的)
【4】npm
npm是包管理器,在新版本里是默认安装好的,可以输入:
npm -v
来查看npm的版本
【5】express框架:
首先安装基础的express框架,他是封装好的web开发框架,里面包含了:
①路由控制、
②log显示、
③解析客户端请求、
④cookies解析、
⑤静态文件的控制
等多种功能。
安装前注:
①有的人只需要简单的
npm install -g express
npm install -g express-generator
就可以愉快的跑起express了,有的人就像向我一样苦逼,尝试各种办法,最后勉强可以用。
如果在这两行命令后,输入(V是大写的)
express -V
会返回版本号,那么直接跳到最后来看,如果不是这样,可以参考我写的记录来安装。
或者直接看后面的终极解决方案
②express设置全局方法:
ln -s /usr/nodejs4.4.7/node-v4.4.7-linux-x64/bin/express /usr/local/bin/express
其他需要全局的方法,理论上同理,即将nodejs安装目录下的bin文件夹下的模块,映射到/usr/local/bin/同名文件即可
③express命令可以使用的人:
输入:
express -t jade myblog
效果是建立一个文件夹名为myblog的文件夹,里面会有一些文件。
正常如下图:
cd myblog
npm install
这时,npm会根据package.json来安装一些东西。
按提示输入:
SET DEBUG=myblog:*
npm start
可以启动项目。
本机的话,通过访问http://127.0.0.1:3000/ 来查看效果
服务器的话,访问其公网ip,端口是3000
查看package.json
"scripts": {
"start": "node ./bin/www"
},
这条属性告诉我们,需要通过bin文件夹下的www来启动,这也就是上面npm start命令的作用。
www文件应该是设置自启动的,然而我这里并不需要。但若直接启动app.js是启动不了的,因为在www文件里面,设置了端口是3000,他是通过www文件来启动监听的。
解决办法:
在app.js里面,在最后一行代码之前,添加:
app.listen(80);
于是,便可以通过app.js来启动服务器了。
访问效果如图:
假如如果像我一样倒霉,无法用express命令,打开npm也特别慢,可以先找个系统,将这些文件下载好,然后将这些文件复制到不可以用express命令的linux系统下面。
或者看最后的终极解决方案
ps:
如果npm很慢的话,可以考虑装cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
然后在npm install这一步,使用cnpm install来替代
————————分割线————————
④超级终极解决方案:
我传一个装好express的压缩包,直接解压缩后就可以用。
链接:
http://download.csdn.net/detail/qq20004604/9587054
⑤另一个启动方法
即不通过app.js来启动;
在之前④的基础上,打开app.js,删除
app.listen(80);
打开package.json,将
"start":"node ./app.js"
改回
start":"node ./bin/www"
然后
cd bin
vi www
将
var port =normalizePort(process.env.PORT||'3000');
修改为
var port =normalizePort(process.env.PORT||'80');
然后
cd ..
npm start
启动成功,可以直接访问地址来访问页面
【6】把启动的app.js放在后台运行,并且在操作端解除控制后依然可以运行
原本使用npm start的地方,输入
nohup npm start&
即可
【7】app.js之解释
app.js可以说是一切的根本,因此请看注释,解释了每行代码的作用:
var express = require('express'); //这个模块在node_modules文件夹中
var path = require('path'); //nodejs自带的,路径相关
var favicon = require('serve-favicon'); //node_modules文件夹中
var logger = require('morgan'); //日志相关,node_modules文件夹中,具体看第19行代码
var cookieParser = require('cookie-parser');//node_modules文件夹中,用来解析cookie的,被express所调用
var bodyParser = require('body-parser'); //用于解析请求体的(应该),被express所调用
var routes = require('./routes/index'); //这里是默认的路由,路径是routes/index.js
var users = require('./routes/users'); //这里是默认的路由,路径是routes/users.js
//关于路由的更多内容看下面的app.use('/', routes);和app.use('/users', users);的注释
var app = express(); //生成一个express的实例
//设置模板引擎
app.set('views', path.join(__dirname, 'views')); //__dirname表示绝对路径
//console.log(__dirname) //可以取消这一行注释,然后看看__dirname的效果
app.set('view engine', 'jade'); //表示express是使用jade格式的模板的
//下面这行代码是设置左上角的小图标的,将其命名为favicon.ico并放在public文件夹下,如果有的话,取消这行注释
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev')); //设置日志显示,如果不需要的话可以注释掉这行代码,建议练手的时候不要注释掉
//可替换参数有default,combined,common,short,tiny,dev(相对tiny有染色)。可以自己试试效果,默认是dev
app.use(bodyParser.json()); //解析客户端的请求,通常是通过post发送的内容
app.use(bodyParser.urlencoded({extended: false})); //另一种解析方式,具体不太明白(如果上面解析失败的话会通过这个来)
app.use(cookieParser()); //cookies的解析
app.use(express.static(path.join(__dirname, 'public'))); //普通静态html文件、js文件、css文件,都放在public文件夹下,可以直接通过url来访问
//路由的处理
app.use('/', routes); //假如访问的网址是根目录,例如http://121.41.66.68/,交给routes这个js文件来处理,具体请查看routes
app.use('/users', users); //假如访问的是/users这样的路径,那么交给users这个js文件来处理,具体略
//我们最后要对这个路由进行处理,让他按照我们想要的方式来做
//这里对非法路径进行处理的,next表示这是一个中间件(即执行完他之后,还会执行下一个,而不是直接在这里结束了)
//如果上面没有静态文件(29行)、没有找到被路由处理的文件(32,33行),就会交给这个来处理。
app.use(function (req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err); //由下面的2个app.use中的一个来处理(一般是第一个,除非第一个被注释)
});
//原注释说是会对错误进行加亮处理
//这部分和下面的区别在于,这部分会将错误信息暴露给用户,而下面的不会,因此注释掉这部分
//if (app.get('env') === 'development') {
// app.use(function (err, req, res, next) {
// res.status(err.status || 500);
// res.render('error', {
// message: err.message,
// error: err
// });
// });
//}
// production error handler
// no stacktraces leaked to user
app.use(function (err, req, res, next) {
res.status(err.status || 500);
res.render('error', { //这里的error指调用views下的error模板
message: err.message,
error: {}
});
});
//这是我自行添加的,用于显示服务器启动的时间
console.log("Server start at :" + new Date());
//导出app,在./bin/www里会被调用
module.exports = app;
【8】页面关系结构
请忽略文件名的大小写问题(手动微笑)
因此,首先有三个需要我们自定义的路由:
①当访问根/时,输出index;
②当访问/login时,输出login
③当访问/reg时,输出reg
其他可能的路由:
④后续可能添加的页面,暂缺;
⑤不符合要求的页面,输出404(express已经完成)
【9】index页面需要包含的功能:
【10】index的路由:
查看app.js中的代码:
app.use('/', routes);
这部分代码已经说明了,当访问根的时候,交给routes来处理;
再次查看导入routes的地方:
var routes = require('./routes/index');
说明负责这部分的文件是routes文件夹的index文件
这时打开index.js,
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
这部分代码,表示当访问根的 ’/’ 页面时,由index这个引擎模板来处理,模板中的title变量,其值为Express。
————————————————————————
代码说明:
router.get('/'
以上这段代码是在路由基础上进行二段捕获url的,举例:
①假如在app.js里,
app.use('/',
然后在路由的处理里,是
outer.get('/'
那么最终针对的url是/
②假如在app.js里,
app.use('/test',
然后在路由的处理里,是
outer.get('/'
那么最终针对的url是/test
③假如在app.js里,
app.use('/,
然后在路由的处理里,是
outer.get('/test'
那么最终针对的url依然是/test
④假如在app.js里,
app.use('/testA,
然后在路由的处理里,是
outer.get('/testB'
那么最终针对的url是/testA/testB
⑤捕获优先度说明:
首先根据app.js中的app.use出现顺序决定,假如②和③同时出现,那么看②和③在app.js中的代码谁先出现,就交给谁来处理;
再不调用next()方法的情况下,只会被最先出现的那个处理;
如果第一个调用了next()方法,那么第一个处理完后会执行第二个的
————————————————————————
index.js代码说明:
var express = require('express'); //调用express
var router = express.Router(); //生成express的Router方法的一个实例
//处理函数
router.get('/', function (req, res, next) { //捕获根url
res.render('index', {title: 'Express'});
//res.render方法渲染一个引擎模板,
//第二个参数是一个对象,对象里的变量可以在引擎中使用,
//第三个参数是回调函数,提供两个参数,分别是err和html,err是错误,html是渲染后的页面。如果使用这个回调函数,那么将不会自动响应,即要用户自己写返回html的命令
});
module.exports = router;
注意,被引擎渲染的文件,默认是在myblog/views文件夹下
【11】使用Bootstrap界面
让我们自己设计界面不是不可以,不过太麻烦,因此我和原书保持一致,使用Twitter Bootstrap风格的页面。
下载他的压缩包文件,并解压缩他,
将css文件放在项目myblog/public/stylesheets下;
将img文件夹直接复制粘贴在myblog/public下;
将js文件放在myblog/public/javascripts下,
我的Bootstrap的版本是v2.3.2
另外,下载jquery,命名为jq.js,放在myblog/public/javascripts下
【12】index.jade说明
下来我们就要修改jade文件了,如果不知道怎么使用的话,可以看我的博客:
http://blog.csdn.net/qq20004604/article/details/51773574
extends layout
block content
h1= title
p Welcome to #{title}
第一行的extends layout表示他导入的layout模板(即layout.jade)
block content表示以下这部分将套入layout的content位置
内容略。
这时候再过去看layout.jade
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
他导入的是style.css这个样式表,页面的title是变量title
上面的index.jade中的内容将导入这里的block content区域(因为变量名一样,会对应替换)。
其结构大概如下:
然后我们根据自己实际需求来改造:
直接给出代码:
首先在stylesheets文件夹下创建一个blog.css文件,内里样式为:
body {
padding-top: 60px;
padding-botom: 40px;
}
#textarea {
resize: none;
width: 300px;
height: 100px;
cursor: text;
}
#postBlog {
position: relative;
left: 20px;
vertical-align: top;
}
#clearBlog {
position: relative;
left: -90px;
top: 27px;
width: 110px;
height: 44px;
}
.myalert {
position: absolute;
}
.displayNONE {
display: none;
}
#scrollToFoot {
border: 1px solid #ccc;
text-align: center;
font-size: 18px;
padding: 20px 0;
}
fotter p {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #aaa;
font-size: 18px;
}
.row {
color: #555;
}
其次是layout.jade
doctype html
html
head
title MyBlog By qq20004604
link(rel='stylesheet', href='./stylesheets/bootstrap.min.css')
link(rel='stylesheet', href='./stylesheets/bootstrap-responsive.min.css')
link(rel='stylesheet', href='./stylesheets/blog.css')
script(type="text/javascript",src='javascripts/jq.js')
script(type="text/javascript",src='javascripts/bootstrap.min.js')
script(type="text/javascript",src='javascripts/blog.js')
body
div.navbar.navbar-fixed-top
div.navbar-inner
div.container
a.btn.btn-navbar(data-toggle="collapse",data-target=".nav-collapse")
span.icon-bar
span.icon-bar
span.icon-bar
a.brand(href="/") MyBlog
div.nav-collapse
ul.nav
li
a(href="/") 首页
li
a(href="/logout") 登出
li
a(href="/login") 登入
li
a(href="/reg") 注册
li
a(href="/language") 切换语言
div#contrainer.container
block content
hr
fotter
p 作者:王冬 QQ:20004604
效果如图:
然后是index.jade:
extends layout
block content
div.hero-unit
h1 我的博客
p 这个是基于Nodejs作为后台,jade作为模板来,使用了Express作为框架
br
br
//这部分暂时用1替代,后续会被更新
if(1)
br
br
a.btn.btn-primary.btn-large(href="/login") 登录
a.btn.btn-large(href="/reg") 立即注册
else
textarea#textarea.uneditable-input
button#postBlog.btn.btn-large 提交微博
button#clearBlog.btn.btn-large 清空
div#submitError.alert.alert-error.displayNONE.myalert
div#submitSuccess.alert.alert-success.displayNONE
div.row.content
div.span4
h2 烟雨江南说
p 当欲望没有了枷锁,就没有了向前的路
p 只有转左,或者向右
p 左边是地狱,右边也是地狱
div.span4
h2 烟雨江南说
p 那些都是极好极好的
p 可是我偏偏不喜欢
div.span4
h2 烟雨江南说
p 我不怕傻
p 只怕
p 遇不到
p 可以让我变傻的人
div.span4
h2 烟雨江南说
p 人在年轻的时候总会有些莫名的坚持,
p 并且以此感动着自己,
p 却时常会在不经意间让真正重要的东西从指间流走。
div.span4
h2 烟雨江南说
p 记忆真是一种奇怪的东西,
p 有时候会涤荡所有的苦难,只留下温情,
p 有时候却磨灭掉曾有的欢乐,唯剩下苍白和丑陋。
div.span4
h2 烟雨江南说
p 那存在的,都是幻影。
p 那永恒的,终将毁灭。
p 世界万物,缤纷色彩,都是被蒙蔽的人心罢了。
div.span4
h2 烟雨江南说
p 诸神以真相示人,而世人却视而不见
div.span4
h2 烟雨江南说
p 只有绵羊会向狮子要求平等,
p 而狮子们从来不会这样想。
div.span4
h2 烟雨江南说
p 愿迷途的旅人,从此得享安息。
p 因理想而不朽,因归返而救赎。
div#scrollToFoot 滚动到底部然后加载内容
效果如图:
但此时,上面的页面切换目前还都是无效状态;
滚动然后加载内容,也正处于无效状态;
注册和登录按钮,点击后也无法正常跳转;
我们需要依次解决这些问题。
【13】登录页面
接下来我们添加登录页面的路由和模板。
路由,打开app.js,在
app.use('/', routes);
app.use('/users', users);
之前添加
var reg = require('./routes/reg');
var login = require('./routes/login');
之后添加:
app.use('/reg', reg); //注册的,reg.js来处理
app.use('/login', login); //登录的,login来处理
这样的话,就添加了注册和登录页面的路由了,但目前,我们还缺其具体文件和代码。
进入routes文件夹,创建reg.js和login.js
reg.js
var express = require('express'); //调用express模块
var router = express.Router(); //调用模块的Router方法
router.get('/', function (req, res, next) {
res.render('reg')
});
module.exports = router;
login.js
var express = require('express'); //调用express模块
var router = express.Router(); //调用模块的Router方法
router.get('/', function (req, res, next) {
res.render('login')
});
module.exports = router;
现在又缺模板文件了。
进入views文件夹,创建reg.jade和login.jade
reg.jade
extends layout
block content
form.form-horizontal(method="post")
fieldset
legend 注册
div.control-group
label.control-label(for="username") 用户名
div.controls
input.input-xlarge#username(type="text",name="username")
p.help-block 你的账户名称,用于登录和提示
div.control-group
label.control-label(for="password") 口令
div.controls
input.input-xlarge#password(type="password",name="password")
div.control-group
label.control-label(for="password-repeat") 重复输入口令
div.controls
input.input-xlarge#password-repeat(type="password",name="password-repeat")
div.form-actions
button.btn.btn-priamry(type="submit") 注册
login.jade
extends layout
block content
form.form-horizontal(method='post')
fieldset
legend 登录
div.control-group
label.control-label(for='username') 用户名
div.controls
input.input-xlarge#username(name='username',type='text')
div.control-group
label.control-label(for='password') 密码
div.controls
input.input-xlarge#password(name='password',type='password')
div.form-actions
button.btn.btn-primary(type="submit") 登录
这个时候,注册和登录页面已经可以访问了(虽然还没有添加逻辑)
【14】在和MySQL交互之前的准备工作
首页、注册、登录页面的页面布局已经做完,下来我们需要完成注册和登录功能。
注册功能的流程:
第①和②略过;
第③步由js完成,因此要对reg.jade进行修改,同时也需要添加处理登录逻辑的js代码;
第④步在第③步验证一致后发起ajax请求,和第③步一并完成;
第⑤步由后端完成,具体流程为:
第⑥步是ajax请求的回调函数,在屏幕上提示不同的内容。
也可以通过服务器重定位,推送一个提示注册成功的页面。
第③步修改:
reg.jade
extends layout
block content
script(type="text/javascript",src='javascripts/reg.js')
div.form-horizontal
fieldset
legend 注册
div.control-group
label.control-label(for="username") 用户名
div.controls
input.input-xlarge#username(type="text",name="username")
p.help-block#tips 你的账户名称,用于登录和提示
p.help-block.displayNONE#usrname-error 用户名不能为空
p.help-block.displayNONE#error-alert
div.control-group
label.control-label(for="password") 口令
div.controls
input.input-xlarge#password(type="password",name="password")
p.help-block.displayNONE#pw-error 密码不能为空
div.control-group
label.control-label(for="password-repeat") 重复输入口令
div.controls
input.input-xlarge#password-repeat(type="password",name="password-repeat")
p.help-block.displayNONE#pw-rp-error 两次密码需要一致
div.form-actions
button.btn.btn-priamry#submit-button 注册
在这里,取消掉原生的表单提交功能,更改为用ajax发起请求。
注册页面的逻辑单独写在一个js里面,因此在public/javascripts下创建文件:
reg.js
$(document).ready(function () {
var time = 0; //用于计时
$("#submit-button").click(function () {
//获取账号密码
var name = $("#username").val();
var pw = $("#password").val();
var pwRepeat = $("#password-repeat").val();
//未输入账号名
if (name.length === 0) {
$("#username").parent().parent().addClass("error");
$("#usrname-error").removeClass("displayNONE");
$("#tips").addClass("displayNONE");
setTimeout(function () {
$("#username").parent().parent().removeClass("error");
$("#usrname-error").addClass("displayNONE");
$("#tips").removeClass("displayNONE");
}, 2000);
return;
}
//密码为空
if (pw.length === 0) {
$("#password").parent().parent().addClass("error");
$("#pw-error").removeClass("displayNONE");
setTimeout(function () {
$("#password").parent().parent().removeClass("error");
$("#pw-error").addClass("displayNONE");
}, 2000);
return;
}
//密码不相同
if (pw !== pwRepeat) {
$("#pw-rp-error").parent().parent().addClass("error");
$("#pw-rp-error").removeClass("displayNONE");
setTimeout(function () {
$("#pw-rp-error").parent().parent().removeClass("error");
$("#pw-rp-error").addClass("displayNONE");
}, 2000);
return;
}
var obj = {
name: name,
password: pw
};
//防止连续点击,先禁用提交按钮,防止重复提交
if (new Date() - time < 3000) {
return;
}
//发起请求前,更新提交时间,
time = new Date();
//发起请求,回调内容是一个对象,注册成功会有success,注册失败会有error属性
$.post("/reg", obj, function (data) {
if ("error" in data) {
$("#error-alert").removeClass("displayNONE")
$("#error-alert").text(data.error);
$("#error-alert").parent().parent().addClass("error");
setTimeout(function () {
$("#error-alert").addClass("displayNONE")
$("#error-alert").parent().parent().removeClass("error");
}, 2000)
time = 0; //注册失败,清空time计时,允许再次提交
} else if ("success" in data) {
location.href = data.success; //注册成功则重定向
}
})
})
})
这里的验证逻辑很简单:
①账号不能为空;
②密码不能为空;
③两次密码应该相同;
为了简化,目前只设置这么多,不考虑例如最短密码长度、最长密码长度、密码需要由字母、数字、特殊符号混合的情况
打开blog.css,添加这样一个样式,用于隐藏某些暂时不需要显示的内容:
.displayNONE {
display: none;
}
【15】和MySQL交互
这里采用连接池的模式和MySQL交互;
①毫无疑问,我们首先应该安装MySQL模块;
进入项目的根目录,输入
npm install mysql --save
等待出现以下画面就表示装好了:
②在项目的根目录下面创建文件夹models,这个文件夹用于保存各种类模型
并在其中新建db.js
ar mysql = require("mysql"); //调用nodejs和mysql交互的模块;
var pool = mysql.createPool({ //创建连接池
host: '127.0.0.1', //表示本地的数据库
user: 'root', //账号
password: '123456', //密码
port: '3306', //默认端口
database: 'blog' //库名
})
var db = {};
db.con = function (callback) { //callback是回调函数,连接建立后的connection作为其参数
pool.getConnection(function (err, connection) { //创建一个链接
if (err) { //对异常进行处理
throw err; //抛出异常
} else {
callback(connection); //如果正常的话,执行回调函数(即请求),具体请求在回调函数中写
}
connection.release(); //释放连接
})
}
module.exports = db;
这段代码做了这些事
我们该如何使用呢,很简单。
①生成一个db的实例;
②调用db的con方法,将请求写在回调函数之中,请求对象是回调函数的参数;
③例如:
我们打开routes/reg.js,将代码修改为如下样子:
var express = require('express'); //调用express模块
var router = express.Router(); //调用模块的Router方法
var db = require('../models/db');
router.get('/', function (req, res, next) {
res.render('reg')
});
router.post('/', function (req, res, next) { //当路由捕捉到url为/reg的post请求时,会执行以下函数
db.con(function (connect) { //db是我们导入的对象,connect是数据库的连接,此时数据库连接无错误,是一个正常的连接
//这是我们自定义的回调函数
connect.query("SELECT 1 + 1 AS solution", function (err, result) { //这里是一个对数据库的请求
if (err) { //假如请求出错,抛出异常
throw err;
return;
}
console.log(result); //控制台输出结果
res.send({ //将结果作为对象的error属性发送给客户端
error: result
});
}) //执行完回调函数后,会跳回db的con方法,执行接下来的属性(即释放连接)
})
})
module.exports = router;
这些代码做了一件事,在用户发起reg请求时,将查询结果(返回);
还记不记得我们在注册页面的js回调函数,假如返回值有error属性,那么他会将error属性显示出来;
当然,由于并不能显示对象(error属性是一个对象),因此显示出来的是这样的
这只是一个测试用的代码,但通过这样的方法,说明我们的前后端交互是走通了的
【16】封装涉及到user的代码
初步考虑,我们现在要注册,因此应该写一个向MySQL插入项的方法;
那是否我们就应该直接将这部分代码直接写在reg.js里面呢?
显然是不好的,这种做法违背了OOP的思想——封装;
简单来说,我们应该将用户视为一个类:
①至少包含两个方法:注册(reg)、查询(login);
②至少包含两个属性:用户名(username)、密码(password)。
因此,新建user.js,放置于models文件夹内。
内容如下:
var db = require('./db')
function User(user) { // 这是一个User类,传递的参数是一个对象,这个对象可以具有两个属性,分别是name和password
this.name = user.name; // 如果传递的user不是空,那么将其赋值给User类的实例的name属性
this.password = user.password; // 同上,赋给password属性
}
// 这个是插入方法
User.prototype.save = function (callback) {
var self = this
if (this.name.length == 0 || this.password.length == 0) { //如果在没账号/密码的情况下就调用插入方法,则提示错误并返回
console.log("You can't save user information without NAME or PASSWORD!");
return callback("You can't save user information without NAME or PASSWORD!");
}
db.con(function (connect) {
// 数据库的表名为user,字段名为name和password
connect.query("INSERT INTO user(name,password) VALUES (?,?)", [self.name, self.password], function (err, result) {
// 上面的两个问号,表示第二个参数的self.name和self.password依次被替换到问号的位置;
// 需要注意的是:
// ①必须以数组形式依次排列;
// ②必须是字符串形式(不能是number)
if (err) { //如果出错,那么错误信息作为回调函数的参数返回
console.log("INSERT name:" + self.name + ", password:" + self.password + " error, the err information is " + err);
return callback(err);
}
callback(null, result); //如果正常执行,那么第一个参数为null(无错误),第二个参数为返回的结果
})
})
}
// 这个是查询方法
User.prototype.get = function (callback) {
var self = this;
if (this.name.length == 0) { //如果在没账号/密码的情况下就调用插入方法,则提示错误并返回
console.log("You can't select user information without NAME!");
return callback("You can't select user information without NAME!");
}
var selectResult;
db.con(function (connect) {
connect.query('SELECT * FROM user WHERE name = ?', [self.name], function (err, result) {
if (err) { //报错
console.log("select name:" + self.name + " error, the err information is " + err);
return callback(err);
}
//注意,这里返回的是带账号和密码的,另外,理论上是有可能有多个元素的,但由于在注册时,用户名限制了重复,因此只会返回一个
selectResult = result; //这里的result是一个数组,只包含一个元素(或者是空)
if (selectResult.length) { //查询到的话,数组是有元素的(即length > 0)
return callback(null, selectResult[0]) //这里的selectResult就是user对象,包含name和password属性
} else {
return callback(null, null); //如果查询不到,两个参数都为空
}
})
})
}
module.exports = User;
这个封装好的User类满足了我们的需求,下来需要在reg里面调用这个类。
下面是修改后的routes/reg.js的代码
var express = require('express'); // 调用express模块
var router = express.Router(); // 调用模块的Router方法
var User = require('../models/user'); // 调用刚才封装好的user类
var crypto = require("crypto"); // 这个是加密用,nodejs本身提供
router.get('/', function (req, res, next) {
res.render('reg')
});
router.post('/', function (req, res, next) { //当路由捕捉到url为/reg的post请求时,会执行以下函数
// 获取md5这个对象;(表示调用这个对象的方法,加密的形式是MD5);
var md5 = crypto.createHash('md5');
// 调用md5的update方法,update的第一个参数是被处理的内容,第二个是可选的,但如果要对中文进行处理,那么就需要加上'utf-8'这个参数;
// 因此,我们的密码其实可以支持中文(理论上)
// digest指的是以什么形式进行编码,这里选择了base64,除此之外,还有hex(十六进制)、binary(二进制)这两种方法;
var password = md5.update(req.body.password, 'utf-8').digest('base64');
var newUser = new User({ //生成一个User的实例,并赋给他name和passowrd属性
name: req.body.name,
password: password //这里的password是加密过的(存储在数据库里也是加密过后的形式)
})
newUser.save(function (err, result) {
//do something
})
})
module.exports = router;
这部分并没有完成,之所以这样,是因为还欠缺一个东西,那就是登录状态的设置。
【17】登录状态和session
如何确定一个人是否在登录中?
有两个办法:①cookie;②session;
简单来说,他们之间的区别在于:
cookie保存在客户端,session保存在服务器端;
顺便补一个Cookie、LocalStorage和SessionStorage之间区别的链接:
http://jerryzou.com/posts/cookie-and-web-storage/
注意,这里的SessionStorage和session并不是同一个东西,前者保存在浏览器端,后者保存在服务器端。
再补一个express使用cookie和session的文章:
http://wiki.jikexueyuan.com/project/node-lessons/cookie-session.html
由于cookie可以在本地被修改,因此用cookie来保存用户状态是不安全的。
这里采用session来保存用户状态。
在高版本的express里(4.x及以上),cookie和session等模块并不直接包含在express里了,因此我们要安装express-session。
在项目的根目录输入:
npm install express-session
安装好后如图:
打开app.js,在
var app = express();
后添加如下代码:
//session
var session = require('express-session');
app.use(session({
secret: 'recommend 128 bytes random string',
cookie: {maxAge: 3600 * 1000}
}))
这种设置表示:
①默认session存储在内存中(默认设置,效果是重启后session失效);
②最大有效时间(过期时间)是3600秒;
然后重新打开reg.js,在原先的空白处,添加以下代码:
if (result) { //如果第二个参数存在,说明用户名重复了,返回提示
return res.send({
error: "Username already exists."
});
}
if (err) { //如果报错,返回报错信息
console.log(err);
return res.send({
error: err
});
}
//此时说明无重复无报错,可以将用户信息存入到数据库之中
newUser.save(function (err, result) {
if (err) { //如果存入时报错
return res.send({
error: err
});
}
//以上,done
//存储成功后返回还有点问题,需要搞明白
req.session.user = {
name: this.name,
password: this.password
};
req.session.success = "注册成功!"; //添加session的注册成功信息
res.send({ //发送一个对象,属性为success,值为/
success: "/"
})
return;
})
但此时还没结束,我们需要对数据库进行操作:(建议用数据库软件完成)
先建立一个库,库名为blog
在库中建立一个表,表名为user;
表中有三个字段:
①第一个为id,类型为int,自动增量,不允许为空;
②第二个为name,类型为varchar,长度为255,字符集为utf8,不允许为空;
③第三个为password,类型为varchar,长度为255,字符集为utf8,不允许为空;
建表(库要自己手动建)的源代码可以如下:
CREATE TABLE `user` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`password` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
于是,注册流程已经走通,用户可以顺利注册了
账号和密码用软件创建时设置如图:
【18】设置登录状态
在之前,已经可以通过session保存用户了,下来,需要根据session的记录,区分用户登录和未登录的状态。
简单来说,有两种情况:
①session中的user属性为空,用户未登录;
②session中的user属性不为空,用户已登录,登录人士为user的name属性;
为了简化,这里并没有对同时登录的检测(避免一个账号在两个地方登录)。
而在访问页面时,应该检测登录状态:
①假如已经登录,那么就不应该显示登录窗口、注册窗口,以及发表微博相关的内容;
②假如还没有登录,那么就不应该显示登出的按钮;
为了完成这个目的,那么应该在引擎渲染时进行检查,最笨的做法是,将变量设置于res.render的第二个参数的属性之中:
打开layout.jade,将代码如下修改:
doctype html
html
head
title MyBlog By qq20004604
link(rel='stylesheet', href='./stylesheets/bootstrap.min.css')
link(rel='stylesheet', href='./stylesheets/bootstrap-responsive.min.css')
link(rel='stylesheet', href='./stylesheets/blog.css')
script(type="text/javascript",src='javascripts/jq.js')
script(type="text/javascript",src='javascripts/bootstrap.min.js')
script(type="text/javascript",src='javascripts/blog.js')
body
div.navbar.navbar-fixed-top
div.navbar-inner
div.container
a.btn.btn-navbar(data-toggle="collapse",data-target=".nav-collapse")
span.icon-bar
span.icon-bar
span.icon-bar
a.brand(href="/") MyBlog
div.nav-collapse
ul.nav
li
a(href="/") 首页
//以下是修改后的部分
if(user && user())
li
a(href="/logout") 登出
else
li
a(href="/login") 登入
li
a(href="/reg") 注册
li
a(href="/language") 切换语言
div#contrainer.container
block content
hr
fotter
p 作者:王冬 QQ:20004604
打开index.jade
extends layout
block content
div.hero-unit
h1 我的博客
p 这个是基于Nodejs作为后台,jade作为模板来,使用了Express作为框架
br
br
//修改以下部分
if(!user())
br
br
a.btn.btn-primary.btn-large(href="/login") 登录
a.btn.btn-large(href="/reg") 立即注册
else
textarea#textarea.uneditable-input
button#postBlog.btn.btn-large 提交微博
button#clearBlog.btn.btn-large 清空
div#submitError.alert.alert-error.displayNONE.myalert
div#submitSuccess.alert.alert-success.displayNONE
div.row.content
div.span4
h2 烟雨江南说
p 当欲望没有了枷锁,就没有了向前的路
p 只有转左,或者向右
p 左边是地狱,右边也是地狱
div.span4
h2 烟雨江南说
p 那些都是极好极好的
p 可是我偏偏不喜欢
div.span4
h2 烟雨江南说
p 我不怕傻
p 只怕
p 遇不到
p 可以让我变傻的人
div.span4
h2 烟雨江南说
p 人在年轻的时候总会有些莫名的坚持,
p 并且以此感动着自己,
p 却时常会在不经意间让真正重要的东西从指间流走。
div.span4
h2 烟雨江南说
p 记忆真是一种奇怪的东西,
p 有时候会涤荡所有的苦难,只留下温情,
p 有时候却磨灭掉曾有的欢乐,唯剩下苍白和丑陋。
div.span4
h2 烟雨江南说
p 那存在的,都是幻影。
p 那永恒的,终将毁灭。
p 世界万物,缤纷色彩,都是被蒙蔽的人心罢了。
div.span4
h2 烟雨江南说
p 诸神以真相示人,而世人却视而不见
div.span4
h2 烟雨江南说
p 只有绵羊会向狮子要求平等,
p 而狮子们从来不会这样想。
div.span4
h2 烟雨江南说
p 愿迷途的旅人,从此得享安息。
p 因理想而不朽,因归返而救赎。
div#scrollToFoot 滚动到底部然后加载内容
打开routes/index.js,将代码如下修改:
var express = require('express'); //调用express
var router = express.Router(); //生成express的Router方法的一个实例
//处理函数
router.get('/', function (req, res, next) { //捕获根url
res.render('index', {
user: function () { //user将读取session的属性,然后给予不同的返回值
if (req.session.user)
return req.session.user;
else
return null;
}
});
//res.render方法渲染一个引擎模板,
//第二个参数是一个对象,对象里的变量可以在引擎中使用,
//第三个参数是回调函数,提供两个参数,分别是err和html,err是错误,html是渲染后的页面。如果使用这个回调函数,那么将不会自动响应,即要用户自己写返回html的命令
});
module.exports = router;
代码解释:
①在之前的代码中,我们如果注册成功,那么会将user属性设置在session之中;
②而在index.js里,会传递一个变量,那就是user;
③这个user会读取session的user属性,由于session默认的属性是没有user的,因此返回值为null(空),而注册成功后,返回值则为user(不为空);
④在layout.jade中,会执行这个user函数。假如未登录,user为空,那么执行else语句(显示登入和注册标签);否则显示登出标签;
⑤在index.jade中,同样执行,假如未登录,显示登录、注册按钮;假如已经登录了,显示输入微博的窗口和提交按钮;
于是,登录后的页面是这样的:
但是这种行为个人觉得比较傻,并且也不利于做多语言扩展,因此换一种方式。
【19】动态视图助手和多语言支持
关于动态视图助手的说明请看我之前的博客:
http://blog.csdn.net/qq20004604/article/details/51934991
简单来说,使用动态视图助手时,我们可以在引擎渲染的时候,直接调用视图助手中的变量;
首先在models下面建立language.js文件,内容如下
function internation() {
}
internation.prototype.languagePacks = {
zh_cn: {
index: {
title: "我的博客",
description: "这个是基于Nodejs作为后台,jade作为模板来,使用了Express作为框架",
login: "登录",
regNow: "立即注册",
submit: "提交微博",
emptyInput: "清空"
},
layout: {
title: "MyBlog By qq20004604",
topTitle: "MyBlog",
indexButton: "首页",
logout: "登出",
login: "登入",
reg: "注册",
language: "切换语言",
foot: "作者:王冬 QQ:20004604"
}
},
eng: {
index: {
title: "My blog",
description: "I use NodeJS, Express, and MySQL to design this site.",
login: "Login",
regNow: "Reg",
submit: "Submit",
emptyInput: "Clear"
},
layout: {
title: "MyBlog By qq20004604",
topTitle: "MyBlog",
indexButton: "Index",
logout: "Logout",
login: "Login",
reg: "Reg",
language: "Language",
foot: "I'm WangDong, you can contact me with QQ:20004604"
}
}
}
//如果有语言设置,则设置为对应的语言,否则,设置为中文;
internation.prototype.getPacks = function (setting) {
if (!setting) {
return this.languagePacks["zh_cn"]
}
if (setting in this.languagePacks) {
return this.languagePacks[setting];
} else {
return this.languagePacks["zh_cn"]
}
}
internation.prototype.set = function (app) {
var self = this;
app.use(function (req, res, next) {
//这个是示例,定义一个动态视图助手变量
//如果有语言设置,则设置为对应的语言,否则,设置为中文;
res.locals = self.getPacks(req.session.language)
//下面两个因为要调用req和res,所以特殊设置
res.locals.user = function () {
if ('session' in req && 'user' in req.session)
return req.session.user;
else
return null;
};
//这里不能用error这个变量名
res.locals.err = function () {
//如果session不存在,或者session中没有err属性
if (!'session' in req || !'err' in req.session)
return null; //返回空
//否则返回err,并将session中的err重置为空(因此只返回一次)
var err = req.session.err;
req.session.err = null;
return err;
};
//注册成功的提示
res.locals.success = function () {
//原理同err
if (!'session' in req || !'success' in req.session)
return null;
var success = req.session.success;
req.session.success = null;
return success;
}
next();
})
}
module.exports = internation;
在以上代码中,程序做了以下事情:
①创建一个语言包,他包含中文和英文两种;
②设置了一个方法,他可以根据session的language属性来决定获取哪个语言包;
但也有一定缺陷:
①假如在zh_cn的index中有一条test属性,他有一个对应的字符串;但在eng中相应的位置不存在,那么切换为eng时,这里就会显示错误;
②调用的时候,需要通过例如index.title这样的形式来调用
然后我们将这个模块加入到我们的代码之中;
打开app.js,在之前添加的关于session的代码后面,添加如下代码:
var language = require('./models/language'); //多语言
var internation = new language();
internation.set(app);
修改layout.jade
octype html
html
head
title #{layout.title}
link(rel='stylesheet', href='./stylesheets/bootstrap.min.css')
link(rel='stylesheet', href='./stylesheets/bootstrap-responsive.min.css')
link(rel='stylesheet', href='./stylesheets/blog.css')
script(type="text/javascript",src='javascripts/jq.js')
script(type="text/javascript",src='javascripts/bootstrap.min.js')
script(type="text/javascript",src='javascripts/blog.js')
body
div.navbar.navbar-fixed-top
div.navbar-inner
div.container
a.btn.btn-navbar(data-toggle="collapse",data-target=".nav-collapse")
span.icon-bar
span.icon-bar
span.icon-bar
a.brand(href="/") #{layout.topTitle}
div.nav-collapse
ul.nav
li
a(href="/") #{layout.indexButton}
//以下是修改后的部分
if(user && user())
li
a(href="/logout") #{layout.logout}
else
li
a(href="/login") #{layout.login}
li
a(href="/reg") #{layout.reg}
li
a(href="/language") #{layout.language}
div#contrainer.container
block content
hr
fotter
p #{layout.foot}
重新访问页面,我们会发现页面跟以前并没有什么区别,说明我们的语言包已经正常运行了。
【20】添加语言切换功能;
根据我们之前的代码,语言切换很简单,只需要在session添加属性即可;
首先,设置路由,routes文件夹下新建language.js
var express = require('express'); //调用express
var router = express.Router(); //生成express的Router方法的一个实例
//处理函数
router.get('/', function (req, res, next) { //捕获根url
if (!('language' in req.session) || req.session.language === 'zh_cn')
req.session.language = 'eng'; //更改属性
else
req.session.language = 'zh_cn';
res.redirect('/'); //重定向
});
module.exports = router;
在app.js的路由代码附近添加如下代码:
var lan = require('./routes/language');
app.use('/language', lan); //切换语言的
重启程序,然后进入首页点击切换语言,页面顶端变为如下状态:
此时如果再次点击‘Language’,那么页面又会切回中文;
我们顺便修改layout.jade文件为:
doctype html
html
head
title #{layout.title}
link(rel='stylesheet', href='./stylesheets/bootstrap.min.css')
link(rel='stylesheet', href='./stylesheets/bootstrap-responsive.min.css')
link(rel='stylesheet', href='./stylesheets/blog.css')
script(type="text/javascript",src='javascripts/jq.js')
script(type="text/javascript",src='javascripts/bootstrap.min.js')
script(type="text/javascript",src='javascripts/blog.js')
body
div.navbar.navbar-fixed-top
div.navbar-inner
div.container
a.btn.btn-navbar(data-toggle="collapse",data-target=".nav-collapse")
span.icon-bar
span.icon-bar
span.icon-bar
a.brand(href="/") #{layout.topTitle}
div.nav-collapse
ul.nav
li
a(href="/") #{layout.indexButton}
//以下是修改后的部分
if(user && user())
li
a(href="/logout") #{layout.logout}
else
li
a(href="/login") #{layout.login}
li
a(href="/reg") #{layout.reg}
li
a(href="/language") #{layout.language}
div#contrainer.container
block content
hr
fotter
p #{layout.foot}
再修改routes/index.js为:
var express = require('express'); //调用express
var router = express.Router(); //生成express的Router方法的一个实例
//处理函数
router.get('/', function (req, res, next) { //捕获根url
res.render('index');
});
module.exports = router;
【21】登录功能的完成
与注册(reg.js)功能的代码类似,
首先在models的语言包中分别添加如下属性:
message: {
reg_success: "注册成功!",
reg_fail: "注册失败!",
login_success: "登录成功!",
login_fail: "登录失败",
errorName: "用户名不存在",
errorPW: "密码错误!"
}
和
message: {
reg_success: "Reg Success!",
reg_fail: "Reg Failed!",
login_success: "Login Success!",
login_fail: "Login Failed!",
errorName: "The username is not exists.",
errorPW: "Error Password!"
}
打开routes/login.js文件
将代码修改如下:
var express = require('express'); //调用express模块
var router = express.Router(); //调用模块的Router方法
var User = require('../models/user'); // 调用刚才封装好的user类
var crypto = require("crypto"); // 这个是加密用,nodejs本身提供
router.get('/', function (req, res, next) {
res.render('login')
});
router.post('/', function (req, res, next) {
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password, 'utf-8').digest('base64');
var newUser = new User({ //生成一个User的实例,并赋给他name和passowrd属性
name: req.body.name
})
newUser.get(function (err, user) {
if (!user) { //用户名不存在
return res.send({errorName: res.locals.message.errorName});
}
else if (user) { //如果第二个参数存在,说明用户名重复了,那么监测密码是否相同
if (user.password === password) { //密码正确,登录成功
req.session.user = user;
req.session.success = res.locals.message.login_success;
return res.send({success: '/'});
} else {
return res.send({errorPW: res.locals.message.errorPW});
}
}
else if (err) { //如果报错,返回报错信息
console.log(err);
return res.send({
error: err
});
}
})
})
module.exports = router;
修改login.jade
xtends layout
block content
script(type="text/javascript",src='javascripts/login.js')
div.form-horizontal
fieldset
legend 登录
div.control-group
label.control-label(for='username') 用户名
div.controls
input.input-xlarge#username(name='username',type='text')
p.help-block.displayNONE#name-alert
div.control-group
label.control-label(for='password') 密码
div.controls
input.input-xlarge#password(name='password',type='password')
p.help-block.displayNONE#pw-alert
div.form-actions
button.btn.btn-primary#submit 登录
在public/javascripts下新建login.js,内容如下:
$(document).ready(function () {
var time = 0;
$("#submit").click(function () {
var user = {
name: $("#username").val(),
password: $("#password").val()
}
//禁止用户名、密码为空
if (user.name.length === 0) {
$("#name-alert").text("用户名不能为空");
return;
} else if (user.password.length === 0) {
$("#pw-alert").text("密码不能为空");
return;
}
if (new Date() - time < 3000)
return
time = new Date();
//发起ajax请求
$.post("login", user, function (data) {
if ("errorName" in data) {
$("#name-alert").removeClass("displayNONE")
$("#name-alert").text(data.errorName);
$("#name-alert").parent().parent().addClass("error");
setTimeout(function () {
$("#name-alert").addClass("displayNONE")
$("#name-alert").parent().parent().removeClass("error");
}, 2000)
time = 0;
} else if ("errorPW" in data) { //
$("#pw-alert").removeClass("displayNONE")
$("#pw-alert").text(data.errorPW);
$("#pw-alert").parent().parent().addClass("error");
setTimeout(function () {
$("#pw-alert").addClass("displayNONE")
$("#pw-alert").parent().parent().removeClass("error");
}, 2000)
time = 0;
} else if ("success" in data) {
location.href = data.success; //注册成功则重定向
}
})
})
})
于是,登录相关的内容算是做完了(但没有做多语言);
【22】登出功能的完成
相比较登录,登出功能就要简单的多,只需要将session的user属性重置为null即可。
在app.js的路由代码附近添加以下代码:
var logout = require('./routes/logout');
app.use('/logout', logout); //登出
在routes文件夹下新建logout.js,代码非常简单:
var express = require('express'); //调用express模块
var router = express.Router(); //调用模块的Router方法
//登出只需要清空session的user属性即可
router.get('/', function (req, res, next) {
req.session.user = null;
res.redirect('/');
})
module.exports = router;
【23】清点需求
重新打开我们之前的需求表:
①有首页;
②支持注册;
③支持登录、登出;
④可以发表博客,发表的博客可以保存到数据库;
⑤可以查看博客,博客从数据库中读取,为了简化,这里设置为查看所有人的博客;
⑥查看博客时,初始查看的数量有限,但可以无限加载新的博客,直到查看完全部博客;
⑦一定程度上实现多语言(即可以切换显示的语言版本),但由于复杂度所限,因此只部分实现(但框架已建立好,可以通过继续完善代码实现整体的国际化);
⑧根据登录状态,对可以访问的页面进行控制(某些允许某些禁止)
红色部分是已经完成的,蓝色部分是尚未完成的。
下来,我们的任务是完成④⑤⑥这三项。
【24】博客的发表
在登录之后,我们会在首页看到一个文本输入框,还有提交和清空按钮;
但目前尚没有逻辑,需要进行添加,打开public\javascripts\blog.js,添加如下代码:
$(document).ready(function () {
//根据url,设置高亮
if (window.location.pathname === '/') {
$(".nav-collapse .nav li:first-child").addClass("active");
}
else if (window.location.pathname === '/login') {
$(".nav-collapse .nav li:nth-child(2)").addClass("active");
}
else if (window.location.pathname === '/reg') {
$(".nav-collapse .nav li:nth-child(3)").addClass("active");
}
//清空输入框
$("#clearBlog").click(function () {
$("#textarea").val("");
})
var lastSubmit = null;
var timeer = null;
var successSubmit = null;
//防止连续点击
function ErrorAlert() {
if (new Date() - lastSubmit < 3000) {
clearTimeout(timeer);
}
lastSubmit = new Date();
timeer = setTimeout(function () {
$("#submitError").addClass("displayNONE");
}, 3000);
}
//提交输入框
$("#postBlog").click(function () {
//防止重复发表,因此需要间隔10秒
if (successSubmit && new Date() - successSubmit < 10000) {
//这里是警告提示,防止连续发送消息
$("#submitError").text("你发的太快了,喝喝茶吧!距离下次可以发送消息的时间还有:" + (new Date() - successSubmit).toFixed(0) + " 秒。");
$("#submitError").removeClass("displayNONE");
//防止连续点击
ErrorAlert()
return;
}
var text = $("#textarea").val();
if (text.length === 0) { //禁止发送空内容
$("#submitError").text("请填写输入内容");
$("#submitError").removeClass("displayNONE");
//防止连续点击
ErrorAlert()
return;
}
var length = 0;
//获取输入长度,英文字母为1,中文汉字为2
for (var i = 0; i < text.length; i++) {
if (text[i].match(/[^\x00-\xff]/ig) != null)
length += 2;
else
length += 1;
}
if (length > 255) {
$("#submitError").text("字符长度过长,限制字符长度为255个字节,你的文本长度为" + length + "个字节");
$("#submitError").removeClass("displayNONE");
ErrorAlert()
return;
}
//先清除输入框再提交
$("#textarea").val("");
successSubmit = new Date();
$.post('/post', {text: text}, function (item) {
if (item.code == 500) {
successSubmit = 0;
$("#submitError").text(item.data);
$("#submitError").removeClass("displayNONE");
setTimeout(function () {
$("#submitError").addClass("displayNONE");
}, 3000);
} else if (item.code == 200) {
$("#submitSuccess").text(item.data);
$("#submitSuccess").removeClass("displayNONE");
setTimeout(function () {
$("#submitSuccess").addClass("displayNONE");
}, 3000);
}
})
})
})
这段代码做了以下事:
①根据当前的url,给对应的标签高亮;
②点击清空按钮,会清除输入框的内容;
③点击提交按钮,会验证输入框内字符串的长度,禁止发送空内容和超过255字节长度的内容;
④当成功发出post请求后,禁止在短时间内连续发送post请求;当发送失败后,取消禁止状态,可以立刻再次发送
⑤验证通过后,将消息内容以post请求发送到服务器,并清空输入框;
⑥返回值有两种,一种是code属性为200,一种为500;前者为发送成功,发出提示消息;后者发送失败,提示失败原因。
接下来首先要在MySQL里创建一张表,用于保存发送的文本内容;
表名为text,字符类型为utf8,表项如下:
id为自动增量;
user为发表这个博客的用户;
text博客的内容;
ctime为博客的创建时间;
然后在routes文件夹下新建post.js
var express = require('express'); //调用express模块
var router = express.Router(); //调用模块的Router方法
var db = require('../models/db')
//给原生的Date添加一个方法,传递的参数是格式
Date.prototype.Format = function (fmt) { //author: meizz
var obj = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"H+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var key in obj)
if (new RegExp("(" + key + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (obj[key]) : (("00" + obj[key]).substr(("" + obj[key]).length)));
console.log(fmt);
return fmt; //返回值是格式化好之后的时间
}
function BlogSubmit(user) {
this.user = user.name;
this.text = user.text;
this.time = new Date().Format("yyyy-MM-dd HH:mm:ss");
}
//存储文本内容
BlogSubmit.prototype.save = function (callback) {
var self = this;
console.log(self.user);
db.con(function (connect) {
connect.query("INSERT INTO text(user,text,ctime) VALUES (?,?,?)", [self.user, self.text, self.time], function (err, result) {
if (err) {
console.log("INSERT text user:" + self.user + ", text:" + self.text + ", ctime: " + self.time + " error, the err information is " + err);
return callback(err);
}
callback(null, result);
})
})
}
router.post('/', function (req, res, next) {
//如果不登录是不能发表的(这个是为了防止登录过期,但是页面还保持在登录时的情况)
if (!req.session.user) {
req.session.err = "请登录"; //因为这里,需要修改layout.jade
return res.redirect('/login');
}
//console.log(req.session.user);
//登录后值为(示例)
//{ Id: 23,
// username: '1',
// userpassword: 'xMpCOKC5I4INzFCab3WEmw==' }
var blog = new BlogSubmit({
name: req.session.user.name,
text: req.body.text
})
blog.save(function (err, result) {
if (err) {
return res.send({
code: 500,
data: "错误描述" + err
})
}
return res.send({
code: 200,
data: "成功发表!"
})
})
})
module.exports = router;
这部分代码干了以下几件事:
①给Date()添加了一个方法,用于格式化日期;
②创建了一个BlogSubmit的类,传递的参数是一个对象,他有name属性和text属性,会将其值传递给这个类的实例的user属性和text属性中。同时,会在创建这个类的时候获取当前时间。于是,text表的三项内容有了;
③给BlogSubmit类添加了一个方法,这个方法的作用是将这个类的实例的属性,存储在MySQL之中;
④由于表限制text中必须保存内容,因此假如用户通过其他方法直接发起一个post请求,也是不可能成功的(会报错);
⑤保存的前提是用户已登录,未登录的时候发表(比如登录过期)会提示错误。
⑥无论成功或者失败,都会给用户返回一个对象,成功的code属性为200,失败的为500;
最后不要忘记在app.js里添加路由:
var postBlog = require('./routes/post');
app.use('/post', postBlog); //提交博客
此时发表博客,会提示:
查看数据库,数据库对应的表是有内容的,说明成功发表了。
【25】博客的加载
博客要满足这些功能;
①初始加载一些博客内容;
②当滚动条拖到最下面的时候,发起ajax请求,取得更多博客内容;
③将博客内容添加到最下方;
④当读取完所有内容时,提示无新博客;
首先,在路由之中添加:
app.use('/loadblog', users); //用户主页,users来处理
将博客的发送交给user来处理;
将以下代码添加进models/blog.js
//读取文本内容(读取所有的),从第count条偏移量开始取9条
BlogSubmit.prototype.getAll = function (count, callback) {
var self = this;
db.con(function (connect) {
//第一次查询数据库还有多少数据
connect.query("SELECT count(*) FROM text", null, function (err, result) {
if (err) {
console.log("SELECT * FROM text limit :" + count + ", 9 error, the err information is " + err);
return callback(err);
}
//console.log(result[0]['count(*)']); //这个是查询出来的值
//只有当请求数据的编号(已经查询到多少个),小于最大数据量的时候,才会从数据库内读取数据
if (Number(count) < result[0]['count(*)']) {
connect.query("SELECT * FROM text limit ?,9", [Number(count)], function (err, result) {
if (err) {
console.log("SELECT * FROM blogtext limit :" + count + ", 9 error, the err information is " + err);
return callback(err);
}
result.offset = Number(count) + result.length;
callback(null, result);
})
} else {
callback(null, null);
}
})
})
}
修改routes/users.js
var express = require('express');
var router = express.Router();
var BlogSubmit = require("../models/blog");
/* GET users listing. */
router.get('/', function (req, res, next) {
res.send('respond with a resource');
});
router.get('/:count', function (req, res, next) {
var blog = new BlogSubmit({
username: null,
text: null
})
blog.getAll(req.params.count, function (err, result) {
if (err) {
res.send({
code: 500,
data: "错误描述" + err
})
} else if (result === null) { //空数据
res.send({
code: 501,
data: "服务器已经没有更多数据了"
})
} else {
res.send({code: 200, data: result, offset: result.offset});
}
})
});
module.exports = router;
修改public/javascripts/blog.js,添加以下内容:(此处有bug,请参照【27】的方式修改)
var lastLoad = 0; //初始时间设置为0
var lastLoadCount = 0; //偏移量,防止读取到重复数据
var loadingBlog = null; //加载用的定时器
//加载更多的内容
function loadMoreContent() {
//控制加载间隔,时间差大于2秒,且当前不处于加载中
if (new Date() - lastLoad > 2000 && $("#scrollToFoot").innerHTML !== "加载中~~~") {
lastLoad = new Date();
$("#scrollToFoot")[0].innerHTML = "加载中~~~";
//设置重置,假如超时10秒,那么允许再次提交
setTimeout(function () {
if ($("#scrollToFoot").innerHTML === "加载中~~~") {
$("#scrollToFoot").text("加载失败");
}
}, 10000);
$.get('/loadblog/' + lastLoadCount, function (obj) {
console.log(obj);
//这里假设item的格式:
if (obj.code === 500) {
$("#scrollToFoot").text("加载失败");
} else if (obj.code === 501) { //加载完毕
$("#scrollToFoot").text(obj.data);
lastLoad = new Date() + 10000000; //禁止继续请求
clearInterval(loadingBlog);
} else {
obj.data.forEach(function (item) {
$("#content").append('<div class="span4"><h2>' + item.user + '说</h2>' +
'<p>' + item.text + '</p></div>');
})
$("#scrollToFoot").innerHTML = "滚动到底部然后加载内容";
lastLoadCount += obj.offset;
}
})
}
}
//加载完毕时,加载一次内容
loadMoreContent();
//向下滚动时加载内容
loadingBlog = setInterval(function () {
if ($(document).height() - $(window).scrollTop() - $(window).height() < 200) {
loadMoreContent();
}
}, 100);
修改index.jade中的
div.row.content
为:
div.row.content#content
到目前为止,可以自动无限加载博客了。
【26】路由控制
现在,就差最后一步了。
毫无疑问,在用户未登录的时候,应该禁止掉用户发送只有登录过的用户才可以做的事情;而用户在登录之后,也应该禁止掉用户在非登录情况下才能做的事情;
前者典型是登出,而后者典型是登录和注册。
比如这样就是不对的
解决办法,利用“路由中间件”来解决问题,这就是我们在router.get之类的函数中,第二个参数的第三个参数next;
所谓路由中间件,首先要理解请求;
假如有一个url为/reg的post请求,当他遇见router.post(‘/reg’,xxxxx)时,执行完这行代码的回调函数后便停止了。
假如我们需要他被两段这样的代码来先后处理,那么显然是不可行的,路由会自动结束请求。
这个时候在第一段代码最后执行回调函数中的第三个参数next,那么就会继续执行第二段代码。
更具体的请看我的博客:
http://blog.csdn.net/qq20004604/article/details/51817625
打开app.js,在之前所有路由代码之前,添加以下代码:
//路由的处理
app.use('/login', checkNotLogin);
app.use('/reg', checkNotLogin);
//必须在已登录情况下才能访问
app.use('/logout', checkLogin);
//未登录检测(已登录情况下执行)
function checkNotLogin(req, res, next) {
if (req.session.user) {
req.session.err = "已登录,请不要重复登录";
return res.redirect('/');
}
next();
}
//已登录检测(未登录情况下执行)
function checkLogin(req, res, next) {
if (!req.session.user) {
req.session.err = "你还没有登录,请登录";
return res.redirect('/login');
}
next();
}
此时重启nodejs程序,登录博客,然后url地址输入/reg,会有以下提示
这说明,我们的路由权限控制已经顺利完成了。
这篇博客的总体结构也基本完成了。
虽然还有很多需要修补的问题,但那已经是下一篇博客的故事了。
【27】bug修复
之前因为更改为定时刷新之后,只有当处于首页时,js才能正常自行;
当进入登录、注册页时,由于无法找到#textarea这个dom,因此会报错,导致代码无法正常运行。
因此,将blog.js分拆,新建一个index.js在public\javascripts目录下,代码分拆后结果如下:
blog.js
$(document).ready(function () {
//根据url,设置高亮
if (window.location.pathname === '/') {
$(".nav-collapse .nav li:first-child").addClass("active");
}
else if (window.location.pathname === '/login') {
$(".nav-collapse .nav li:nth-child(2)").addClass("active");
}
else if (window.location.pathname === '/reg') {
$(".nav-collapse .nav li:nth-child(3)").addClass("active");
}
})
index.js
$(document).ready(function () {
//清空输入框
$("#clearBlog").click(function () {
$("#textarea").val("");
})
var lastSubmit = null;
var timeer = null;
var successSubmit = null;
//防止连续点击
function ErrorAlert() {
if (new Date() - lastSubmit < 3000) {
clearTimeout(timeer);
}
lastSubmit = new Date();
timeer = setTimeout(function () {
$("#submitError").addClass("displayNONE");
}, 3000);
}
//提交输入框
$("#postBlog").click(function () {
//防止重复发表,因此需要间隔10秒
if (successSubmit && new Date() - successSubmit < 10000) {
//这里是警告提示,防止连续发送消息
$("#submitError").text("你发的太快了,喝喝茶吧!距离下次可以发送消息的时间还有:" + (10 - (new Date() - successSubmit) / 1000).toFixed(0) + " 秒。");
$("#submitSuccess").addClass("displayNONE");
$("#submitError").removeClass("displayNONE");
//防止连续点击
ErrorAlert()
return;
}
var text = $("#textarea").val();
if (text.length === 0) { //禁止发送空内容
$("#submitError").text("请填写输入内容");
$("#submitError").removeClass("displayNONE");
//防止连续点击
ErrorAlert()
return;
}
var length = 0;
//获取输入长度,英文字母为1,中文汉字为2
for (var i = 0; i < text.length; i++) {
if (text[i].match(/[^\x00-\xff]/ig) != null)
length += 2;
else
length += 1;
}
if (length > 255) {
$("#submitError").text("字符长度过长,限制字符长度为255个字节,你的文本长度为" + length + "个字节");
$("#submitError").removeClass("displayNONE");
ErrorAlert()
return;
}
//先清除输入框再提交
$("#textarea").val("");
successSubmit = new Date();
$.post('/post', {text: text}, function (item) {
console.log(item);
if (item.code == 403) {
location.href = item.data;
}
else if (item.code == 500) {
successSubmit = 0;
$("#submitError").text(item.data);
$("#submitError").removeClass("displayNONE");
setTimeout(function () {
$("#submitError").addClass("displayNONE");
}, 3000);
} else if (item.code == 200) {
$("#submitSuccess").text(item.data);
$("#submitError").addClass("displayNONE");
$("#submitSuccess").removeClass("displayNONE");
setTimeout(function () {
$("#submitSuccess").addClass("displayNONE");
}, 3000);
}
})
})
var lastLoad = 0; //初始时间设置为0
var lastLoadCount = 0; //偏移量,防止读取到重复数据
var loadingBlog = null; //加载用的定时器
//加载更多的内容
function loadMoreContent() {
//控制加载间隔,时间差大于2秒,且当前不处于加载中
if (new Date() - lastLoad > 2000 && $("#scrollToFoot").innerHTML !== "加载中~~~") {
lastLoad = new Date();
$("#scrollToFoot")[0].innerHTML = "加载中~~~";
//设置重置,假如超时10秒,那么允许再次提交
setTimeout(function () {
if ($("#scrollToFoot").innerHTML === "加载中~~~") {
$("#scrollToFoot").text("加载失败");
}
}, 10000);
$.get('/loadblog/' + lastLoadCount, function (obj) {
console.log(obj);
//这里假设item的格式:
if (obj.code === 500) {
$("#scrollToFoot").text("加载失败");
} else if (obj.code === 501) { //加载完毕
$("#scrollToFoot").text(obj.data);
lastLoad = new Date() + 10000000; //禁止继续请求
clearInterval(loadingBlog);
} else {
obj.data.forEach(function (item) {
$("#content").append('<div class="span4"><h2>' + item.user + '说</h2>' +
'<p>' + item.text + '</p></div>');
})
$("#scrollToFoot").innerHTML = "滚动到底部然后加载内容";
lastLoadCount += obj.offset;
}
})
}
}
//加载完毕时,加载一次内容
loadMoreContent();
//向下滚动时加载内容
loadingBlog = setInterval(function () {
if ($(document).height() - $(window).scrollTop() - $(window).height() < 200) {
loadMoreContent();
}
}, 100);
})