如何在 Node 和 Express 中添加高级照片上传功能

简介

在构建Node应用程序时,我们有时会遇到上传照片(通常来自表单)作为应用程序中用户的个人资料照片的问题。此外,为了方便访问,我们通常必须将照片存储在本地文件系统中(在开发期间),甚至存储在云中。由于这是一项非常常见的任务,因此我们可以利用许多工具来处理流程的各个部分。

在本教程中,我们将了解如何上传照片并在将其写入存储之前对其进行操作(调整大小、裁剪、灰度等)。为简单起见,我们将仅将文件存储在本地文件系统中。

前提条件

我们将使用以下包来构建我们的应用程序:

  • Express :非常流行的节点服务器。
  • lowash :是一个非常流行的JAVASCRIPT库,包含许多实用函数,用于处理数组、字符串、对象和函数式编程。 *multer :从MultiPart/Form-Data请求中提取文件的包。 *jimp :图像处理包。 *dotenv :将.env变量添加到Process.env中的包。 *mkdirp :用于创建嵌套目录结构的包。 *concat-stream :用于创建可写流的包,该可写流将流中的所有数据连接在一起,并使用结果调用回调。 *streamifier :将缓冲区/字符串转换为可读流的包。

项目目标

我们希望从**Multer** 接管上传的文件流,然后操作流缓冲区(IMAGE),但是我们希望在将映像写入存储(LOCAL FILESYSTEM)之前使用** Jimp** ,。这将需要我们创建一个与Multer一起使用的定制存储引擎--我们将在本教程中这样做。

以下是我们将在本教程中构建的最终结果:

[YouTube oedBD8Bnyls 480 854]

第一步--入门

我们将首先使用Express生成器创建一个新的Express应用程序。如果您还没有Express生成器,则需要首先通过在命令行终端上运行以下命令来安装它:

1npm install express-generator -g

拥有Express生成器后,现在可以运行以下命令来创建新的Express应用程序并安装Express的依赖项。我们将使用ejs作为我们的视图引擎:

1express --view=ejs photo-uploader-app
2cd photo-uploader-app
3npm install

接下来,我们将安装项目所需的其余依赖项:

1npm install --save lodash multer jimp dotenv concat-stream streamifier mkdirp

第二步-配置基础知识

在我们继续之前,我们的应用程序需要一些表单配置。我们将在工程根目录下创建一个.env文件,并添加一些环境变量。.env文件应如下所示。

1AVATAR_FIELD=avatar
2AVATAR_BASE_URL=/uploads/avatars
3AVATAR_STORAGE=uploads/avatars

接下来,我们将使用 dotenv 将我们的环境变量加载到process.env中,以便我们可以在应用程序中访问它们。为此,我们将向app.js文件添加以下行。确保在加载依赖项的位置添加此行。它必须出现在所有路由导入之前和创建Express应用实例之前。

1[label app.js]
2var dotenv = require('dotenv').config();

现在我们可以使用cess.env访问我们的环境变量了。例如:Process.env.AVATAR_STORAGE中应包含UPLOADS/avatars。我们将继续编辑我们的索引路由文件routes/index.js,以添加我们的视图中需要的一些本地变量。我们将添加两个局部变量:

  • 标题 :我们首页的标题:Upload Avatar
  • 头像_字段 :头像照片的输入域名称。我们将从Process.env.AVATAR_FIELD获取此文件

修改Get/路由如下:

1[label routes/index.js]
2router.get('/', function(req, res, next) {
3res.render('index', { title: 'Upload Avatar', avatar_field: process.env.AVATAR_FIELD });
4});

第三步-准备视图

