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

来源:互联网 发布:mac光盘怎么弹出 编辑:程序博客网 时间:2024/04/30 14:02

版权声明:本文为博主原创文章,转载请注明出处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);//导出routermodule.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对象,正确的书写顺序如下图所示

这里写图片描述

  接下来需要配置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中间件,可以用于请求网页的logovar logger = require('morgan');                    //引入用于记录日志的中间件morganvar 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模块,之后便可以解析cookieapp.use(bodyParser.json());                        //装载一个只解析json的中间件body-parserapp.use(bodyParser.urlencoded({extended: false})); // bodyParser.urlencoded是用来解析我们通常的form表单提交的数据,也就是请求头中包含这样的信息: Content-Type: application/x-www-form-urlencodedapp.use('/movie', router);                       //http://localhost:3000/api下的请求都经过router文件拦截app.set('views', path.join(__dirname, 'public'));       //设置模版文件夹的路径为/publicapp.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

这里写图片描述

  看到启动端口号为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的运行按钮了,控制台没有报错则说明连接成功了。

  后端编写完成后的项目目录结构如下图所示
这里写图片描述

4.前端实现

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

  module.js

//在js文件第一行加上'use strict'使该文件在严格模式下执行,就是对代码书写规范更严格,否则就会报错'use strict';//声明一个'app'模块,并设置该模块依赖ui.router、ui.bootstrap和ngFileUploadvar 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>

  完成后的录入界面如下图所示
这里写图片描述

  点击完提交按钮之后,打开mongod.exe,输入use moviesite(你定义的db名字) 进入我们定义的db,然后输入db.Movie.find() (Movie为你定义的表名) 查看数据,就可以看到刚插入的数据,如下图所示
这里写图片描述

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

3 0
原创粉丝点击