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

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

这里写图片描述

  接下来需要配置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

这里写图片描述

  看到启动端口号为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和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>

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

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

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

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值