让我们首先通过修改views/index.ejs文件为照片上传表单创建基本标记。为了简单起见,我们将直接在视图上添加样式,只是为了给它一个稍微漂亮的外观。请参阅下面的代码以获取我们页面的标记。

 1[label views/index.ejs]
 2<html class="no-js">
 3<head>
 4<meta charset="utf-8">
 5<meta http-equiv="X-UA-Compatible" content="IE=edge">
 6<meta name="viewport" content="width=device-width, initial-scale=1">
 7<title><%= title %></title>
 8<style type="text/css">
 9* {
10font: 600 16px system-ui, sans-serif;
11}
12form {
13width: 320px;
14margin: 50px auto;
15text-align: center;
16}
17form > legend {
18font-size: 36px;
19color: #3c5b6d;
20padding: 150px 0 20px;
21}
22form > input[type=file], form > input[type=file]:before {
23display: block;
24width: 240px;
25height: 50px;
26margin: 0 auto;
27line-height: 50px;
28text-align: center;
29cursor: pointer;
30}
31form > input[type=file] {
32position: relative;
33}
34form > input[type=file]:before {
35content: 'Choose a Photo';
36position: absolute;
37top: -2px;
38left: -2px;
39color: #3c5b6d;
40font-size: 18px;
41background: #fff;
42border-radius: 3px;
43border: 2px solid #3c5b6d;
44}
45form > button[type=submit] {
46border-radius: 3px;
47font-size: 18px;
48display: block;
49border: none;
50color: #fff;
51cursor: pointer;
52background: #2a76cd;
53width: 240px;
54margin: 20px auto;
55padding: 15px 20px;
56}
57</style>
58</head>
59<body>
60<form action="/upload" method="POST" enctype="multipart/form-data">
61<legend>Upload Avatar</legend>
62<input type="file" name="<%= avatar_field %>">
63<button type="submit" class="btn btn-primary">Upload</button>
64</form>
65</body>
66</html>

