Meteor 分頁

2022-06-30 13:59 更新

分頁

Microscope 的功能看起來不錯。我們可以想象當(dāng)它 release 之后會很受歡迎。

因此我們需要考慮一下隨著新帖子越來越多所帶來的性能問題。

之前我們說過客戶端集合會包含服務(wù)器端數(shù)據(jù)的一個子集。我們在帖子和評論集合已經(jīng)實現(xiàn)了這些。

但是現(xiàn)在,如果我們還是一口氣發(fā)布所有帖子給所有的連接用戶。當(dāng)有成千上萬的新帖子時,這會帶來一些問題。為了解決這些,我們需要給帖子分頁。

添加更多的帖子

首先是我們的初始化數(shù)據(jù),我們需要添加足夠的帖子來使分頁有意義:

// Fixture data
if (Posts.find().count() === 0) {

  //...

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: new Date(now - 12 * 3600 * 1000),
    commentsCount: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: new Date(now - i * 3600 * 1000),
      commentsCount: 0
    });
  }
}

運行完 meteor reset 重啟你的 app, 你會看到如下:

無限分頁

我們將實現(xiàn)一個"無限"的分頁。意思是在第一屏顯示 10 條帖子和一個在底部顯示的 "load more" 鏈接。點擊 "load more" 鏈接再加載另外 10 條帖子,諸如此類無限的加載。這意味著我們只用一個參數(shù)來實現(xiàn)分頁,控制在屏幕上顯示帖子的數(shù)量。

現(xiàn)在需要一個方法告訴服務(wù)器端返回給客戶端帖子的數(shù)量。這些發(fā)生在路由訂閱帖子的過程,我們會利用路由來實現(xiàn)分頁。

最簡單的限制返回帖子數(shù)量的方式是將返回數(shù)量加到 URL 中,如 http://localhost:3000/25。使用 URL 記錄數(shù)量的另一個好處是,如果不小心刷新了頁面,還會返回 25 條帖子。

為了恰當(dāng)?shù)膶崿F(xiàn)分頁,我們需要修改帖子的訂閱方法。就像我們之前在評論那章做的,我們需要將訂閱部分的代碼從 router 級變?yōu)?route 級。

這個改變內(nèi)容會比較多,通過代碼可以看的比較清楚。

首先,停止 Router.configure() 代碼塊中的 posts 訂閱。即刪除 Meteor.subscribe('posts'),只留下 notifications 訂閱:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  notFoundTemplate: 'notFound',
  waitOn: function() {
    return [Meteor.subscribe('notifications')]
  }
});

我們在路由路徑中加入?yún)?shù) postsLimt。 參數(shù)后面的 ? 表示參數(shù)是可選的。這樣路由就能同時匹配 http://localhost:3000/50http://localhost:3000。

//...

Router.route('/:postsLimit?', {
  name: 'postsList',
});

//...

需要注意每個路徑都會匹配路由 /:parameter?。因為每個路由都會被檢查是否匹配當(dāng)前路徑。我們要組織好路由來減少特異性。

話句話說,更特殊的路由會優(yōu)先選擇,例如:路由 /posts/:_id 會在前面,而路由 postsList 會放到路由組的最后,因為它太泛泛了可以匹配所有路徑。

是時候處理難題了,處理訂閱和找到正確的數(shù)據(jù)。我么需要處理 postsLimit 參數(shù)不存在的情況。我們給它一個默認(rèn)值 5, 這樣我們能更好的演示分頁。

//...

Router.route('/:postsLimit?', {
  name: 'postsList',
  waitOn: function() {
    var limit = parseInt(this.params.postsLimit) || 5;
    return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
  }
});

//...

你注意到我們在訂閱 posts 時傳了一個 js 對象 ({sort: {submitted: -1}, limit: postsLimit}), 這個 js 對象會作為服務(wù)器端查詢方法 Posts.find() 的可選參數(shù)。下面是服務(wù)器端的實現(xiàn)代碼:

Meteor.publish('posts', function(options) {
  check(options, {
    sort: Object,
    limit: Number
  });
  return Posts.find({}, options);
});

Meteor.publish('comments', function(postId) {
  check(postId, String);
  return Comments.find({postId: postId});
});

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});

傳遞參數(shù)

我們的訂閱代碼告訴服務(wù)器端,我們信任客戶端傳來的 JavaScript 對象 (在我們的例子中是 {limit: postsLimit}) 作為 find() 方法的 options 參數(shù)。這樣我們能通過 browser consle 來傳任何 option 對象。

在我們的例子中,這樣沒什么害處,因為用戶可以做的無非是改變帖子順序,或者修改 limit 值(這是我們想讓用戶做的)。但是對于一個 real-world app 我們必須做必要的限制!

