基于MEAN的仿豆瓣电影网站开发实战(1)

时间:2021-08-19 12:33:27

版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/lilythy2016/article/details/52810082

  本帖讲的是仿豆瓣电影网的电影录入功能实现,环境采用的是全JavaScript的MEAN框架实现,对MEAN框架有不清楚的读者可以参考Webstorm下MEAN环境搭建

服务端实现

  在项目根目录下新建一个src目录,用于存放后端代码。要实现电影信息录入功能,我们需要先定义一个与数据库相对应的模板文件,主要用于定义电影的相关属性和方法。在src目录下新建schemas目录,在里面新建一个movieSchema.js文件,代码如下

  movieSchema.js

//引入mongoose模块
var mongoose = require('mongoose');
//引用mongoose的Schema模块
var Schema = mongoose.Schema;

// 创建一个MovieSchema对象,并定义其相关属性
var MovieSchema = new Schema({
moviename:{ //电影名称,设置unique为true表示电影名称必须唯一
unique: true,
type: String
},
director:String, //导演
writers:String, //编剧
actors:String, //主演
type:String, //电影类型
countries:String, //制片国家
language:String, //语言
meta: { //把时间相关的属性封装在meta对象里
createAt: { //创建数据的时间,默认为录入时的系统时间
type: Date,
default: Date.now()
},
updateAt: { //更新数据的时间,默认为更新数据时的系统时间
type: Date,
default: Date.now()
},
showDate:{ //上映时间,默认为录入数据时的系统时间
type:Date,
default: Date.now()
}
},
moviepic:String, //电影图片的名称
runtime:String, //时长
starnum:Number, //电影评分
starclass:Number //电影评价的星级数
});

/*调用mongoose的pre方法,它起到了起到中间件的作用,会在执行save方法之前调用,这里会给movie对象设置createAt、updateAt的值,我们在处理逻辑的时候就不用管这两个属性了*/
MovieSchema.pre('save',function(next){
var movie = this;

if(this.isNew){
this.meta.createAt = this.meta.updateAt = Date.now();
}else{
this.meta.updateAt = Date.now();
}

next();
});

//给MovieSchema定义一些静态的方法,可以在model层直接调用,跟mongoose封装的model上的save,find等方法平级
MovieSchema.statics = {
//定义了遍历数据库方法fetch
fetch: function(cb) {
return this
.find({})
.sort('meta.updateAt')
.exec(cb);
},
//定义了通过ID查找某条数据的findById方法
findById: function(id, callback) {
return this.findOne({_id: id}).exec(callback);
}
};
//导出MovieSchema,以便其他文件使用
module.exports = MovieSchema;

  然后在src目录下新建目录models,用于存放由schema文件生成的model,怎么理解schema和model呢?schema只是定义movie对象的骨架,设置了movie对象的属性和方法,而Model定义了具体的操作数据库的增删改查等方法,我们可以调用mongoose.model()方法来根据具体的schema生成一个具体的model实体,将该实体暴露出去就可以供控制层文件使用,进行操作数据库了。在models目录下新建一个movieModel.js,代码如下

  movieModel.js

//引用mongoose模块
var mongoose = require('mongoose');
//引用movieSchema.js文件
var MovieSchema = require('../schemas/movieSchema.js');
//通过MovieSchema来创建MovieModel。第一个参数表示MovieModel的名字,第二个参数表示依赖的scheme名称,
// 第三个参数表示数据库中collection的名字(相当于关系型数据库中的表名)
var MovieModel = mongoose.model('Movie',MovieSchema,'Movie');

//导出MovieModel供控制层文件使用
module.exports = MovieModel;

  这里说明一下mongoose.model(‘Movie’, MovieSchema, ‘Movie’) 这句代码,如果不设置第三个参数的话,mongodb数据库会以model的名称忽略掉大小写,然后变成复数来作为表名,也就是写成mongoose.model(‘Movie’, MovieSchema’)的话,数据库里只有movies表,注意第一个参数是model的名字,并不是表名!如果想要清楚地知道自己数据存在哪里,建议设置第三个参数。
  由于后面代码会用到上传路径这个参数,不妨将这个参数提出来设为全局变量,方便维护。所以先在src目录下新建config文件夹,用于存放全局性的配置文件,在config目录下新建paramsConf.js,代码如下

  paramsConf.js

