序章

Nodeアプリケーションを構築する際に、アプリでユーザーのプロフィール写真として使用する写真(通常はフォームから)をアップロードする必要がありました。 さらに、通常、簡単にアクセスできるように、写真をローカルファイルシステム(開発中)またはクラウドに保存する必要があります。 これは非常に一般的なタスクであるため、プロセスの個々の部分を処理するために活用できるツールがたくさんあります。

このチュートリアルでは、写真をアップロードして、ストレージに書き込む前に操作(サイズ変更、トリミング、グレースケールなど)する方法を説明します。 簡単にするために、ローカルファイルシステムにファイルを保存することに限定します。

前提条件

アプリケーションのビルドには、次のパッケージを使用します。

  • express :非常に人気のあるノードサーバー。
  • lodash :配列、文字列、オブジェクト、関数型プログラミングを操作するための多くのユーティリティ関数を備えた非常に人気のあるJavaScriptライブラリ。
  • multer :ファイルを抽出するためのパッケージ multipart/form-data リクエスト。
  • jimp :画像操作パッケージ。
  • dotenv :追加するためのパッケージ .env 変数 process.env.
  • mkdirp :ネストされたディレクトリ構造を作成するためのパッケージ。
  • concat-stream :ストリームからのすべてのデータを連結し、結果を使用してコールバックを呼び出す書き込み可能なストリームを作成するためのパッケージ。
  • streamifier :バッファ/文字列を読み取り可能なストリームに変換するパッケージ。

プロジェクトの目標

Multer からアップロードされたファイルストリームを引き継ぎ、ストリームバッファー( image )を操作しますが、イメージをストレージに書き込む前に、Jimpを使用します。 (ローカルファイルシステム)。 これには、Multerで使用するカスタムストレージエンジンを作成する必要があります。これは、このチュートリアルで行います。

このチュートリアルで構築する内容の最終結果は次のとおりです。

ステップ1—はじめに

まず、Expressジェネレーターを使用して新しいExpressアプリを作成します。 Expressジェネレーターをまだお持ちでない場合は、コマンドラインターミナルで次のコマンドを実行して、最初にExpressジェネレーターをインストールする必要があります。

  1. npm install express-generator -g

Expressジェネレーターを入手したら、次のコマンドを実行して新しいExpressアプリを作成し、Expressの依存関係をインストールできます。 使用します ejs ビューエンジンとして:

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

次に、プロジェクトに必要な残りの依存関係をインストールします。

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

ステップ2—基本を構成する

続行する前に、アプリでフォームを構成する必要があります。 作成します .env プロジェクトのルートディレクトリにファイルを作成し、いくつかの環境変数を追加します。 The .env ファイルは次のスニペットのようになります。

AVATAR_FIELD=avatar
AVATAR_BASE_URL=/uploads/avatars
AVATAR_STORAGE=uploads/avatars

次に、環境変数をにロードします process.env dotenv を使用して、アプリでそれらにアクセスできるようにします。 これを行うには、次の行をに追加します app.js ファイル。 依存関係をロードするポイントにこの行を追加してください。 すべてのルートをインポートする前、およびExpressアプリインスタンスを作成する前に行う必要があります。

app.js
var dotenv = require('dotenv').config();

これで、を使用して環境変数にアクセスできます process.env. 例えば: process.env.AVATAR_STORAGE 値が含まれている必要があります uploads/avatars. インデックスルートファイルの編集に進みます routes/index.js ビューで必要になるローカル変数を追加します。 2つのローカル変数を追加します。

  • title :インデックスページのタイトル: Upload Avatar
  • avatar_field :アバター写真の入力フィールドの名前。 これはから取得します process.env.AVATAR_FIELD

を変更します GET / 次のようにルーティングします。

ルート/index.js
router.get('/', function(req, res, next) {
res.render('index', { title: 'Upload Avatar', avatar_field: process.env.AVATAR_FIELD });
});

ステップ3—ビューの準備

写真アップロードフォームの基本的なマークアップを作成することから始めましょう。 views/index.ejs ファイル。 わかりやすくするために、ビューにスタイルを直接追加して、少し見栄えを良くします。 このページのマークアップについては、次のコードを参照してください。