幸好通過 check() 方法我們知道用戶不能偷偷加入額外的 options (例如 fields, 在某些情況下需要對外暴露 ducoments 的私有數(shù)據(jù))。

然而,更安全的做法是傳遞單個參數(shù)而不是整個對象,通過這樣確保數(shù)據(jù)安全:

Meteor.publish('posts', function(sort, limit) {
  return Posts.find({}, {sort: sort, limit: limit});
});

現(xiàn)在我們在 route 級訂閱數(shù)據(jù),同樣的我們可以在這里設(shè)置數(shù)據(jù)的 context。我們要偏離一下之前的模式,我們讓 data 函數(shù)返回一個 js 對象而不是一個 cursor。 這樣我們可以創(chuàng)建一個命名的數(shù)據(jù) context。我們稱之為 posts

這意味著我們的數(shù)據(jù) context 將存在于 posts 中,而不是簡單的在模板中隱式的存在于 this 中。除去這一點,代碼看起來很相似:

//...

Router.route('/:postsLimit?', {
  name: 'postsList',
  waitOn: function() {
    var limit = parseInt(this.params.postsLimit) || 5;
    return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
  },
  data: function() {
    var limit = parseInt(this.params.postsLimit) || 5;
    return {
      posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
    };
  }
});

//...

因為我們在 route 級設(shè)置數(shù)據(jù) context, 現(xiàn)在我們可以去掉在 posts_list.js 文件中 posts 模板的幫助方法。

我們的數(shù)據(jù) context 叫做 posts (和 helper 同名),所以我們甚至不需要修改 postsList 模板!

下面是我們修改過的 router.js 代碼:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  notFoundTemplate: 'notFound',
  waitOn: function() {
    return [Meteor.subscribe('notifications')]
  }
});

Router.route('/posts/:_id', {
  name: 'postPage',
  waitOn: function() {
    return Meteor.subscribe('comments', this.params._id);
  },
  data: function() { return Posts.findOne(this.params._id); }
});

Router.route('/posts/:_id/edit', {
  name: 'postEdit',
  data: function() { return Posts.findOne(this.params._id); }
});

Router.route('/submit', {name: 'postSubmit'});

Router.route('/:postsLimit?', {
  name: 'postsList',
  waitOn: function() {
    var limit = parseInt(this.params.postsLimit) || 5;
    return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
  },
  data: function() {
    var limit = parseInt(this.params.postsLimit) || 5;
    return {
      posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
    };
  }
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn()) {
      this.render(this.loadingTemplate);
    } else {
      this.render('accessDenied');
    }
  } else {
    this.next();
  }
}

Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});

試一下我們的分頁?,F(xiàn)在我們可以通過 URL 參數(shù)來控制頁面顯示帖子的數(shù)量,試一下 http://localhost:3000/3。你可以看到如下:

為什么不用傳統(tǒng)的分頁?

為什么我們使用“無限分頁”而不用每頁顯示 10 條帖子的連續(xù)分頁,就像 Google 的搜索結(jié)果分頁一樣?這是由于 Meteor 的實時性決定的。

讓我們想象一下使用類似 Google 搜索結(jié)果的連續(xù)分頁模式,我們在第2頁,顯示的是 10 到 20 條帖子。這是碰巧有另外一個用戶刪除了前面 10 條帖子中的帖子。

因為 app 是實時的,我們的數(shù)據(jù)集會馬上變化,這樣第 10 條帖子變成了第 9 條,從當(dāng)前頁面消失了,第 21 條帖子會出現(xiàn)在頁面中。這樣用戶會覺得沒什么原由的結(jié)果集變了!

即使我們可以容忍這種怪異的 UX, 由于技術(shù)的原因傳統(tǒng)的分頁還是很難實現(xiàn)。

讓我們回到前一個例子。我們從 Posts 集合中發(fā)布第 10 到 20 條帖子,但是在客戶端我們?nèi)绾握业竭@些帖子?我們不能在客戶端選擇第 10 到 20 條帖子,因為客戶端集合只有 10 個帖子。

一個簡單的方案是服務(wù)器端發(fā)布 10 條帖子,在客戶端執(zhí)行一下 Posts.find() 找到這 10 條發(fā)布的帖子。

這個方案在只有一個用戶訂閱的情況下有效,但是如果有多個用戶訂閱呢,下面我們會看到。

我們假設(shè)一個用戶需要第 10 到 20 條帖子,而另一個需要第 30 到 40。這樣在客戶端我們有兩個 20 條帖子,我們不能區(qū)分他們屬于哪個訂閱。