/**/
//设置文件上传的路径,第一个斜杠为转义符
exports.uploadDir = '.\\public\\upload\\';

  紧接着来看具体的逻辑处理代码,该文件定义了一个处理图片上传的方法uploadPic和一个解析表单数据并将数据存入数据库中的create方法。因为我是把电影海报和其他电影信息异步提交的,先上传图片,然后服务端的uploadPic方法会返回唯一的图片名称给前端,防止图片重名,并将这个图片名称赋值给表单里的隐藏input,最后点击提价按钮的时候和其他电影信息一起提交至后台处理。在src目录下新建controllers文件夹,在里面新建movies.js,调用上面的配置文件paramsConf.js,定义这两个后台处理方法,代码如下

  movies.js

//引用movieModel.js文件
var MovieModel = require('../models/movieModel');
//引入express模块
var express = require('express');
//引入formidable,用于解析表单提交的数据
var formidable = require('formidable');
//引入fs模块,用于文件操作
var fs = require('fs');
//引入superagent模块,superagent是客户端请求代理模块,用于处理get,post,put,delete,head请求
var request = require('superagent');
//引入自定义的参数配置文件
var paramsConfig = require('../config/paramsConf.js');

//上传图片方法
exports.uploadPic = function(req,res){
//创建Formidable.IncomingForm对象
var form = new formidable.IncomingForm();
//设置上传图片的位置,就是配置文件里定义的路径
form.uploadDir = paramsConfig.uploadDir;
//保留后缀格式
form.keepExtensions = true;
//解析表单数据,如果有key:value的键值对数据则保存在fields里,这里只处理图片文件,所以fields里面没有数据,files为解析出来的文件对象集合
form.parse(req, function(err, fields, files) {
if (err) {
console.log(err);
}
//获取文件对象集合里的file对象路径,如'.\\public\\upload\\123.jpg'
var path = files.file.path;
//获取路径中最后一个'\'的索引,"\\"表示\,前面一个斜杠表示转义符
var index = path.lastIndexOf("\\");
//截取path中最后一个斜杠后面的字符串,即图片存入数据库的名称('123.jpg')
var serverPicName = path.substring(index + 1, path.length);
//把数据库中的图片名称返回给客户端
res.send(serverPicName);
});
};

// 新增一条电影数据方法
exports.create = function(req, res){
//将req.body赋值给一个新对象mymovie
var mymovie = req.body;
//将req.body提交过来的showDate放入movieSchema里定义的meta属性里
mymovie.meta = {};
mymovie.meta.showDate = mymovie.showDate;
//以mymovie对象实例化一个MovieModel对象:newMovie
var newMovie = new MovieModel(mymovie);
//newMovie调用mongoose的save方法将数据存入mongodb数据库
newMovie.save(function (err, movie) {
if (err) {
console.log(err);
}
//res.send({message: 'add a movie'})
});
};

  上述代码引用的formidable和superagent这两个模块还没有安装的话,需要先通过npm自行安装一下,否则引用会报错哈。

  写完逻辑处理代码之后,我们需要将url路径对应到具体的处理方法上,这时就需要express的路由了。在src目录下创建routers目录,在里面新建movieRouter.js,代码如下。

  movieRouter.js

//引入express模块
var express = require('express');
//由express创建一个路由
var router = express.Router();
//引入movies.js文件
var movies = require('../controllers/movies.js');

/* 设置post方法路由映射. */
router.post('/',movies.create);
router.post('/pic',movies.uploadPic);

//导出router
module.exports = router;

  路由编写完成后需要在app.js中使用才有效,打开app.js,加上引用路由的代码

//引用自定义的路由文件
var router = require('./src/routers/movieRouter.js');

//http://localhost:3000/api下的请求都经过router文件拦截
app.use('/movie', router);

  这里要说一下app.js文件里通过app.use 是用于加载处理http请求的中间件,当一个请求来的时候,会依次被这些中间件处理。执行的顺序是你在app.js里定义的顺序,所以顺序一定要写正确,不然会在运行的时候出现错误。就像这里,如果不把加载bodyParser的代码写在路由之前,后台则获取不到解析的json对象,正确的书写顺序如下图所示

基于MEAN的仿豆瓣电影网站开发实战(1)

  接下来需要配置mongoose来连接mongodb数据库,以便进行数据存储。在config目录下新建dbConfig.js,用来配置连接数据库的参数,代码如下:

  dbConfig.js

//mongodb数据库参数配置
var user_name = 'lilythy'; //用户名
var password = '123456'; //密码
var db_url = 'localhost'; //主机名
var db_port = 27017; //端口
var db_name = 'moviesite'; //database名称

