實時開發框架Meteor 實際應用系列---文件的上傳和下載

寫在前面的話

  1. 該篇博客主要講文件的下載和上傳
  2. 基於0.8.0版本
  3. 基於iron-router包
  4. 本篇博客在ubuntu系統下操作。
  5. 本博客講到是應用,需要掌握一些基礎前提,如meteor文件夾機制等,這裏不會詳說。不明白到地方可以看下我寫Meteor API博客系列。
  6. 博客地址:http://blog.csdn.net/a6383277/article/details/23023269 轉載請註明出處

由於Meteor是單頁應用沒有自帶路由功能,所以通常情況下,需要一個第三包 來實現路由功能。文件打上傳和下載也是需要利用到router.因此在這裏使用iron router 包. 在利用iron到基礎上,文件到下載和上傳都是非常簡單到。因此 本篇博客還會簡寫一下 我解決問題的過程。

iron-router

在創建 創建app以及安裝iron-router前 ,我們需要用到第三方包管理器meteorite(貌似不支持windows), 
這裏不多介紹,具體到應用和說明 如果有興趣可以去 https://github.com/oortcloud/meteorite 瞭解一下

 sudo npm install -g meteorite 

安裝完成後,添加iron router

meteor create test
cd test
mrt add iron-router #使用meteorite

在添加iron-router時可能需要段時間,或者會卡住,請耐心。估計是網絡不好到原因

先看文件上傳代碼: 
/client/test.html

<head>
  <title>testUpload</title>
</head>

<body>
  {{> hello}}
</body>

<template name="hello">
  <form action="/files" method="post" enctype="multipart/form-data">
    <p>要上傳的文件2<input type="file" name="file2"/></p>
    <p><input type="submit" value="上傳" /></p>
</form>
</template>

/server/index.js

Router.map(function () {
  this.route('serverFile', {
    where: 'server',
    path: '/files',

    action: function () {
     //下面這句是重點
      console.log(this.request.files);
      this.response.writeHead(200, {'Content-Type': 'text/html'});
      this.response.end('hello from server');
    }
  });
});

啓動meteor

meteor

訪問 localhost:3000

可以看到一個文件上傳到表單,進行文件上傳操作。 可以看到後臺打印了類似下面的信息

I20140406-12:19:34.064(8)? { file2: 
I20140406-12:19:34.214(8)?    { originalFilename: 'install.sh',
I20140406-12:19:34.214(8)?      path: '/tmp/4066-1jiytiv.sh',
I20140406-12:19:34.214(8)?      headers: 
I20140406-12:19:34.215(8)?       { 'content-disposition': 'form-data; name="file2"; filename="install.sh"',
I20140406-12:19:34.215(8)?         'content-type': 'application/x-shellscript' },
I20140406-12:19:34.215(8)?      ws: 
I20140406-12:19:34.215(8)?       { _writableState: [Object],
I20140406-12:19:34.215(8)?         writable: true,
I20140406-12:19:34.215(8)?         domain: null,
I20140406-12:19:34.215(8)?         _events: [Object],
I20140406-12:19:34.216(8)?         _maxListeners: 10,
I20140406-12:19:34.216(8)?         path: '/tmp/4066-1jiytiv.sh',
I20140406-12:19:34.216(8)?         fd: null,
I20140406-12:19:34.216(8)?         flags: 'w',
I20140406-12:19:34.216(8)?         mode: 438,
I20140406-12:19:34.217(8)?         start: undefined,
I20140406-12:19:34.217(8)?         pos: undefined,
I20140406-12:19:34.217(8)?         bytesWritten: 6711,
I20140406-12:19:34.217(8)?         closed: true,
I20140406-12:19:34.217(8)?         open: [Function],
I20140406-12:19:34.217(8)?         _write: [Function],
I20140406-12:19:34.217(8)?         destroy: [Function],
I20140406-12:19:34.218(8)?         close: [Function],
I20140406-12:19:34.218(8)?         destroySoon: [Function],
I20140406-12:19:34.218(8)?         pipe: [Function],
I20140406-12:19:34.218(8)?         write: [Function],
I20140406-12:19:34.218(8)?         end: [Function],
I20140406-12:19:34.218(8)?         setMaxListeners: [Function],
I20140406-12:19:34.218(8)?         emit: [Function],
I20140406-12:19:34.219(8)?         addListener: [Function],
I20140406-12:19:34.219(8)?         on: [Function],
I20140406-12:19:34.219(8)?         once: [Function],
I20140406-12:19:34.220(8)?         removeListener: [Function],
I20140406-12:19:34.220(8)?         removeAllListeners: [Function],
I20140406-12:19:34.220(8)?         listeners: [Function] },
I20140406-12:19:34.220(8)?      size: 6711,
I20140406-12:19:34.220(8)?      name: 'install.sh' } }

