📜 ⬆️ ⬇️

Real development experience at Meteor

This is a story about my experience in developing a live project on the Meteor framework. The framework is very interesting, the development approach is conceptually different from most existing PHP / JS frameworks. In fact, Meteor has to re-learn web development.



First, a few words about the project. This is a promo page for one local dating site. The task was to create a separate page with a competition for the best photo among the participants. Only 8 participants. Anyone can vote, no registration or authorization is required. On the page there will be a countdown to the end of the competition.

Meteor was a good choice for this project. Or the project turned out to be good as my first job at Meteor, equivalently. The main feature of Meteor is the so-called. reactivity ( Reactivity ). The idea is that the programmer declaratively describes the logic, without thinking about the communication protocol between the client and the server. Data update on the client occurs automatically as soon as the data has changed on the server. This means no more AJAX requests in the project code.
Input data on Meteor do not want to duplicate. There are some good videos on the site www.meteor.com , as well as several articles on Habré.
')
Next will be a technical description of the project. The main development approaches used in Meteor using this project will also be explained. The project structure is as follows:



Collections


MongoDB is used as a database. The client part has access to the database data in the same way as the server one. Even the access interface is the same - to simulate database requests on the client side, Minimongo is used. The client through Minimongo operates JavaScript arrays, unlike the server that makes direct requests to the MongoDB database.

file: model.js
//       Members = new Meteor.Collection('members'); 


In the example above, the members collection is declared. Since this file is accessible to both the client and server parts of the project, access to the Members variable is on both the client and the server. This can be verified simply by opening the console in the browser and executing typeof Members or Members.find (). Fetch (). The only difference is in the implementation, because on the server the Members methods will operate on MongoDB directly, and on the client with JavaScript arrays via Minimongo a wrapper.

These collections are managed by Meteor itself - it decides when the data needs to be updated on the client. The programmer can limit the amount of data that will be presented to the Members variable on the client. This will be a subset of the data from the server. This is done with the help of Meteor.publish () and Meteor.subscribe ().

file: client / client.js
 Meteor.subscribe('members'); 

In this case, all participants with all their data should be accessible to the client, so no artificial restrictions are imposed.