views / index.ejs
<html class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= title %></title>
<style type="text/css">
* {
font: 600 16px system-ui, sans-serif;
}
form {
width: 320px;
margin: 50px auto;
text-align: center;
}
form > legend {
font-size: 36px;
color: #3c5b6d;
padding: 150px 0 20px;
}
form > input[type=file], form > input[type=file]:before {
display: block;
width: 240px;
height: 50px;
margin: 0 auto;
line-height: 50px;
text-align: center;
cursor: pointer;
}
form > input[type=file] {
position: relative;
}
form > input[type=file]:before {
content: 'Choose a Photo';
position: absolute;
top: -2px;
left: -2px;
color: #3c5b6d;
font-size: 18px;
background: #fff;
border-radius: 3px;
border: 2px solid #3c5b6d;
}
form > button[type=submit] {
border-radius: 3px;
font-size: 18px;
display: block;
border: none;
color: #fff;
cursor: pointer;
background: #2a76cd;
width: 240px;
margin: 20px auto;
padding: 15px 20px;
}
</style>
</head>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<legend>Upload Avatar</legend>
<input type="file" name="<%= avatar_field %>">
<button type="submit" class="btn btn-primary">Upload</button>
</form>
</body>
</html>

ビューでローカル変数を使用して、アバター入力フィールドのタイトルと名前を設定したことに注目してください。 あなたは私たちが使用していることに気付くでしょう enctype="multipart/form-data" ファイルをアップロードするので、フォームに表示します。 また、フォームを作成するように設定したこともわかります。 POST にリクエスト /upload 提出時にルート(後で実装します)。

それでは、初めてアプリを起動しましょう npm start.

  1. npm start

正しくフォローしていれば、すべてがエラーなしで実行されるはずです。 訪問するだけ localhost:3000 ブラウザで。 ページは次のスクリーンショットのようになります。

ステップ4— MulterStorageEngineを作成する

これまでのところ、アップロードリクエストのハンドラーを作成していないため、フォームから写真をアップロードしようとするとエラーが発生します。 実装します /upload 実際にアップロードを処理するためのルートであり、そのためにMulterパッケージを使用します。 Multerパッケージにまだ慣れていない場合は、GithubMulterパッケージを確認できます。

Multerで使用するカスタムストレージエンジンを作成する必要があります。 プロジェクトルートに名前の付いた新しいフォルダを作成しましょう helpers 新しいファイルを作成します AvatarStorage.js 私たちのカスタムストレージエンジンのためにその中に。 ファイルには、次のブループリントコードスニペットが含まれている必要があります。

helpers / AvatarStorage.js
// Load dependencies
var _ = require('lodash');
var fs = require('fs');
var path = require('path');
var Jimp = require('jimp');
var crypto = require('crypto');
var mkdirp = require('mkdirp');
var concat = require('concat-stream');
var streamifier = require('streamifier');

// Configure UPLOAD_PATH
// process.env.AVATAR_STORAGE contains uploads/avatars
var UPLOAD_PATH = path.resolve(__dirname, '..', process.env.AVATAR_STORAGE);

// create a multer storage engine
var AvatarStorage = function(options) {

// this serves as a constructor
function AvatarStorage(opts) {}

// this generates a random cryptographic filename
AvatarStorage.prototype._generateRandomFilename = function() {}

// this creates a Writable stream for a filepath
AvatarStorage.prototype._createOutputStream = function(filepath, cb) {}

// this processes the Jimp image buffer
AvatarStorage.prototype._processImage = function(image, cb) {}

// multer requires this for handling the uploaded file
AvatarStorage.prototype._handleFile = function(req, file, cb) {}

// multer requires this for destroying file
AvatarStorage.prototype._removeFile = function(req, file, cb) {}

// create a new instance with the passed options and return it
return new AvatarStorage(options);

};

// export the storage engine
module.exports = AvatarStorage;

リストされた関数の実装をストレージエンジンに追加し始めましょう。 コンストラクター関数から始めます。