请注意,我们如何在视图上使用局部变量来设置化身输入字段的标题和名称。您会注意到,我们在表单上使用了enctype=)发出POST`请求。

现在让我们使用npm start第一次启动这款应用。

1npm start

如果您一直按照正确的方式操作,那么一切运行起来都应该没有错误。只需在浏览器上访问本地主机:3000即可。页面应该类似于以下屏幕截图:

上传Page

Step 4 -创建Multer存储引擎

到目前为止,尝试通过我们的表单上传照片将导致错误,因为我们还没有为上传请求创建处理程序。我们将实现/pload路径来实际处理上传,为此我们将使用Multer包。如果您还不熟悉穆特程序包,可以查看Github上的穆特程序包.

我们将不得不创建一个自定义的存储引擎与Multer一起使用。让我们在项目根目录下创建一个名为helpers的新文件夹,并在其中为我们的自定义存储引擎创建一个新文件AvatarStorage.js。该文件应包含以下蓝图代码片段:

 1[label helpers/AvatarStorage.js]
 2// Load dependencies
 3var _ = require('lodash');
 4var fs = require('fs');
 5var path = require('path');
 6var Jimp = require('jimp');
 7var crypto = require('crypto');
 8var mkdirp = require('mkdirp');
 9var concat = require('concat-stream');
10var streamifier = require('streamifier');
11
12// Configure UPLOAD_PATH
13// process.env.AVATAR_STORAGE contains uploads/avatars
14var UPLOAD_PATH = path.resolve(__dirname, '..', process.env.AVATAR_STORAGE);
15
16// create a multer storage engine
17var AvatarStorage = function(options) {
18
19// this serves as a constructor
20function AvatarStorage(opts) {}
21
22// this generates a random cryptographic filename
23AvatarStorage.prototype._generateRandomFilename = function() {}
24
25// this creates a Writable stream for a filepath
26AvatarStorage.prototype._createOutputStream = function(filepath, cb) {}
27
28// this processes the Jimp image buffer
29AvatarStorage.prototype._processImage = function(image, cb) {}
30
31// multer requires this for handling the uploaded file
32AvatarStorage.prototype._handleFile = function(req, file, cb) {}
33
34// multer requires this for destroying file
35AvatarStorage.prototype._removeFile = function(req, file, cb) {}
36
37// create a new instance with the passed options and return it
38return new AvatarStorage(options);
39
40};
41
42// export the storage engine
43module.exports = AvatarStorage;

让我们开始为存储引擎中列出的函数添加实现。我们将从构造函数开始。

 1// this serves as a constructor
 2function AvatarStorage(opts) {
 3
 4var baseUrl = process.env.AVATAR_BASE_URL;
 5
 6var allowedStorageSystems = ['local'];
 7var allowedOutputFormats = ['jpg', 'png'];
 8
 9// fallback for the options
10var defaultOptions = {
11storage: 'local',
12output: 'png',
13greyscale: false,
14quality: 70,
15square: true,
16threshold: 500,
17responsive: false,
18};
19
20// extend default options with passed options
21var options = (opts && _.isObject(opts)) ? _.pick(opts, _.keys(defaultOptions)) : {};
22options = _.extend(defaultOptions, options);
23
24// check the options for correct values and use fallback value where necessary
25this.options = _.forIn(options, function(value, key, object) {
26
27switch (key) {
28
29case 'square':
30case 'greyscale':
31case 'responsive':
32object[key] = _.isBoolean(value) ? value : defaultOptions[key];
33break;
34
35case 'storage':
36value = String(value).toLowerCase();
37object[key] = _.includes(allowedStorageSystems, value) ? value : defaultOptions[key];
38break;
39
40case 'output':
41value = String(value).toLowerCase();
42object[key] = _.includes(allowedOutputFormats, value) ? value : defaultOptions[key];
43break;
44
45case 'quality':
46value = _.isFinite(value) ? value : Number(value);
47object[key] = (value && value >= 0 && value <= 100) ? value : defaultOptions[key];
48break;
49
50case 'threshold':
51value = _.isFinite(value) ? value : Number(value);
52object[key] = (value && value >= 0) ? value : defaultOptions[key];
53break;
54
55}
56
57});
58
59// set the upload path
60this.uploadPath = this.options.responsive ? path.join(UPLOAD_PATH, 'responsive') : UPLOAD_PATH;
61
62// set the upload base url
63this.uploadBaseUrl = this.options.responsive ? path.join(baseUrl, 'responsive') : baseUrl;
64
65if (this.options.storage == 'local') {
66// if upload path does not exist, create the upload path structure
67!fs.existsSync(this.uploadPath) && mkdirp.sync(this.uploadPath);
68}
69
70}

在这里,我们定义了我们的构造函数来接受几个选项。我们还为这些选项添加了一些缺省(备用)值,以防它们未提供或无效。您可以根据需要调整此选项以包含更多选项,但在本教程中,我们将坚持使用存储引擎的以下选项。

  • 存储 :存储文件系统。对于本地文件系统,仅允许的值为‘本地’‘。默认为‘本地’‘。如果您愿意,您可以实现其他存储文件系统(如Amazon S3)。
  • 输出 :图像输出格式。可以是‘jpg’‘png’。默认为‘png’
  • 灰度 :如果设置为true,则输出图像为灰度。默认为False
  • 画质 :0:100之间的数字,决定输出画面的画质。默认为70
  • 正方形 :如果设置为true,则图像将被裁剪为正方形。默认为False
  • 阈值 :限制输出图像最小维度的数值,单位为px。默认值为500。如果图像的最小尺寸超过该数字,则调整图像的大小,使最小尺寸等于阈值。
  • 响应 :如果设置为true,则会创建三个不同大小的输出镜像(lgmdsm)并存储在各自的文件夹中。默认为False

让我们实现创建随机文件名和写入文件的输出流的方法:

 1// this generates a random cryptographic filename
 2AvatarStorage.prototype._generateRandomFilename = function() {
 3// create pseudo random bytes
 4var bytes = crypto.pseudoRandomBytes(32);
 5
 6// create the md5 hash of the random bytes
 7var checksum = crypto.createHash('MD5').update(bytes).digest('hex');
 8
 9// return as filename the hash with the output extension
10return checksum + '.' + this.options.output;
11};
12
13// this creates a Writable stream for a filepath
14AvatarStorage.prototype._createOutputStream = function(filepath, cb) {
15
16// create a reference for this to use in local functions
17var that = this;
18
19// create a writable stream from the filepath
20var output = fs.createWriteStream(filepath);
21
22// set callback fn as handler for the error event
23output.on('error', cb);
24
25// set handler for the finish event
26output.on('finish', function() {
27cb(null, {
28destination: that.uploadPath,
29baseUrl: that.uploadBaseUrl,
30filename: path.basename(filepath),
31storage: that.options.storage
32});
33});
34
35// return the output stream
36return output;
37};

这里,我们使用CRYPTO 创建一个随机的MD5哈希作为文件名,并将选项的输出附加为文件扩展名。我们还定义了帮助器方法,以便从给定的文件路径创建可写流,然后返回该流。请注意,回调函数是必需的,因为我们在流事件处理程序上使用它。

接下来,我们将实现进行实际图像处理的_cessImage()方法。具体实现如下:

  1// this processes the Jimp image buffer
  2AvatarStorage.prototype._processImage = function(image, cb) {
  3
  4// create a reference for this to use in local functions
  5var that = this;
  6
  7var batch = [];
  8
  9// the responsive sizes
 10var sizes = ['lg', 'md', 'sm'];
 11
 12var filename = this._generateRandomFilename();
 13
 14var mime = Jimp.MIME_PNG;
 15
 16// create a clone of the Jimp image
 17var clone = image.clone();
 18
 19// fetch the Jimp image dimensions
 20var width = clone.bitmap.width;
 21var height = clone.bitmap.height;
 22var square = Math.min(width, height);
 23var threshold = this.options.threshold;
 24
 25// resolve the Jimp output mime type
 26switch (this.options.output) {
 27case 'jpg':
 28mime = Jimp.MIME_JPEG;
 29break;
 30case 'png':
 31default:
 32mime = Jimp.MIME_PNG;
 33break;
 34}
 35
 36// auto scale the image dimensions to fit the threshold requirement
 37if (threshold && square > threshold) {
 38clone = (square == width) ? clone.resize(threshold, Jimp.AUTO) : clone.resize(Jimp.AUTO, threshold);
 39}
 40
 41// crop the image to a square if enabled
 42if (this.options.square) {
 43
 44if (threshold) {
 45square = Math.min(square, threshold);
 46}
 47
 48// fetch the new image dimensions and crop
 49clone = clone.crop((clone.bitmap.width: square) / 2, (clone.bitmap.height: square) / 2, square, square);
 50}
 51
 52// convert the image to greyscale if enabled
 53if (this.options.greyscale) {
 54clone = clone.greyscale();
 55}
 56
 57// set the image output quality
 58clone = clone.quality(this.options.quality);
 59
 60if (this.options.responsive) {
 61
 62// map through the responsive sizes and push them to the batch
 63batch = _.map(sizes, function(size) {
 64
 65var outputStream;
 66
 67var image = null;
 68var filepath = filename.split('.');
 69
 70// create the complete filepath and create a writable stream for it
 71filepath = filepath[0] + '_' + size + '.' + filepath[1];
 72filepath = path.join(that.uploadPath, filepath);
 73outputStream = that._createOutputStream(filepath, cb);
 74
 75// scale the image based on the size
 76switch (size) {
 77case 'sm':
 78image = clone.clone().scale(0.3);
 79break;
 80case 'md':
 81image = clone.clone().scale(0.7);
 82break;
 83case 'lg':
 84image = clone.clone();
 85break;
 86}
 87
 88// return an object of the stream and the Jimp image
 89return {
 90stream: outputStream,
 91image: image
 92};
 93});
 94
 95} else {
 96
 97// push an object of the writable stream and Jimp image to the batch
 98batch.push({
 99stream: that._createOutputStream(path.join(that.uploadPath, filename), cb),
100image: clone
101});
102
103}
104
105// process the batch sequence
106_.each(batch, function(current) {
107// get the buffer of the Jimp image using the output mime type
108current.image.getBuffer(mime, function(err, buffer) {
109if (that.options.storage == 'local') {
110// create a read stream from the buffer and pipe it to the output stream
111streamifier.createReadStream(buffer).pipe(current.stream);
112}
113});
114});
115
116};

这种方法做了很多工作,但下面是它所做工作的总结:

  • 生成随机文件名,解析jimp输出图像MIME类型,并获取图像尺寸。
  • 如有需要,可根据阈值要求调整图像大小,以确保最小尺寸不超过阈值。
  • 如果在选项中启用,则将图像裁剪为正方形。
  • 如果在选项中启用,则将图像转换为灰度。
  • 从选项中设置图像输出质量。
  • 如果开启了响应,则会针对每个响应大小(lgmdsm)进行克隆和缩放,然后使用_createOutputStream()方法为每个大小的镜像文件创建输出流。每种大小的文件名都采用[random_filename_hash]_[size].[output_extension].格式然后将图像克隆和流放在一批中进行处理。
  • 如果关闭响应,则只会将当前图像及其输出流批量放入处理。
  • 最后,使用 Streamiator** 将jimp图像缓冲区转换为可读流,然后通过管道将可读流输送到输出流,从而对批次中的每一项进行处理。

现在,我们将实现其余的方法,我们将完成存储引擎。

 1// multer requires this for handling the uploaded file
 2AvatarStorage.prototype._handleFile = function(req, file, cb) {
 3
 4// create a reference for this to use in local functions
 5var that = this;
 6
 7// create a writable stream using concat-stream that will
 8// concatenate all the buffers written to it and pass the
 9// complete buffer to a callback fn
10var fileManipulate = concat(function(imageData) {
11
12// read the image buffer with Jimp
13// it returns a promise
14Jimp.read(imageData)
15.then(function(image) {
16// process the Jimp image buffer
17that._processImage(image, cb);
18})
19.catch(cb);
20});
21
22// write the uploaded file buffer to the fileManipulate stream
23file.stream.pipe(fileManipulate);
24
25};
26
27// multer requires this for destroying file
28AvatarStorage.prototype._removeFile = function(req, file, cb) {
29
30var matches, pathsplit;
31var filename = file.filename;
32var _path = path.join(this.uploadPath, filename);
33var paths = [];
34
35// delete the file properties
36delete file.filename;
37delete file.destination;
38delete file.baseUrl;
39delete file.storage;
40
41// create paths for responsive images
42if (this.options.responsive) {
43pathsplit = _path.split('/');
44matches = pathsplit.pop().match(/^(.+?)_.+?\.(.+)$/i);
45
46if (matches) {
47paths = _.map(['lg', 'md', 'sm'], function(size) {
48return pathsplit.join('/') + '/' + (matches[1] + '_' + size + '.' + matches[2]);
49});
50}
51} else {
52paths = [_path];
53}
54
55// delete the files from the filesystem
56_.each(paths, function(_path) {
57fs.unlink(_path, cb);
58});
59
60};

我们的存储引擎现在可以与Multer一起使用了。

第五步-实现POST/UPLOAD路由

在我们定义路线之前,我们需要设置Multer以便在我们的路线中使用。让我们继续编辑routes/index.js文件,以添加以下内容:

 1[label routes/index.js]
 2
 3var express = require('express');
 4var router = express.Router();
 5
 6/**
 7 * CODE ADDITION
 8 * 
 9 * The following code is added to import additional dependencies
10 * and setup Multer for use with the /upload route.
11 */
12
13// import multer and the AvatarStorage engine
14var _ = require('lodash');
15var path = require('path');
16var multer = require('multer');
17var AvatarStorage = require('../helpers/AvatarStorage');
18
19// setup a new instance of the AvatarStorage engine 
20var storage = AvatarStorage({
21square: true,
22responsive: true,
23greyscale: true,
24quality: 90
25});
26
27var limits = {
28files: 1, // allow only 1 file per request
29fileSize: 1024 * 1024, // 1 MB (max file size)
30};
31
32var fileFilter = function(req, file, cb) {
33// supported image file mimetypes
34var allowedMimes = ['image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'];
35
36if (_.includes(allowedMimes, file.mimetype)) {
37// allow supported image files
38cb(null, true);
39} else {
40// throw error for invalid files
41cb(new Error('Invalid file type. Only jpg, png and gif image files are allowed.'));
42}
43};
44
45// setup multer
46var upload = multer({
47storage: storage,
48limits: limits,
49fileFilter: fileFilter
50});
51
52/* CODE ADDITION ENDS HERE */

在这里,我们启用方形裁剪、响应图像并为我们的存储引擎设置阈值。我们还在Multer配置中增加了限制,以确保最大文件大小为`1MB‘,并确保不上传非镜像文件。