有點經驗到程序員通過這些信息應該知道怎麼處理這個上傳的文件了。這裏不詳說了。

接下來看文件下載部分。這個部分也簡單。這裏只是一個demo。具體如何應用就是nodejs api 的response部分了。 
在/server/test.js增加以下代碼,變成:

Router.map(function () {
  this.route('serverFile', {
    where: 'server',
    path: '/upload',

    action: function () {
     //下面這句是重點
      console.log(this.request.files);
      this.response.writeHead(200, {'Content-Type': 'text/html'});
      this.response.end('hello from server');
    }
  });
});
//文件下載部分
Router.map(function () {
  this.route('serverFile', {
    where: 'server',
    path: '/download',
    action: function () {
      this.response.writeHead(200, {
          'Content-type': 'text/html',
          'Content-Disposition': "attachment; filename=test.txt"
      });
      this.response.end('hello from server');
    }
  });
});

運行程序,在瀏覽器打開localhost:3000/download 即可下載文件了。

總的來說是非常簡單的。可能最後問題解決的辦法非常簡單,但是在解決問題的過程沒有這麼簡單了。下面說一下,解決問題的思路。 
如果你只是需要解決問題,那麼到此就可以到此爲止了。下面的部分就可以跳過。


首先,我在考慮文件上傳過程中第一個想到的是需要一個url來接受這個文件上傳請求。 
因此涉及到了 router的功能,在0.8.0之前有個router的包,可以在升級以後暫時不能用了,所以又搜索了一下,找到了iron router這個包。於是查找它的api看看有沒有服務端的路由功能。在這裏https://github.com/EventedMind/iron-router/blob/master/DOCS.md#server-side-routing 我看到了有關服務端的路由功能。

Server action functions (RouteControllers) have different properties and methods available. Namely, there is no rendering on the server yet. So the render method is not available. Also, you cannot waitOn subscriptions or call the wait method on the server. Server routes get the bare request, response, and next properties of the Connect request

因此後臺是可以用request和response.那麼應該就可以通過事件監聽來處理數據了。但是具體的應用的,api文檔裏面沒有繼續寫了,那這個request或者response 對象是否經過封裝還是原生的reponse和request呢? 
於是我就把iron router 的源碼clone來了。文件不多。 
第一步分析這個包的 package.js 因爲寫過meteor第三方包就知道,這個文件包含了代碼的組織結構,包括把文件加載到客戶端還是服務端。 
iron router的package.js的內容如下(截取部分):


