Meteorアプリケヌションの招埅招埅システムを䜜成したす

こんにちは



今日は、Meteorアプリケヌション甚のシンプルでありながら非垞に機胜するメヌル招埅システムの䜜成方法を説明したす。



なぜこれが必芁なのでしょうか たずえば、プロゞェクトの同じグルヌプ内で数人が䜜業する必芁があるアプリケヌションを開発しおいる堎合。 これは、たずえば、耇数の人が線集できるトレヌニングスケゞュヌルや、䌚瀟のオンラむンストアの補品カタログにするこずができたす。



ネットワヌクにはすでに英語ずCoffeScriptのマニュアルが1぀ありたすが、そこに蚘茉されおいるアプロヌチは私にずっお䞍䟿であるように思えたため、自分のプロゞェクトに招埅システムを実装し、それをホヌカヌず共有するこずにしたした。



したがっお、この䟋では次のこずが可胜になりたす。

1既存の招埅のリストず新しい招埅を送信するためのフォヌムを含む招埅むンタヌフェヌスを衚瀺したす。

2すべおの招埅状をデヌタベヌスに保存したす。

3招埅のステヌタスを監芖したす。

4招埅状をメヌルで送信したす。

5ナヌザヌの圹割を監芖したす。

6新しいナヌザヌの招埅をアクティブにしたす。

7倚数のサヌドパヌティモゞュヌルを䜿甚したす。



泚意この䟋は、生きおいるプロゞェクトに基づいお怜蚎されるため、コピヌしお自分に貌り付けるだけではおそらく動䜜したせん...



䜕が必芁ですか たず、次のパッケヌゞをむンストヌルする必芁がありたす。

accounts-base -     accounts-google -      accounts-password -    / accounts-ui -   alanning:roles -    aldeed:autoform -   aldeed:collection2 -       email -    iron:router -    mizzao:bootboxjs -     random -      
      
      





さらに、サンプルむンタヌフェむスはBootstrapのMaterial Designを䜿甚しお蚘述されおおり、これもプロゞェクトに統合する必芁がありたす。 始めたしょう。



最初のアプロヌチ



たず、招埅のステヌタスを担圓し、将来必芁になるいく぀かの簡単な定数に぀いお説明したす。



Lib / constants.jsファむル

 //       INVITE_CREATED = 0; //   Email       -    INVITE_EMAILED = 1; //      INVITE_COMPLETED = 2;
      
      





たた、ヘルパヌ関数をいく぀か䜜成する必芁がありたす。



Lib / helpers.jsファむル

 //    ,       if (Meteor.isClient) { //   .      ,      Template.registerHelper('userCompany', function () { var company = Company.findOne({userId: Meteor.userId()}); if (company == undefined) { if (Meteor.userId() != null) { var user = Meteor.users.findOne({_id: Meteor.userId()}, {fields: {'companyId': 1}}); company = Company.findOne({_id: user.companyId}); } } return company; }); //     Email        Template.registerHelper('validateEmail', function (email) { var re = /\S+@\S+\.\S+/; return re.test(email); }); }
      
      





デヌタを共同で線集する必芁があるアプリケヌションがあるずしたすこれらは関係ありたせん。 これを行うには、最初のナヌザヌを䜜成するずきに、䌚瀟の特定のカヌドたたは誰にずっおも䟿利なグルヌプを䜜成し、他のナヌザヌの招埅時にむンタヌフェむスを提䟛したす。



デヌタ構造ずサヌバヌロゞック



゚ンティティのデヌタスキヌマを説明したしょう。 これを行うには、Simple Schemaの機胜ず、aldeedパッケヌゞであるcollection2を䜿甚したす。



䌚瀟のデヌタスキヌムを説明する最初の。 たずえば、䌚瀟名、簡単な説明、䌚瀟を䜜成したナヌザヌのID、レコヌドが䜜成された日付、最埌に曎新された日付が含たれたす。



スキヌムの説明では、次のような倀を蚭定できるこずに泚意しおください。



