Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Meteor peddles


May 10, 2021 Meteor


Table of contents


Peddle

Microscope looks great. We can imagine that when it comes to release, it will be very popular.

So we need to think about the performance issues that come with more and more new posts.

As we said earlier, the client collection contains a subset of server-side data. We've done this in our collection of posts and comments.

But now, if we still post all the posts in one go to all the connected users. T his can cause problems when there are thousands of new posts. To solve this, we need to page the post.

Add more posts

The first is our initialization data, and we need to add enough posts to make pedding meaningful:

// 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
    });
  }
}

After running meteor reset app, you'll see the following:

Meteor peddles

Unlimited plying

We will implement an "infinite" peddle. T his means that 10 posts are displayed on the first screen and a "load more" link is displayed at the bottom. C lick on the "load more" link to load another 10 posts, such as Unlimited Load. This means that we use only one parameter to achieve page and control the number of posts displayed on the screen.

Now you need a way to tell the server how many posts are returned to the client. These occur in the process of 帖子 to posts, and we use routing to achieve pedding.

The easiest way to limit the number of posts returned is to add the number of returns to a URL, http://localhost:3000/25 Another benefit of using the number of URL records is that 25 posts are returned if the page is accidentally refreshed.

In order to properly implement pedding, we need to modify the subscription method for posts. As we did in the comments chapter before, we need to change the code for the subscription section from the router level to the route level.

This change will be more content, through the code can be seen more clearly.

First, stop Router.configure() block posts code. That Meteor.subscribe('posts') leaving only notifications subscription:

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

We add the parameter postsLimt T he ? after ? indicates that the argument is optional. This allows the route to http://localhost:3000/50 and http://localhost:3000

//...

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

//...

Note that each path matches the /:parameter? B ecause each route is checked to match the current path. We need to organize routes to reduce specificity.

In other words, more specific routes are preferred, for /posts/:_id is preceded, and postsList because it is too general to match all paths.

It's time to deal with challenges, handle subscriptions, and find the right data. D o I need to deal with postsLimit parameter does not exist? Let's give it a default value of 5, so we can better demonstrate pedding.

//...

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

//...

You notice that when we subscribe to posts we pass a js object ('sort: 'submitted: -1', limit: postsLimit'), which serves as an optional parameter for Posts.find() Here's the server-side implementation code:

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});
});

Pass the parameters

Our subscription code tells the server side that we trust the JavaScript object from the client (in our case, the {limit: postsLimit} as the options parameter of the find() method. This allows us to pass any option object through browser consle.

In our case, this is no harm, because what the user can do is change the order of posts or modify the limit value (which is what we want the user to do). But for a real-world app we have to make the necessary restrictions!

Fortunately, through check() know that users cannot secretly add additional options (for fields in some cases requiring external exposure of private data for ducoments).

However, it is safer to pass a single parameter instead of the entire object to ensure data security by:

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

Now that we subscribe to the data at the route level, we can also set the context of the data here. W e're going to deviate from the previous data we're going to have the data function return a js object instead of a cursor. T his way we can create a named data context. We call it posts

This means that our data context will exist in posts not simply implicitly in this Deseeding this, the code looks similar:

//...

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})
    };
  }
});

//...

Since we set up data context at the route level, we can now remove posts_list.js the posts posts in the file file.

Our data context is called posts (and helper with the same name), so we don't even need to postsList template!

Here's our router.js code:

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'});

Try our page. N ow we can control the number of posts displayed on the page through the URL parameter, http://localhost:3000/3 Here's what you can see:

Meteor peddles

Why not use traditional plying?

Why do we use Infinite Paged instead of showing 10 consecutive posts per page, just like Google's search results page? This is due to Meteor's real-time nature.

Let's imagine using a continuous page pattern similar to Google search results, where we show 10 to 20 posts on page 2. This is a post that happens to have another user delete the previous 10 posts.

Because the app is real-time, our data assembly changes immediately, so that the 10th post becomes the 9th, disappears from the current page, and the 21st post appears on the page. In this way, users will feel that there is no original result set changed!

Even if we can tolerate this weird UX, traditional plying is difficult for technical reasons.

Let's go back to the previous example. W e post posts 10 to 20 from the Posts collection, but how do we find them on the client side? We can't select articles 10 to 20 on the client because the client collection has only 10 posts.

A simple scenario is to post 10 posts on the server side and find them by performing Posts.find() on the client side.

This scenario works with only one user subscription, but if there are multiple user subscriptions, we'll see below.

Let's assume that one user needs a 10th to 20th post and the other needs a 30th to 40th. So we have two 20 posts on the client side, and we can't tell which subscription they belong to.

For these reasons, we can't use traditional plying in Meteor.

