NodeJS-6-x-蓝图-二-

70 阅读21分钟

NodeJS 6.x 蓝图(二)

原文:zh.annas-archive.org/md5/9B48011577F790A25E05CA5ABA4F9C8B

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:构建多媒体应用程序

在 Node.js 应用程序中最常讨论的话题之一无疑是文件的加载和存储,无论是文本、图像、音频还是视频。也有许多方法可以做到这一点;我们不会深入技术细节,但会简要概述两种最重要的方法。

一种是直接以二进制格式保存文件在数据库中,另一种方式是直接将文件保存在服务器上(服务器的硬盘),或者简单地将文件存储在云中。

在本章中,我们将看到一种非常实用的方式,可以直接将文件上传到硬盘,并将文件名记录在我们的数据库中作为参考。这样,如果需要,我们可以使用可扩展的云存储服务。

在本章中,我们将涵盖以下主题:

  • 如何将不同的文件上传到硬盘

  • 如何使用 Stream API 读写文件

  • 处理多部分表单上传

  • 如何配置 Multer 模块将文件存储在本地机器上

  • 如何获取文件类型并应用简单的文件验证

  • 如何使用动态用户 gravatar 生成器

我们正在构建什么?

我们将构建一个使用 MongoDB 和 Mongoose 进行用户身份验证的上传图像和视频的应用程序;然后我们可以看到这些图像将成为我们工作的最终结果。

在这个例子中,我们将使用另一种方式开始我们的项目;这次我们将从package.json文件开始。

以下截图展示了我们最终应用程序的样子:

图像屏幕视频屏幕
我们正在构建什么?我们正在构建什么?

从 package.json 开始

正如我们在前几章中所解释的,packages.json文件是应用程序的核心。创建必要文件的步骤如下:

  1. 创建一个名为chapter-03的文件夹。

  2. 创建一个名为package.json的新文件,并将其保存在chapter-03文件夹中,包含以下代码:

      { 
        "name": "chapter-03", 
        "description": "Build a multimedia Application with Node.js", 
        "license": "MIT", 
        "author": { 
          "name": "Fernando Monteiro", 
          "url": "https://github.com/newaeonweb/node-6-blueprints" 
       }, 
        "repository": { 
          "type": "git", 
          "url": "https://github.com/newaeonweb/node-6-blueprints.git" 
        }, 
        "keywords": [ 
          "MVC", 
          "Express Application", 
          "Expressjs", 
          "Expressjs images upload", 
          "Expressjs video upload" 
        ], 
        "version": "0.0.1", 
        "private": true, 
        "scripts": { 
          "start": "node app.js" 
        }, 
         "dependencies": { 
          "bcrypt-nodejs": "0.0.3", 
          "body-parser": "~1.13.2", 
          "connect-flash": "⁰.1.1", 
          "connect-mongo": "¹.1.0", 
          "cookie-parser": "~1.3.5", 
          "debug": "~2.2.0", 
          "ejs": "~2.3.3", 
           "express": "~4.13.1", 
          "express-session": "¹.13.0", 
          "gravatar": "¹.4.0", 
          "mongoose": "⁴.4.5", 
          "morgan": "~1.6.1", 
          "multer": "¹.1.0", 
          "node-sass-middleware": "0.8.0", 
          "passport": "⁰.3.2", 
          "passport-local": "¹.0.0", 
          "serve-favicon": "~2.3.0" 
        }, 
        "devDependencies": { 
          "nodemon": "¹.9.1" 
        } 
      } 

添加基线配置文件

现在让我们向项目添加一些有用的文件:

  1. 创建一个名为.editorconfig的文件,并将其保存在chapter-03文件夹中,包含以下代码:
      # http://editorconfig.org 
      root = true 

      [*] 
      indent_style = tab 
      indent_size = 4 
      charset = utf-8 
      trim_trailing_whitespace = true 
      insert_final_newline = true 

      [*.md] 
      trim_trailing_whitespace = false 

  1. 创建一个名为.gitignore的文件,保存在chapter-03中,并包含以下代码:
      # Logs 
      logs 
      *.log 

      # Runtime data 
      pids 
      *.pid 
      *.seed 

      # Directory for instrumented libs generated by jscoverage/JSCover 
      lib-cov 

      # Coverage directory used by tools like istanbul 
          coverage 

      # Grunt intermediate 
      storage (http://gruntjs.com/creating-plugins#storing-task-files) 
      .grunt 

      # node-waf configuration 
      .lock-wscript 

      # Compiled binary addons (http://nodejs.org/api/addons.html) 
      build/Release 

      # Dependency directory 
      # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-
      my-node_modules-folder-into-git- 
      node_modules 

      # Debug log from npm 
      npm-debug.log 

提示

请记住,我们正在使用 git 作为源代码控制。虽然这个文件不是运行应用程序所必需的,但我们强烈建议您使用源代码版本控制系统。

  1. 创建一个名为app.js的文件。

添加服务器文件夹

要完成应用程序的基本创建,我们现在将创建存储控件、模板和应用程序其他文件的目录:

  1. 创建一个名为public的文件夹,并在其中创建以下文件夹:
  • /images

  • /javascripts

  • /stylesheets

  • /uploads

  • /视频

  1. 创建一个名为server的文件夹,并在其中创建这些文件夹:
  • /config

  • /controllers

  • /models

  • /views

  1. 此时,我们的项目已经具有了所有基本目录和文件;让我们从package.json中安装 Node 模块。

  2. 在项目根目录打开您的终端/ shell,并输入以下命令:

npm install

在执行步骤 1、2 和 3 之后,项目文件夹将具有以下结构:

添加服务器文件夹

文件夹结构

让我们开始创建app.js文件内容。

配置 app.js 文件

我们将逐步创建app.js文件;它将与第一章中创建的应用程序有许多相似的部分。但是,在本章中,我们将使用不同的模块和不同的方式来创建应用程序控件。

Node.js中使用 Express 框架,有不同的方法来实现相同的目标:

  1. 打开项目根目录下的app.js文件,并添加以下模块:
      // Import basic modules 
      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'); 

      // import multer 
      var multer  = require('multer'); 
      var upload = multer({ dest:'./public/uploads/', limits: {fileSize:
       1000000, files:1} }); 

提示

请注意,我们正在使用 Multer 模块来处理multipart/form-data。您可以在github.com/expressjs/multer找到有关multer的更多信息。

  1. multer导入之后添加以下行:
       // Import home controller 
       var index = require('./server/controllers/index'); 
       // Import login controller 
       var auth = require('./server/controllers/auth'); 
       // Import comments controller 
       var comments = require('./server/controllers/comments'); 
       // Import videos controller 
       var videos = require('./server/controllers/videos'); 
       // Import images controller 
       var images = require('./server/controllers/images'); 

提示

不要担心这些控制文件。在本书的后面,我们会逐个看到它们。此时,我们将专注于创建app.js

  1. controller导入器之后添加以下行:
       // ODM with Mongoose 
       var mongoose = require('mongoose'); 
       // Modules to store session 
       var session = require('express-session'); 
       var MongoStore = require('connect-mongo')(session); 
       // Import Passport and Warning flash modules 
       var passport = require('passport'); 
       var flash = require('connect-flash'); 
       // start express application in variable app. 
       var app = express(); 

上述代码设置了带有消息系统的用户会话,还使用 Passport 进行身份验证。

  1. 现在让我们设置模板引擎和与应用程序数据库的连接。在express app变量之后添加以下代码:
      // view engine setup 
      app.set('views', path.join(__dirname, 'server/views/pages')); 
      app.set('view engine', 'ejs'); 
      // Database configuration 
      var config = require('./server/config/config.js'); 
      // connect to our database 
      mongoose.connect(config.url); 
       // Check if MongoDB is running 
     mongoose.connection.on('error', function() { 
         console.error('MongoDB Connection Error. Make sure MongoDB is
         running.'); 
     }); 
     // Passport configuration 
     require('./server/config/passport')(passport); 

以下行设置了一些默认中间件并初始化了Passport-local和用户会话。

  1. 在上一个代码块之后添加以下行:
      app.use(logger('dev')); 
      app.use(bodyParser.json()); 
      app.use(bodyParser.urlencoded({ extended: false })); 
      app.use(cookieParser()); 
      app.use(require('node-sass-middleware')({ 
      src: path.join(__dirname, 'public'), 
      dest: path.join(__dirname, 'public'), 
      indentedSyntax: true, 
      sourceMap: true 
      })); 
      // Setup public directory 
      app.use(express.static(path.join(__dirname, 'public'))); 

      // required for passport 
      // secret for session 
      app.use(session({ 
      secret: 'sometextgohere', 
      saveUninitialized: true, 
      resave: true, 
           //store session on MongoDB using express-session + connect mongo 
      store: new MongoStore({ 
      url: config.url, 
      collection : 'sessions' 
          }) 
      })); 

      // Init passport authentication 
     app.use(passport.initialize()); 
      // persistent login sessions 
      app.use(passport.session()); 
      // flash messages 
      app.use(flash()); 

现在让我们添加所有应用程序路由。我们本可以使用外部文件来存储所有路由,但是我们将其保留在此文件中,因为我们的应用程序中不会有太多路由。

  1. app.use(flash())之后添加以下代码:
      // Application Routes 
      // Index Route 
      app.get('/', index.show); 
      app.get('/login', auth.signin); 
      app.post('/login', passport.authenticate('local-login', { 
         //Success go to Profile Page / Fail go to login page 
      successRedirect : '/profile', 
      failureRedirect : '/login', 
      failureFlash : true 
      })); 
      app.get('/signup', auth.signup); 
      app.post('/signup', passport.authenticate('local-signup', { 
          //Success go to Profile Page / Fail go to Signup page 
      successRedirect : '/profile', 
      failureRedirect : '/signup', 
      failureFlash : true 
      })); 

      app.get('/profile', auth.isLoggedIn, auth.profile); 

      // Logout Page 
      app.get('/logout', function(req, res) { 
      req.logout(); 
      res.redirect('/'); 
       }); 

      // Setup routes for comments 
      app.get('/comments', comments.hasAuthorization, comments.list); 
      app.post('/comments', comments.hasAuthorization, comments.create); 

      // Setup routes for videos 
      app.get('/videos', videos.hasAuthorization, videos.show); 
      app.post('/videos', videos.hasAuthorization, upload.single('video'),
      videos.uploadVideo); 

      // Setup routes for images 
      app.post('/images', images.hasAuthorization, upload.single('image'),
      images.uploadImage); 
      app.get('/images-gallery', images.hasAuthorization, images.show); 

提示

在这里,您可以看到我们正在使用第一章中的示例应用程序相同的路由和函数,使用 MVC 设计模式构建类似 Twitter 的应用程序;我们保留了路由、身份验证和评论,并进行了一些小的更改。

要采取的最后一步是添加error函数并配置我们的应用程序将使用的服务器端口

  1. 在上一个代码块之后添加以下代码:
      // catch 404 and forward to error handler 
      app.use(function(req, res, next) { 
      var err = new Error('Not Found'); 
      err.status = 404; 
      next(err); 
       }); 

      // development error handler 
      // will print stacktrace 
      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 
              }); 
         }); 
      } 

      // production error handler 
      // no stacktraces leaked to user 
      app.use(function(err, req, res, next) { 
      res.status(err.status || 500); 
      res.render('error', { 
       message: err.message, 
      error: {} 
          }); 
      }); 

      module.exports = app; 

      app.set('port', process.env.PORT || 3000); 

      var server = app.listen(app.get('port'), function() { 
      console.log('Express server listening on port ' +
      server.address().port); 
      }); 

