快速使用node.js进行web开发详解
首先关于node.js的学习,这里推荐一本比较好的教程,nodejs web开发指南,该书通俗易懂地将node.js语言特性讲解完之后,又从一个项目角度带领读者使用node.js学习web开发。相信这是一个比较好的学习模式和过程。由于这本书是2012年出的,书中的一个web教学项目是开发一个微博。从2012到现在,node.js及其生态环境发生了很大改变,所以关于该书的学习如果照着书本显然是过于陈旧的。到目前为止,node.js的web开发框架已经升级到了Express4.12.1,对于MongoDB的操作更多是使用mongoose这个对象模型,而不是之前mongoDB 官方提供的原生node.js的API,所以本文将基于nodejsV0.1033 + MongoDBV3.0.2+ Jade1.9.2 + mogooseV4.0.1来重构该书中的微博项目,这个组合也是目前最新的使用node.js进行web开发的常用组合之一,如果需要入门使用node.js进行web开发,正在学习nodejs web开发指南的和想快速了解node.js web开发模式的朋友,相信本文是有一定帮助意义的。
1.express框架安装
1)在node命令行模式下输入以下命令
npm install -g express
该命令在全局环境下安装express框架,在安装完这一步之后,并不能直接使用express命令来生成express项目,需要再安装一个express项目生成器,在express2.X的版本中是不需要的,express4.X版本之后将项目生成器和express本身分离了出来,如果不安装express-generator这个生成器就使用express命令来生成项目,会遇到报express不是内部或外部命令这个错误,这是需要注意的地方,nodejs web开发指南原书中是没有安装express-generator这一步的。
2)安装express-generator
npm install -g express-generator
3)生成一个项目
cd .. mkdir microblog cd microblog express micorblog
这里随意在硬盘某个目录下创建一个microblog的文件夹,进入该文件夹,然后使用express microblog命令创建了一个microblog的express项目。
生成结构如下:
其中app.js是项目入口文件,package.json是npm 包管理文件,bin文件夹里面的www.js放一些全局配置项以及命令行配置等。public 文件夹是用来存放项目静态文件目录如js,css以及图片,routes文件夹是用来存放路由监听的代码相关文件。views文件夹用来存放模板文件,这里需要注意的是express4.X使用jade作为项目的默认模板引擎,而在原书中是使用ejs作为模板引擎的,所以这里默认生成的是jade文件。无可否认ejs是要简单些,但是原理都是一样的,我们使用jade作为开发的模板引擎。
4)启动项目并查看
cd microblog npm install npm start
进入到microblog文件夹,安装项目所需相关模块(根据pacakge.json文件),然后启动项目,这时候打开浏览器查看项目输入地址localhost:3000,结果如下说明一切正常,
到目前为止,我们已经拥有了一个在浏览器中运行的web项目雏形。下面进行开发,原书中的微博项目的主要功能是用户能够注册登录,权限控制并让用户发布微博在用户个人主页和项目首页分别显示,这些功能完整版代码会提供,由于篇幅原因,这里以用户注册登录模块来说明如何进行一个完整流程的web开发。
2.页面布局
依照web开发流程,我们首先来构建一个项目主页,项目主页是由布局文件layout.jade和内容文件index.jade组成,关于的jade的学习,这里提供两个地址,对于以前使用过类似模板引擎如smarty,razor等的,可以看看文档就能够上手做了,基本原理都是大同小异。
打开views文件,将layout.jade文件代码改写如下:
doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') body nav.header ul.list li.logo a(href='/') Microblog li a(href='/') 首页 li a(href='/login') 登录 li a(href='/reg') 注册 div.container block content hr footer.footer p a(href='http://myzhibie.coding.io') myzhibie | @2015
需要注意父级元素和子元素的换行之间缩进,jade是利用缩进来区别代码层级的。
首页内容文件index.jade
extends layout block content main.main section.intro if message h3.indexmes #{message} //如果用户登录或者注册成功并且没有在登录状态下点击注册或者登录 if success&&user h1.welcome #{success},欢迎 #{user} 来到 Microblog else if !success&&user h1.welcome 欢迎 #{user} 来到 Microblog else h1.welcome 欢迎来到 Microblog h3.tech Microblog是一个基于Node.js,使用express4.12.1,jade1.9.2以及MongoDB搭建起来的微博系统,是对Node.js开发指南一书中教学项目的重构。 p.btnlist if user a.login(href='/logout') 退出 a.userlink(href='/users/#{user}') 发表文章 else a.login(href='/login') 登录 a.register(href='/reg') 立即注册 section.show each val in posts article.col h3.author #{val.user}说 p | #{val.post}
首页内容是继承了模板文件layout.jade.原书中使用的bootstrap来构建页面的css布局和样式,这里我自己手写了一个仿bootstrap风格的布局样式,没有应用bootstrap,style.css文件如下:
body { padding: 50px; font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; } html,body,ul,p,hr,h3{ margin:0; padding: 0; } a { color: #00B7FF; } .header{ background:#337aB7; width: 100%; height: 60px; color: #fff; font-size: 22px; overflow: hidden; } .list{ line-height: 60px; } .navigation{ overflow: hidden; } .list li{ list-style: none; float: left; display: inline-block; margin-left: 20px; margin-right: 20px; } .list li a{ text-decoration: none; color: #fff; } .list li a:hover{ } .list li:not(:first-child) a:hover{ font-size: 26px; color: #F5F5F5; } .logo{ font-size: 26px; font-weight: 700; } .container{ min-height: 500px; text-align: center; width: 100%; } .footer{ width: 100%; height: 50px; font-size: 22px; background:#F5F5F5 ; line-height: 50px; } .footer a{ color:#337aB7; text-decoration: none; } .main{ color: #000000; width: 96%; margin: 30px auto; } .intro{ width: 100%; margin:0 auto; border-radius: 5px; height: 300px; background:#F5F5F5 ; } .userintro{ width: 100%; margin:0 auto; border-radius: 5px; height: 200px; background:#F5F5F5 ; } .welcome{ padding-top: 50px; padding-left:50px; font-size: 50px; text-align: left; padding-bottom: 0; margin: 0; } .tech{ text-align: left; padding-left:50px; margin: 0; } .show{ overflow: hidden; width: 100%; } .show li{ text-align: left; font-size: 18px; } .col{ display: inline-block; float: left; width: 32%; height: 100px; overflow: hidden; padding-right: 20px; text-align: left; text-overflow: ellipsis; } .author{ margin-top: 10px; margin-bottom: 3px; } .btnlist{ padding-left: 50px; text-align: left; } .login{ display: inline-block; padding-left: 15px; padding-right: 15px; height: 38px; line-height: 40px; background: -webkit-gradient(linear, left top, left bottom, from(#0068A6), to(#337aB7)); color: #fff; text-align: center; border-radius: 5px; font-size: 20px; font-weight: 600; border: 1px solid #ccc; text-decoration: none; margin-right: 10px; } .register{ display: inline-block; padding-left: 15px; padding-right: 15px; height: 38px; line-height: 40px; background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#F5F5F5)); color: #000; text-align: center; border-radius: 5px; font-size: 20px; font-weight: 600; border: 1px solid #ccc; text-decoration: none; } .field{ margin-top: 20px; margin-left: 50px; text-align: left; margin-bottom: 20px; border:none; border-bottom: 1px solid #ccc; } .label{ font-size: 18px; font-weight: 600; line-height: 100%; display: inline-block; width: 10%; vertical-align: middle; text-align: right; padding-right: 10px; } .regheader{ text-align: left; font-size: 24px; font-weight: 600; } .regform{ text-align: left; padding-left: 100px; margin-bottom: 20px; } .regform input[type='text'],input[type='password']{ width: 200px; height: 20px; } .regform input[type='submit']{ width: 120px; height: 30px; color: #fff; background:-webkit-gradient(linear, left top, left bottom, from(#0068A6), to(#337aB7)); border-radius: 5px; font-size: 20px; } .item{ margin:20px; width: 100%; } .mess{ font-size: 18px; color: #E73C3C; background: #F2DEDE; border-radius: 5px; width: 300px; text-align: center; margin-left: 100px; } .indexmes{ height: 30px; line-height: 30px; background: #F2DEDE; color: #E73C3C; } .article{ width: 60%; height: 30px; border-radius: 3px; border: 1px solid #A3C732; margin-top: 5px; font-size: 20px; } .submit{ height: 40px; vertical-align: middle; padding: 0; margin-top: -5px; margin-left: 5px; width: 80px; background: #A3c732; font-size: 20px; border: none; border-radius: 5px; color: #fff; } .submitform{ margin-top: 25px; margin-left: -10px; } .userlink{ display: inline-block; text-decoration: none; line-height: 38px; height: 38px; vertical-align: middle; padding: 0; margin-top: -8px; margin-left: 5px; width: 90px; text-align: center; background: #A3c732; font-size: 20px; font-weight: 600; border-radius: 5px; color: #fff; border: 1px solid #ccc; } .usertitle{ text-align: left; padding-top: 5px; padding-bottom: 0; padding-left: 5px; margin-bottom: 8px; } .usersuccess{ height: 30px; background: #DFF0D8; line-height: 30px; color: #3C7668; }
这个css文件是项目中所有的css全部包含在这里,所以比较庞大。到目前为止,可以查看首页效果如下:
首页中的数据都是之前自己测试过程中加入的,这里主要为了查看首页效果,可以忽略这些数据。
由于这里要演示用户注册登录模块,用户注册模块的模板文件reg.jade如下:
extends layout block content h3.field.regheader #{title} form.regform(method='post') p.mess #{message} div.item label.label(for='username') 用户名 input(type='text',placeholder='输入注册用户名',id='username',name='username') div.item label.label(for='password') 用户密码 input(type='password',placeholder='用户密码',id='password',name='password') div.item label.label(for='passwordconf') 重复密码 input(type='password',placeholder='重复密码',id='passwordconf',name='passwordconf') div.item label.label input(type='submit' id='sub',name='sub' value='注册')
用户登陆模板login.jade如下:
extends layout block content h3.field.regheader #{title} form.regform(method='post') p.mess #{message} div.item label.label(for='username') 用户名 input(type='text',placeholder='输入登陆用户名',id='username',name='username') div.item label.label(for='password') 用户密码 input(type='password',placeholder='用户密码',id='password',name='password') div.item label.label input(type='submit' id='sub',name='sub' value='登陆')
最终用户注册效果如下:
用户登录模块和这个效果相仿,就不查看了,少了一个重复密码的input而已。
下面我们需要编写用户注册的逻辑,在编写用户注册逻辑的前,用户数据需要持久化,所以首先要安装MongoDB数据库在自己的机器上.
MongoDB这种nosql类型的数据库,非常适合用户存储JSON对象类型的数据,有了mongoDB,就可以免去数据库表设计部分的工作,对比以前使用的mysql,sqlserver以及oracle还是非常方便的。关于mongoDB数据库的熟悉和学习,推荐其官网,官网详细介绍了该数据库的一切。英文不好可以去中文社区。同时为了使用nodejs来操作mongoDB数据库,我们使用mongoose这个对象模型,它是将mongoDB中的一个集合映射为nodejs中的一个model,然后在该model上提供操作这个集合的一些方法,使用它就可以避免我们自己利用nodejs提供的原生操作mongoDB数据库的语法去手写数据库CURD的方法,大大见晒了工作量,提高了开发效率。关于mongoose的学习,推荐去其官网,里面详述了它的安装,使用以及API调用情况。
解决了mongoDB安装和操作问题,我们来对数据库操作的model类,首先在项目路径下建立一个db.js文件,用来连接数据库并对数据库进行全局配置,如下
db.js
var settings=require("./settings"); var mongoose=require('mongoose'); mongoose.connect("mongodb://"+settings.ip+"/"+settings.db); var db=mongoose.connection; module.exports={ "dbCon":db, "mongoose":mongoose };
这里首先加载了配置文件settings.js文件,为了数据库便于灵活修改,我们将某些信息存储在配置文件中。然后加在了之前安装的mongoose模块,然后调用该模块的connect方法来连接我们配置的数据库,然后将连接以对象的形式返回供外部调用。
settings.js
module.exports={ "ip":"localhost", "db":"microblog", "host":27071 };
MongoDB的默认端口是27071,一般可以使用默认端口即可,数据库连接大时候可以不指定端口,数据库名为microblog.
然后以db.js返回的数据库连接对象为基础,我们在项目根目录下创建一个models文件夹,用来存放数据模型。创建一个user.js映射我们数据库中的user集合(可以理解为user表),代码如下:
var mongoose=require('../db').mongoose; var schema=new mongoose.Schema({ name:'string', password:'string' }); var User=mongoose.model('User',schema); module.exports=User;
这里首先获得db.js中定义的连接对象,并以该对象为基础构造一个Schema(架构),mogoose操作数据库是以架构为基础的,类似于我们其他ORM模型中属性和方法的定义。这里我们定义了一个架构,拥有两个属性,name和password,都是string类型,对应用户的用户名和密码。然后利用该架构去创建一个model,该model上定义了对数据集合的增删改查等方法,不用我们自己再去定义和编写其他代码。在原书中这一节是利用node.js操作MongoDB数据库的原生API去定义了一个user对象,然后在user对象上自定义了一些CRUD的方法。可以看出,直接使用Mongoose可以大大减少开发量并且拥有更好的效率和性能。
到目前为止,我们已经有了界面(view),数据模型(model),就差逻辑代码(controller)没有编写了。在编写逻辑代码之前需要先说下express框架的特点以及它的整体运行方式。由于本人使用过一些类似的如Asp.net mvc,Yii以及thinkphp等MVC框架,使用express之后最大的感觉是这个框架够轻量级,尤其是express4.X之后,它仅仅保留了静态文件路径映射模块作为该框架本身的内置模块,其他的功能都以中间件的形式采用require(modulename)进行引入,只有引入后才能够使用该模块提供的功能。
express的工作原理是客户端发送一个request,express接到该请求,可以将它进行处理之后传递给其他中间件进行处理,最终处理完成之后,采用respond.end或者response.render进行页面渲染或响应,进行页面渲染的时候,采用参数传递页面需要的数据给对应模板引擎,模板引擎收到数据然后按照自己的语法进行替换生成对应的html,最终返回给浏览器进行渲染。
在express中,最关键的部分就是路有机制,我们所有基于请求做出的响应都是对该路由进行监听捕获的结果。举个例子,如果我们请求一个路径为http://localhost:3000/user,那么必须在routes文件夹下面的路径监听(暂且叫做监听吧)的js文件中编写对该请求的响应代码,诸如app.post('/user',function(...){...})之类的代码,如果不存在这样的代码,就会报一个404错误,因为请求没有得到响应,express实例不知道怎么去响应这个请求。以上就是express大致的原理和工作流程,对于它的学习,推荐去express官网直接去看文档,讲的很详细。
现在回到用户注册模块,我们注册用户常见的做法是注册成功之后就默认用户已经登录,直接跳转到欢迎登陆界面。在这里我们需要将用户数据在注册成功之后保存在session中,express框架对于session的支持是通过中间件express-session来的,使用方式依然是在npm 下安装,然后在项目主文件中使用require加载,最后调用其提供的API,为了使用session,必须先安装cookie的支持,这里利用cookie-parser这个中间件来为express框架提供cookie支持,它的具体使用方式可以去上面提供的地址自行查看。对于session,我们常见框架的做法是在服务器端将其存放到文件当中,由于这里我们有了MongoDB数据库,更理想的状态是将它存在数据库中,这样可以更灵活去控制。使用connect-mongo中间件可以将session存储到mongoDB中,具体使用方式可按地址查看。
上述概念明确之后,我们在项目根目录下的app.js(项目入口文件)中加载我们需要的中间件模块和自定义的模块如下:
app.js模块加载代码:
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 routes = require('./routes/index'); var users = require('./routes/users'); var session = require("express-session"); var MongoStore=require('connect-mongo')(session); var db = require('./db'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // uncomment after placing your favicon in /public //app.use(favicon(__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(session({ secret:"myzhibie", store:new MongoStore({ mongooseConnection:db.dbCon }) })); app.use('/', routes); app.use('/users', users);
上述代码就是加载各个中间件模块并采用app.use来load这个模块,其中上述代码的最后一指定了将session存储在MongoDB数据库中,secret属性是对session的签名,通常是一个字符串,这是必选项,如果不写,
是无法完成将session存储进入数据库的,关于该功能的更详细介绍请查看文档,最后两句app.use('/',routes)和app.use('/users',users)代表对于这两个路由的访问处理代码我们封装在了routes和users模块中,er这两个模块都在routes文件夹下面。
完成了模块引入加载和一些基本的设置,现在来编写用户注册的逻辑代码,上面说到对于路径/的访问处理在routes模块中,这个模块指的就是routes文件夹下面的index.js,部分代码如下:
var express = require('express'); var crypto = require('crypto'); var router = express.Router(); var db=require('../db'); var User=require('../models/user'); var Post=require('../models/post'); /* GET home page. */ router.get('/', function(req, res, next) { Post.find({},function(err,posts){ if(err){ req.session.message=err.message; return res.redirect('/'); } res.render('index',{ posts:posts }); }); }); //发表微博 router.post('/post',function(req, res, next){ var currentUser=req.session.user; var post=new Post({ user:currentUser.name, post:req.body.article, updated:getTime(new Date()) }); post.save(function(err){ if(err){ req.session.message=err.message; return res.redirect('/reg'); } req.session.success="发表成功"; res.redirect('/users/'+currentUser.name); }); }); function getTime(date){ return date.getFullYear()+ "-"+date.getMonth()+1+"-"+ date.getDate()+" "+ date.getHours()+":"+ date.getMinutes(); } router.get('/reg', isLogin); //用户进入注册页面 router.get('/reg',function(req,res){ res.render('reg',{title:"用户注册"}); }); router.post('/reg', isLogin); //用户点击注册按钮 router.post('/reg',function(req,res){ if(req.body['password']!= req.body['passwordconf']){ req.session.error="两次密码不一致"; return res.redirect('/reg'); } var md5=crypto.createHash('md5'); var password=md5.update(req.body.password).digest('base64'); var newUser=new User({ name:req.body['username'], password:password }); User.findOne({name:newUser.name},function(err,user){ if(user){ err="用户名已经存在"; } if(err){ req.session.error=err; return res.redirect('/reg'); } newUser.save(function(err){ if(err){ req.session.error=err.message; return res.redirect('/reg'); } req.session.user=newUser; req.session.success="注册成功"; res.redirect('/'); }); }); }); router.get('/login',isLogin); router.get('/login',function(req,res){ res.render('login',{title:"用户登陆"}); }); router.post('/login',isLogin); router.post('/login',function(req,res){ var md5=crypto.createHash('md5'); var password=md5.update(req.body.password).digest('base64'); User.findOne({name:req.body.username},function(err,user){ if(!user){ req.session.error="用户不存在"; return res.redirect('/login'); } if(user.password!=password){ req.session.error="密码错误"; return res.redirect('/login'); } req.session.user=user; req.session.success="登录成功"; res.redirect('/'); }); }); router.get('/logout',function(req,res){ req.session.user=null; res.redirect('/'); }); function isLogin(req,res,next){ if(req.session.user){ req.session.message="用户已登录"; return res.redirect('/'); } next(); } module.exports = router;
上述代码1-6行都是对外部模块的引入,8-19行是对首页路由/的处理代码。117行将该模块定义为router供外部调用。我们主要看54-83行,这些代码就是用户注册的代码,54行监听来自用户对于/reg路由的post请求,首先判断两次密码是否一致,如果不一致在session中存储一个错误信息然后跳转到到当前页面显示错误信息,该错误信息供模板引擎显示给用户。如果两次密码一致首先对密码进行md5加密,使用的是nodejs提供的核心模块crypto,并生成一个对象模型User,该对象模型是mongoose中提供的一个model的实例,mongoose在它上面定义了一些操作数据库的方法。然后调用这个实例的findOne方法检测该用户是否已经存在,如果存在就保存错误信息到session并跳转到当前页显示错误。如果不存在这样一个用户就使用save方法进行用户信息保存,注册成功后将用户信息保存在session中,并保存一个success的提示信息,然后跳转到首页。这里需要注意一个坑,以前做php或者.net的时候,我们通常都是先查询数据库等数据库返回结果提示用户是否存在之后再进行用户的save然后在跳转,这是一种同步方式,跳转操作需要等待findOne操作返回结果之后才能进行。而nodejs中采用异步IO,最后的跳转操作需要放在findOne操作的回调函数中进行,跳转操作不必等待findone操作结束后执行,两者是异步的。如果将最后的redirect操作放在findOne操作外部而不是回调函数中,你会在控制台上得到一个Can't set headers after they are sent的错误,这是因为在fineOne以及save操作之前已经进行行了跳转,response响应已经结束,不能够重复响应请求。
到目前为止,用户注册模块基本上已经差不多完成了,最后需要说一下如何在页面上显示提示信息或者错误信息,之前我们将提示信息或者错误信息都保存在了session中,jade要显示错误信息,它是不能够直接访问session的,在express2.X即原书中是利用req.flash API+动态视图助手来实现的,就是发生错误的时候先将其利用req.flash方法存储下来,然后利用动态视图助手结合模板去渲染给用户。express4.X废弃了这种方式,我们可以利用req.flash 的原理来自己模拟一个这种机制,同时利用res.locals变量被保存起来,模板在渲染的时候是能够访问到服务端这个变量的。关于res.locals的更多介绍请查看文档。
为了模拟这种req.flash机制,我们在项目入口文件app.js(项目根目录下)添加一段代码如下:
app.use(function(req,res,next){ // res.locals.user=req.session.user; var err=req.session.error; var success=req.session.success; var user=req.session.user; var mess=req.session.message; delete req.session.success; delete req.session.error; delete req.session.message; if(err){ res.locals.message="*"+err; } if(mess){ res.locals.message="*"+mess; } if(success){ res.locals.success=success; } if(user){ res.locals.user=user.name; } next(); });
这段代码的意思是用户请求和响应的时候,捕获session中存储的错误信息和用户提示,将其存储在response.locals变量中,这样模板就能够获取。对于错误信息和提示,由于只使用一次,存储后立即使用delete删除,对于用户信息,需要持久保存下来,则不删除。
这样,就能够显示用户提示或者错误信息。
下面演示一下完整的用户注册流程以及错误信息提示。
当用户名存在或密码不一致时,
当注册成功后跳转到首页并显示用户注册成功
同时对于注册成功和登陆成功拥有不同提示,如果该用户已经是登录状态则显示退出和发表文章按钮,如果没有登录,则显示的是登陆和立即注册按钮。
以上就是利用nodejs及express,mongoose,mongoDB,jade进行web开发的主要流程,由于该项目是对nodejs web开发指南一书中微博项目的重构,所以完整版的项目代码还有用户权限控制(已登录用户不能够注册或登陆并提示),用户进入个人页面发布微博并列表显示,同时首页显示最近发布的微博信息等功能。完整版代码点这里,由于篇幅以及时间问题,上述要点不可能一一展开讨论,本文就作为一个提纲,是对nodejs web开发的一个综述。