// this serves as a constructor
function AvatarStorage(opts) {

var baseUrl = process.env.AVATAR_BASE_URL;

var allowedStorageSystems = ['local'];
var allowedOutputFormats = ['jpg', 'png'];

// fallback for the options
var defaultOptions = {
storage: 'local',
output: 'png',
greyscale: false,
quality: 70,
square: true,
threshold: 500,
responsive: false,
};

// extend default options with passed options
var options = (opts && _.isObject(opts)) ? _.pick(opts, _.keys(defaultOptions)) : {};
options = _.extend(defaultOptions, options);

// check the options for correct values and use fallback value where necessary
this.options = _.forIn(options, function(value, key, object) {

switch (key) {

case 'square':
case 'greyscale':
case 'responsive':
object[key] = _.isBoolean(value) ? value : defaultOptions[key];
break;

case 'storage':
value = String(value).toLowerCase();
object[key] = _.includes(allowedStorageSystems, value) ? value : defaultOptions[key];
break;

case 'output':
value = String(value).toLowerCase();
object[key] = _.includes(allowedOutputFormats, value) ? value : defaultOptions[key];
break;

case 'quality':
value = _.isFinite(value) ? value : Number(value);
object[key] = (value && value >= 0 && value <= 100) ? value : defaultOptions[key];
break;

case 'threshold':
value = _.isFinite(value) ? value : Number(value);
object[key] = (value && value >= 0) ? value : defaultOptions[key];
break;

}

});

// set the upload path
this.uploadPath = this.options.responsive ? path.join(UPLOAD_PATH, 'responsive') : UPLOAD_PATH;

// set the upload base url
this.uploadBaseUrl = this.options.responsive ? path.join(baseUrl, 'responsive') : baseUrl;

if (this.options.storage == 'local') {
// if upload path does not exist, create the upload path structure
!fs.existsSync(this.uploadPath) && mkdirp.sync(this.uploadPath);
}

}

ここでは、いくつかのオプションを受け入れるようにコンストラクター関数を定義しました。 また、これらのオプションが提供されていない場合や無効な場合に備えて、これらのオプションにいくつかのデフォルト(フォールバック)値を追加しました。 これを微調整して、必要に応じてより多くのオプションを含めることができますが、このチュートリアルでは、ストレージエンジンの次のオプションを使用します。

  • storage :ストレージファイルシステム。 許可される値は 'local' ローカルファイルシステム用。 デフォルトは 'local'. 他のストレージファイルシステムを実装できます( Amazon S3) ご希望の場合。
  • output :画像出力形式。 することができます 'jpg' また 'png'. デフォルトは 'png'.
  • グレースケール:に設定されている場合 true、出力画像はグレースケールになります。 デフォルトは false.
  • quality :出力画像の品質を決定する0〜100の数値。 デフォルトは 70.
  • square :に設定されている場合 true、画像は正方形にトリミングされます。 デフォルトは false.
  • threshold :最小寸法を制限する数値( px)出力画像の。 デフォルト値は 500. 画像の最小サイズがこの数値を超えると、最小サイズがしきい値と等しくなるように画像のサイズが変更されます。
  • レスポンシブ:に設定されている場合 true、サイズの異なる3つの出力画像(lg, mdsm)が作成され、それぞれのフォルダに保存されます。 デフォルトは false.

ランダムなファイル名を作成するためのメソッドと、ファイルに書き込むための出力ストリームを実装しましょう。


// this generates a random cryptographic filename
AvatarStorage.prototype._generateRandomFilename = function() {
// create pseudo random bytes
var bytes = crypto.pseudoRandomBytes(32);

// create the md5 hash of the random bytes
var checksum = crypto.createHash('MD5').update(bytes).digest('hex');

// return as filename the hash with the output extension
return checksum + '.' + this.options.output;
};

// this creates a Writable stream for a filepath
AvatarStorage.prototype._createOutputStream = function(filepath, cb) {

// create a reference for this to use in local functions
var that = this;

// create a writable stream from the filepath
var output = fs.createWriteStream(filepath);

// set callback fn as handler for the error event
output.on('error', cb);

// set handler for the finish event
output.on('finish', function() {
cb(null, {
destination: that.uploadPath,
baseUrl: that.uploadBaseUrl,
filename: path.basename(filepath),
storage: that.options.storage
});
});

// return the output stream
return output;
};

ここでは、 crypto を使用してランダムなmd5ハッシュを作成し、ファイル名として使用し、オプションからの出力をファイル拡張子として追加します。 また、指定されたファイルパスから書き込み可能なストリームを作成してからストリームを返すヘルパーメソッドを定義しました。 ストリームイベントハンドラーで使用しているため、コールバック関数が必要であることに注意してください。

次に、 _processImage() 実際の画像処理を行う方法。 実装は次のとおりです。


// this processes the Jimp image buffer
AvatarStorage.prototype._processImage = function(image, cb) {

// create a reference for this to use in local functions
var that = this;

var batch = [];

// the responsive sizes
var sizes = ['lg', 'md', 'sm'];

var filename = this._generateRandomFilename();

var mime = Jimp.MIME_PNG;

// create a clone of the Jimp image
var clone = image.clone();

// fetch the Jimp image dimensions
var width = clone.bitmap.width;
var height = clone.bitmap.height;
var square = Math.min(width, height);
var threshold = this.options.threshold;

// resolve the Jimp output mime type
switch (this.options.output) {
case 'jpg':
mime = Jimp.MIME_JPEG;
break;
case 'png':
default:
mime = Jimp.MIME_PNG;
break;
}

// auto scale the image dimensions to fit the threshold requirement
if (threshold && square > threshold) {
clone = (square == width) ? clone.resize(threshold, Jimp.AUTO) : clone.resize(Jimp.AUTO, threshold);
}

// crop the image to a square if enabled
if (this.options.square) {

if (threshold) {
square = Math.min(square, threshold);
}

// fetch the new image dimensions and crop
clone = clone.crop((clone.bitmap.width: square) / 2, (clone.bitmap.height: square) / 2, square, square);
}

// convert the image to greyscale if enabled
if (this.options.greyscale) {
clone = clone.greyscale();
}

// set the image output quality
clone = clone.quality(this.options.quality);

if (this.options.responsive) {

// map through the responsive sizes and push them to the batch
batch = _.map(sizes, function(size) {

var outputStream;

var image = null;
var filepath = filename.split('.');

// create the complete filepath and create a writable stream for it
filepath = filepath[0] + '_' + size + '.' + filepath[1];
filepath = path.join(that.uploadPath, filepath);
outputStream = that._createOutputStream(filepath, cb);

// scale the image based on the size
switch (size) {
case 'sm':
image = clone.clone().scale(0.3);
break;
case 'md':
image = clone.clone().scale(0.7);
break;
case 'lg':
image = clone.clone();
break;
}

// return an object of the stream and the Jimp image
return {
stream: outputStream,
image: image
};
});

} else {

// push an object of the writable stream and Jimp image to the batch
batch.push({
stream: that._createOutputStream(path.join(that.uploadPath, filename), cb),
image: clone
});

}

// process the batch sequence
_.each(batch, function(current) {
// get the buffer of the Jimp image using the output mime type
current.image.getBuffer(mime, function(err, buffer) {
if (that.options.storage == 'local') {
// create a read stream from the buffer and pipe it to the output stream
streamifier.createReadStream(buffer).pipe(current.stream);
}
});
});

};

この方法では多くのことが行われていますが、これが行っていることの要約です。

  • ランダムなファイル名を生成し、Jimp出力画像のmimeタイプを解決して、画像のサイズを取得します。
  • 必要に応じて、しきい値の要件に基づいて画像のサイズを変更し、最小のサイズがしきい値を超えないようにします。
  • オプションで有効になっている場合は、画像を正方形にトリミングします。
  • オプションで有効になっている場合は、画像をグレースケールに変換します。
  • オプションから画像出力品質を設定します。
  • レスポンシブが有効になっている場合、画像はレスポンシブサイズごとに複製および拡大縮小されます(lg, mdsm)次に、を使用して出力ストリームが作成されます。 _createOutputStream() それぞれのサイズの各画像ファイルのメソッド。 各サイズのファイル名は次の形式を取ります [random_filename_hash]_[size].[output_extension]. 次に、イメージクローンとストリームがバッチに入れられて処理されます。
  • レスポンシブが無効になっている場合は、現在の画像とその出力ストリームのみがバッチに入れられて処理されます。
  • 最後に、バッチ内の各アイテムは、 streamifier を使用してJimpイメージバッファーを読み取り可能なストリームに変換し、読み取り可能なストリームを出力ストリームにパイプすることによって処理されます。

次に、残りのメソッドを実装し、ストレージエンジンを使用します。


// multer requires this for handling the uploaded file
AvatarStorage.prototype._handleFile = function(req, file, cb) {

// create a reference for this to use in local functions
var that = this;

// create a writable stream using concat-stream that will
// concatenate all the buffers written to it and pass the
// complete buffer to a callback fn
var fileManipulate = concat(function(imageData) {

// read the image buffer with Jimp
// it returns a promise
Jimp.read(imageData)
.then(function(image) {
// process the Jimp image buffer
that._processImage(image, cb);
})
.catch(cb);
});

// write the uploaded file buffer to the fileManipulate stream
file.stream.pipe(fileManipulate);

};