基于這些原因,我們在 Meteor 中不能使用傳統(tǒng)的分頁。

創(chuàng)建路由控制器

你可能已經(jīng)注意到了我們代碼中重復(fù)了 var limit = parseInt(this.params.postsLimit) || 5; 兩次。而且硬編碼數(shù)字 5,這不是個理想的做法。雖然這不會導(dǎo)致世界末日,但是我們最好還是遵循 DRY 原則 (Don't Repeat Yourself), 讓我們看看如何能把代碼重構(gòu)的更好些。

我們將介紹 Iron Router 的一個新功能, Route Controllers。Route controller 是通過簡單的方式將一組路由特性打包,其他的 route 可以繼承他們。現(xiàn)在我們只在一個路由中使用它,在下一章我們會看到它如何派上用場。

//...

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5,
  postsLimit: function() {
    return parseInt(this.params.postsLimit) || this.increment;
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.postsLimit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  data: function() {
    return {posts: Posts.find({}, this.findOptions())};
  }
});

//...

Router.route('/:postsLimit?', {
  name: 'postsList'
});

//...

讓我們一步接一步的往下看。首先,我們的創(chuàng)建一個繼承 RouteController 的控制器。然后像之前一樣設(shè)置 template 屬性,然后添加一個新的 increment 屬性。

然后我們定義一個 postsLimit 函數(shù)用來返回當(dāng)前限制的數(shù)量,然后定義一個 findOptions 函數(shù)用來返回 options 對象。這看起來像是個對于的步驟,但是我們后面會用到它。

接下來我們定義 waitOndata 函數(shù),除了他們現(xiàn)在會用到新的 findOptions 函數(shù)外其余和之前相同。

因為我們的控制器叫做 PostsListController 路由叫做 postsList, Iron Router 會自動使用他們。因此我們只需要從路由定義中移除 waitOndata (因為路由已經(jīng)會處理他們了)。如果我們需要給路由起別的名字,我們可以使用 controller 選項(我們將在下一章看到一個例子)。

添加加載更多鏈接

我們現(xiàn)在實現(xiàn)了分頁,代碼看起來還不錯。只有一個問題:我們的分頁需要手工修改 URL。這顯然不是一個好的用戶體驗,現(xiàn)在讓我們來修改它。

我們要做的很簡單。我們將在帖子列表的下面加一個 "load more" 按鈕,點擊按鈕將增加 5 條帖子。如果當(dāng)前的 URL 是 http://localhost:3000/5, 點擊 "load more" 按鈕 URL 將變成 http://localhost:3000/10。如果你之前已經(jīng)實現(xiàn)過這種功能,我們相信你很強!

因為在前面,我們的分頁邏輯是在 route 中。記得我們是什么時候顯式命名數(shù)據(jù)上下文,而非使用匿名 cursor 的么? 沒有規(guī)則說我們的 data 函數(shù)只能使用 cursors, 因此,我們將用同樣的技巧來生成 "load more" 按鈕的 URL。

//...

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5,
  postsLimit: function() {
    return parseInt(this.params.postsLimit) || this.increment;
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.postsLimit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().count() === this.postsLimit();
    var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
    return {
      posts: this.posts(),
      nextPath: hasMore ? nextPath : null
    };
  }
});

//...

讓我們來深入的看一下 router 帶來的魔術(shù)。記住 postsList route (它將繼承 PostsListController 控制器) 使用一個 postsLimit 參數(shù)。

因此當(dāng)我們給 this.route.path() 傳遞參數(shù) {postsLimit: this.postsLimit() + this.increment} 時,我們告訴 postsList route 使用這個 js 對象做數(shù)據(jù)上線文建立自己的 path。

換句話說,這和使用 {{pathFor 'postsList'}} Spacebars 幫助方法一樣, 除了我們用自己的數(shù)據(jù)上下文替換了隱式的 this。

我們使用這個路徑并將它添加到我們模板的數(shù)據(jù)上下文中,但是只有多條帖子時會顯示。我們的實現(xiàn)方法有一點小花招。

我們知道 this.limit() 方法會返回當(dāng)前我們想要顯示帖子的數(shù)量,它可能是當(dāng)前 URL 中的值,如果 URL 中沒有參數(shù)它會是默認(rèn)值 (5)。

另一方面, this.posts 引用當(dāng)前的 cursor, 因此 this.posts.count() 的值是在 cursor 中帖子的數(shù)量。

因此我們說當(dāng)我們要求發(fā)揮 n 條帖子,實際返回了 n 條帖子,我們將繼續(xù)顯示 "load more" 按鈕。但是如果我們要求返回 n 條帖子,而實際返回的數(shù)量比 n 少,這樣我們就知道記錄已經(jīng)到頭了,我們就不再顯示加載按鈕。