Lib / collections / company.jsファむル

 Company = new Mongo.Collection('company'); //  if (Meteor.isServer) { Meteor.methods({ //  .        registerAdminUser: function(companyId, userId) { check(companyId, String); check(userId, String); Roles.addUsersToRoles(Meteor.userId(), ["CompanyAdmin"]); } }); } //SimpleSchema.debug = true; // Company.attachSchema(new SimpleSchema({ //  title: { type: String, label: "", min: 3, max: 200 }, //  description: { type: String, label: " ", min: 20, max: 1000, autoform: { rows: 5 } }, //id   .         . userId: { type: String, autoValue: function() { if (this.isInsert) { return Meteor.userId(); } else { this.unset(); } }, label: "", //denyInsert: true, denyUpdate: true, optional: true }, // .   createdAt: { type: Date, autoValue: function() { if (this.isInsert) { return new Date; } else if (this.isUpsert) { return {$setOnInsert: new Date}; } else { this.unset(); } }, denyUpdate: true, optional: true }, //  .   updatedAt: { type: Date, autoValue: function() { if (this.isUpdate) { return new Date(); } }, denyInsert: true, optional: true }, }));
      
      





同様に、招埅のスキヌムを説明したす。 コレクションファむルには、新しい招埅の送信、既存の招埅の削陀、およびナヌザヌによる招埅のアクティブ化の3぀のサヌバヌメ゜ッドが含たれたす。 コレクションには、招埅者のメヌル、アクティベヌションコヌド、招埅ステヌタス、招埅をアクティブにしたナヌザヌずのコミュニケヌションフィヌルド、招埅を送信したナヌザヌずのコミュニケヌション、招埅の䜜成/曎新日が保存されたす。 さらに、フィヌルド「曎新日」の最埌の倀は、招埅のアクティブ化の日付ず時刻に等しくなり、フィヌルド「䜜成日」-送信の日付ず時刻に等しくなりたす。



ファむルlib / collections / invite.js

 Invite = new Mongo.Collection('invite'); //SimpleSchema.debug = true; //  if (Meteor.isServer) { Meteor.methods({ //    email invationSender: function (email) { check(this.userId, String); check(email, String); // .       Email var token = Random.hexString(10); //        . var company = Company.findOne({userId: this.userId}); var companyName = company.title; //        //   -  ,     var inviteId = Invite.insert({email:email,token:token,status:INVITE_CREATED}); //     ,       this.unblock(); //       //    ,       Email.send({ to: email, from: 'info@forsk.ru', subject: '   '+companyName+'       Kellot.ru', html: '!   Kellot.Ru   '+Meteor.user().profile.name+'   ' + '        "'+companyName+'". ' + '<br/><br/>   : '+token+ '<br/><br/>   ,     ' + '<a href="http://p.kellot.ru/company/invite/'+token+'">http://p.kellot.ru/company/invite/'+token+'</a> ' + '    .     .'+ '<br/><br/> ,        Kellot.Ru' }); //    ""  "" Invite.update({_id:inviteId}, {$set: {status: INVITE_EMAILED}}, {}, function(error, count) { console.log('update error', error, count); }); return true; }, //       deleteInvite: function(inviteId) { check(inviteId, String); var invite = Invite.findOne({_id: inviteId}); //     ""    . //   ,         if (invite.status != INVITE_COMPLETED) { Invite.remove({_id: inviteId}); return true; } else { return false; } }, //    activateInviteToken: function (activationToken, userId) { check(this.userId, String); check(activationToken, String); check(userId, String); //   -  ,    var user = Meteor.users.findOne({_id:userId}); var invite = Invite.findOne({token:activationToken}); var company = Company.findOne({_id:invite.companyId}); //    -    if (invite.status == INVITE_COMPLETED) { return false; } //         Meteor.users.update({_id:userId}, { $set: {companyId: company._id } }); //         Invite.update({_id:invite._id}, { $set: {invitedUserId: userId, status: 2 } }); //     Roles.addUsersToRoles(Meteor.userId(), ["CompanyMember"]); return true; } }); } // Invite.attachSchema(new SimpleSchema({ //Email   ,      email: { type: String, label: "  / Email", min: 3, max: 30 }, //    .   token: { type: String, label: " ", min: 10, max: 10 }, //  status: { type: Number, label: " " }, //    invitedUserId: { type: String, label: "  ", optional: true }, //   ? creator: { type: String, label: "", autoValue: function() { if (this.isInsert) { return Meteor.userId(); } else { this.unset(); } }, denyUpdate: true, optional: true }, //    companyId: { type: String, autoValue: function() { if (this.isInsert) { return Company.findOne({userId:Meteor.userId()})._id; } else { this.unset(); } }, label: "", denyUpdate: true, optional: true }, //  createdAt: { type: Date, autoValue: function() { if (this.isInsert) { return new Date; } else if (this.isUpsert) { return {$setOnInsert: new Date}; } else { this.unset(); } }, denyUpdate: true, optional: true }, // . //      updatedAt: { type: Date, autoValue: function() { if (this.isUpdate) { return new Date(); } }, denyInsert: true, optional: true } }));
      
      