//导出mongodb数据库的连接信息
exports.db_str = 'mongodb://' + user_name + ':' + password + '@' + db_url + ':' + db_port + '/' + db_name;

  然后在app.js文件里引用这个数据库配置文件,通过mongoose的connect方法来连接数据库,代码如下

var mongoose = require('mongoose'); 
var Conf = require('./src/config/dbConfig.js'); //引入数据库配置文件,提供了连接数据库所需的参数

mongoose.connect(Conf.db_str); //通过配置文件内的链接连接mongodb数据库

  完整的app.js文件如下

  app.js

var express = require('express');                  //引入express框架
var path = require('path'); //引用NodeJS中的Path模块,用于处理和转换文件路径
var favicon = require('serve-favicon'); //引入serve-favicon中间件,可以用于请求网页的logo
var logger = require('morgan'); //引入用于记录日志的中间件morgan
var cookieParser = require('cookie-parser'); //引入cookieParser中间件,用于获取web浏览器发送的cookie中的内容
var bodyParser = require('body-parser'); //引入body-parser模块,用于对请求进行拦截和解析

var app = express(); //express()表示创建express应用程序。
var router = require('./src/routers/movieRouter.js'); //引用自定义的路由文件
var mongoose = require('mongoose'); //引入mongoose模块
var Conf = require('./src/config/dbConfig.js'); //引入数据库配置文件,提供了连接数据库所需的参数

mongoose.connect(Conf.db_str); //通过配置文件内的链接连接mongodb数据库

app.use(logger('dev')); //将请求信息打印在控制台,便于开发调试
app.use(cookieParser()); //装载cookie-parser模块,之后便可以解析cookie
app.use(bodyParser.json()); //装载一个只解析json的中间件body-parser
app.use(bodyParser.urlencoded({extended: false})); // bodyParser.urlencoded是用来解析我们通常的form表单提交的数据,也就是请求头中包含这样的信息: Content-Type: application/x-www-form-urlencoded

app.use('/movie', router); //http://localhost:3000/api下的请求都经过router文件拦截

app.set('views', path.join(__dirname, 'public')); //设置模版文件夹的路径为/public
app.engine('.html', require('jade').__express); //设置jade引擎支持.html后缀
app.set('view engine', 'html'); //在调用render函数时能自动为我们加上'.html' 后缀
app.use(express.static(path.join(__dirname, 'public'))); //设置静态文件目录为/public

//所有http://localhost:3000下的请求都被拦截,然后渲染为/public目录下的index.html页面
app.use('/', function (req, res) {
res.sendFile('index.html', {root: path.join(__dirname, 'public')});
});

// 如果404错误就交给错误处理程序
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

// 开发环境错误处理程序将会打印出错误
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
});
});
}

app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});

module.exports = app;

  接下来需要在mongodb数据库那边做一些相应的配置,打开cmd窗口,然后通过cd命令进入到mongodb安装目录的bin文件夹,然后输入mongod –dbpath db文件夹路径 来启动mongodb,这样数据库文件就会存放到这个db文件夹,如下图所示来启动mongodb

基于MEAN的仿豆瓣电影网站开发实战(1)

  看到启动端口号为27017就说明连接成功了,然后点击bin文件夹下的mongo.exe,输入下面这段代码来创建用户名和密码,设置该用户有读写权限,并设置db的名字为moviesite。

db.createUser(
{
user: "lilythy",
pwd: "123456",
roles: [ { role: "readWrite", db: "moviesite"
} ]
}
)

  设置成功后,在cmd窗口按ctrl+c退出连接,然后按方向键“↑”重新启动mongodb,启动后千万不要关闭cmd窗口,mongod.exe窗口可以关,你就可以大胆地点击webstorm的运行按钮了,控制台没有报错则说明连接成功了。

  后端编写完成后的项目目录结构如下图所示
基于MEAN的仿豆瓣电影网站开发实战(1)

4.前端实现

  在public目录下新建一个 js文件夹,然后在里面新建一个module.js文件,声明一个 ‘app’模块,并设置其依赖模块,有需要的话还可以定义全局常量或变量,代码如下

  module.js

//在js文件第一行加上'use strict'使该文件在严格模式下执行,就是对代码书写规范更严格,否则就会报错
'use strict';
//声明一个'app'模块,并设置该模块依赖ui.router、ui.bootstrap和ngFileUpload
var app = angular.module('app', [
'ui.router',
'ui.bootstrap',
'ngFileUpload'
]);

