2019独角兽企业重金招聘Python工程师标准>>>
前段时间忙项目没时间学node,前两天负责人提出要做直播,考虑使用websocket,马上我就想到node的socket.io了,于是打算多花点时间赶紧把node搞熟。express 作为http协议的web开发框架用起来还是蛮顺手的,感觉比较直白,而且毕竟是js,函数式编程和Java有很大的不同。今天来体验一把。
入手demo就写一个 个人相册 好了,相册涉及了文件上传,查看全部相册,查看单张照片。
我先把我最初的设计写出来,然后写我完善后的。
1.express等环境搭建:
首先安装express, 安装express真是没把我坑死,找的教程说安装完express后使用express命令可快速搭建express工程,然而卵用没有。。。后来发现那个教程讲的是express3.x 我用的是express4.x,所以还请大家务必注意,express4.x的安装和使用和3有一些差异,命令如下:
npm install -express -save
npm install -express-generator --save
第二条命令式为了安装express构建工具的,他能帮你快速构建一个express项目,有一点编程经验的人还是不要自己一个个建文件夹和配置文件,太慢了,用工具建好后看看目录结构和配置文件,了解一下就好,特别是用过maven, grandle的看起来so easy;细节的不明白的日后慢慢理解也好。
如果用的webstorm的话,要记得以依赖下node的global库。
安装完express后,最好把express加到你的环境变量,找到express.cmd后放到你的环境变量(环境变量配置 同 Java配置方法)。
然后这时候你使用express -e demo 就能看到一个demo 项目出来了,当然这里我的名字叫 photo。下面是我构建完的工程:
2.初始工作
app.js就是你webapp工程入口,运行一下看看,可能会报错,因为也许你的依赖没有添加完,比如 body-parse,cookie-parse等 库,你运行一下看看报什么错,就添加什么库好了,我大概加了四个后就没问题了。当然这里还要额外加俩库,就是multer(文件上传相关),mongoose(操作mongodb)和ejs(模板库)
views里面都是模板试图,routes是路由,类比controller filter. public一般是设置静态资源的,前端js,css,html等放在这里面;node_modules你不用管,你每添加一个依赖就会在里面多一个模块;lib一般是你自己写的,包括业务逻辑,data access等等,其实我感觉可以划分的细一些,比如 server,db,util。
列一下看看我们都依赖了什么库:
var express = require("express");
var path = require("path");
var bodyParser = require("body-parser");
var multer = require("multer");
打开app.js看看里面的有什么?你会看到有很多require被添加进来,其实就类似C/C++的include,java的import。再就是express方法,调了很多use()方法,如下:
app.use(express.static("public"));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(multer({dest: '/tmp/'}).array('image'));
use方法的设计思路感觉和jquery的css等函数一样,链式编程,有点像挂载,你把很多组件写好了,use一下,这个功能就被挂载进来了。这种写法Java C/C++中不曾见过,其实jquery中也是这样,你给一个控件添加样式就是不断地把样式挂载到控件实例上。以上代码的解读就是 给express添加 静态资源,设置为public路径下的都是静态资源;添加表单上传编码“application/x-www-form-urlencoded ;添加cookie识别转换,添加文件上传功能(这里设置了临时文件位置)。
为了要使用ejs视图,还是要设置一下试图引擎:
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', "ejs");
这里set方法采用键值对,设置视图路径和视图引擎,一般set功能都放在服务器启动时设置启动参数 比如端口,ip,运行环境什么的,可以用get获取。这个方法可能用的比较少,毕竟set是在设置webapp的启动配置参数,你看看Java的spring配置文件,会设置很多东西,在启动时会被加载,也就是说如果你在开发有关express插件的话,可能用的比较多吧?不知道我这么说对不对。
3.业务逻辑之----- 文件上传
首先你需要一个页面用来上传文件,这个好说就一个表单,其次你需要一个页面来展示你数据库中存储的全部图片,然后还有个页面是显示单张图片;这里,显示单张和显示多张我放在一起。需要俩页面,分别叫index.ejs(照片上传) show.ejs(显示照片).
在app.js代码基础上我们开始写逻辑了。先写照片上传。上传请求为post方法请求,所以先写一个路由来接受post请求
var router = express.Router();
router就是一个路由对象,router对象有post方法,就是用来接收post请求。
router.post("/upload", function (req,res) {
......
}
回调函数在上传文件请求接收到后执行。具体细节应该就是文件上传,并把详细信息存入数据库了。
文件信息上传后通过req.file[0]对象来接受,该对象就是表单中file控件选中的文件,属性包括originalname,
path等等,原名好理解,path大概是临时路径,我们用了multer插件,设置过临时路径。
我们的文件是不可能放在临时文件内的,而且临时文件的命名方式很BT。。。 所以我们要把他移动到我们服务器默认的路径下,我们默认在public/images下好了,那么先拼出来这个路径
path.join(__dirname,"./public/images/") + req.files[0].originalname
path也是node中的工具,需要 require("path)才能使用,这个工具就是为了方便我们拼接路径的。
下面我们有了临时路径和指定路径了,就要做文件转移了。需要使用fs模块的rename方法。rename默认是异步发的方法,所以我们需要把数据库操作放在这个回调中,才能保证文件在上传完后把信息存到数据库。
fs.(req.[].image_file(errdata) {
(err){ err}
{
data= {
:path.(req.[].):req.[].}
这样就完成了文件的转移,数据库里面我们自然是只存文件路径了,mongodb键值对结构,所以我们定一个photo这样一个json字符串,包括文件名和文件路径。
nodejs原本有一套mongodb的库来操作数据库,这里我使用mongoose这个库来开发,相对简单方便。
和传统关系型数据库一样,我们需要先把服务开开,windows下 在 开始 中输入 service 找到mongodb的服务并打开它。
代码中,第一步必然是打开数据库连接了。
.((err) {
(err) {err}
{
console.()}
})
指定数据库。回调中打好log.
接下来就增删改查直接写吗? 当然不是,为了方便数据操作和使用,mongoose推荐我们建立schema,schema理解为一个标准,也就是我们要定一个pojo用来描述我们要插入的对象及其类型。当我们定义好类型后,我们可以经这个类型赋值使其成为一个实例,真正的拥有数据。这一点可以类比Java中对象和类的概念,类就好比一个schema,对象就是一个具体的schema了。定义格式如下:
var photos = new mongoose.Schema({
name:String,
path:String
});
var Photo = mongoose.model("photo",photos)
这样Photo就是一个pojo.注意mongoose挺奇葩的,model中的photo我本以为是mongodb中定义的数据表名字,结果发现他去我表中创建了一个photos ,注意多了个 s .。也就是说你模板中起的名字对应你数据表名字都要加一个s,这个问题我试了很多遍确实是这样,不知道为何要如此设计。photos表示传入的数据,只有传入一个数据才能是一个可用的有数据的对象。
接下来操作mongodb无非就是CRUD了,上传文件的话就先写 增。
photo.save(function (err) {
if (err){console.log(err);}
else {console.log("data saved! ");fn()}
});
data就是上面写的那个json,里面就是请求里面的文件信息。
photo就是Photo类的实例,photo这个对象是mongoose.model这个方法生成的模板类,也就带有数据库的curd方法。这一点和传统编程语言相差甚远,我们Java或C#中定义的pojo是不包括curd方法的,curd需要用dao或者DBUtil的insert update等等方法;而这里的pojo就是自带pojo的,其实自带的curd也是来自model方法,直接pojo.insert pojo.find即可。所以这里你会看到photo.save()这个方法。回调函数表示数据库存储完毕。
写一个get请求接受“/upload”显示上传界面。
router.get("/upload", function (req,res) {
res.render("index");
});
render方法类似于java的springmvc框架 controller中方法的返回值,能映射到配置好的试图,这里写index,他就会去views路径下找名字为 index.ejs的文件,前面的引擎如果为html,那就回去找index.html 直截了当不多说。
末尾,app.listen(3000)开个端口试试。上传界面自己随便写写,访问localhost:3000/upload.
4.业务逻辑之--------图片查看
既然上传会了,查看也就不难了,因为我们添加了静态资源路径,所以前台只需要读取/images/****.jpg就能看到了,但是为了看到所有的照片,还是要从数据库去除所有连接,反映到视图层遍历出来。那么这块就需要用到ejs模板技术了。ejs可以理解为 和jsp,asp 差不多的,方便视图层写业务逻辑的模板。
前面express已经设置了试图引擎为ejs,那么在views中建立一个文件叫show.ejs。
下面写一下模板中核心逻辑
<% photo.((data) {%>
<%=data.%><%=data.%><%=data.%><%})%>
photo是服务器传递到模板的对象,不管数据库有几个照片信息,我们都视为list,并在页面中遍历他,显示图片名和图片,并且将图片名做成超链接,点击后查看单张。
这里有一个问题了,photo是如何传递进来的呢?用jsp的时候我们都知道可以把数据放到session或者request中模板直接调用内置函数或对象获得数据,这里photo的传递方式稍微不一样,前面我们访问“index.ejs”的时候调用了render方法去视图层查找这个文件,其实这个方法不止一个参数,还有个参数就是用来设置对象的,这样视图层就能调用你传递的这个参数对象。
res.render("show",{photo:data});
如上,如果data是一个集合,而且json中格式遵循了path,name,就能被模板使用。
这里我们开一个get方法用来接收“/”根请求,每个人访问localhost:3000的时候都能看到相册中的相片。
这里也就需要用到photo对象(前面新建的)的find方法了
Photo.Photo.find({}, function (err,photos) {
if(err){console.log(err);return {}}
console.log("method find : "+photos);
fn(photos);
});
这个时候我们是直接用了Photo对象,并没有新建实例,大概因为我们不需要调用者是一个实例,需要调用者调用这个方法找出一个实例而已。find的第一个参数是查询条件,这里为空,表示查出所有,大概接触过mongodb的人会知道db.find里面放的也是一个json表示查询方法。,回调函数的photos是查询结果,查询完小勇render找到视图层并把结果传导视图层。
这样我们访问以下localhost:3000就能看到所有图片了。
5.业务逻辑之--------查看单张
查看单张逻辑没啥特殊,就是需要在find中加条件而已,大家可以尝试findOne或者findById,其中findById应该是只需要传递id进去就好了,其他的要穿json表示你要查询什么。特别之处在于如果你是按照_id查询,你需要多做一步。因为mongodb中的_id类型不是String而是ObjectId,是用正则表达式判断这个id的格式的,正则如下:
/^[0-9a-fA-F]{24}$/
你还可以用一个插件 叫 objcetid 这个插件内置了这个正则,而且做了额外包装,能够让你判断一个字付出是不是这个格式的数据。因为在用id查询的时候,如果你的格式是错的,node直接异常抛出来了,所以你用这个插件先判断是不是objectid格式的,不是就肯定查不出数据,也省了走数据库的开销。
var objectid = require("objectid");
添加完依赖,然后看看怎么用的:
var _id = objectid.isValid(id)?id:objectid();
判断是不是objectid,不是随便生成一个objectid,mongodb既然用objectid做主键,说明基本不会重复,不考虑大并发的话,我们先这么用。
其他的操作和find的一样的就不重复了,需要强调的是,这里采用restful风格的请求,我们开一个get请求来查看某个照片:
router.get("/photo/:id", function (req, res) {....}
看到:id了吗?不奇怪,Springmvc中经常用,就是个占位符罢了,使用req.params.id就能获取到了。
5.业务逻辑之--------优化
照片上传查看到这里就结束了,剩下一点内容我想写的就是优化。因为长期受Java编程习惯的影响,我总是喜欢把操作分门别类,比如router.post("/upload") 我视其为控制器(node中成为路由),Photo类的建立我成为pojo或者model. photo.save这样的叫dao(或者db),文件存储和转移我成为util
那么我们就要把app.js这个文件一分为四了,app.js保留,作为webapp的入口,所有路由部分我们单独放到routers文件夹中,当道route.js内,照片对象和数据库访问和文件上传转移我们放到lib下,分别叫photo.js,db.js,util.js
先从简单的来,定义Photo的schema 如下:photo.js
/**
* Created by Administrator on 2015/10/29.
*/
var mongoose = require("mongoose");
var photos = new mongoose.Schema({
name:String,
path:String
});
exports.Photo = mongoose.model("photo",photos);
这个类导出Phot对象,外部想用的时候,调用require的对象然后new就行,当然还要传一个json,
然后写一下db部分,看看他们如何依赖:
/**
* Created by Administrator on 2015/10/31.
*/
var Photo =require("../lib/photo");
var objectid = require("objectid");
var mongoose = require("mongoose");
mongoose.connect("mongodb://127.0.0.1:27017/tonghang", function (err) {
if (err) {throw err;}
else {
console.log("mongoDB connected ");
}
});
exports.insert = function (data,fn) {
var photo = new Photo.Photo(data);
photo.save(function (err) {
if (err){console.log(err);}
else {console.log("data saved! ");fn()}
});
};
exports.findById = function (id,fn) {
var _id = objectid.isValid(id)?id:objectid();
Photo.Photo.find({_id:_id}, function (err,photo) {
if(err){console.log(err);}
fn(photo);
});
};
exports.find = function (fn) {
Photo.Photo.find({}, function (err,photos) {
if(err){console.log(err);return {}}
console.log("method find : "+photos);
fn(photos);
});
}
正如大家所见,Photo对象是Photo模块中exports的副本,在db模块的save中需要先new出来才能存储。
这里我用了nodejs的回调机制(有点函数编程的感觉),导出的find函数需要一个回调函数,在查找完成之后才会执行回调。如果这里用回调,仅仅是导出函数的返回值,那么你肯定查不出数据,因为代码执行速度肯定比你访问数据库快,还没查出来你就render去了。。。。
和db直接相关的就是路由,路由将请求分发到不同的db操作,当然在大项目中还需要有个service来处理若干业务逻辑比如数据的拼接啊,计算啊等等,这里就先不写了:
var express = require('express');
var path = require("path");
var fs = require("fs");
var futil = require("../lib/futil");
var router = express.Router();
var Db = require("../lib/db");
/* GET home page. */
router.get("/", function (req,res,next) {
Db.find(function (data) {
res.render("show",{photo:data});
});
console.log("mongodb 查询");
});
router.get("/photo/:id", function (req, res) {
Db.findById(req.params.id, function (photo) {
res.render("show",{photo:photo});
});
console.log("mongodb 查询条件:"+req.params.id);
})
router.get("/upload", function (req,res) {
res.render("index");
});
/*POST Method*/
router.post("/upload", function (req,res) {
var image_file = path.join(__dirname,"../public/images/") + req.files[0].originalname;
console.log("images_file: "+image_file);
futil.fileUpload(req.files[0].path,image_file,function () {
var photo = {
path:path.join("images/",req.files[0].originalname),
name:req.files[0].originalname
};
Db.insert(photo, function (err) {
if(err){
res.render("error");
}else{
res.redirect("/");
}
});
})
});
module.exports = router;
路由中集合了路由功能和业务逻辑处理,所以看起来很多,大家在正规开发中还是分一个service出来比较好,路由还是要遵循以简单为主,只用来接收数据或者将数据做简单的调整。一切db util都在service中处理,service就好比是集线器一样,这样以后项目在扩展接口的时候看起来就会比较轻松了。
router模块再和文件上传工具耦合(其实还是要交给service).下面是futil.js:
/**
* Created by Administrator on 2015/10/31.
*/
var path = require("path");
var fs = require("fs");
exports.fileUpload = function (originpath,newpath,fn) {
fs.rename(originpath,newpath, function (err, data) {
if(err){ throw err; }
else{
fn();
}
});
}
当然路由模块要导出Router实例,干什么用的?当然是挂载到app.js上,想想最开始讲的挂载,起作用就是将一个webapp中具备的功能(中间件这个词看似比较专业,但是初学者感觉不好理解,我的理解就是具有某些功能的模块)全部添加到express上(老实说express内部的细节我是确实不了解,建议大家学完express后看看connect,他是在connect的基础上包装的) ,我们的webapp需要支持/upload / 等请求,所以将这个功能挂载到express上。 不多说,直接看代码:
exvar express = require("express");
var path = require("path");
var bodyParser = require("body-parser");
var multer = require("multer");
var router = require("./routes/router")
var cookieParser = require('cookie-parser');
var fs = require("fs");
var app = express();
app.use(express.static("public"));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(multer({dest: '/tmp/'}).array('image'));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', "ejs");
app.use(router);
app.listen(3000);
module.exports = app;
着重看加粗的就是我上面说的挂载router了,这里的outer不再是express中的Router模块了,而是我们自己写的“个性化”的router了(个性化这个词我觉得不错)。
好了,运行下看看吧!我把我运行的截图发来:
分层完了以后的项目目录看起来还是蛮不错的: