ホームページ > 記事 > ウェブフロントエンド > Node.js_node.js で HTTP を使用してファイルをアップロードする方法
開発環境
開発環境として Visual Studio Express 2013 for Web を使用しますが、Node.js 開発には使用できません。このためには、Node.js Tools for Visual Studio をインストールする必要があります。インストール後、Visual Studio Express 2013 for Web は Node.js IDE 環境に変換され、このアプリケーションの作成に必要なものがすべて提供されます。ここで提供されるガイダンスに基づいて、次のものが必要です:
インストールが完了したら、Visual Studio Express 2013 for Web を実行し、Node.js 対話型ウィンドウを使用してインストールを確認します。Node.js 対話型ウィンドウは、[表示] -> [その他のウィンドウ] -> で表示できます。 Node.js インタラクティブ [ウィンドウ] の下にあります。Node.js インタラクティブ ウィンドウが実行されたら、いくつかのコマンドを入力して、すべてが正常であるかどうかを確認する必要があります。
図 1 Node.js 対話型ウィンドウ
インストールを確認したので、GB レベルのファイルのアップロードをサポートする Node.js デーモンの作成を開始する準備が整いました。まず、新しいプロジェクトを作成し、空の Node.js Web アプリケーション テンプレートを選択します。 .
図 2 空の Node.js Web アプリケーション テンプレートを使用した新しいプロジェクト
プロジェクトが作成されると、server.js というファイルとノード パッケージ マネージャー (npm) がソリューション ブラウザーに表示されます。
図 3 ソリューション マネージャーの Node.js アプリケーション
server.js ファイルには、Node.js を使用して基本的な Hello World アプリケーションを作成するために必要なコードが含まれています。
図 4 Hello World アプリケーション
次に、server.js からこのコードを削除し、G レベル ファイル アップロード用のバックエンド コードを Node.js に挿入します。次に、npm を使用して、このプロジェクトに必要な依存関係をインストールする必要があります:
図 5 npm を使用して必要なモジュールをインストールする
モジュールがインストールされると、ソリューション エクスプローラーからモジュールを確認できるようになります。
図 6 ソリューション エクスプローラーにインストールされているモジュールが表示される
次のステップでは、ソリューション エクスプローラーで新しい「Scripts」フォルダーを作成し、そのフォルダーに「workeruploadchunk.js」と「workerprocessfile.js」を追加する必要があります。また、
jQuery 2.xおよび SparkMD5 ライブラリをダウンロードし、「Scripts」フォルダーに追加する必要があります。 最後に、「Default.html」ページを追加する必要があります。
Node.js バックエンドの作成まず、Node.js の "require()" 関数を使用して、G レベル ファイルをバックグラウンドでアップロードするモジュールをインポートする必要があります。 「path」モジュールと「crypto」モジュールもインポートしたことに注意してください。 「パス」モジュールは、アップロードされたファイルのチャンクのファイル名を生成するメソッドを提供します。 「crypto」モジュールは、アップロードされたファイルの MD5 チェックサムを生成するメソッドを提供します。
// The required modules var express = require('express'); var formidable = require('formidable'); var fs = require('fs-extra'); var path = require('path'); var crypto = require('crypto');コードの次の行は、奇跡を目撃する瞬間です。
这行代码是用来创建express应用的。express应用是一个封装了Node.js底层功能的中间件。如果你还记得那个由Blank Node.js Web应用模板创建的"Hello World" 程序,你会发现我导入了"http"模块,然后调用了"http.CreateServer()"方法创建了 "Hello World" web应用。我们刚刚创建的express应用内建了所有的功能。
现在我们已经创建了一个express应用,我们让它呈现之前创建的"Default.html",然后让应用等待连接。
// Serve up the Default.html page app.use(express.static(__dirname, { index: 'Default.html' })); // Startup the express.js application app.listen(process.env.PORT || 1337); // Path to save the files var uploadpath = 'C:/Uploads/CelerFT/';
express应用有app.VERB()方法,它提供了路由的功能。我们将使用app.post()方法来处理"UploadChunk" 请求。在app.post()方法里我们做的第一件事是检查我们是否在处理POST请求。接下去检查Content-Type是否是mutipart/form-data,然后检查上传的文件块大小不能大于51MB。
// Use the post method for express.js to respond to posts to the uploadchunk urls and // save each file chunk as a separate file app.post('*/api/CelerFTFileUpload/UploadChunk*', function(request,response) { if (request.method === 'POST') { // Check Content-Type if (!(request.is('multipart/form-data'))){ response.status(415).send('Unsupported media type'); return; } // Check that we have not exceeded the maximum chunk upload size var maxuploadsize =51 * 1024 * 1024; if (request.headers['content-length']> maxuploadsize){ response.status(413).send('Maximum upload chunk size exceeded'); return; }
一旦我们成功通过了所有的检查,我们将把上传的文件块作为一个单独分开的文件并将它按顺序数字命名。下面最重要的代码是调用fs.ensureDirSync()方法,它使用来检查临时目录是否存在。如果目录不存在则创建一个。注意我们使用的是该方法的同步版本。
// Get the extension from the file name var extension =path.extname(request.param('filename')); // Get the base file name var baseFilename =path.basename(request.param('filename'), extension); // Create the temporary file name for the chunk var tempfilename =baseFilename + '.'+ request.param('chunkNumber').toString().padLeft('0', 16) + extension + ".tmp"; // Create the temporary directory to store the file chunk // The temporary directory will be based on the file name var tempdir =uploadpath + request.param('directoryname')+ '/' + baseFilename; // The path to save the file chunk var localfilepath =tempdir + '/'+ tempfilename; if (fs.ensureDirSync(tempdir)) { console.log('Created directory ' +tempdir); }
正如我之前提出的,我们可以通过两种方式上传文件到后端服务器。第一种方式是在web浏览器中使用FormData,然后把文件块作为二进制数据发送,另一种方式是把文件块转换成base64编码的字符串,然后创建一个手工的multipart/form-data encoded请求,然后发送到后端服务器。
所以我们需要检查一下是否在上传的是一个手工multipart/form-data encoded请求,通过检查"CelerFT-Encoded"头部信息,如果这个头部存在,我们创建一个buffer并使用request的ondata时间把数据拷贝到buffer中。
在request的onend事件中通过将buffer呈现为字符串并按CRLF分开,从而从 multipart/form-data encoded请求中提取base64字符串。base64编码的文件块可以在数组的第四个索引中找到。
通过创建一个新的buffer来将base64编码的数据重现转换为二进制。随后调用fs.outputFileSync()方法将buffer写入文件中。
// Check if we have uploaded a hand crafted multipart/form-data request // If we have done so then the data is sent as a base64 string // and we need to extract the base64 string and save it if (request.headers['celerft-encoded']=== 'base64') { var fileSlice = newBuffer(+request.headers['content-length']); var bufferOffset = 0; // Get the data from the request request.on('data', function (chunk) { chunk.copy(fileSlice , bufferOffset); bufferOffset += chunk.length; }).on('end', function() { // Convert the data from base64 string to binary // base64 data in 4th index of the array var base64data = fileSlice.toString().split('\r\n'); var fileData = newBuffer(base64data[4].toString(), 'base64'); fs.outputFileSync(localfilepath,fileData); console.log('Saved file to ' +localfilepath); // Send back a sucessful response with the file name response.status(200).send(localfilepath); response.end(); }); }
二进制文件块的上传是通过formidable模块来处理的。我们使用formidable.IncomingForm()方法得到multipart/form-data encoded请求。formidable模块将把上传的文件块保存为一个单独的文件并保存到临时目录。我们需要做的是在formidable的onend事件中将上传的文件块保存为里一个名字。
else { // The data is uploaded as binary data. // We will use formidable to extract the data and save it var form = new formidable.IncomingForm(); form.keepExtensions = true; form.uploadDir = tempdir; // Parse the form and save the file chunks to the // default location form.parse(request, function (err, fields, files) { if (err){ response.status(500).send(err); return; } //console.log({ fields: fields, files: files }); }); // Use the filebegin event to save the file with the naming convention /*form.on('fileBegin', function (name, file) { file.path = localfilepath; });*/ form.on('error', function (err) { if (err){ response.status(500).send(err); return; } }); // After the files have been saved to the temporary name // move them to the to teh correct file name form.on('end', function (fields,files) { // Temporary location of our uploaded file var temp_path = this.openedFiles[0].path; fs.move(temp_path , localfilepath,function (err){ if (err) { response.status(500).send(err); return; } else { // Send back a sucessful response with the file name response.status(200).send(localfilepath); response.end(); } }); }); // Send back a sucessful response with the file name //response.status(200).send(localfilepath); //response.end(); } }
app.get()方法使用来处理"MergeAll"请求的。这个方法实现了之前描述过的功能。
// Request to merge all of the file chunks into one file app.get('*/api/CelerFTFileUpload/MergeAll*', function(request,response) { if (request.method === 'GET') { // Get the extension from the file name var extension =path.extname(request.param('filename')); // Get the base file name var baseFilename =path.basename(request.param('filename'), extension); var localFilePath =uploadpath + request.param('directoryname')+ '/' + baseFilename; // Check if all of the file chunks have be uploaded // Note we only wnat the files with a *.tmp extension var files =getfilesWithExtensionName(localFilePath, 'tmp') /*if (err) { response.status(500).send(err); return; }*/ if (files.length !=request.param('numberOfChunks')){ response.status(400).send('Number of file chunks less than total count'); return; } var filename =localFilePath + '/'+ baseFilename +extension; var outputFile =fs.createWriteStream(filename); // Done writing the file // Move it to top level directory // and create MD5 hash outputFile.on('finish', function (){ console.log('file has been written'); // New name for the file var newfilename = uploadpath +request.param('directoryname')+ '/' + baseFilename + extension; // Check if file exists at top level if it does delete it //if (fs.ensureFileSync(newfilename)) { fs.removeSync(newfilename); //} // Move the file fs.move(filename, newfilename ,function (err) { if (err) { response.status(500).send(err); return; } else { // Delete the temporary directory fs.removeSync(localFilePath); varhash = crypto.createHash('md5'), hashstream = fs.createReadStream(newfilename); hashstream.on('data', function (data) { hash.update(data) }); hashstream.on('end', function (){ var md5results =hash.digest('hex'); // Send back a sucessful response with the file name response.status(200).send('Sucessfully merged file ' + filename + ", " + md5results.toUpperCase()); response.end(); }); } }); }); // Loop through the file chunks and write them to the file // files[index] retunrs the name of the file. // we need to add put in the full path to the file for (var index infiles) { console.log(files[index]); var data = fs.readFileSync(localFilePath +'/' +files[index]); outputFile.write(data); fs.removeSync(localFilePath + '/' + files[index]); } outputFile.end(); } }) ;
注意Node.js并没有提供String.padLeft()方法,这是通过扩展String实现的。
// String padding left code taken from // http://www.lm-tech.it/Blog/post/2012/12/01/String-Padding-in-Javascript.aspx String.prototype.padLeft = function (paddingChar, length) { var s = new String(this); if ((this.length< length)&& (paddingChar.toString().length > 0)) { for (var i = 0; i < (length - this.length) ; i++) { s = paddingChar.toString().charAt(0).concat(s); } } return s; } ;
其中一件事是,发表上篇文章后我继续研究是为了通过域名碎片实现并行上传到CeleFT功能。域名碎片的原理是访问一个web站点时,让web浏览器建立更多的超过正常允许范围的并发连接。 域名碎片可以通过使用不同的域名(如web1.example.com,web2.example.com)或者不同的端口号(如8000, 8001)托管web站点的方式实现。
示例中,我们使用不同端口号托管web站点的方式。
我们使用 iisnode 把 Node.js集成到 IIS( Microsoft Internet Information Services)实现这一点。 下载兼容你操作系统的版本 iisnode (x86) 或者 iisnode (x64)。 下载 IIS URL重写包。
一旦安装完成(假定windows版Node.js已安装),到IIS管理器中创建6个新网站。将第一个网站命名为CelerFTJS并且将侦听端口配置为8000。
图片7在IIS管理器中创建一个新网站
然后创建其他的网站。我为每一个网站都创建了一个应用池,并且给应用池“LocalSystem”级别的权限。所有网站的本地路径是C:\inetpub\wwwroot\CelerFTNodeJS。
图片8 文件夹层级
我在Release模式下编译了Node.js应用,然后我拷贝了server.js文件、Script文件夹以及node_modules文件夹到那个目录下。
要让包含 iisnode 的Node.js的应用工作,我们需要创建一个web.config文件,并在其中添加如下得内容。
<defaultDocument> <files> <add value="server.js" /> </files> </defaultDocument> <handlers> <!-- indicates that the server.js file is a node.js application to be handled by the iisnode module --> <add name="iisnode" path="*.js" verb="*" modules="iisnode" /> </handlers> <rewrite> <rules> <rule name="CelerFTJS"> <match url="/*" /> <action type="Rewrite" url="server.js" /> </rule> <!-- Don't interfere with requests for node-inspector debugging --> <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true"> <match url="^server.js\/debug[\/]?" /> </rule> </rules> </rewrite>
web.config中各项的意思是让iisnode处理所有得*.js文件,由server.js 处理任何匹配"/*"的URL。
如果你正确的做完了所有的工作,你就可以通过http://localhost:8000浏览网站,并进入CelerFT "Default.html"页面。
下面的web.config项可以改善 iisnode中Node.js的性能。
为了使用域名碎片来实现并行上传,我不得不给Node.js应用做些修改。我第一个要修改的是让Node.js应用支持跨域资源共享。我不得不这样做是因为使用域碎片实际上是让一个请求分到不同的域并且同源策略会限制我的这个请求。
好消息是XMLttPRequest 标准2规范允许我这么做,如果网站已经把跨域资源共享打开,更好的是我不用为了实现这个而变更在"workeruploadchunk.js"里的上传方法。
// 使用跨域资源共享 // Taken from http://bannockburn.io/2013/09/cross-origin-resource-sharing-cors-with-a-node-js-express-js-and-sencha-touch-app/ var enableCORS = function(request,response, next){ response.header('Access-Control-Allow-Origin', '*'); response.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); response.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content- Length, X-Requested-With' ) ; // 拦截OPTIONS方法 if ('OPTIONS' ==request.method){ response.send(204); } else { next(); } } ; // 在表达式中使用跨域资源共享 app. use ( enableCORS ) ;
为了使server.js文件中得CORS可用,我创建了一个函数,该函数会创建必要的头以表明Node.js应用支持CORS。另一件事是我还需要表明CORS支持两种请求,他们是:
简单请求:
1、只用GET,HEAD或POST。如果使用POST向服务器发送数据,那么发送给服务器的HTTP POST请求的Content-Type应是application/x-www-form-urlencoded, multipart/form-data, 或 text/plain其中的一个。
2、HTTP请求中不要设置自定义的头(例如X-Modified等)
预检请求:
1、使用GET,HEAD或POST以外的方法。假设使用POST发送请求,那么Content-Type不能是application/x-www-form-urlencoded, multipart/form-data, or text/plain,例如假设POST请求向服务器发送了XML有效载荷使用了application/xml or text/xml,那么这个请求就是预检的。
2、在请求中设置自定义头(比如请求使用X-PINGOTHER头)。
在我们的例子中,我们用的是简单请求,所以我们不需要做其他得工作以使例子能够工作。
在 "workeruploadchunk.js" 文件中,我向 self.onmessage 事件添加了对进行并行文件数据块上传的支持.
// We are going to upload to a backend that supports parallel uploads. // Parallel uploads is supported by publishng the web site on different ports // The backen must implement CORS for this to work else if(workerdata.chunk!= null&& workerdata.paralleluploads ==true){ if (urlnumber >= 6) { urlnumber = 0; } if (urlcount >= 6) { urlcount = 0; } if (urlcount == 0) { uploadurl = workerdata.currentlocation +webapiUrl + urlnumber; } else { // Increment the port numbers, e.g 8000, 8001, 8002, 8003, 8004, 8005 uploadurl = workerdata.currentlocation.slice(0, -1) + urlcount +webapiUrl + urlnumber; } upload(workerdata.chunk,workerdata.filename,workerdata.chunkCount, uploadurl, workerdata.asyncstate); urlcount++; urlnumber++; }
Default.html ページで現在の URL を保存しました。これは、この情報をファイル アップロード ワーカーに送信するためです。
54bdf357c58b8a65c66d7c19c8e4d114
b3e769adb7113c2495028565420f063b以下のコードは、アップロード メッセージに加えられた変更を示しています。54bdf357c58b8a65c66d7c19c8e4d114e30c1d30ab413cff565a603651aae9f010075490a139ced27a913d426cd6c7fda50f63e85abe05c10a77b4f843f3ffa154bdf357c58b8a65c66d7c19c8e4d114
54bdf357c58b8a65c66d7c19c8e4d114
c6dfd69bf52022bacbf9b7667503873b// ウェブワーカーにメッセージを送信してアップロードします54bdf357c58b8a65c66d7c19c8e4d114
"background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;">8d4f5d8d2e74fe9b1ecee719105d62d0case54bdf357c58b8a65c66d7c19c8e4d114683b5369b2fc896dad53b4c207baeefc 54bdf357c58b8a65c66d7c19c8e4d1148106effeb632fdac022d3b80e58defcf'upload'54bdf357c58b8a65c66d7c19c8e4d114fd4795f1fbd84b416d530d0194f21abf:54bdf357c58b8a65c66d7c19c8e4d114
c6dfd69bf52022bacbf9b7667503873b// 54bdf357c58b8a65c66d7c19c8e4d114adaa99a5e4032576506e793bb19df116バックエンドが並列アップロードをサポートしているかどうかを確認してください
varParalleluploads =false;
if ($('#select_Parallelupload').prop('checked')) {
パラレルアップロード = true;
}
Uploadworkers[data.id].postMessage({ 'chunk': data.blob, 'filename':data.filename,
'ディレクトリ': $("#select_directory").val(), 'chunkCount':data.chunkCount,
'asyncstate':data.asyncstate,'Paralleluploads':Paralleluploads, 'currentlocation':
currentLocation, 'id': data.id });
休憩;
CelerFT インターフェイスのサポート来并行上传が最終変更されました。
带行上传的CelerFT
この项目的代网可以再我的 github 资源库上找到