// multer requires this for destroying file
AvatarStorage.prototype._removeFile = function(req, file, cb) {

var matches, pathsplit;
var filename = file.filename;
var _path = path.join(this.uploadPath, filename);
var paths = [];

// delete the file properties
delete file.filename;
delete file.destination;
delete file.baseUrl;
delete file.storage;

// create paths for responsive images
if (this.options.responsive) {
pathsplit = _path.split('/');
matches = pathsplit.pop().match(/^(.+?)_.+?\.(.+)$/i);

if (matches) {
paths = _.map(['lg', 'md', 'sm'], function(size) {
return pathsplit.join('/') + '/' + (matches[1] + '_' + size + '.' + matches[2]);
});
}
} else {
paths = [_path];
}

// delete the files from the filesystem
_.each(paths, function(_path) {
fs.unlink(_path, cb);
});

};

これで、ストレージエンジンをMulterで使用できるようになりました。

ステップ5—実装 POST /upload ルート

ルートを定義する前に、ルートで使用するためにMulterをセットアップする必要があります。 先に進んで編集しましょう routes/index.js 以下を追加するファイル:

ルート/index.js

var express = require('express');
var router = express.Router();

/**
 * CODE ADDITION
 * 
 * The following code is added to import additional dependencies
 * and setup Multer for use with the /upload route.
 */

// import multer and the AvatarStorage engine
var _ = require('lodash');
var path = require('path');
var multer = require('multer');
var AvatarStorage = require('../helpers/AvatarStorage');

// setup a new instance of the AvatarStorage engine 
var storage = AvatarStorage({
square: true,
responsive: true,
greyscale: true,
quality: 90
});

var limits = {
files: 1, // allow only 1 file per request
fileSize: 1024 * 1024, // 1 MB (max file size)
};

var fileFilter = function(req, file, cb) {
// supported image file mimetypes
var allowedMimes = ['image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'];

if (_.includes(allowedMimes, file.mimetype)) {
// allow supported image files
cb(null, true);
} else {
// throw error for invalid files
cb(new Error('Invalid file type. Only jpg, png and gif image files are allowed.'));
}
};

// setup multer
var upload = multer({
storage: storage,
limits: limits,
fileFilter: fileFilter
});

/* CODE ADDITION ENDS HERE */

ここでは、正方形のトリミング、応答性の高い画像を有効にし、ストレージエンジンのしきい値を設定しています。 また、Multer構成に制限を追加して、最大ファイルサイズが 1 MB 非画像ファイルがアップロードされないようにするため。

次に、を追加しましょう POST /upload 次のようにルーティングします。

/* routes/index.js */

/**
 * CODE ADDITION
 * 
 * The following code is added to configure the POST /upload route
 * to upload files using the already defined Multer configuration
 */

router.post('/upload', upload.single(process.env.AVATAR_FIELD), function(req, res, next) {

var files;
var file = req.file.filename;
var matches = file.match(/^(.+?)_.+?\.(.+)$/i);

if (matches) {
files = _.map(['lg', 'md', 'sm'], function(size) {
return matches[1] + '_' + size + '.' + matches[2];
});
} else {
files = [file];
}

files = _.map(files, function(file) {
var port = req.app.get('port');
var base = req.protocol + '://' + req.hostname + (port ? ':' + port : '');
var url = path.join(req.file.baseUrl, file).replace(/[\\\/]+/g, '/').replace(/^[\/]+/g, '');

return (req.file.storage == 'local' ? base : '') + '/' + url;
});

res.json({
images: files
});

});

/* CODE ADDITION ENDS HERE */

ルートハンドラの前にMulterアップロードミドルウェアをどのように渡したかに注目してください。 The single() メソッドでは、に保存されるファイルを1つだけアップロードできます req.file. 最初のパラメータとして、アクセス元のファイル入力フィールドの名前を取ります process.env.AVATAR_FIELD.

それでは、を使用してアプリを再起動しましょう npm start.

  1. npm start

訪問 localhost:3000 ブラウザで写真をアップロードしてみてください。 これは、現在の構成オプションを使用してPostmanでアップロードルートをテストして得たスクリーンショットの例です。

Multerセットアップでストレージエンジンの構成オプションを微調整して、さまざまな結果を得ることができます。

結論

このチュートリアルでは、 Jimp を使用してアップロードされた画像を操作し、それらをストレージに書き込むMulterで使用するカスタムストレージエンジンを作成することができました。 このチュートリアルの完全なコードサンプルについては、GithubのAdvanced-multer-node-sourcecodeリポジトリを確認してください。