创建 config.js 文件

server/config/文件夹内创建一个名为config.js的文件,并添加以下代码:

// Database URL 
module.exports = { 
    'url' : 'mongodb://localhost/mvc-app-multimedia' 
}; 

server/config/文件夹内创建一个名为passport.js的文件。

在这一步中,我们将使用第一章中使用的Passport模块的相同配置文件,在 Node.js 中使用 MVC 设计模式构建类似 Twitter 的应用程序。您可以从www.packtpub.com或从 GitHub 的官方存储库下载示例代码。

创建控制器文件

现在让我们在server/controllers中创建控制器文件:

  1. 创建一个名为auth.js的文件,并添加以下代码:
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 
      var passport = require('passport'); 

      // Signin GET 
      exports.signin = function(req, res) { 
          // List all Users and sort by Date 
      res.render('login', { title: 'Login Page', message:
      req.flash('loginMessage') }); 
      }; 
      // Signup GET 
      exports.signup = function(req, res) { 
          // List all Users and sort by Date 
      res.render('signup', { title: 'Signup Page', message:
      req.flash('signupMessage') }); 

      }; 
      // Profile GET 
      exports.profile = function(req, res) { 
          // List all Users and sort by Date 
      res.render('profile', { title: 'Profile Page', user : req.user,
      avatar:gravatar.url(req.user.email ,  {s: '100', r: 'x', d: 'retro'},
      true) }); 
      }; 
      // Logout function 
      exports.logout = function () { 
      req.logout(); 
      res.redirect('/'); 
      }; 

      // check if user is logged in 
      exports.isLoggedIn = function(req, res, next) { 
      if (req.isAuthenticated()) 
      return next(); 
      res.redirect('/login'); 
      }; 

  1. 创建一个名为comments.js的文件,并添加以下代码:
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 
      // get Comments model 
      var Comments = require('../models/comments'); 

      // List Comments 
      exports.list = function(req, res) { 
         // List all comments and sort by Date 
      Comments.find().sort('-created').populate('user',
      'local.email').exec(function(error, comments) { 
       if (error) { 
       returnres.send(400, { 
       message: error 
                  }); 
              } 
              // Render result 
       res.render('comments', { 
       title: 'Comments Page', 
       comments: comments, 
       gravatar: gravatar.url(comments.email ,  {s: '80', r: 'x', d:
        'retro'}, true) 
               }); 
        }); 
      }; 
      // Create Comments 
      exports.create = function(req, res) { 
         // create a new instance of the Comments model with request body 
      var comments = new Comments(req.body); 
          // Set current user (id) 
      comments.user = req.user; 
          // save the data received 
      comments.save(function(error) { 
      if (error) { 
      returnres.send(400, { 
       message: error 
                  }); 
              } 
              // Redirect to comments 
      res.redirect('/comments'); 
           }); 
      }; 
      // Comments authorization middleware 
       exports.hasAuthorization = function(req, res, next) { 
         if (req.isAuthenticated()) 
         return next(); 
        res.redirect('/login'); 
      }; 

  1. 创建一个名为index.js的文件,并添加以下代码:
      // Show home screen 
      exports.show = function(req, res) { 
         // Render home screen 
       res.render('index', { 
               title: 'Multimedia Application', 
               callToAction: 'An easy way to upload and manipulate files
                 with Node.js' 
         }); 
      }; 

现在让我们创建控制器来处理图像和视频上传到我们的应用程序。我们将使用 Node.js API 流来读取和写入我们的文件。

提示

您可以在nodejs.org/api/stream.html找到此 API 的完整文档。

  1. 创建一个名为images.js的文件。

  2. 添加以下代码:

      // Import modules 
      var fs = require('fs'); 
      var mime = require('mime'); 
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 

      var Images = require('../models/images'); 
      // set image file types 
      var IMAGE_TYPES = ['image/jpeg','image/jpg', 'image/png']; 

  1. importer模块之后添加以下代码:
      // Show images gallery 
      exports.show = function (req, res) { 

      Images.find().sort('-created').populate('user',
      'local.email').exec(function(error, images) { 
      if (error) { 
        returnres.status(400).send({ 
          message: error 
         }); 
      } 
      // Render galley 
      res.render('images-gallery', {  
      title: 'Images Gallery', 
      images: images, 
      gravatar: gravatar.url(images.email ,  {s: '80', r: 'x', d: 'retro'},
      true) 
              }); 
          }); 
      }; 

