用Node.js 寫web框架(五)

一週沒更新啊...不過這周確實挺忙的(我纔不說我偷懶來着呢)。 html

今天主要是完成POST方法提交multipart的支持(就是文件上傳啦)。
首先貼一下改進過的參數包裝函數: 數組

exports.wrap = function(req, callback) {
	if(req.method == POST_METHOD) {
		if(req.headers[TYPE] != POST_DATA) { // POST data
			var chunks = [];
			req.on(DATA_EVENT, function(chunk) {
				chunks.push(chunk);
			})
			req.on(END_EVENT, function() {
				var buffer = Buffer.concat(chunks);
				var param = QS.parse(buffer.toString());
				callback(param);
			})
		} else { // POST multipart form data
			parseMultipart(req, callback);
		}
	} else { // GET(and other kinds of methods) data
		var queryStr = URL.parse(req.url).query;
		if(!queryStr) {
			callback({});
		}
		callback(QS.parse(queryStr));
	}
}

上次的版本是使用Buffer.copy,後來查了一下API,發現有一個Buffer.concat是直接把數組中的全部Buffer鏈接起來,很不錯。其餘的跟上一個版本沒有什麼大變化,這裏主要說的是parseMultipart函數。 瀏覽器

說這個函數之前,必須提一下HTTP協議這部分的細節了(協議都不知道,還解析啥)。這個屬於HTTP協議比較複雜的一部分: 服務器

一、POST方法發送的Request中協議頭部分的區別: app

正常狀況下,POST方法發送的協議頭中,content-type這個字段的值爲:application/x-www-form-urlencoded。而當在form中添加了enctype="multipart/form-data"這個屬性以後,content-type字段爲:multipart/form-data; boundary=----WebKitFormBoundarycE8JeFAEuGHTJnRh。這裏有兩個部分,第一個部分標識這個POST請求的類別爲:multipart/form-data,第二部分則標識在整個chunk中,boundary(也就是分隔符)是什麼。這裏這個boundary是瀏覽器生成的,不會與文件內容相同 函數

二、下面作一個完整的測試過來,來看看chunk裏究竟是什麼樣的: post

首先我創建一個表單: 性能

<form action="/test/testUpload" method="post" enctype="multipart/form-data">
	filename : <input type="text" name="name"/>
	file : <input type="file" name="file"/>
    <input type="submit"/>
</form>

文件內容爲: 測試

測試內容1
測試內容2
ABCDabcd1234


aaa

而後後臺接受的chunks以下: url

------WebKitFormBoundarycE8JeFAEuGHTJnRh
Content-Disposition: form-data; name="name"

asdfasdf
------WebKitFormBoundarycE8JeFAEuGHTJnRh
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain

測試內容1
測試內容2
ABCDabcd1234


aaa
------WebKitFormBoundarycE8JeFAEuGHTJnRh--

首先是分隔符\r\n,而後是這部分的Disposition(我就翻譯成配置了,沒見到國內有什麼好的翻譯)。這裏會標記出這部分的name是什麼,若是這個表單域是文件,還會標識filename。
若是該表單域是文件,那麼還會有一個Content-Type來標識文件類別是什麼(就目前看解析意義不大,這個我最近會看看其餘服務器是如何處理這部分的)。
而後\r\n\r\n,下面接着的就是表單內容(或者文件內容),並以\r\n結束。
以上內容循環,直到最後一個表單域結束,結尾以分隔符--\r\n結束。

更詳細的內容見:Form-based File Upload in HTML

這樣咱們的解析過程就是:
一、首先從請求頭獲取boundary
二、獲取所有的chunk,並拼接到一個buffer(這個我之後會改爲每次來一個chunk,就直接解析)
三、對這個buffer按着上面的過程進行解析

解析的所有代碼我就不放上來了,有點長(100行+)。反正根據上面的分析過程你們都能寫的出來。

下面的另外一個問題是如何包裹上傳好的文件參數。這裏我先說一下這部分的總體流程:
一、將解析後的chunk中的文件內容存入一個臨時文件夾,並分配一個隨機文件名
二、將文件原始文件名(能夠從chunk中獲得),臨時文件名,其餘參數,包裹爲一個object,並傳給views函數。

隨機文件名我是使用了當前時間的16進製做爲文件名的:

var name = new Date().getTime().toString(16);
而後的問題出在寫文件上。完整看下來的人可能知道我在讀取服務器配置的時候是使用了同步的API,這是由於我以爲這部分只有啓動的時候纔會執行一次,使用同步不會出現性能問題。可是在服務器運行期間,我對本身的要求是禁止使用同步API的。這就致使可能文件還沒寫入完畢,views函數就已經開始執行了。

這裏我使用了一個barrier的方案:

var barrier = [];
barrier.push(false);

req.on(END_EVENT, function(){
	//..解析

	if(checkBarrier()){
		barrier[0] = true;
		callback(param);
	}
})

function writeFile(filename, content){
	var fileIndex = barrier.length;
	var filepath = TMP_DIR + '/' + filename;

	barrier.push(false);
	FS.writeFile(filepath, content, function(err){
		if(err){
			throw err;
		}
		barrier[fileIndex] = true;
		if(checkBarrier()){
			callback(param);
		}
	})
}

function checkBarrier(){
	for(var i = 0; i < barrier.length; i++){
		if(!barrier[i]){
			return false;
		}
	}
	return true;
}
這個barrier裏存放的是當前解析過程,全部文件寫入過程是否結束。只有當所有都結束了的時候,才調用callback來執行下一部分的veiws方法。
相關文章
相關標籤/搜索