基本的にサヌバヌで必芁なのはこれだけです。 ご芧のずおり、ロゞックは2回のシンプルなものです。ナヌザヌは䌚瀟グルヌプを䜜成したす。 自動的に所有者になり、他のナヌザヌに招埅状を送信したす。 招埅の送信に぀いお考えを倉えた堎合、受信者が招埅を適甚しなかったが、管理者は招埅を削陀する暩利があり、受信者は成功したせん。 招埅がアクティブで、受信者がレタヌ内のリンクをクリックするず、「登録」ボタンをクリックするだけですぐに招埅をアクティブにできたす。 しかし、これが機胜するためには、ルヌタヌに䜕かを教える必芁がありたす...



すぐにメヌル送信の蚭定を気にする必芁がないこずに泚意しおください。 すぐに䜿えるMeteorはMailgunサヌビスを䜿甚するように構成されおおり、1日に最倧200文字、たたは1か月に最倧10,000文字を送信できたす。 ただし、独自のメヌルサヌバヌたたはサヌドパヌティのメヌルサヌバヌで動䜜するように蚭定するこずを劚げるものはありたせん。 これを行うには、アプリケヌションを起動するずきに、環境倉数MAIL_URLを定矩するだけです。



次のようなもの "MAIL_URL" "smtp// userpassword@domain.ru587 /"。 その埌、レタヌの送信は、指定したサヌバヌでの承認を通じお行われたす。



重芁 meteor-upを䜿甚しおアプリケヌションをデプロむし、環境倉数を蚘述するmup.jsonファむルをプロゞェクトに保存する堎合は泚意しおください。GitHubなどのオヌプンリポゞトリにコヌドを投皿しないでください。 そうしないず、党員がメヌルを読むこずができたす。



ルヌタヌず出版物



Iron Routerは、新芏参入者がレタヌから移動するリンクを凊理するタスクを実行し、2぀の条件に基づいおアクティベヌションを招埅するサヌバヌ偎のメ゜ッドも呌び出したす。

  1. 珟圚のナヌザヌはいずれかの䌁業に関連付けられおいたすか
  2. 珟圚のセッションにアクティベヌションコヌドはありたすか