现在让我们添加负责将文件保存到临时目录并将其传输到public文件夹中的目录的函数。

  1. 在上一个代码块之后添加以下行:
      // Image upload 
      exports.uploadImage = function(req, res) { 
      var src; 
      var dest; 
      var targetPath; 
      var targetName; 
      var tempPath = req.file.path; 
      console.log(req.file); 
          //get the mime type of the file 
      var type = mime.lookup(req.file.mimetype); 
         // get file extension 
      var extension = req.file.path.split(/[. ]+/).pop(); 
         // check support file types 
      if (IMAGE_TYPES.indexOf(type) == -1) { 
        returnres.status(415).send('Supported image formats: jpeg, jpg,
        jpe, png.'); 
       } 
          // Set new path to images 
      targetPath = './public/images/' + req.file.originalname; 
         // using read stream API to read file 
      src = fs.createReadStream(tempPath); 
         // using a write stream API to write file 
      dest = fs.createWriteStream(targetPath); 
      src.pipe(dest); 

          // Show error 
      src.on('error', function(err) { 
      if (err) { 
        returnres.status(500).send({ 
        message: error 
                  }); 
              } 
       }); 
          // Save file process 
      src.on('end', function() { 
             // create a new instance of the Images model with request body 
      var image = new Images(req.body); 
             // Set the image file name 
      image.imageName = req.file.originalname; 
             // Set current user (id) 
      image.user = req.user; 
             // save the data received 
      image.save(function(error) { 
      if (error) { 
        return res.status(400).send({ 
        message: error 
                      }); 
                  } 
              }); 
              // remove from temp folder 
      fs.unlink(tempPath, function(err) { 
      if (err) { 
          return res.status(500).send('Woh, something bad happened here'); 
                } 
                // Redirect to galley's page 
          res.redirect('images-gallery'); 

              }); 
          }); 
      }; 

添加检查用户是否经过身份验证并被授权插入图像的函数。

  1. 在文件末尾添加以下代码:
      // Images authorization middleware 
      exports.hasAuthorization = function(req, res, next) { 
      if (req.isAuthenticated()) 
      return next(); 
      res.redirect('/login'); 
      }; 

  1. 现在让我们重复这个过程来控制videos.js,然后创建一个名为videos.js的文件,并添加以下代码:
      // Import modules 
      var fs = require('fs'); 
      var mime = require('mime'); 
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 

      // get video model 
      var Videos = require('../models/videos'); 
      // set image file types 
      var VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg',
       'video/ogv']; 

      // List Videos 
      exports.show = function(req, res) { 

      Videos.find().sort('-created').populate('user',
      'local.email').exec(function(error, videos) { 
      if (error) { 
        return res.status(400).send({ 
        message: error 
                  }); 
              } 
              // Render result 
      console.log(videos); 
      res.render('videos', { 
      title: 'Videos Page', 
      videos: videos, 
      gravatar: gravatar.url(videos.email ,  {s: '80', r: 'x', d: 'retro'},
      true) 
              }); 
          }); 
      }; 
      // Create Videos 
      exports.uploadVideo = function(req, res) { 
      var src; 
      var dest; 
      var targetPath; 
      var targetName; 
      console.log(req); 
      var tempPath = req.file.path; 
          //get the mime type of the file 
      var type = mime.lookup(req.file.mimetype); 
         // get file extenstion 
      var extension = req.file.path.split(/[. ]+/).pop(); 
        // check support file types 
      if (VIDEO_TYPES.indexOf(type) == -1) { 
      return res.status(415).send('Supported video formats: mp4, webm, ogg,
      ogv'); 
          } 
          // Set new path to images 
      targetPath = './public/videos/' + req.file.originalname; 
         // using read stream API to read file 
      src = fs.createReadStream(tempPath); 
        // using a write stream API to write file 
      dest = fs.createWriteStream(targetPath); 
      src.pipe(dest); 

          // Show error 
      src.on('error', function(error) { 
      if (error) { 
        return res.status(500).send({ 
        message: error 
                  }); 
              } 
        }); 

          // Save file process 
      src.on('end', function() { 
              // create a new instance of the Video model with request body 
      var video = new Videos(req.body); 
              // Set the video file name 
      video.videoName = req.file.originalname; 
             // Set current user (id) 
      video.user = req.user; 
             // save the data received 
      video.save(function(error) { 
      if (error) { 
        return res.status(400).send({ 
        message: error 
                      }); 
                 } 
              }); 
              // remove from temp folder 
      fs.unlink(tempPath, function(err) { 
      if (err) { 
        return res.status(500).send({ 
        message: error 
                      }); 
                  } 
                 // Redirect to galley's page 
      res.redirect('videos'); 

              }); 
          }); 
      }; 
      // Videos authorization middleware 
      exports.hasAuthorization = function(req, res, next) { 
      if (req.isAuthenticated()) 
         return next(); 
         res.redirect('/login'); 
      };

如您所见,我们使用与图像控制器相同的概念来创建视频控制器。

由于流,Node.js API 可以使用createReadStream()createWriteStream()函数处理任何类型的文件。

创建模型文件

现在让我们创建应用程序的模板文件。由于我们在第一章中使用了 mongoose 中间件,在 Node.js 中使用 MVC 设计模式构建类似 Twitter 的应用程序,我们将保持相同类型的配置:

  1. server/models文件夹内创建一个名为comments.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var commentSchema = mongoose.Schema({ 
      created: { 
       type: Date, 
      default: Date.now 
          }, 
      title: { 
      type: String, 
      default: '', 
      trim: true, 
      required: 'Title cannot be blank' 
          }, 
      content: { 
      type: String, 
      default: '', 
      trim: true 
          }, 
      user: { 
      type: Schema.ObjectId, 
      ref: 'User' 
          } 
      }); 

      module.exports = mongoose.model('Comments', commentSchema); 

  1. server/models文件夹内创建一个名为user.js的文件,并添加以下代码:
      // Import Mongoose and password Encrypt 
      var mongoose = require('mongoose'); 
      var bcrypt   = require('bcrypt-nodejs'); 

      // define the schema for User model 
      var userSchema = mongoose.Schema({ 
          // Using local for Local Strategy Passport 
      local: { 
      name: String, 
      email: String, 
      password: String, 
          } 

      }); 

      // Encrypt Password 
      userSchema.methods.generateHash = function(password) { return
      bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 
      }; 

      // Verify if password is valid 
      userSchema.methods.validPassword = function(password) { return
      bcrypt.compareSync(password, this.local.password); 
      }; 

      // create the model for users and expose it to our app 
      module.exports = mongoose.model('User', userSchema); 

  1. 然后,在server/models文件夹内创建一个名为images.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var imagesSchema = mongoose.Schema({ 
      created: { 
      type: Date, 
      default: Date.now 
          }, 
      title: { 
      type: String, 
      default: '', 
      trim: true, 
      required: 'Title cannot be blank' 
          }, 
      imageName: { 
      type: String 
          }, 
      user: { 
      type: Schema.ObjectId, 
      ref: 'User' 
          } 
      }); 

      module.exports = mongoose.model('Images', imagesSchema); 

  1. 接下来,在server/models文件夹内创建一个名为videos.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var videosSchema = mongoose.Schema({ 
        created: { 
          type: Date, 
          default: Date.now 
        }, 
        title: { 
          type: String, 
          default: '', 
          trim: true, 
          required: 'Title cannot be blank' 
        }, 
        videoName: { 
         type: String 
        }, 
        user: { 
          type: Schema.ObjectId, 
          ref: 'User' 
        } 
      }); 

      module.exports = mongoose.model('Videos', videosSchema); 

创建视图文件

在本节中,我们将使用与第一章中相同的view文件来处理以下文件:

  • views/partials/javascripts.ejs

  • views/partials/stylesheets.ejs

  • views/pages/login.ejs

  • views/pages/signup.ejs

  • views/pages/profile.ejs

  • views/pages/index.ejs

  • views/pages/comments.js

  • views/pages/error.ejs

如前所述,您可以从 Packt 网站或本书的官方 GitHub 存储库下载这些文件。

除了这些文件之外,我们将为照片和视频页面创建views文件,并将这些路由添加到应用程序菜单中:

  1. views/partials文件夹内创建一个名为footer.ejs的文件,并添加以下代码:
      <footer class="footer"> 
      <div class="container"> 
       <span>&copy 2016\. Node-Express-MVC-Multimedia-App</span> 
      </div> 
      </footer> 

  1. 然后在views/partials文件夹内创建一个名为header.ejs的文件,并添加以下代码:
      <!-- Fixed navbar --> 
      <div class="pos-f-t"> 
        <div class="collapse" id="navbar-header"> 
          <div class="container bg-inverse p-a-1"> 
            <h3>Collapsed content</h3> 
            <p>Toggleable via the navbar brand.</p> 
          </div> 
        </div> 
        <nav class="navbarnavbar-light navbar-static-top"> 
        <div class="container"> 
          <button class="navbar-toggler hidden-sm-up" type="button"
          data-toggle="collapse" data-target="#exCollapsingNavbar2"> 
          Menu 
          </button> 
          <div class="collapse navbar-toggleable-xs"
            id="exCollapsingNavbar2"> 
            <a class="navbar-brand" href="/">MVC Multimedia App</a> 
            <ul class="navnavbar-navnavbar-right"> 
              <li class="nav-item"> 
                <a class="nav-link" href="/login">Sign in</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/signup">Sign up</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/profile">Profile</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/comments">Comments</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/videos">Videos</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/images-gallery">Photos</a> 
              </li> 
            </ul> 
          </div> 
        </div> 
        </nav> 
      </div> 
      <!-- Fixed navbar --> 

  1. views/pages文件夹内创建一个名为images-gallery.ejs的文件,并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
        <title><%= title %></title> 
        <% include ../partials/stylesheet %> 
      </head> 
      <body> 
        <% include ../partials/header %> 
        <div class="container"> 
          <div class="row"> 
            <div class="col-lg-6"> 
              <h4 class="text-muted">Images</h4> 
            </div> 
          <div class="col-lg-6"> 
            <button type="button" class="btnbtn-secondary pull-right"
             data-toggle="modal" data-target="#createImage"> 
                    Image Upload 
             </button> 
           </div> 
         </div> 
         <!-- Modal --> 
         <div class="modal fade" id="createImage" tabindex="-1"
          role="dialog" aria-labelledby="myModalLabel"
          aria-hidden="true"> 
           <div class="modal-dialog" role="document"> 
             <div class="modal-content"> 
               <form action="/images" method="post"
                 enctype="multipart/formdata"> 
               <div class="modal-header"> 
                 <button type="button" class="close" data-dismiss="modal"
                  aria-label="Close"> 
                   <span aria-hidden="true">&times;</span> 
                 </button> 
                 <h4 class="modal-title" id="myModalLabel">
                   Upload a imagefile
                 </h4>
               </div> 

               <div class="modal-body"> 
                 <fieldset class="form-group"> 
                 <label  for="itle">Title</label> 
                   <input type="text" id="itle" name="title" class="form-
                    control" placeholder="Image Title" required=""> 
                 </fieldset> 
                 <label class="file" style="width: 100%"> 
                   <input type="file" id="image" name="image"> 
                   <span class="file-custom"></span> 
                 </label> 
               </div> 
               <div class="modal-footer"> 
                 <button type="button" class="btnbtnsecondary" data
                  dismiss="modal">Close
                  </button> 
                 <button type="submit" class="btnbtn-primary">
                    Savechanges
                 </button> 
               </div> 
               </form> 
             </div> 
           </div> 
         </div> 
         <hr> 
         <div class="row"> 
           <% images.forEach(function(images){ %> 
           <div class="col-lg-4"> 
             <figure class="figure"> 
              <img src="img/<%= images.imageName %>" class="figure-img
               img-fluid img-rounded" alt="<%= images.imageName %>"> 
              <figcaption class="figure-caption"><%= images.title%>
              </figcaption> 
              <small>Upload by: <%= images.user.local.email %></small> 
             </figure> 
           </div> 
           <% }); %> 
         </div> 
       </div> 
       <% include ../partials/footer %> 
       <% include ../partials/javascript %> 
      </body> 
      </html> 

上述代码设置了一个表单 HTML 标签,使用enctype="multipart/form-data类型,并创建一个循环来显示添加到图库中的所有图像。

  1. views/pages文件夹内创建一个名为videos.ejs的文件,并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
        <title><%= title %></title> 
        <% include ../partials/stylesheet %> 
      </head> 
      <body> 
      <% include ../partials/header %> 
      <div class="container"> 
      <div class="row"> 
      <div class="col-lg-6"> 
      <h4 class="text-muted">Videos</h4> 
      </div> 
      <div class="col-lg-6"> 
      <button type="button" class="btn btn-secondary pull-right"
       data-toggle="modal" data-target="#createVideo"> 
                    Video Upload 
       </button> 
      </div> 
      </div> 
      <!-- Modal --> 
      <div class="modal fade" id="createVideo" tabindex="-1" role="dialog"
       aria-labelledby="myModalLabel" aria-hidden="true"> 
      <div class="modal-dialog" role="document"> 
      <div class="modal-content">  
      <form action="/videos" method="post" enctype="multipart/form-data"> 
      <div class="modal-header"> 
      <button type="button" class="close" data-dismiss="modal"
       aria-label="Close"> 
      <span aria-hidden="true">&times;</span> 
      </button> 
      <h4 class="modal-title" id="myModalLabel">Upload a video file</h4> 
       </div> 

      <div class="modal-body"> 
      <fieldset class="form-group"> 
      <label  for="inputitle">Title</label> 
      <input type="text" id="inputitle" name="title" class="form-control"
       placeholder="Video Title" required=""> 
      </fieldset> 
       <label class="file" style="width: 100%"
         onclick="$('input[id=lefile]').click();"> 
       <input type="file" id="video" name="video"> 
       <span class="file-custom"></span> 
       </label> 
       </div> 
      <div class="modal-footer"> 
      <button type="button" class="btnbtn-secondary"
       data-dismiss="modal">Close</button> 
      <button type="submit" class="btnbtn-primary">Save changes</button> 
      </div> 
      </form> 
      </div> 
      </div> 
      </div> 
      <hr> 
      <div class="row"> 
      <% videos.forEach(function(videos){ %> 
      <div class="col-lg-4"> 
      <h4 class="list-group-item-heading"><%= videos.title %></h4> 
      <video width="320" height="240" controls preload="auto"
      codecs="avc1.42E01E, mp4a.40.2"> 
      <source src="img/<%= videos.videoName %>" type="video/mp4" /> 
      </video> 
      <small>Upload by: <%= videos.user.local.email %></small> 
       </div> 
      <% }); %> 
      </div> 
      </div> 
      <% include ../partials/footer %> 
      <% include ../partials/javascript %> 
      </body> 
      </html> 

创建 public 文件夹内容

在这个阶段,我们已经完成了在server目录中创建文件夹和文件的所有必要步骤,如控制器、模型和视图。

现在我们需要复制在第一章中创建的public文件夹中的内容:

  1. 复制以下文件夹及其内容,并将它们粘贴到chapter-03根项目文件夹中:
  • public/images

  • public/javascripts

  • bootstrap.min.js

  • jquery.min.js

  • public/stylesheets

  • bootstrap.min.css

  • style.css

  • style.css.map

  • style.sass

  1. public文件夹内创建一个名为uploads的文件夹。

  2. 然后,在public文件夹内创建一个名为videos的文件夹。

使用上传表单在应用程序中插入图像

现在是测试应用程序的时候了,需要注意的是,为此您应该启动您的 MongoDB。否则,应用程序在连接时会返回失败错误:

  1. 在项目根目录打开您的终端/Shell,并输入以下命令:
npm start

  1. 转到http://localhost:3000/signup并输入以下数据:
  1. 转到http://localhost:3000/images-gallery,点击图像上传按钮,填写表单标题并选择图像(请注意,我们设置了图像大小限制为1MB,仅用于示例目的)。您将看到一个模型表单,如下截图所示:使用上传表单在应用程序中插入图像

图像上传表单

  1. 选择图像后,点击保存更改按钮,完成!您将在http://localhost:3000/images-gallery页面看到以下截图:使用上传表单在应用程序中插入图像

图库图像屏幕

使用上传表单在应用程序中插入视频文件

与插入图像到我们的应用程序一样,让我们按照相同的步骤来插入视频:

  1. 转到http://localhost:3000/videos,点击视频上传按钮,填写表单标题并选择视频文件(请注意,我们再次将图像大小限制设置为 1MB,视频格式设置为MP4, WEBM,仅用于示例目的)。您将看到一个模型表单,如下截图所示:使用上传表单在应用程序中插入视频文件

视频上传表单

关于图像和视频上传的重要说明

Node.js 为我们提供了一个完整的处理文件的 API(流 API),如图像、视频、pdf 和其他格式。还有几种将文件上传到服务器或存储在云中的方法,正如前面已经提到的。此外,Node.js 生态系统为我们提供了许多模块,用于处理不同类型的文件,并使用带有enctype="multipart/form-data"的表单。

在本章中,我们使用了Multer模块。Multer 是一个完整的中间件,用于处理各种上传和文件存储的方法。

在这个例子中,我们只在 MongoDB 中存储了文件名,并直接将文件发送到服务器文件夹。还有另一种上传方式,我们将文件以二进制格式发送到数据库,尽管重要的是要记住 MongoDB 存储BSON文件的容量为16MB。

如果您选择将文件存储在 MongoDB 数据库中,您可以使用 MongoDB 的 GridFS 功能和 Node.js 模块,如GridFS-stream,作为上传中间件,就像我们使用 Multer 一样。

在本章的示例中,我们将上传大小限制为 1 MB,在下面突出显示的行中可以看到:

      var upload = multer({ 
         dest:'./public/uploads/', 
         limits: {fileSize: 1000000, files:1} 
      }); 

提示

您可以在 Multer 的官方文档中找到有关限制的更多信息,网址为github.com/expressjs/multer#limits

总结

在本章中,我们构建了一个完整的 Node MVC 应用程序,用于上传图像和视频文件;我们还设置了一个用户会话,使用电子邮件和密码进行身份验证,并将加密密码存储在 MongoDB 上。

此外,本章使您能够构建模块化、健壮和可扩展的 MVC 应用程序。

此外,我们还使用了在 Web 应用程序中非常常见的功能,如访问数据库、登录和注册以及文件上传。

在下一章中,我们将看到如何使用云服务操纵和上传图像,并将元数据存储在 MongoDB 中。

第四章:不要拍照,制作它-摄影师的应用程序

在本章中,我们将讨论 Node.js 社区在全球范围内广泛讨论的一个话题-使用 Node.js 架构和云进行图像处理。

正如我们在上一章中看到的,我们有两种方式来存储图像和文件,一种是在服务器上使用硬盘,另一种是直接存储到云中。在第三章中,构建多媒体应用程序,我们使用了直接上传到服务器的方法,将图像存储在硬盘上。

在本章中,我们将使用云中的服务器来存储和处理我们相册应用程序中的图像。

我们将使用一个名为 Cloudinary 的服务来存储和处理图像。

在本章中,我们将涵盖以下主题:

  • 如何使用 generator-express 设置 MVC 应用程序

  • 如何安装 cloudinary npm 模块

  • 实现 Materialize.css 框架

  • 如何上传图像到云并在 MongoDB 上保存元数据

  • 如何使用点文件中的全局变量

  • 设置 Cloudinary 帐户并创建文件夹

  • 如何使用 Cloudinary API 上传图像

  • 如何使用 Cloudinary API 中的 URL 参数呈现模板

我们正在构建的内容

在本章结束时,我们将创建以下示例应用程序,一个强大而可扩展的相册:

我们正在构建的内容

相册应用程序的主屏幕

创建基线应用程序

在本章中,我们将使用稍微修改过的express-generator的版本,这是我们在之前章节中使用的。

这个生成器叫做generator-express;它在express generator的基础上进行了大量的改进。

以下是我们安装的步骤:

  1. 打开终端/ shell 并输入:
 npm install -g generator-express 

  1. 创建一个名为chapter04的文件夹。

  2. chapter04文件夹中打开您的终端/ shell,并输入以下命令:

 yo express 

现在,按照以下顺序填写问题:

  • 选择N,我们已经在步骤 2中创建了项目文件夹

  • 选择MVC作为应用程序类型

  • 选择Swig作为模板引擎

  • 选择None作为 CSS 预处理器

  • 选择None作为数据库(在本章后面,我们将手动设置数据库)

  • 选择Gulp进行 LiveReload 和其他内容

提示

不要担心Gulp,如果你从未听说过它。在本书的后面,我们将看到并解释一些构建工具。

在生成器的最后,我们有以下目录结构:

创建基线应用程序

应用程序文件夹结构

更改应用程序结构

与我们在第一章中使用的示例不同,使用 MVC 设计模式构建类似 Twitter 的应用程序,我们不会对当前的结构进行重大更改;我们只会更改views文件夹。

作为示例应用程序,将有一本图片书;我们将在views文件夹中添加一个名为 book 的文件夹:

  1. app/views文件夹中创建一个名为book的文件夹。

  2. 现在我们将为 Cloudinary 服务创建一个配置文件。在本章后面,我们将讨论有关 Cloudinary 的所有细节;现在,只需创建一个新文件。

  3. 在根文件夹中创建一个名为.env的文件。

现在,我们有必要的基础来继续前进。

添加处理图像和 Cloudinary 云服务的 Node 模块

现在我们将在package.json文件中为我们的应用程序添加必要的模块。

  1. 将以下突出显示的代码行添加到package.json文件中:
      { 
         "name": "chapter-04", 
         "description": "Don't take a photograph, make it - An app for
           photographers", 
         "license": "MIT", 
         "author": { 
           "name": "Fernando Monteiro", 
           "url": "https://github.com/newaeonweb/node-6-blueprints" 
         }, 
         "repository": { 
           "type": "git", 
           "url": "https://github.com/newaeonweb/node-6-blueprints.git" 
         }, 
         "keywords": [ 
           "MVC", 
           "Express Application", 
           "Expressjs", 
           "Expressjs cloud upload", 
           "Expressjs cloudinary upload" 
         ], 
         "version": "0.0.1", 
         "private": true, 
        "scripts": { 
          "start": "gulp" 
        }, 
        "dependencies": { 
          "body-parser": "¹.13.3", 
          "cloudinary": "¹.3.1", 
          "compression": "¹.5.2", 
          "connect-multiparty": "².0.0", 
          "cookie-parser": "¹.3.3", 
          "dotenv": "².0.0", 
          "express": "⁴.13.3", 
          "glob": "⁶.0.4", 
          "jugglingdb": "².0.0-rc3", 
          "jugglingdb-mongodb": "⁰.1.1", 
          "method-override": "².3.0", 
          "morgan": "¹.6.1", 
          "serve-favicon": "².3.0", 
          "swig": "¹.4.2" 
        }, 
        "devDependencies": { 
          "gulp": "³.9.0", 
          "gulp-nodemon": "².0.2", 
          "gulp-livereload": "³.8.0", 
          "gulp-plumber": "¹.0.0" 
        } 
      } 

只需几个模块,我们就可以构建一个非常强大和可扩展的应用程序。让我们描述每一个:

模块名称描述更多信息
cloudinary用于存储和管道图像和视频文件的云服务www.npmjs.com/package/cloudinary
connect-multiparty用于接受多部分表单上传的中间件www.npmjs.com/package/connect-multiparty
dotenv加载环境变量www.npmjs.com/package/dotenv
jugglingdb跨数据库 ORMwww.npmjs.com/package/jugglingdb
jugglingdb-mongodbMongoDB 连接器www.npmjs.com/package/jugglingdb-mongodb

创建书籍控制器

我们将遵循生成器建议的相同生成器代码模式;使用此生成器的优势之一是我们已经可以使用 MVC 模式。

提示

请记住,您可以从 Packpub 网站或直接从 GitHub 书库下载示例文件。

  1. controllers文件夹中创建一个名为books.js的文件。

  2. 将以下代码添加到book.js文件中:

      var express = require('express'), 
          router = express.Router(), 
          schema = require('../models/book'), 
          Picture = schema.models.Picture, 
          cloudinary = require('cloudinary').v2, 
          fs = require('fs'),     
          multipart = require('connect-multiparty'), 
          multipartMiddleware = multipart(); 

      module.exports = function (app) { 
          app.use('/', router); 
      }; 
      // Get pictures list 
      router.get('/books', function (req, res, next) { 
          Picture.all().then(function (photos) { 
              console.log(photos); 
          res.render('book/books', { 
              title: 'PhotoBook', 
              photos: photos, 
              cloudinary: cloudinary 
          }) 
          }); 
       }); 
       // Get form upload 
       router.get('/books/add', function (req, res, next) { 
           res.render('book/add-photo', { 
           title: 'Upload Picture' 
        }); 
      }); 
      // Post to 
      router.post('/books/add', multipartMiddleware, function (req, res,
       next)
      {
          // Checking the file received 
          console.log(req.files); 
          // create a new instance using Picture Model 
          var photo = new Picture(req.body); 
          // Get temp file path 
          var imageFile = req.files.image.path; 
          // Upload file to Cloudinary 
          cloudinary.uploader.upload(imageFile, { 
              tags: 'photobook', 
              folder: req.body.category + '/', 
              public_id: req.files.image.originalFilename 
              // eager: { 
              //   width: 280, height: 200, crop: "fill", gravity: "face" 
             // } 
          }) 
            .then(function (image) { 
              console.log('Picture uploaded to Cloudinary'); 
              // Check the image Json file 
              console.dir(image); 
              // Added image informations to picture model 
              photo.image = image; 
              // Save photo with image metadata 
              return photo.save(); 
            }) 
            .then(function (photo) { 
                console.log('Successfully saved'); 
                // Remove image from local folder 
                var filePath = req.files.image.path; 
                fs.unlinkSync(filePath); 
            }) 
            .finally(function () { 
                // Show the result with image file 
                res.render('book/posted-photo', { 
                    title: 'Upload with Success', 
                    photo: photo, 
                    upload: photo.image 
                }); 
            }); 
      }); 

让我们解释一些关于前面代码示例的重要点:

  • 为了在视图中使用 Cloudinary API,我们需要将cloudinary变量传递给我们的视图:
      res.render('book/books', { 
                title: 'PhotoBook', 
                photos: photos, 
                cloudinary: cloudinary 
      }) 

  • 在使用multipartMiddleware时,为了最佳实践,我们需要清理上传到云中的每个文件:

      .then(function (photo) { 
            console.log('Successfully saved'); 
            // Remove image from local folder 
            var filePath = req.files.image.path; 
            fs.unlinkSync(filePath); 
      }) 

稍后我们将讨论更多关于 Cloudinary API 的内容。

提示

请注意,当您使用多部分连接时,默认情况下会将图像加载到硬盘上的文件夹中,因此您应该始终删除应用程序中加载的所有文件。

创建书籍模型文件

为此应用程序创建模型的过程与我们在前几章中看到的非常相似;几乎每个模块的ORM/ODM都有非常相似的操作。

让我们看看如何为书籍对象创建模型:

  1. app/models文件夹中创建一个名为book.js的文件,并放入以下代码:
      var Schema = require('jugglingdb').Schema; 
      // Pay attention, we are using MongoDB for this example. 
      var schema = new Schema('mongodb', {url: 'mongodb://localhost
      /photobookapp'}); 

      // Setup Books Schema 
      var Picture = schema.define('Picture', { 
        title : { type: String, length: 255 }, 
        description: {type: Schema.Text}, 
        category: {type: String, length: 255 }, 
        image : { type: JSON} 
      }); 

      module.exports = schema; 

提示

请注意,我们使用 MongoDB 来存储书籍模型。还记得在启动应用程序之前必须使本地 MongoDB 运行起来。

向应用程序添加 CSS 框架

在本书的所有章节中,我们将始终使用最新的技术,就像在前几章中使用新的 Bootstrap(Alpha Release)一样。

特别是在本章中,我们将使用一种称为Material Design的设计模式。

提示

您可以在www.google.com/design/spec/material-design/introduction.html上阅读更多关于设计材料的信息。

为此,我们将使用一个名为Materialize.css的简单CSS框架。

提示

您可以在此链接找到有关 Materialize 的更多信息:materializecss.com/

  1. 用以下代码替换app/views/layout.swig文件中的所有内容:
      <!doctype html> 
      <html lang="en"> 
      <head> 
         <meta charset="UTF-8"> 
         <meta name="viewport" content="width=device-width"> 
         <title>{{ title }}</title> 
         <!--Let browser know website is optimized for mobile--> 
         <meta name="viewport" content="width=device-width, initial-
         scale=1.0"/>
         <!-- Import Google Material font and icons -->
         <link href="https://fonts.googleapis.com/icon?family=
         Material+Icons" rel="stylesheet"> 
          <!-- Compiled and minified CSS --> 
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/
          ajax/libs/materialize/0.97.6/css/materialize.min.css"> 
         <link rel="stylesheet" href="/css/style.css"> 
     </head> 
     <body> 
          <nav class="orange darken-4" role="navigation"> 
              <div class="nav-wrapper container"><a id="logo-container"
              href="/" class="brand-logo">Logo</a> 
                  <ul class="right hide-on-med-and-down"> 
                      <li><a href="/books">Books</a></li> 
                      <li><a href="/books/add">Add Picture</a></li> 
                  </ul> 

                  <ul id="nav-mobile" class="side-nav" style="transform:
                   translateX(-100%);"> 
                      <li><a href="/books">Books</a></li> 
                      <li><a href="/books/add">Add Picture</a></li> 
                  </ul> 
                  <a href="#" data-activates="nav-mobile" class="button-
                  collapse">
                  <i class="material-icons">menu</i></a> 
              </div> 
          </nav> 
          {% block content %}{% endblock %} 
          <!-- Footer --> 
          <footer class="page-footer orange darken-4"> 
              <div class="container"> 
                  <div class="row"> 
                     <div class="col l6 s12"> 
                          <h5 class="white-text">Some Text Example</h5> 
                          <p class="grey-text text-lighten-4">Lorem ipsum
                          dolor sit amet, consectetur adipiscing elit,
                          sed do eiusmod tempor incididunt ut labore et
                          dolore magnaaliqua. Ut enim ad minim veniam, quis
                          nostrud exercitation ullamco laboris nisi ut 
                          aliquip ex ea commodo consequat. Duis aute irure 
                          dolor in reprehenderit in voluptate velit esse 
                          cillum dolore eu fugiat nulla pariatur.</p> 
                      </div> 
                      <div class="col l3 s12"> 
                          <h5 class="white-text">Sample Links</h5> 
                          <ul> 
                            <li><a class="white-text" href="#!">Link 1
                              </a></li> 
                            <li><a class="white-text" href="#!">Link 2
                               </a></li> 
                            <li><a class="white-text" href="#!">Link 3
                               </a></li>
                            <li><a class="white-text" href="#!">Link 4
                               </a></li> 
                          </ul> 
                      </div> 
                    <div class="col l3 s12"> 
                        <h5 class="white-text">Sample Links</h5> 
                         <ul> 
                            <li><a class="white-text" href="#!">Link 1
                              </a></li> 
                            <li><a class="white-text" href="#!">Link 2
                              </a></li> 
                            <li><a class="white-text" href="#!">Link 3
                              </a></li>
                            <li><a class="white-text" href="#!">Link 4
                              </a></li>
                          </ul> 
                    </div> 
                </div> 
            </div> 
              <div class="footer-copyright"> 
                <div class="container"> 
                  MVC Express App for: <a class="white-text text-darken-2"
                  href="#">Node.js 6 Blueprints Book</a> 
                </div> 
              </div> 
          </footer> 
          <!-- Place scripts at the bottom page--> 
          {% if ENV_DEVELOPMENT %} 
          <script src="img/livereload.js"></script> 
          {% endif %} 

          <!--Import jQuery before materialize.js--> 
          <script type="text/javascript"
            src="img/jquery-2.1.1.min.js"></script> 
          <!-- Compiled and minified JavaScript --> 
          <script src="https://cdnjs.cloudflare.com/
          ajax/libs/materialize/0.97.6/js/materialize.min.js"></script> 
          <!-- Init Rsponsive Sidenav Menu  --> 
          <script> 
          (function($){ 
              $(function(){ 
                $('.button-collapse').sideNav(); 
                $('.materialboxed').materialbox(); 
              }); 
          })(jQuery); 
          </script> 
      </body> 
      </html> 

提示

为了避免 CSS 冲突,请清理您的public/css/style.css文件。

重构视图文件夹

现在我们将对app/views文件夹进行一些小改动并添加一些文件:

  1. 首先,让我们编辑app/views/index.js。用以下代码替换原始代码:
      {% extends 'layout.swig' %} 

      {% block content %} 

      <div class="section no-pad-bot" id="index-banner"> 
        <div class="container"> 
              <br><br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center">
                <h5 class="header col s12 light">Welcome to 
                    {{ title }}
                </h5> 
              </div> 
              <div class="row center"> 
                  <a href="books/add" id="download-button" class="btn-large
                   waves-effect waves-light orange">Create Your Photo
                   Book
                  </a> 
              </div> 
              <br><br> 
          </div> 
      </div> 
      <div class="container"> 
          <div class="section"> 

              <!--   Icon Section   --> 
              <div class="row"> 
                <div class="col s12 m4"> 
                  <div class="icon-block"> 
                    <h5 class="center">Animals</h5> 
                    <img src="img/animals"/> 
                    <p class="light">Lorem ipsum dolor sit amet,
                      consectetur adipiscing elit, sed do eiusmod
                      tempor incididunt ut laboreet dolore magna-
                      aliqua.</p> 
                  </div> 
                </div> 

                  <div class="col s12 m4"> 
                    <div class="icon-block"> 
                      <h5 class="center">Cities</h5> 
                      <img src="img/city"/> 
                      <p class="light">Lorem ipsum dolor sit amet,
                        consectetur adipiscing elit, sed do eiusmod
                        tempor incididunt ut laboreet dolore magna-
                        aliqua.</p> 
                    </div> 
                  </div> 

                  <div class="col s12 m4"> 
                    <div class="icon-block"> 
                      <h5 class="center">Nature</h5> 
                      <img src="img/nature"/> 
                      <p class="light">Lorem ipsum dolor sit amet,
                        consectetur adipiscing elit, sed do eiusmod
                         tempor incididunt ut laboreet dolore magna-
                          aliqua..</p> 
                    </div> 
                  </div> 
              </div> 

          </div> 
          <br><br> 

          <div class="section"> 

          </div> 
      </div> 

      {% endblock %} 

  1. 创建一个名为add-photo.swig的文件,并添加以下代码:
      {% extends '../layout.swig' %} 

      {% block content %} 
      <div class="section no-pad-bot" id="index-banner"> 
          <div class="container"> 
              <br>
              <br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center"> 
                  <h5 class="header col s12 light">Welcome to 
                    {{ title }}</h5> 
              </div> 

              <div class="photo"> 
                <h2>{{ photo.title }}</h2> 
                {% if photo.image.url %} 
                <img src="img/{{ photo.image.url }}" height='200' width='100%'>
                </img> 
                <a href="{{ photo.image.url }}" target="_blank"> 
                  {{ cloudinary.image(photo.image.public_id, {width: 150,
                   height: 150, quality:80,crop:'fill',format:'png',
                   class:'thumbnail inline'})  }} 
                </a> 
                {% endif %} 
              </div> 

              <div class="card"> 
                  <div class="card-content orange-text"> 
                   <form action="/books/add" enctype="multipart/form-data"
                     method="POST"> 
                          <div class="input-field"> 
                             <input id="title" name="title" type="text"
                               value="{{ photo.title }}" class="validate"> 
                              <label for="title">Image Title</label> 
                          </div> 
                          <div class="file-field input-field"> 
                              <div class="btn orange"> 
                                  <span>Choose File</span> 
                                  <input type="file" name="image"> 
                                <input id="photo_image_cache"
                                 name="image_cache" type="hidden" /> 
                              </div> 
                              <div class="file-path-wrapper"> 
                               <input class="file-path validate"
                                 type="text"> 
                              </div> 
                          </div> 
                          <div class="input-field col s12"> 
                              <select class="browser-default" id="category"
                               name="category"> 
                                <option value="" disabled selected>Choose a
                                  category</option> 
                                <option value="animals">Animals</option> 
                                <option value="cities">Cities</option> 
                               <option value="nature">Nature</option> 
                               </select> 
                          </div> 
                          <div class="input-field"> 
                              <input id="description" name="description"
                               type="text" value="{{ photo.description }}"
                                class="validate"> 
                              <label for="description">Image Text
                                 Description</label> 
                          </div> 
                          <br> 
                          <br> 
                          <div class="row center"> 
                              <button class="btn orange waves-effect waves
                               light" type="submit" name="action">
                                  Submit
                               </button> 
                          </div> 
                      </form> 
                  </div> 
              </div> 
              <br> 
              <br> 
              <br> 
          </div> 
      </div> 

      {% endblock %} 

  1. 然后创建一个名为books.swig的文件,并添加以下代码:
      {% extends '../layout.swig' %} 

      {% block content %} 
      <div class="section no-pad-bot" id="index-banner"> 
          <div class="container"> 
              <br><br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center"> 
               < h5 class="header col s12 light">Welcome to {{ title }}
               </h5> 
              </div> 
              <br><br> 

              {% if photos.length == 0 %} 
                <div class="row center"> 
                    <div class="card-panel orange lighten-2">No photos yet,
                      click add picture to upload</div> 
                </div> 
                {% endif %} 

              <div class="row"> 
              {% for item in photos %} 
                  <div class="col s12 m4"> 
                      <div class="icon-block"> 
                          <h5 class="center">{{ item.title }}</h5> 
                            {{ cloudinary.image(item.image.public_id, { 
                              width:280, height: 200, quality:80, 
                               crop:'fill',format:'png', effect:
                               'brightness:20', radius: 5, class:
                                'materialboxed' }) | safe }} 
                          {# 
                            Swig comment tag 
                            <img class="materialboxed" src="
                            {{ item.image.url }}" height='200' 
                             width='100%' alt="{{ item.title }}"
                             data-caption="{{item.description}}"></img> 
                          #} 
                          <p class="light">{{ item.description }}</p> 
                      </div> 
                  </div> 
              {% endfor %} 
              </div> 
          </div> 
      </div> 
      {% endblock %} 

  1. 创建一个名为posted-photo.swig的文件,并添加以下代码:
      {% extends '../layout.swig' %} 

      {% block content %} 
      <div class="section no-pad-bot" id="index-banner"> 
          <div class="container"> 
              <br><br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center"> 
                  <h5 class="header col s12 light">Welcome to 
                    {{ title }}
                  </h5> 
              </div> 
              <div class="photo col s12 m12"> 
                <h2>{{ photo.title }}</h2> 
                {% if photo.image.url %} 
                <img src="img/{{ photo.image.url }}" width='100%'></img> 
                <a href="{{ photo.image.url }}" target="_blank"> 
                  {{ cloudinary.image(photo.image.public_id, {width: 150,
                    height: 150, quality: 80,crop:'fill',format:'png', 
                    class:'thumbnail inline'})  }} 
                </a> 
                {% endif %} 
              </div> 
              <br> 
              <br> 
             <br> 
          </div> 
       </div> 

      {% endblock %} 

创建.env.js 文件

在这一点上,我们将创建env.js文件;此文件验证了 Cloudinary 服务的配置。在config文件夹中创建一个名为env.js的文件,并放入以下代码:

// get Env variable / cloudinary 
module.exports = function(app, configEnv) { 

    var dotenv = require('dotenv'); 
    dotenv.load(); 
    var cloudinary = require('cloudinary').v2; 
    // Log some messages on Terminal 
    if ( typeof(process.env.CLOUDINARY_URL) == 'undefined' ){ 
      console.log('Cloudinary config file is not defined'); 
      console.log('Setup CLOUDINARY_URL or use dotenv mdule file') 
    } else { 
      console.log('Cloudinary config, successfully used:'); 
      console.log(cloudinary.config()) 
    } 
} 

现在我们有一个完全配置的应用程序;但是,我们仍然需要在 Cloudinary 服务上创建一个帐户。

创建和配置 Cloudinary 账户

Cloudinary 是用于存储和处理图像和视频文件的云服务;您可以在cloudinary.com找到有关 Cloudinary 服务的更多信息:

  1. 转到cloudinary.com/users/register/free并注册一个免费帐户。

提示

在注册表单的末尾,您可以为您的云设置一个名称。我们选择了n6b(Node.js 6 蓝图);选择您自己的名称。

  1. 从您的帐户中复制数据(环境变量)并将其直接放到仪表板面板上,如下面的屏幕截图所示:创建和配置 Cloudinary 帐户

Cloudinary 仪表板面板

  1. 现在在.env.js文件中使用您自己的凭据更新以下代码:
      PORT=9000
      CLOUDINARY_URL=cloudinary://82499XXXXXXXXX:dXXXXXXXXXXX@n6b

Cloudinary 的工作原理

除了我们在 Cloudinary 上存储文件之外,我们还可以使用强大的 API 来操作和转换图像,应用效果,调整大小,以及在我们的机器上不使用任何软件做更多的事情。

让我们回到books.js控制器,查看我们使用了什么。我们从 promises 函数中提取了额外的代码,以便专注于突出显示的代码行:

      cloudinary.uploader.upload(imageFile, 
            { 
              tags: 'photobook', 
              folder: req.body.category + '/', 
              public_id: req.files.image.originalFilename 
              // eager: { 
              //   width: 280, height: 200, crop: "fill", gravity: "face" 
             // } 
            }) 
              .then(function (image) { 
               ... 
             }) 
             .then(function (photo) { 
                ... 
              }) 
              .finally(function () { 
                ... 
              }); 

在这里,我们设置了一个文件夹,folder: req.body.category,用于存储我们的图像,并覆盖了默认的public_id: req.files.image.originalFilename,以使用图像名称。这是一个很好的做法,因为 API 为我们提供了一个带有随机字符串的public_id——没有错,但非常有用。例如,查看这样的链接:http://res.cloudinary.com/demo/image/upload/mydog.jpg

而不是这个:res.cloudinary.com/demo/image/upload/8jsb1xofxdqamu2rzwt9q.jpg

注释的eager属性使我们能够转换图像并生成一个带有所有急切选项的新图像。在这种情况下,我们可以保存一个宽度为280px,高度为200px的转换图像,裁剪填充内容,如果图片中有一些脸,缩略图将居中显示在脸上。这是一个非常有用的功能,可以保存图像配置文件。

您可以在上传方法上使用任何转换组合;以下是 API 返回的JSON的示例:

      { title: 'Sample01',
          description: 'Example with Transformation',
          image:
           { public_id: 'cpl6ghhoiqphyagwvbaj',
             version: 1461269043,
             signature: '59cbbf3be205d72fbf7bbea77de8e7391d333363',
             width: 845,
             height: 535,
             format: 'bmp',
             resource_type: 'image',
             created_at: '2016-04-21T20:04:03Z',
             tags: [ 'photobook' ],
             bytes: 1356814,
             type: 'upload',
             etag: '639c51691528139ae4f1ef00bc995464',
             url: 'http://res.cloudinary.com/n6b/image/upload/v146126904
                   /cpl6ghhoiqphyagwvbaj.bmp',
             secure_url: 'https://res.cloudinary.com/n6b/image/upload
               /v1461269043/cpl6ghhoiqphyagwvbaj.bmp',
               coordinates: { faces: [ [ 40, 215, 116, 158 ] ] },  original_filename: 'YhCmSuFxm0amW5TFX9FqXt3F',  eager:[ { transformation: 'c_thumb,g_face,h_100,w_150', 
			   width: 150, height: 100,url: 'http://res.cloudinary.com
               /n6b/image/upload/c_thumb,g_face,h_100,w_150/v1461269043
               /cpl6ghhoiqphyagwvbaj.bmp', secure_url:
                'https://res.cloudinary.com/n6b/image/upload
                 /c_thumb,g_face,h_100,w_150/v1461269043
                 /cpl6ghhoiqphyagwvbaj.bmp' } ] }

注意带有 URL 转换的突出显示的代码:

c_thumb,g_face,h_100,w_150

提示

您可以在以下链接找到有关 Cloudinary 上传 API 的更多信息:cloudinary.com/documentation/node_image_upload

运行应用程序

现在是时候执行应用程序并上传一些照片了:

  1. 打开您的终端/Shell 并输入以下命令:
 npm start

  1. 转到http://localhost:3000/books,您将看到以下屏幕:运行应用程序

书籍屏幕

上传和显示图像

现在让我们插入一些图像,并检查我们应用程序的行为:

  1. 转到http://localhost:3000/books/add,并填写表单:上传和显示图像

上传表单

添加以下信息:

标题:图像示例 02

文件:选择 sample02.jpg 文件。

类别:城市

描述:Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

  1. 让我们检查一下我们的 MongoDB,看看在我们继续之前发生了什么。

  2. 打开您的RoboMongo并选择第一个对象:上传和显示图像

来自 MongoDB(RoboMongo)的屏幕截图

提示

请注意,您必须从左侧面板菜单中选择正确的数据库。

  1. 当我们上传一张图片时,API 会返回一个包含与该图片相关的所有信息的 JSON。我们将这个 JSON 存储为我们的书籍模型的图像属性,存储在 MongoDB 中,正如我们在之前的图片中所看到的。

  2. 通过Sample02Sample06重复步骤 1

检查 MongoDb 图片集合

让我们在 MongoDB 上看一下图片集合:

  1. 打开 RoboMongo 并从左侧面板中选择正确的数据库。

  2. 打开collections文件夹,双击图片集合

  3. 在面板的右上方,单击以表格模式查看结果图标。

现在您可以在RoboMongo界面上看到以下屏幕截图:

检查 MongoDb 图片集合

来自图片集合的屏幕截图

在 Cloudinary 仪表板中创建文件夹

如前所述,我们设置了文件夹(folder: req.body.category)。在这种情况下,文件夹名称将是类别名称。为了更好地在云中组织我们的图像,就像我们以编程方式做的那样,我们需要直接在 Cloudinary 仪表板中创建它们:

  1. 登录到您的 Cloudinary 帐户。

  2. 转到cloudinary.com/console/media_library在 Cloudinary 仪表板中创建文件夹

创建文件夹截图

注意

不要担心 Cloudinary 仪表板上的其他图像;它们是每个帐户中的默认图像。如果您愿意,可以删除它们。

  1. 点击右侧的输入字段(文件夹名称)并创建一个名为animals的文件夹。

  2. 点击右侧的输入字段(文件夹名称)并创建一个名为cities的文件夹。

  3. 点击右侧的输入字段(文件夹名称)并创建一个名为nature的文件夹。

您可以在图像顶部看到所有创建的类别,如下面的截图所示:

在 Cloudinary 仪表板中创建文件夹

类别截图

现在当您选择一个类别时,您只会看到属于该类别的图像,例如animals,如下图所示:

在 Cloudinary 仪表板中创建文件夹

动物文件夹的截图

这是一种更有效的组织所有照片的方式,您可以创建几个相册,例如:

 my-vacations/germany/berlin 
      road-trip/2015/route-66

URL 转换渲染

作为 Cloudinary API 的一部分,我们可以通过使用 URL 参数设置来操纵图像,就像我们在书籍页面上所做的那样:

  1. 转到http://localhost:3000/books

  2. 打开您的 Web 检查器并检查第一张图像的呈现代码;您将看到以下代码:

      <img src="http://res.cloudinary.com/n6b/image/upload
       /c_fill,e_brightness:20,h_200,q_80,r_5,w_280/v1/cities/sample01.jpg
       .png" class="materialboxed initialized" height="200" width="280"> 

API 创建img标签,并应用app/views/books.swig中定义的对象属性作为 URL 参数,如下面的代码所示:

      {{ cloudinary.image(item.image.public_id, { width: 280, height: 200,
       quality: 80,crop: 'fill',format:'png', effect: 'brightness:20',
       radius: 5, class:'materialboxed' }) | safe }} 

对象属性URL 参数
width: 280w_280
height: 200h_200
crop: fillc_fill
quality: 80q_80
effect:brightness:20e_brightness:20
半径:5r_5

花括号和安全过滤器{{... | safe}}Swig模板引擎的标记,用于在视图上安全地呈现变量。

此外,我们还可以直接使用img标签,如下面的代码所示:

 <img class="materialboxed" src="img/{{ item.image.url }}" height='200'
       width='100%' alt="{{ item.title }}" 
       data-caption="{{item.description}}">
      </img>

添加原始图像的直接链接

我们还可以使用 API 生成原始图像链接,而不应用任何转换:

  1. 打开app/views/books.swig并添加以下突出显示的代码:
      <div class="icon-block"> 
      <h5 class="center">{{ item.title }}</h5> 
        {{ cloudinary.image(item.image.public_id, { width: 280, height:
          200, quality: 80,crop: 'fill',format:'png', effect:
          'brightness:20', radius:5,class:'materialboxed' }) | safe }} 
       {# 
          Swig comment tag 
          <img class="materialboxed" src="img/{{ item.image.url }}"
           height='200' width='100%' alt="{{ item.title }}"
           data-caption="{{item.description}}">
          </img> 
      #} 
      <p class="light">{{ item.description }}</p> 
      <a href="{{ cloudinary.url(item.image.url) }}" target="_blank"> 
      Link to original image 
      </a> 
      </div> 

  1. 现在当我们点击链接到原始图像时,我们可以在另一个浏览器窗口中看到完整的图像:添加原始图像的直接链接

带有原始图像链接的书籍页面截图

重要的是要注意,我们还使用了Materialize.css框架中的简单colorbox,因此当我们将鼠标悬停在图像上时,我们可以看到一个图标,显示全尺寸的图像。

总结

我们已经到达了另一章的结尾。通过这一章,我们完成了一系列四章,讨论了使用 Node.js 进行软件开发的 MVC 模式。

在本章中,我们看到了如何构建一个使用云服务上传和操作图像的应用程序,还展示了如何应用效果,如亮度和边框半径。此外,我们还看到了如何使用简单的界面框架Materialize.css构建简单的图像库。

我们探索了一种不同的使用 ORM 模块的方式,并直接以 JSON 格式将所有有关图像的信息保存在 MongoDB 中。

在下一章中,我们将看到如何使用 Node 和 Firebase 云服务构建 Web 应用程序。