Package.on_use(function (api) {
  api.use('reactive-dict', ['client', 'server']);
  api.use('deps', ['client', 'server']);
  api.use('underscore', ['client', 'server']);
  api.use('ejson', ['client', 'server']);
  api.use('jquery', 'client');

  // default ui manager
  // use unordered: true becuase of circular dependency

  // for helpers
  api.use('ui', 'client', {weak: true});

  // default ui manager
  // unordered: true because blaze-layout package weakly
  // depends on iron-router so it can register itself with
  // the router. But we still want to pull in the blaze-layout
  // package automatically when users add iron-router.
  api.use('blaze-layout', 'client', {unordered: true});

  api.add_files('lib/utils.js', ['client', 'server']);
  api.add_files('lib/route.js', ['client', 'server']);
  api.add_files('lib/route_controller.js', ['client', 'server']);
  api.add_files('lib/router.js', ['client', 'server']);

  api.add_files('lib/client/location.js', 'client');
  api.add_files('lib/client/router.js', 'client');
  api.add_files('lib/client/wait_list.js', 'client');
  api.add_files('lib/client/hooks.js', 'client');
  api.add_files('lib/client/route_controller.js', 'client');
  api.add_files('lib/client/ui/helpers.js', 'client');

  api.add_files('lib/server/route_controller.js', 'server');
  api.add_files('lib/server/router.js', 'server');

  api.use('webapp', 'server');

很清晰的看到

api.add_files('lib/server/route_controller.js', 'server');
api.add_files('lib/server/router.js', 'server');

這兩個文件是加載在服務端的,其他文件是同時加載在客戶端和服務端的。先不去分析,看看這兩個文件。 
通過查看lib/server/route_controller.js ,裏面代碼比較少 發現沒有request相關代碼,於是打開lib/server/router.js 
發現了以下代碼:

if (typeof __meteor_bootstrap__.app !== 'undefined') {
  connectHandlers = __meteor_bootstrap__.app;
} else {
  connectHandlers = WebApp.connectHandlers;
}
...
...
  constructor: function (options) {
    var self = this;
    IronRouter.__super__.constructor.apply(this, arguments);
    Meteor.startup(function () {
      setTimeout(function () {
        if (self.options.autoStart !== false)
          self.start();
      });
    });
  },

  start: function () {
    connectHandlers
      .use(connect.query())
      .use(connect.bodyParser())
      .use(_.bind(this.onRequest, this));
  },
...

發現是connectHandlers來接管了http的請求並使用了connect,繼續在代碼裏找,於是在代碼的最上面發現:

var connect = Npm.require('connect');

原來最後使用了connect來處理請求。 
那麼接着我們去找connect包。看看裏面的request和response到底封裝了什麼功能。 
在github找到connect https://github.com/senchalabs/connect 
發現這個項目是express這個團隊在維護。當然這是閒話,還是先找文檔再說。 
在connect文檔中發現,connect是一箇中間件的集合。所有的請求都會經過這些中間件的處理。 那麼我先找bodyparse()看看裏面有沒有對文件的處理, 於是發現了body-parser這個鏈接 進去看看於是到了 
https://github.com/expressjs/body-parser,發現文檔很簡單,不是很詳細,到底它做了些什麼呢?看下源碼試試,涉及到的文件很少,只有個index.js ,那就看這個文件吧(如果文件很多,回去分析package.json這個文件,看看入口文件是哪個,如果沒有main配置,那默認情況下是index.js)。發現下面這段代碼:

    getBody(req, {
      limit: options.limit || '100kb',
      length: req.headers['content-length'],
      encoding: 'utf8'
    }, function (err, buf) {
      if (err) return next(err);

      var first = buf.trim()[0];

      if (0 == buf.length) {
        return next(error(400, 'invalid json, empty body'));
      }

      if (strict && '{' != first && '[' != first) return next(error(400, 'invalid json'));
      try {
        req.body = JSON.parse(buf, options.reviver);
      } catch (err){
        err.body = buf;
        err.status = 400;
        return next(err);
      }
      next();

沒有找到關於文件流的讀寫。不過到現在能確定的是request是原生態的request,不過給它添加了一下屬性。好吧,看樣子body-parser並沒有添加對文件的分析,而是在最後 執行了next(),看樣子是交給了其他中間件處理。本來到這裏,我沒有繼續下去了,準備用request的原始方法事件監聽來處理文件上傳的數據。但是我回到connect主頁時,偶然發現了這段:

Some middleware previously included with Connect are no longer supported by the Connect/Express team, are replaced by an alternative module, or should be superceded by a better module. Use one of these alternatives intead:

這下面有些非官方維護的中間件其中就有個

connect-multiparty

發現 和文件上傳表單的enctype="multipart/form-data"類似,抱着試試一試的心態 進去看看https://github.com/andrewrk/connect-multiparty,哈哈在readme裏發現個有趣的東東

var multipart = require('connect-multiparty');
var multipartMiddleware = multipart();
app.post('/upload', multipartMiddleware, function(req, resp) {
  console.log(req.body, req.files);
  // don't forget to delete all req.files when done
});

竟然有個 req.files難道這個中間件處理文件上傳?那就在test測試裏面試試? 
結果打印this.request.files真發現了上傳文件有關信息了。接着嘗試讀取一下,正確無誤!文件上傳就這麼 簡單的搞定了。根本不用自己去監聽request data事件了。 
至此搞定。

其實接下來我還是去看來下這個connect-multiparty的源碼,發現這個中間件其實也沒有直接處理文件流而是調用封裝了另外一個庫 multiparty 地址:https://github.com/andrewrk/node-multiparty/,在這裏纔是真正處理了文件流的讀取操作。
在整個問題的處理過程中,主要是對中間件(middleware)概念有個基礎的瞭解(ps:個人理解爲 類似於java裏面的filter進行http請求的逐層處理),然後知道在connect處理http進行過很多處理,在這些中間件中找到了對文件的處理結果。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章