file: server / server.js
 Meteor.publish('members'); Meteor.startup(function () { if (Members.find().count() === 0) { Members.insert({ name: ' ', title: '', url: 'http://mariels.ru/member/profile_alexandra_igorevna.html', photo: 'images/member/ .jpg', thumb: 'http://mariels.ru/$userfiles/thumb_450_1136_94.jpg', vote: 0 }); Members.insert({ name: ' ', title: '', url: 'http://mariels.ru/member/profile_Alionushka.html', photo: 'images/member/ .jpg', thumb: 'http://mariels.ru/$userfiles/thumb_444_1120_90.jpg', vote: 0 }); //   ... } }); 


In the above code, the standard way to initialize the collection in Meteor. Since the code is in the file server / server.js, it is executed only on the server.

HTML Templates and Reactivity


There is data, now they need to be displayed in the browser. Meteor uses the JavaScript Handlebars JavaScript Template by default. In fact, a template curve is pretty straightforward and to perform a simple task like “accessing an array index in a foreach loop”, you have to write a new tag handler. But, having got used, it is possible to work with it.



file: client / view / members.html
 <template name="members"> <div id="members"> {{#render_members members}} <span class="member span6"> <span class="info-cont"> <span class="shadow"></span> <a href="{{member.url}}" class="account"> <img src="{{member.thumb}}" width="" height="" class="avatar"/> <span>{{member.name}}</span> </a> </span> <img src="{{member.photo}}" class="image" /> <span class="rate-cont"> <span class="shadow"></span> <button class="btn {{#if voted}}btn-info{{else}}btn-warning{{/if}} pull-center btn-large" data-id="{{member._id}}" {{#if voted}}disabled{{/if}}> {{#if voted}}    {{else}}   <span>{{member.title}}</span> {{/if}} </button> </span> </span> {{/render_members}} </div> </template> 


The render_members tag was created only to divide the output into lines (display <div class = "row"> every two entries), but in general this is the usual foreach loop. There is only one variable accessible to the template - an array of members. In the body of render_members, all the fields of each object from the members array are available. If to be absolutely accurate, then members are not an array, but a cursor, but this is not the point.

file: client / client.js
 Template.members.members = function() { return Members.find({}, { sort: { vote: -1 }}); } 


Members.find () returns a cursor, whereas Members.find (). Fetch () is a simple JavaScript array. Using the cursor as the member variable and wrapping it in function () {} we activate the reactivity of Meteor on this variable template. This means that as soon as the data of the Members collection on the server changes and the updates are transferred to the client, the template will be automatically redrawn using the new data. And for this you do not need any additional code on the client!

Server methods


file: server / server.js
 //     Votes = new Meteor.Collection('votes'); 


All votes will be stored in the Votes collection and it can grow to several thousand records. We cannot allow such a huge amount of data to run between the server and the client for obvious reasons. In addition, on the client, we absolutely do not need to know the data of each voice, such as IP and date. For these reasons, the variable is declared only in the code running on the server, and Meteor.publish () / Meteor.subscribe () is not called.

file: server / server.js
 //   IP     var CanVote = Match.Where(function(ip) { check(ip, String); if (ip.length > 0) { var yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); //       return Votes.find({ ip: ip, date: { $gt: yesterday } }).count() == 0; } return false; }); //  ,   Meteor.methods({ //  true      false    canVote: function() { return Match.test(headers.get('x-forwarded-for'), CanVote); }, //    vote: function(memberId) { check(memberId, String); check(headers.get('x-forwarded-for'), CanVote); var voteId = Votes.insert({ memberId: memberId, ip: headers.get('x-forwarded-for'), date: new Date() }); //  SQL JOIN Members.update(memberId, { $set: { vote: Votes.find({ memberId: memberId }).count() } }); return voteId; }, //      getMemberVotes: function(memberId) { check(memberId, String); return Votes.find({memberId:memberId}).count(); }, //      getTotalVotes: function() { return Votes.find().count(); } }); 


Using Meteor.methods (), a communication interface between the client and the server is declared within the project. Since the Votes collection is not available on the client, here methods are declared to obtain the necessary data about this collection, such as the number of votes per participant and the total number of votes.

A new entry is added to the voting function in the Votes collection, and the number of votes of the corresponding entry in the Members collection is updated. The latter is needed to use reactivity in the output of the list of participants (sorted by votes) and the rating graph.

In general, Meteor.methods () can be defined in model.js, then the client side will create wrappers for these methods and, when called, the data on the client will be updated instantly, and then corrected if the server behaves differently. This is called Latency Compensation . But in this case, the Votes collection is not available on the client, which means there is no point in this. You still have to wait for a response from the server.

More Reactivity




file: client / views / ratings.html
 <template name="ratings"> <div id="ratings" class="well"> <h1 class="heading uppercase"></h1> <div class="chart"> {{#each_with_index members}} <div class="rating num{{index}}"> <img src="{{data.thumb}}" class="avatar"/> </div> {{/each_with_index}} </div> <div class="pull-center pull-center-1"> <div id="votes">{{votes}}</div> <div><strong></strong></div> </div> </div> </template> 


file: client / client.js
 Session.setDefault('totalVotes', 0); Meteor.startup(function() { //   totalVotes  Deps.autorun(function() { var total = 0; Members.find().forEach(function(m) { total += m.vote; }); Session.set('totalVotes', total); }); //    -5 Deps.autorun(function() { var top = Members.findOne({}, { sort: { vote: -1 }}); //    // update ratings chart Members.find({}, { sort: { vote: -1 }, limit: 5 }).forEach(function(m, i) { var height = top ? Math.floor((m.vote / top.vote) * 190) + 100 : 100; $('.rating.num'+(i+1)).css('height', height); }); }); }); Template.ratings.members = function() { return Members.find({}, {limit: 5, sort: { vote: -1 }}); }; Template.ratings.votes = function() { return Session.get('totalVotes'); }; 


Session exists only on the client and it is not persistent, that is, it is reset when the page is updated. The Session object as well as the collections cursor activates reactivity, so if you change the value of totalVotes in the session, the ratings template will be redrawn.

Deps.autorun () is executed each time the reactive data in the function changes. In this case, this is the Members.find () cursor. The idea is that as soon as the server updates the votes of any participant, the totalVotes session value of all clients will be updated, and this will lead to a redrawing of the rating block. Deps.autorun () is used to add a callback to change data on the client. There are ways to subscribe to specific events of collections such as added, changed, removed more here .
Thus, if someone voted while the visitor sees the rating block, the rating bars will change their height, and the counter will increase.

Also here you can notice the use of jQuery. It can be mixed with Meteor client code with almost no restrictions. By the way, Meteor.startup (function {}) and jQuery (function () {}) are identical.

file: client / client.js
 Session.setDefault('voted', false); //       Template.members.voted = function() { return Session.get('voted'); } Template.members.events = { 'click button': function(event) { var $btn = $(event.currentTarget); //  ,   Latency Compensation   //         DOM Session.set('voted', true); //     Meteor.call('vote', $btn.data('id'), function(error, vote) { if (error) { Session.set('voted', false); } }); } } 


A simple example of calling a server method. The call occurs asynchronously, so while waiting for the response we mark the session value voted as true. When analyzing the answer, we can already roll it back if an error has occurred. The answer of the server is basically not important for us here, I only wonder if there was an error in voting or a vote was counted.

This code also has an example of using DOM events. In principle, you can use jQuery.on (), but I decided to go the canonical way.

file: client / views / index.html
 <head> <!--  meta   ..  SEO   --> <link href="stylesheets/project.css" media="screen" rel="stylesheet" type="text/css" /> <!-- ...   CSS --> <script type="text/javascript" src="js/flipclock/flipclock.min.js"></script> <!--   JavaScript --> <title>  2013</title> </head> <body> <div class="page-header-bg"></div> {{>header}} <div class="container-fluid"> <div class="container"> <div class="page-header"> <h1 class="header-gradient">  2013</h1> </div> {{>page}} </div> </div> </body> <template name="page"> {{#if contestInProgress}} {{>countdown}} {{>members}} {{>social}} {{>ratings}} {{>terms}} {{>footer}} {{else}} {{>winner}} {{>social}} {{>footer}} {{/if}} </template> 


Meteor processes all JavaScript, HTML, CSS files found in the project and combines them according to certain rules. However, files in the public folder are considered static, accessible as is, and not processed by Meteor. The styles could be brought under the control of Meteor, but it was decided to use the standard approach - to include links to static files in the HTML header.

Some third-party JavaScript libraries are also included as static files, although they could be transferred to the client folder and also used from their client-side JavaScript code. The fact is that not all libraries are written in such a way that they can be used in Meteor, in such cases you can always return to the standard inclusion in the HTML header. If there is a difference in the way a third-party library is enabled, using Meteor in client code is equally natural.

file: client / client.js
 contestEndDate = new Date('01/30/2014 12:00'); Session.set('inProgress', new Date() < contestEndDate); Template.header.contestInProgress = Template.page.contestInProgress = Template.footer.contestInProgress = function() { return Session.get('inProgress'); } Meteor.startup(function() { //   var targetDate = contestEndDate; var currentDate = new Date(); var offsetSeconds = (targetDate.getTime() - currentDate.getTime()) / 1000; offsetSeconds = Math.max(0, offsetSeconds); var clock = $('#countdown').FlipClock(offsetSeconds, { clockFace: 'DailyCounter', defaultClockFace: 'DailyCounter', countdown: true, callbacks: { stop: function() { Session.set('inProgress', false); } } }); }); 


In index.html you can see another application of reactivity. The variable contestInProgress indicates the status of the competition - in progress or already completed. The appearance of the page changes completely depending on this status. The status is set when the page is initialized, and also changes by the client when the stop event of the FlipClock counter occurs.

The variable contestInProgress is in three templates and its value is the same. Templates are independent of each other and are redrawn separately.

From the code it is clear that the value of the Meteor client session changes from an event handler triggered by a third-party FlipClock library. And this is despite the fact that the FlipClock library is loaded by the client’s browser when the page loads.
Here opens the unobvious advantage of Meteor. Since it is so easy to redraw the page at the end of the countdown, so why not do it ? This is just one line of code, but it will look spectacular if someone at this point will view the page.
If the project was developed in PHP + AJAX, this would be a separate task. Simple, but considering that this event will happen only once for the entire existence of the project, it is possible that the programmer simply doesn’t get around to updating the page status. And why waste time on it if a couple of people see it? The rest will simply get the page with the winner. This is the beauty of Meteor - the programmer does not need to think about the communication protocol and he can concentrate on those little things that would have been put off in the long box.

Climax


Well, in the end I will summarize a certain result. The project was successful and, I think, Meteor reactivity played a significant role here. Development of such a small web project on Meteor is a pleasure, although sometimes it takes a long time to look for a solution to trivial tasks. I would definitely not make a page with so many interactive elements if I used PHP.

Advantages:

  1. No need to think about the communication protocol between the client and the server
  2. Server and client code is written in the same language.
  3. Conveniently debug code directly from the browser
  4. You can quickly demonstrate the status of a project to a client using meteor deploy
  5. Active community, including on stackoverflow


Disadvantages:

  1. Still in the preview stage, not suitable for large projects.
  2. In the code, some trivial tasks have to be solved with cumbersome constructions.
  3. Installation on the combat server requires a node.js server, and if there is already an http server, then the proxy settings from port 80
  4. Not at all suitable for mobile devices due to the large amount of javascript
  5. All content in the browser is generated using javascript, this may adversely affect SEO


The contest itself has already ended, and you can poke the test version here promo.meteor.com
Project sources github.com/imajus/promo

Source: https://habr.com/ru/post/204262/


All Articles