這就是說,我們的系統(tǒng)在一種情況下會有點問題:當(dāng)我們的數(shù)據(jù)庫恰好n 條記錄時。如果是這樣,當(dāng)客戶端要求返回 n 條帖子,我們得到了 n 條,然后繼續(xù)顯示 "load more" 按鈕,這是我們不知道其實已經(jīng)沒有記錄可以繼續(xù)返回了。

不幸的是,我們沒有好的方法去解決這個問題,因此我們不得不接受這個不算完美的實現(xiàn)方式。

下面剩下的就是在帖子列表下面加上 "load more" 鏈接,并且保證在還有帖子時才顯示它:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>

下面是你帖子列表現(xiàn)在看上去的樣子:

更好的用戶體驗

現(xiàn)在我們的分頁可以工作了,但是有個煩人小問題: 每次我們點擊 "load more" 按鈕向 router 加載更多的帖子時,Iron Router 的 waitOn 特性會在我們等待時顯示 loading 模板。當(dāng)結(jié)果到來時我們又會回到頁面的頂端,我們每次都要滾動頁面回到之前看的位置。

因此,首先我們要告訴 Iron Router 不要 waintOn 訂閱,我們將定義自己的訂閱在一個 subscriptions hook 中。

注意我們我們不是在 hook 中返回這個訂閱。返回它(這是一般 訂閱 hook 常做的工作)將觸發(fā)一個全局的 loading hook, 這正是我們想要避免的。我們只是想在 subscriptions hook 中定義我們的訂閱,就像使用一個 onBeforeAction hook。

我們還要在我們的數(shù)據(jù)上下文中傳入一個 ready 變量,它指向 this.postsSub.ready。它會告訴我們帖子訂閱何時加載完畢。

//...

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5,
  postsLimit: function() {
    return parseInt(this.params.postsLimit) || this.increment;
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.postsLimit()};
  },
  subscriptions: function() {
    this.postsSub = Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().count() === this.postsLimit();
    var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
    return {
      posts: this.posts(),
      ready: this.postsSub.ready,
      nextPath: hasMore ? nextPath : null
    };
  }
});

//...

我們將在模板中檢查 ready 變量的狀態(tài),并在加載帖子時在帖子列表的下面顯示一個加載圖標(biāo)(spinner):

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{else}}
      {{#unless ready}}
        {{> spinner}}
      {{/unless}}
    {{/if}}
  </div>
</template>

訪問任何帖子

現(xiàn)在我們默認(rèn)每次加載 5 條新帖子,但是當(dāng)用戶訪問某個帖子的單獨頁面時會發(fā)生什么?

試一下,我們會得到一個 "not found" 錯誤。這是有原因的: 我們告訴 router 當(dāng)我們加載 postList route 時訂閱 帖子 發(fā)布。但是我們沒有說訪問 postPage route 時該做什么。

但是到目前,我們知道如何訂閱一個 n 個最新帖子的列表。我們?nèi)绾蜗蚍?wù)器端要求單個具體帖子的內(nèi)容? 我們將告訴你一個小秘密: 對于一個 collection 你可以有多個 publication!

讓我們找回丟失的帖子,我們定義一個新的 publication singlePost,它只發(fā)布一個帖子,用 _id 鑒別。

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('singlePost', function(id) {
  check(id, String)
  return Posts.find(id);
});

//...

現(xiàn)在,讓我們在客戶端訂閱正確的帖子。我們已經(jīng)在 postPage route 的 wainOn 函數(shù)中訂閱了 comments 發(fā)布,因此我們可以也在這里加入 singlePost 訂閱。讓后別忘了在 postEdit route 中加入我們的訂閱, 因為那里也需要相同的數(shù)據(jù):

//...

Router.route('/posts/:_id', {
  name: 'postPage',
  waitOn: function() {
    return [
      Meteor.subscribe('singlePost', this.params._id),
      Meteor.subscribe('comments', this.params._id)
    ];
  },
  data: function() { return Posts.findOne(this.params._id); }
});

Router.route('/posts/:_id/edit', {
  name: 'postEdit',
  waitOn: function() {
    return Meteor.subscribe('singlePost', this.params._id);
  },
  data: function() { return Posts.findOne(this.params._id); }
});

//...

有了分頁,我們的程序?qū)⒉辉偈芤?guī)模問題的困擾了,用戶可以加入更多的帖子。如果有某種方法可以給帖子鏈接加上等級 (rank) 不是更好么?我們將在下一章去實現(xiàn)它!

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號