Create a routing controller

You may have noticed that our code repeats the var limit s var limit = parseInt(this.params.postsLimit) || 5; twice. A nd hard-coded number 5, which is not ideal. While this won't lead to the end of the world, it's best to follow the DRY principle (Don't Repeat Yourself), so let's see how we can refactor the code better.

We'll introduce a new feature of Iron Router, Route Controllers. R oute controllers package a set of routing attributes in a simple way, and other routes can inherit them. Now that we're only using it in one route, we'll see how it doesn't come in handy in the next chapter.

//...

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'
});

//...

Let's look down step by step. F irst, we create a controller that RouteController Then set the template property as before, and then add a new increment

Then we define a postsLimit return the current limit, and findOptions return the options object. This may seem like the right step, but we'll use it later.

Next we define waitOn data all of which are the same as before, except that they will now use the new findOptions function.

Because our controllers are PostsListController are postsList Iron Router automatically uses them. S o we just need to remove waitOn and data from the data (because the route already processes them). If we need to give the route a different name, we can use controller option (we'll see an example in the next chapter).

Add more links to load

We've now implemented pedding, and the code looks good. T here's only one problem: our peddles need to be manually modified. This is obviously not a good user experience, now let's modify it.

What we have to do is very simple. W e'll add a "load more" button below the list of posts, and clicking the button will add 5 posts. I f the current URL http://localhost:3000/5 clicking on the "load more" button URL http://localhost:3000/10 If you've done this before, we believe you're strong!

Because earlier, our plying logic is in route. R emember when we explicitly named the data context instead of using anonymous cursor? There are no rules that data function can only use cursors, so we'll use the same technique to generate the URL of the "load more" button.

//...

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
    };
  }
});

//...

Let's take a deep look at the magic that router brings. Remember postsList route (which PostsListController controller) uses a postsLimit parameter.

So when we this.route.path() {postsLimit: this.postsLimit() + this.increment} postsList

In other words, this {{pathFor 'postsList'}} the same as using the spacebars help method, except that we replaced the implicit this with our own data this

We use this path and add it to the data context of our template, but only when there are multiple posts. Our approach to implementation is a little tricky.

We this.limit() the current number of posts we want to display, it may be a value in the current URL, and if there are no parameters in the URL it will be the default (5).

this.posts to the current cursor, so this.posts.count() is the number of posts in cursor.

So we said that when we asked n posts and actually n we continued to display the "more load" button. But if we ask for n to be returned and the actual number n so that we know the record is over, we won't show the load button.

That said, our system can have a problem in one case: when our database happens to have n records. If so, when the client requests n n and then continue to display the "more load" button, which we don't know is actually no longer recorded and can continue to go back.

Unfortunately, we don't have a good way to solve this problem, so we have to accept that this is not a perfect way to achieve it.

All that's left is to add a "load more" link under the list of posts and make sure you show it when there are posts:

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

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

Here's what your list of posts looks like now:

Meteor peddles

A better user experience

Now our pics are working, but there's a annoying little problem: Every time we click the "load more" button to load more posts to the router, Iron waitOn displays loading wait. When the results arrive, we go back to the top of the page, and each time we scroll the page back to where we saw it before.

So first we'll tell Iron Router not waintOn and we'll define our subscriptions

Note that we are not returning this subscription in hook. R eturning it (which is what 订阅 subscription hook does often) triggers a global loading hook, which is exactly what we want to avoid. We just want to define our subscription in subscriptions hook, just like using onBeforeAction hook.

We also want to pass in a ready variable in the ready data, which points to this.postsSub.ready It tells us when the post subscription is loaded.

//...

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
    };
  }
});

//...

We'll check the status ready variable in the template and display a load icon (spinner) below the post list when the post is loaded:

<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>

Visit any post

Now we load 5 new posts each time by default, but what happens when a user visits a separate page of a post?

Meteor peddles

Try it and we'll get a "not found" error. T here's a reason for this: We tell the router to subscribe to post posts when we 帖子 postList route. But we didn't say what to do when we visited postPage route.

But so far, we know how to subscribe to a list of n recent posts. H ow do we ask the server side for the content of a single specific post? We'll tell you a little secret: For a collection you can have multiple publications!

Let's retrieve the lost post, and we define singlePost that posts only one _id the page.

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

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

//...

Now, let's subscribe to the correct post on the client. W e've subscribed to the wainOn function of postPage comments so we can also add singlePost here. Let's not forget to include our subscription in postEdit route, because the same data is needed there:

//...

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); }
});

//...

With pedding, our program will no longer be plagued by scale issues, and users can add more posts. W ouldn't it be better if there was a way to level a post link? We'll implement it in the next chapter!