//声明'app'模块的全局常量'DIR'
app.constant('DIR','\\upload\\') ;

  接着在js文件夹下新建一个routes.js文件,定义路由状态,前端页面会根据链接的状态进行页面跳转,代码如下

  routes.js

//# (function (app) {})(angular.module('app')),将angular声明的app模块传给function的参数'app',表示在app模块的作用域里执行以下语句
(function (app) {
//# 使用严格模式
'use strict';
/*# 利用config方法做一些注册工作,这些工作需要在模块加载时完成
*# $stateProvider用于配置路由状态;
**'# $urlRouterProvider负责监视$location,当$location改变后,$urlRouterProvider将从一个列表,一个接一个查找匹配项,直到找到;
*# $locationProvider用于配置$location服务,去掉单页面应用链接中的"#" */

app.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
//AngularJS框架提供了一种HTML5模式的路由,设置为true就可以直接去掉#号
$locationProvider.html5Mode(true);
//访问其他不存在的路径时都跳到'/'
$urlRouterProvider.otherwise('/');
$stateProvider
/* 设置路由状态'add',路由为'/addmovie',对应的html页面为views文件夹下的'addmovie.html',作用于该页面的控制器名称为'AddController' */
.state('add', {
url: '/addmovie',
templateUrl: '/views/addmovie.html',
controller: 'AddController'
});
});
})(angular.module('app'));

  然后在js目录下新建一个service文件夹,在里面新建movieService.js,用来定义一些请求后台数据的方法,可供conreoller或config使用,代码如下

  movieService.js

/*在app模块下定义自定义服务*/
(function (app) {
'use strict';
//通过factory()方法创建一个服务MovieService,可以供controller或config使用
app.factory('MovieService', function ($http, $q) {
return {
//将表单数据提交至后台处理
addMovie:function(movieEntity){
//后台处理链接
var url = "http://localhost:3000/movie/";
/*利用$q服务实现Deferred/Promise方法,利用$q.defer()生成deffered 对象,该对象有三个方法:
* 1.resolve(value):如果异步操作成功,resolve方法将Promise对象的状态变为“成功”。
* 2.reject(reason):如果异步操作失败,则用reject方法将Promise对象的状态变为“失败”。
* 3.notify(value) :表明promise对象为“未完成”状态,在resolve或reject之前可以被多次调用。
* 当创建deferred实例时会创建一个新的promise对象,并可以通过 deferred.promise 得到该引用。*/

var deferred = $q.defer();
//通过angular的$http服务将表单的Json数据提交给后台,并监听结果
$http.post(url, movieEntity).then(
//成功则将数据返回给deferred对象
function success(respData){
var movies = respData.data;
deferred.resolve(movies);
},
//失败则返回原因给deferred对象
function error(reason) {
deferred.reject(reason);
}
)
//通过deferred.promise获得返回给deferred对象的结果
return deferred.promise;
}
}
});
})(angular.module('app'));

  定义完service后就可以在controller里调用这个服务了,接下来在js目录下新建controller目录,然后在里面新建addController.js文件,代码如下

  addController.js