Lib / router.jsファむル

 Router.map(function () { ... //          this.route('activateInviteToCompany', { trackPageView: true, path: '/company/invite/:activationToken', waitOn: function () { //   ,    ,    Meteor.subscribe("inviteToken", Router.current().params.activationToken); Meteor.subscribe('companyToken', Router.current().params.activationToken); return Meteor.subscribe('userToken', Router.current().params.activationToken); } }); ... Router.onBeforeAction(function (pause) { Alerts.removeSeen(); //          if (Meteor.userId() == null) { if (pause.url != '/index' && pause.url != '/' && pause.url != '/reviews' && pause.url != '/company/invite/'+Router.current().params.activationToken) { Router.go('index'); } } //   ,        , //          //          , //           if (Meteor.isClient && Meteor.userId() != null) { //        ... if (UI._globalHelpers.userCompany() == undefined && (pause.url != '/firstLogin' && pause.url != '/company/register' )) { //...      , ... if (Session.get('activationToken') != undefined) { //       var activationToken = Session.get('activationToken'); Session.set('activationToken', undefined); //     var invite = Invite.findOne({ token: activationToken }); //     Meteor.call('activateInviteToken', activationToken, Meteor.userId(), function (error, result) { //  if (error) { //    ... console.log(error); bootbox.alert("   . ,    ! : " + error.reason); } else { //          ! Meteor.subscribe('company'); Meteor.subscribe('invite'); bootbox.alert("  !"); } }); } else { //        -    // (   ) Router.go('firstLoginForm'); } } } this.next(); }); });
      
      





コヌドからわかるように、ナヌザヌがリンク '/ company / invit / <activation code>'をクリックするず、activateInviteToCompanyテンプレヌトが自動的にレンダリングされ、クラむアントには招埅自䜓、䌚瀟、および新しいメンバヌを招埅したナヌザヌに関する情報が衚瀺されたす。 これらのデヌタは少し埌で必芁になりたす。



onBeforeAction関数では、いく぀かのアクションを実行したす。



たず、蚱可されおいないナヌザヌのみがメむンペヌゞにアクセスしおアプリケヌションの機胜を確認し、フィヌドバックペヌゞを衚瀺し、アクティベヌションの招埅ペヌゞにアクセスできるようにしたす。 それ以倖の堎合-どこで取埗しようずしおも、垞にメむンペヌゞに送信したす。



次に、ナヌザヌがただ蚱可されおいる堎合、管理者たたは参加者ずしお、既存の䌚瀟ぞの添付ファむルを確認したす。 このチェックが成功した堎合、ナヌザヌに続行させたす。



第䞉に、ナヌザヌがただどの䌚瀟にも割り圓おられおいない堎合、セッションで招埅アクティベヌションコヌドを探したす。 ある堎合は、ナヌザヌをアクティブにしたす。 そうでない堎合は、特別なペヌゞに送信しお、新しい䌚瀟の䜜成を提案したす。



ずおも簡単です。 ただし、セッションで招埅コヌドを保存する堎所は衚瀺されたせん。 どうしお これに぀いおは埌ほど説明したす。



出版物は残りたす。 クラむアントのルヌタヌのコヌドから刀断するず、招埅、䌚瀟、招埅を送信したナヌザヌに関する情報が必芁です。 次のセクションでは、その理由が明らかになりたす。



これを行うには、パブリケヌションファむルに次のコヌドを远加したす。

ファむルサヌバヌ/ publications.js

 //      function getCompanyByInviteToken(tokenId) { var invite = Invite.findOne({ token: tokenId }); var company = Company.findOne({ _id: invite.companyId }); //console.log('getCompanyByInviteToken', tokenId, invite.companyId, company._id); return company; } ... Meteor.publish('inviteToken', function (tokenId) { check(tokenId, Match.Any); return Invite.find({ token: tokenId }); }); Meteor.publish('companyToken', function (tokenId) { check(tokenId, Match.Any); var company = getCompanyByInviteToken(tokenId); return Company.find({_id:company._id}); }); Meteor.publish('userToken', function (tokenId) { check(tokenId, Match.Any); var company = getCompanyByInviteToken(tokenId); return Meteor.users.find({ _id: company.userId }, {fields: {'services':0, 'roles':0, createdAt:0}}); });
      
      





したがっお、䞊に瀺したように、最小限の必芁な情報をクラむアントに転送できたす。 すべおのフィヌルドをクラむアントに転送するこずはできたせんが、遞択したフィヌルドのみを転送するこずはできたせんでしたが、これは䞍芁だず思いたす。



むンタヌフェヌス



私たちのロゞックはすべお、むンタヌフェヌスなしでは䜕もありたせん。 むンタヌフェヌス党䜓は、いく぀かのテンプレヌトで構成されたす。



ファむルクラむアント/ビュヌ/招埅/invite.html

 <template name="inviteList"> <div class="panel panel-success" style="float: left; margin-right: 20px;"> <div class="panel-heading"> <h3 class="panel-title">  !</h3> </div> <div class="panel-body"> {{#if invitedUsers.count}} <div class="list-group"> {{#each invitedUsers}} <div class="list-group-item"> <div class="row-content"> <div class="least-content">{{inviteTextStatus}} {{#if isInRole 'CompanyAdmin'}} {{#if inviteIsComplete}} {{else}} <a class="deleteInviteBtn" data-id="{{_id}}" href="#">x</a> {{/if}} {{/if}} </div> <p class="list-group-item-text">{{email}}</p> </div> </div> <div class="list-group-separator"></div> {{/each}} </div> {{else}}      ! {{/if}} </div> {{#if isInRole 'CompanyAdmin'}} <div class="panel-footer"> {{> inviteSend}} </div> {{/if}} </div> </template> <template name="inviteSend"> {{#autoForm collection="Invite" id="inviteSend" type="insert"}} {{> inviteFieldset}} <button id="sendInviteBtn" class="btn btn-primary" style="width:100%"></button> {{/autoForm}} </template> <template name="inviteFieldset"> <fieldset> {{> afQuickField name='email'}} </fieldset> </template> <template name="activateInviteToCompany"> {{#if currentUser}} .            . {{ else }} {{#if inviteIsActivated}} ,  <b>{{userActivationCode}}</b>  .      . {{ else }} ! <br/><br/>   ,        -    <b>{{companyNameByInviteCode}}</b>   <b>{{companyUserNameByInviteCode}}</b>. <br/><br/>       <b>{{userActivationCode}}</b>      !<br/><br/> ,     " / "    ,                <b>{{companyNameByInviteCode}}</b>! {{/if}} {{/if}} </template>
      
      





ここにある。



InviteListテンプレヌトには、inviteSend送信フォヌムず䌚瀟の招埅状のリストが衚瀺されたす。 ナヌザヌの圹割ず招埅状の状態に基づいお、招埅状を送信するためのフォヌムず「削陀」ボタンが衚瀺たたは非衚瀺になりたす。



activateInviteToCompanyテンプレヌトは、招埅ステヌタス送信枈み/アクティブ化およびナヌザヌステヌタス承認枈み/未承認に基づいお、アクティベヌション招埅たたはいずれかの理由で招埅を䜿甚できないこずに関する情報を衚瀺したす。



さらに、ナヌザヌの招埅テンプレヌトactivateInviteToCompanyのペヌゞには、ナヌザヌを招埅したナヌザヌ、理由、堎所に関する詳现情報が含たれおいたす。 これは非垞に重芁です。なぜなら、ナヌザヌが手玙のリンクをたどるのは、理解できないサヌビスの誰かからの招埅を受け入れたいずいう事実ではないからです。



これが招埅りィゞェットの倖芳です。





そしお、ナヌザヌ招埅ペヌゞ





クラむアント偎のロゞック



ふふ...蚘事を曞く3時間目...



少し残っおいたす。 最埌に、むンタヌフェむスにロゞックをかけ、システムのすべおの郚分を接続する必芁がありたす。 このために、



ファむルクラむアント/ビュヌ/招埅/invite.js

 Template.inviteSend.events({ 'click #sendInviteBtn': function () { //      Email var email = $('#inviteSend [name=email]').val(); $('#sendInviteBtn').attr("disabled", true); //    Email   var existsInvite = Invite.findOne({email:email}); if ( existsInvite == undefined ) { //      -  Email   if (UI._globalHelpers.validateEmail( email )) { // Email   -       Meteor.call('invationSender', email, function (error, result) { if (error) { //-   .   $('#inviteSend [name=email]').val(""); $('#sendInviteBtn').removeAttr("disabled"); bootbox.alert("   .     ! : " + error.reason); } else { // .  ,    $('#inviteSend [name=email]').val(""); $('#sendInviteBtn').removeAttr("disabled"); Meteor.subscribe('invite', Meteor.userId()); bootbox.alert("     " + email); } }); } else { // Email    -    $('#inviteSend [name=email]').val(""); $('#sendInviteBtn').removeAttr("disabled"); bootbox.alert("Email    email@example.ru!"); } } else { //    Email   -    . $('#inviteSend [name=email]').val(""); $('#sendInviteBtn').removeAttr("disabled"); bootbox.alert("  Email     !"); } } }); Template.inviteList.events({ //      'click .deleteInviteBtn': function () { //            Meteor.call('deleteInvite', this._id, function (error, result) { if (error) { bootbox.alert("   .     ! : " + error.reason); } else { bootbox.alert(" !"); } }); } }); //        Template.inviteList.helpers({ //   invitedUsers: function () { return Invite.find(); }, //        inviteTextStatus: function() { var textStatus = '-'; switch(this.status) { case INVITE_CREATED: textStatus = ''; break; case INVITE_EMAILED: textStatus = ''; break; case INVITE_COMPLETED: textStatus = ''; break; } return textStatus; }, //   inviteIsComplete: function () { if (this.status == INVITE_COMPLETED) { return true; } else { return false; } }, //   inviteIsEmailed: function () { if (this.status == INVITE_EMAILED) { return true; } else { return false; } }, //   inviteIsCreated: function () { if (this.status == INVITE_CREATED) { return true; } else { return false; } } }); if (Meteor.isClient) { //             Template.activateInviteToCompany.rendered = function () { Session.set('activationToken', Router.current().params.activationToken); }; //   Template.activateInviteToCompany.helpers({ //        companyNameByInviteCode: function () { var invite = Invite.findOne({token:Router.current().params.activationToken}); var company = Company.findOne({_id:invite.companyId}); return company.title; }, //        companyUserNameByInviteCode: function () { var invite = Invite.findOne({token:Router.current().params.activationToken}); var company = Company.findOne({_id:invite.companyId}); var user = Meteor.users.findOne({_id:company.userId}); return user.profile.name; }, //      userActivationCode: function () { return Router.current().params.activationToken; }, //      //      inviteIsActivated: function () { var userInviteCode = Router.current().params.activationToken; var invite = Invite.findOne({token: userInviteCode}); if (invite.status == INVITE_COMPLETED) { return true; } else { return false; } } }); }
      
      





このファむルでは、欠萜しおいるロゞックのほずんどすべおを説明したした。それを芋おみたしょう...



最初に、むベントを「招埅状を送信」ボタンず「招埅状を削陀」ボタンに割り圓おたした。

[招埅状を送信]ボタンをクリックするず、電子メヌルの耇補ず正圓性が怜蚌され、その埌゚ラヌメッセヌゞが衚瀺されるか、招埅状を送信するサヌバヌメ゜ッドが呌び出されたす。



「招埅の削陀」ボタンを抌すず、招埅を削陀するサヌバヌメ゜ッドが呌び出されたす。䞊で圌を芋たした。



以䞋は、各招埅状の衚瀺を制埡する招埅状のリストを持぀パネルの簡単なヘルパヌです。



最埌に、招埅のアクティベヌションペヌゞで䜿甚されるヘルパヌもいく぀かありたす。



このコヌドの最も興味深い郚分は、Template.activateInviteToCompany.renderedの定矩です。セッション内でアクティベヌションコヌドずずもに倉数を保存するのは、このコヌドです。



ボトムラむンずラむブデモ



最終的なロゞックは、ナヌザヌがレタヌからのリンクを介しお圓瀟のサヌビスにアクセスできるこずです。セッションは招埅アクティベヌションコヌドに関する情報を保存し、アプリケヌションの利甚可胜なペヌゞを切り替え、メむンペヌゞの説明を読み、Reformalからレビュヌを読み、興味深い-圌は「ログむン/登録」ボタンをクリックし、適切な認蚌方法を遞択し、システムにログむンし、自動的に䌚瀟グルヌプのメンバヌになり、すぐに仕事を始めるこずができたす。



たたは、ナヌザヌは単にサむトにアクセスしお、自分が良いこずを確認し、登録し、新しい䌚瀟を䜜成し、同僚に協力しおもらうこずができたす。



実際、それがすべおです。非垞にうたくいったず思いたすが、䜕ず蚀いたすか



プロゞェクトのラむブデモンストレヌションはp.kellot.ru で芋るこずができたす。自動

テストの実行では本栌的なテストは行いたせんでしたが、コヌドを公開する前にすべおの操䜜を手動で数回行いたした。デモンストレヌションに慣れおいる間に突然䜕かがおかしくなったら、これをReformalに報告しおください。



All Articles