现在我们添加post/pload路由,如下所示:

 1/* routes/index.js */
 2
 3/**
 4 * CODE ADDITION
 5 * 
 6 * The following code is added to configure the POST /upload route
 7 * to upload files using the already defined Multer configuration
 8 */
 9
10router.post('/upload', upload.single(process.env.AVATAR_FIELD), function(req, res, next) {
11
12var files;
13var file = req.file.filename;
14var matches = file.match(/^(.+?)_.+?\.(.+)$/i);
15
16if (matches) {
17files = _.map(['lg', 'md', 'sm'], function(size) {
18return matches[1] + '_' + size + '.' + matches[2];
19});
20} else {
21files = [file];
22}
23
24files = _.map(files, function(file) {
25var port = req.app.get('port');
26var base = req.protocol + '://' + req.hostname + (port ? ':' + port : '');
27var url = path.join(req.file.baseUrl, file).replace(/[\\\/]+/g, '/').replace(/^[\/]+/g, '');
28
29return (req.file.storage == 'local' ? base : '') + '/' + url;
30});
31
32res.json({
33images: files
34});
35
36});
37
38/* CODE ADDITION ENDS HERE */

请注意我们是如何在路由处理程序之前传递Multer上传中间件的。Single()方法只允许我们上传一个文件,该文件将存储在req.file中。它以文件输入字段的名称作为第一个参数,文件输入字段的名称是从Process.env.AVATAR_FIELD访问的。

现在让我们使用npm start再次启动应用程序。

1npm start

在浏览器上访问本地主机:3000,并尝试上传照片。以下是我在邮递员,使用我们当前的配置选项)上测试上传路径后获得的示例屏幕截图:

Postman-screen

您可以在我们的Multer设置中调整存储引擎的配置选项以获得不同的结果。

总结

在本教程中,我们已经能够创建用于MULTER)的自定义存储引擎,该引擎使用JIMP处理上传的图像,然后将它们写入存储。有关本教程的完整代码样例,请查看Github上的advanced-multer-node-sourcecode资源库。

Published At
Categories with 技术
comments powered by Disqus