(function (app) {
'use strict';
//app.controller()方法的第一个参数是controller名称,第二个参数为数组,数组的前面是声明注入的内容,可以是n个,最后一个是个function,function的参数个数也必须是n个,必须跟前面声明注入的内容一一对应
app.controller('AddController',['$scope', 'Upload', '$state','MovieService', 'DIR', function ($scope, Upload, $state, MovieService, DIR) {
//初始化数据库存的图片名称
$scope.serverPicName = '';
//引用自定义路径,并赋值给该作用域的dir变量
$scope.dir = DIR;
//定义图片预览的img标签是否显示,true则显示
$scope.isShow = false;
//定义上传图片方法
$scope.uploadFiles = function(file, errFiles) {
//将文件参数赋值给该作用域的f
$scope.f = file;
//$scope.size = ($scope.f.size/1024/1024).toFixed(2);
//将文件错误参数传给该作用域的errFile
$scope.errFile = errFiles && errFiles[0];
//如果是文件的话,则调用Upload插件上传该文件
if (file) {
//调用Upload的upload()方法将data数据(即文件)上传至后台url处理
file.upload = Upload.upload({
url: 'http://localhost:3000/movie/pic'
data: {file: file}
});
//then方法定义文件上传成功后执行的代码,这里表示上传成功后则html页面显示预览图片的img标签并将返回的数据赋给serverPicName
file.upload.then(function (response) {
$scope.isShow = true;
$scope.serverPicName = response.data;
}, function (response) { //不成功则返回错误信息
if (response.status > 0)
$scope.errorMsg = response.status + ': ' + response.data;
}, function (evt) { //执行上传行为时返回上传进度
file.progress = Math.min(100, parseInt(100.0 *
evt.loaded / evt.total));
});
}
}

//点击提交按钮后提交表单数据给后台的方法
$scope.postMovie = function(){
//获得隐藏input的值,即上传图片后后台返回前端唯一的图片名称
var moviepic = document.getElementById("serverPicName").value;
//生成一个1~10之间的星级数
$scope.starNum = (Math.random()*10+1).toFixed(1);
//将后台返回的图片名称赋给movie对象的moviepic属性
$scope.movie.moviepic = moviepic;
//将生成的星级数赋值给movie对象的starnum属性
$scope.movie.starnum = $scope.starNum;
//生成决定html页面使用哪个类的参数
var starClass = (Math.round($scope.starNum)/2)*10;
$scope.movie.starclass = starClass;
//调用自定义服务MovieService里的addMovie方法,并将返回结果赋值给promise
var promise = MovieService.addMovie($scope.movie);
//数据提交成功后,刷新页面
promise.then(function (data) {
window.location.reload();
});
}
}]);
})(angular.module('app'));

  最后提供一下录入界面的html,由于这里注重的是功能代码的实现,前端页面的表单验证和时间选择插件等还没有优化,以后有时间我会持续完善已上传至github的项目代码,本帖最后会给出链接。

  addmovie.html

<form class="container formwidth" role="form" name="form" id="movieForm">
<div class="form-group">
<div class="imgPreview"><img ng-show="isShow" ng-src='
{{dir}}{{serverPicName}}' alt="{{f.name}}"/></div>
<button class="upload-btn" type="file" ngf-select="uploadFiles($file, $invalidFiles)"
accept="image/*" ngf-max-height="1000" ngf-max-size="1MB">

Select File</button>
<input id="serverPicName" value="
{{serverPicName}}" type="hidden"/>

<span id="fileName">
{{f.name}} {{errFile.name}} {{errFile.$error}}</span>
<span class="progress" ng-show="f.progress >= 0 && f.progress < 100">
<div class="progress-bar" style="width:
{{f.progress}}%"
ng-bind="f.progress + '%'">
</div>
</span>

</div>
<div class="form-group movieNameBar">
<label for="moviename">电影名称</label>
<input type="text" class="form-control" id="moviename"
placeholder="请输入电影名称" ng-model="movie.moviename">

</div>
<div class="form-group">
<label for="director">导演</label>
<input type="text" id="director" class="form-control" ng-model="movie.director">
</div>
<div class="form-group">
<label for="writers">编剧</label>
<input type="text" id="writers" class="form-control" ng-model="movie.writers">
</div>
<div class="form-group">
<label for="actors">主演</label>
<input type="text" id="actors" class="form-control" ng-model="movie.actors">
</div>
<div class="form-group">
<label for="type">类型</label>
<input type="text" id="type" class="form-control" ng-model="movie.type">
</div>
<div class="form-group">
<label for="countries">制片国家</label>
<input type="text" id="countries" class="form-control" ng-model="movie.countries">
</div>
<div class="form-group">
<label for="language">语言</label>
<input type="text" id="language" class="form-control" ng-model="movie.language">
</div>
<div class="form-group">
<label for="showDate">上映日期</label>
<input type="text" id="showDate" class="form-control" ng-model="movie.showDate">
</div>
<div class="form-group">
<label for="runtime">片长</label>
<input type="text" id="runtime" class="form-control" ng-model="movie.runtime">
</div>

<button type="submit" class="btn btn-prima。
y"
ng-click="postMovie()">
提交</button>
</form>

  完成后的录入界面如下图所示
基于MEAN的仿豆瓣电影网站开发实战(1)

  点击完提交按钮之后,打开mongod.exe,输入use moviesite(你定义的db名字) 进入我们定义的db,然后输入db.Movie.find() (Movie为你定义的表名) 查看数据,就可以看到刚插入的数据,如下图所示
基于MEAN的仿豆瓣电影网站开发实战(1)

  至于怎么实现豆瓣电影网站首页数据的展示,这个我会在下篇帖子中介绍。
  本帖实例代码已上传至github,后面会持续完善,有需要的话请点击这里查看。