工作中碰到了這樣的需求,需要用nodejs 來上傳文件,之前也只是知道怎么通過瀏覽器來上傳文件, 用nodejs的話, 相當于模擬瀏覽器的行為。 google 了一番之后, 明白了瀏覽器無非就是利用http協議來給服務器傳輸數據, 具體協議就是《RFC 1867 - Form-based File Upload in HTML》, 在瀏覽器上通過form 表單來上傳文件就是通過這個協議,我們可以先看看瀏覽器給服務端發送了什么數據, 就可以依葫蘆畫瓢的把上傳功能實現出來。說起form 表單上傳文件的話, 大家應該很熟悉:
<form action="http://www.qq.com/" method="post"> <input type="text" name="text1" /><br /> <input type="text" name="text2" /><br /> <input type="submit" /> </form>
提交時, 用fiddler 抓包可以看到向服務端發出這樣的數據:
POST http://www.qq.com/ HTTP/1.1 Host: www.qq.com Content-Length: 23 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 text1=hello&text2=world
值得注意的是Content-Type默認為application/x-www-form-urlencoded,所以消息會經過URL編碼。比如你好會編碼為 %E4%BD%A0%E5%A5%BD。
接下來我們看一下通過form 表單是怎么上傳的。大家應該也不陌生:
<form action="http://www.qq.com" method="post" enctype="multipart/form-data"> <input type="file" name="myfile" /> <input type="submit" value="submit" /> </form>
然后新建一個只有hello world字樣的upload.txt文本文件上傳上去,我們再吃用fiddler 來抓下包, 可以發現發送過去的數據稍微復雜了一些(已經去掉了很多的其它沒關系的請求行,比如緩存控制和cookie之類的):
POST http://www.qq.com/ HTTP/1.1 Host: www.qq.com Content-Length: 199 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywr3X7sXBYQQ4ZF5G ------WebKitFormBoundarywr3X7sXBYQQ4ZF5G Content-Disposition: form-data; name="myfile"; filename="upload.txt" Content-Type: text/plain hello world ------WebKitFormBoundarywr3X7sXBYQQ4ZF5G--
根據RFC 1867的定義,我們需要生成一段邊界數據,這個數據不能在內容的其它地方出現,這個可以自己定義, 在每個瀏覽器的生成算法可能都不一樣, 上面的boundary就是分隔數據,生成了分隔數據之后, 就可以把分隔數據放在頭部的Content-Type里面傳送給服務端, 也就是上文的 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywr3X7sXBYQQ4ZF5G, 另外,上傳的內容,需要用分隔數據來分隔成若干個段,然后每段數據里面都有文件的文件名,還有上傳時候的name,服務端就是用這個name來接收文件,還有文件的類型Content-Type,在這個例子里是 text/plain,如果上傳的是png圖片就是image/png。文件類型的一個空行后就是所上傳的文件的內容,在這個例子里也是為了容易理解所以上傳的是文本文件所以內容直接就能夠顯示出來,如果上傳的是圖片文件, 因為是二進制文件,fiddler 就顯示的是亂碼。 文件的內容結束之后就是一個空行再加上邊界數據。
了解了發送格式的細節之后, 下一步就是使用nodejs來編程實現,簡單來講, 就是按照格式把數據發送給服務端就行了。
const http=require('http'); const fs=require('fs'); //post地址為本地服務的一個php,用于測試上傳是否成功 var options={ hostname: 'localhost', port: 80, path: '/get.php', method: 'POST' } //生成分隔數據 var boundaryKey='----WebKitFormBoundaryjLVkbqXtIi0YGpaB'; //讀取需要上傳的文件內容 fs.readFile('./upload.txt', function (err, data) { //拼裝分隔數據段 var payload='--' + boundaryKey + '\r\n' + 'Content-Disposition:form-data; name="myfile"; filename="upload.txt"\r\n' + 'Content-Type:text/plain\r\n\r\n'; payload +=data; payload +='\r\n--' + boundaryKey + '--'; //發送請求 var req=http.request(options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('body:' + chunk); }); }); req.on('error', function(e) { console.error("error:"+e); }); //把boundary、要發送的數據大小以及數據本身寫進請求 req.setHeader('Content-Type', 'multipart/form-data; boundary='+boundaryKey+''); req.setHeader('Content-Length', Buffer.byteLength(payload, 'utf8')); req.write(payload); req.end; });
本文重點在于了解協議并且用代碼實現出來, 代碼組織上面還有很多優化的地方。
最后在本地apache,簡單寫一個php來保存上傳的文件來用作測試:
<?php $filePath='./upload.txt'; move_uploaded_file($_FILES['myfile']['tmp_name'] , $filePath); echo "ok"; ?>
另外,根據RFC 1867 還可以實現一次上傳多個文件的功能, 這個在這里就不詳述, 需要的話可以詳細參考RFC 1867來實現。
標:
上傳文件、文件夾
給你三秒鐘,思考下可能用到哪些知識點
前期準備
1.1 整體流程
1. index.js模塊: 程序入口, 有服務器(server.js)和路由(route.js)和處理(handler.js)模塊組成;
2. server.js模塊: 專門處理不同路由, 并做相應的處理;
3. route.js模塊: 請求的地址, 處理方法, 響應, 請求;
4. handler.js模塊: 封裝不同的方法,交由路由route.js模塊來選擇調用;
5. HTML文件: 存在HTML文件; 6. package.json模塊: CommonJS規定的規范;
我想這個不同于理論的實踐,會慢慢為接下來打下堅實的基礎的。
1.2 模塊包
formidable是一個用于處理文件、圖片、視頻等數據上傳的模塊,支持GB級上傳數據處理,支持多種客戶端數據提交。有極高的測試覆蓋率,非常適合在生產環境中使用。
npm配置安裝
npm install formidable@latest
util是一個Node.js核心模塊,util模塊設計的主要目的是為了滿足Node內部API的需求。其中包括:格式化字符串、對象的序列化、實現對象繼承等常用方法。要使用util模塊中的方法,只需require('util')引入即可。
1.3 package.json文件
package.json:定義了這個項目所需要的各種模塊,以及項目的配置信息(比如名稱、版本、許可證等元數據)。npm install命令根據這個配置文件,自動下載所需的模塊.
1. scripts指定了運行腳本命令的npm命令行縮寫,比如start指定了運行npm run start時,所要執行的命令。
2. dependencies字段指定了項目運行所依賴的模塊;
3. devDependencies指定項目開發所需要的模塊。
指定版本:比如1.2.2,遵循“大版本.次要版本.小版本”的格式規定,安裝時只安裝指定版本。
波浪號(tilde)+指定版本:比如~1.2.2,表示安裝1.2.x的最新版本(不低于1.2.2),但是不安裝1.3.x,也就是說安裝時不改變大版本號和次要版本號。
插入號(caret)+指定版本:比如?1.2.2,表示安裝1.x.x的最新版本(不低于1.2.2),但是不安裝2.x.x,也就是說安裝時不改變大版本號。
需要注意的是,如果大版本號為0,則插入號的行為與波浪號相同,這是因為此時處于開發階段,即使是次要版本號變動,也可能帶來程序的不兼容。 latest:安裝最新版本。
例子如下:
{
"name": "upload",
"version": "1.0.1",
"description": "文件上傳下載",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "hhw",
"license": "ISC"
//依賴模塊
"dependencies": {
"formidable":"formidable@1.0.17",
},
}
第二部分 代碼實現部分
2.1 服務器server.js代碼實現
//引入http utl 模塊
var http=require('http');
var url=require('url');
//程序入口, 選擇路由, 并做相應的處理
function start(route, handler){
//選擇路由
function onRequest(request, response) {
//url.parse(): 輸入 URL 字符串,返回一個對象。
//對象中 pathname 所代表的路徑
var pathname=url.parse(request.url).pathname;
route(pathname, handler, response, request);
}
//服務器對不同的路由做出不同的響應
http.createServer(onRequest).listen(3000);
}
//導出接口
exports.start=start;
2.2 路由選擇代碼
//具體處理路由的功能模塊
//請求的地址, 處理方法<upload download...>, 響應, 請求
function route(pathname, handler, response, request){
//如果請求的方法是存在的函數就返回交給服務器處理
if (typeof handler[pathname]==='function'){
return handler[pathname](request, response);
} else{
//否則返回404
response.writeHead(404, {'Content-Type': 'text/html'});
response.write('404 Not Found!');
response.end();
}
}
exports.route=route;
2.3 具體處理不同需求的代碼實現
node-formidable
//解析字符串, fs模塊, 格式化模塊
var fs=require('fs'),
formidable=require('formidable'),
util=require('util'),
http=require('http');
const path=require('path');
//程序入口展示HTML文件
function home(request, response){
//加載本地頁面HTML
fs.readFile('./index.html', function(err, data) {
response.writeHead(200, {'Content-Type': 'text/html'});
response.write(data);
response.end();
});
}
//formidable是一個用于處理文件、圖片、視頻等數據上傳的模塊,支持GB級上傳數據處理,支持多種客戶端數據提交。有極高的測試覆蓋率,非常適合在生產環境中使用。
function uploads(request, response) {
//form表單
var form=new formidable.IncomingForm();
//保留后綴
form.keepExtensions=true;
form.encoding='utf-8';
//上傳的數據保存的路徑
form.uploadDir='./';
//該方法會轉換請求中所包含的表單數據,callback會包含所有字段域和文件信息
// fields 是普通表單數據
// files 是文件數據
form.parse(request, function(err, fields, files) {
response.writeHead(200, {'Content-type': 'text/plain'});
//上傳文件的名稱
var filename=files.upload.name;
var path=files.upload.path;
//更改名稱
fs.rename(path, form.uploadDir + filename);
//響應 格式化打印
response.end(util.inspect(form));
});
}
function show (response, request) {
console.log('show module');
fs.readFile ('/tmp/test.png ', 'binary', function (error, file) {
if (error) {
response.writeHead(200, {'Content-Type': 'text/html'});
response.write(error);
response.end();
} else {
response.writeHead(200, {'Content-Type': 'image/png'});
response.write(file, 'binary');
response.end();
}
});
}
function downloads(requset, response){
//node官網圖標
var URL="http://nodejs.cn/static/images/logos/nodejs-green.png";
//request
request
.get(URL)
//監聽response
.on('response', function(res) {
console.log(res.statusCode); // 200
console.log(res.headers['content-type']); // 'image/png'
console.log(res.headers['content-length']);
//打印接受的數據大小
res.on('data', function(data){
console.log('接受到數據' + data.length )
})
})
//監聽錯誤信息
.on('error', function(error){
consloe.log(error);
})
//以流的形式寫入創建的test.png文件
.pipe(fs.createWriteStream('test.png'));
response.writeHead(200);
response.write('請前往同級目錄下查看下載內容');
response.end();
}
exports.home=home;
exports.upload=uploads;
exports.download=downloads;
2.4 程序入口調用<待補充>
//引用模塊
var server=require('./server');
var router=require('./router');
var handler=require('./handler');
var formidable=require('formidable');
//調用具體方法
var method={};
method['/']=handler.home;
method['/home']=handler.home;
method['/upload']=handler.upload;
//交給服務器模塊處理
server.start(router.route, method);
2.5 首頁中上傳模塊HTML文件 <待補充>
最早的HTTP協議中是不支持文件上傳的,在1995年制定的rfc1867規范中,在HTTP POST請求的內容類型Content-Type
中擴展了multipart/form-data
類型,該類型用于向服務器發送二進制數據,以便支持文件的上傳。
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
//上傳文件時需要通過enctype
屬性將編碼方式設置為multipart/form-data
<form action="/upload", enctype="multipart/form-data" method="post">
<input type="file" name="upload" multiple="multipart">
<input type="submit" value="提888交">
</form>
<form action="/download", enctype="multipart/form-data" method="get">
<input type="submit" value='下載'>
</form>
</body>
</html>
者:訣九 前端名獅
轉發鏈接:https://mp.weixin.qq.com/s/BMg8bFUwa4gmm6v2acAe7Q
*請認真填寫需求信息,我們會在24小時內與您取得聯系。