Meteor 延時補償

2022-06-30 13:57 更新

延時補償

在上一章,我們介紹了 Meteor 的一個新概念:內置方法。

Meteor 的內置方法是一種在服務器上執(zhí)行一系列命令的結構化方法。在示例中,我們使用內置方法是為了確保新帖子是通過作者的姓名和 ID ,以及當前服務器時間去標記。

然而,如果 Meteor 用最基本的方式去執(zhí)行內置方法,我們會注意到一些問題。想一想下面事件的序列(注:為演示方便,時間戳值是隨機的生成的):

  • +0ms: 用戶單擊提交按鈕,瀏覽器觸發(fā)內置方法的調用。
  • +200ms: 服務器更改 Mongo 數(shù)據(jù)庫。
  • +500ms: 客戶端接收到這些變化,并更新頁面反映更改結果。

如果這是 Meteor 的操作方式,它會有一個很短的時間差去看到這樣的執(zhí)行操作的結果(延時的多少會取決于你的服務器性能)。但我們不可能讓這些情況出現(xiàn)在 Web 應用程序中!

延遲補償

為了避免這個問題,Meteor 引入了一個叫做延遲補償(Latency Compensation)的概念。如果我們把 post 方法的定義放在 collections/ 目錄下。這意味著它在服務端和客戶端上都存在,而且是同時運行!

當你使用內置方法的時候,客戶端會發(fā)送請求到服務器去調用,同時還模仿服務器內置方法去操作本地數(shù)據(jù)庫集合。所以現(xiàn)在我們的工作流程是:

  • +0ms: 用戶單擊提交按鈕,瀏覽器觸發(fā)內置方法的調用。
  • +0ms: 客戶端模仿內置方法操作本地的數(shù)據(jù)集合和以及通過更改頁面來反映這一變化。
  • +200ms: 服務器更改 Mongo 數(shù)據(jù)庫。
  • +500ms: 客戶端接收這些更改并取消剛剛的模仿操作,根據(jù)服務器的更改覆蓋它們(通常是相同的)。頁面的變化反映了這一過程。

這樣用戶就會立刻看到變化。服務器的響應返回一段時間后,根據(jù)服務器數(shù)據(jù)庫發(fā)送過來的更改請求,本地數(shù)據(jù)庫可能會或可能不會有明顯的改變。因此,我們應該學會確保本地數(shù)據(jù)盡可能地與服務器數(shù)據(jù)庫保持一致。

觀察延遲補償

我們可以對 post 內置方法的調用稍作改動。為此,我們將會通過 npm 包 futures ,使用一些高級的編程方式去把延遲對象放到我們的內置方法調用里面。

我們將使用 isServer 去問 Meteor 現(xiàn)在所調用的內置方法是在客戶端被調用(作為一個存根 Stub)或是在服務器端。這個存根 stub 是模仿內置方法在客戶端運行的模擬方法,而“真正的”內置方法是在服務器上運行的。

所以我們會詢問 Meteor 這部分代碼是否在服務器端執(zhí)行。如果是,我們會在帖子的標題后面添加 (server) 字符串。如果不是,我們將添加 (client) 字符串:

Posts = new Mongo.Collection('posts');

Meteor.methods({
  postInsert: function(postAttributes) {
    check(this.userId, String);
    check(postAttributes, {
      title: String,
      url: String
    });

    if (Meteor.isServer) {
      postAttributes.title += "(server)";
      // wait for 5 seconds
      Meteor._sleepForMs(5000);
    } else {
      postAttributes.title += "(client)";
    }

    var postWithSameLink = Posts.findOne({url: postAttributes.url});
    if (postWithSameLink) {
      return {
        postExists: true,
        _id: postWithSameLink._id
      }
    }

    var user = Meteor.user();
    var post = _.extend(postAttributes, {
      userId: user._id, 
      author: user.username, 
      submitted: new Date()
    });

    var postId = Posts.insert(post);

    return {
      _id: postId
    };
  }
});

如果我們到此為止,這個演示就不那么有意義。當前,看起來就像是帖子表單提交后暫停了5秒鐘,然后轉到主帖子列表,沒發(fā)生其他事情。

為了理解這是為什么,讓我們看看帖子提交的事件 handler:

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    };

    Meteor.call('postInsert', post, function(error, result) {
      // display the error to the user and abort
      if (error)
        return alert(error.reason);

      // show this result but route anyway
      if (result.postExists)
        alert('This link has already been posted');

      Router.go('postPage', {_id: result._id});  
    });
  }
});

我們在方法 call 回調函數(shù)中放了 Router.go() 路由函數(shù)。

現(xiàn)在的行為通常是正確的。畢竟,你不能在確定他們帖子提交是否有效之前去跳轉用戶,只是因為如果立即跳轉走,然后幾秒鐘后再轉回到原始帖子頁面去更正數(shù)據(jù),這會非常令人困惑。

但是對于這個例子而言,我們想立即看看結果。所以我們將路由更改到 postsList 路由(我們還不能路由到帖子,因為在方法之外我們不知道帖子的 _id),把它從回調函數(shù)中移出來,看看會發(fā)生什么:

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    };

    Meteor.call('postInsert', post, function(error, result) {
      // display the error to the user and abort
      if (error)
        return alert(error.reason);

      // show this result but route anyway
      if (result.postExists)
        alert('This link has already been posted');
    });

    Router.go('postsList');  

  }
});

如果我們現(xiàn)在創(chuàng)建一個帖子,我們可以清楚地看到延遲補償。首先,插入一個標題帶 (client) 的帖子(列表的第一個帖子,鏈接到 GitHub):

接著,五秒之后,它就會被服務器插入的真正帖子文檔所替代:

客戶端集合內置方法

通過上面所說的,你可能會認為內置方法很復雜,但事實上它們也可以相當簡單。實際上我們已經(jīng)用過三個非常簡單的內置方法:集合的操作方法 insert、updateremove

當你定義一個服務器集合稱為 'posts' ,你已經(jīng)隱式地定義了這三個內置方法: posts/insert、posts/updateposts/delete。換句話說,當你在本地集合中調用 Posts.insert(),你已經(jīng)在調用延時補償方法來做下面這兩件事:

  1. 檢查我們是否允許通過調用 allowdeny 方法的回調去操作集合(然而這并不需要發(fā)生在內置方法的模擬)。
  2. 實際地修改底層的數(shù)據(jù)庫。

內置方法的相互調用

你可能已經(jīng)意識到當我們插入帖子的時候,post 的內置方法調用了另一個內置方法(posts/insert)。這是如何工作的呢?

當模擬方法(客戶端版本的內置方法)開始運行,模擬方法執(zhí)行 insert(插入的是本地集合)時,我們不叫它真正的服務器端 insert,但我們會認為服務器端post 也將會同樣的被插入。

因此,當服務器端的 post 調用內置方法 insert 的時候更加沒有必要去擔心的客戶端模擬方法,它肯定能夠在客戶端順利地插入。

像之前一樣,在閱讀下一章之前,不要忘記還原你所做的更改。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號