nodejs
and mongodb
, also the meteor does not have Windows support, and if you are going to touch a meteor you need to get a Linux or MacOS operating system.npm install -g meteor
, in this case just download the old version, you need to run it in the console to properly install: $ curl https://install.meteor.com/ | sh
$ meteor create 'todo-list' todo-list: created. To run your new app: cd todo-list meteor $ cd todo-list $ meteor [[[[[ ~/dev/meteor-getting-started/todo-list ]]]]] => Started proxy. => Started MongoDB. => Started your app. => App running at: http://localhost:3000/
.meteor
directory was created, various service information about the project is stored there, and even automatically generated .gitignore
. By the way, for manual package management, you can change the packages
file, but console utilities are also quite convenient.nodejs
, mongodb
and meteor
installation, for example, I have the following configuration on my computer now: $ node -v v0.10.33 $ mongod --version db version v2.4.12 $ meteor --version Meteor 1.0
$ meteor add <package-name>
less
, jade
and coffeescript
, which means it's time to install them. All the packages used in the lesson and a bunch of others can be found on the Atmosphere website. Actually package names:less
, coffeescript
is official packages, so they do not contain the name of the author;mquandalle:jade
- but this is not an official package, so the name consists of two components, but it is well made, and I have not had any problems with its use.sourcemap
support is built into the packages of less
and coffeescript
, so the sourcemap
process in the browser will be simple, the sourcemap
supported by the meteor itself: it provides the necessary api to connect this functionality, so we donβt have to customize something.jquery
and underscore
already included in the meteor, like many other packages, the full list can be found in the ./.meteor/versions
file in the created project.grant
or gulp
to compile the templates, styles and scripts, the meteor has already taken care of this. For scaffolding there is a project for Yeoman, but it is more pleasant for me to create everything manually. In the previous project, I used about the following folder structure: todo-list/ - βββ client - β βββ components - β β , β βββ config - β βββ layouts - , β βββ lib - , β β β βββ routes - β βββ styles - βββ collections - βββ lib - , βββ public - : , robots.txt βββ server - β βββ methods - , , β β β βββ publications - β βββ routes - , β β http β βββ startup -
public
folder in the project root will be available to users via links from the browser and will not automatically connect to the project;server
folder are in the root; only the server part of the application is available;client
folder are in the root; only the client part of the application is available;lib
, then all * folders and files are loaded in alphabetical order;main.
loaded last.lib
directory in the project root will be loaded, the client
folder will be further processed, files from lib
will be loaded first, and then in alphabetical order: components
-> config
- > ... -> styles
. And after the files from the collections
folder. Files from the public
and server
folders will not be loaded into the browser, but, for example, the scripts stored in the public
folder can be connected via the script
tag, as we used to do in other projects, but the developers of the framework do not recommend this approach. if Meteor.isClient # , if Meteor.isServer # ,
Meteor.startup(<func>)
method to control the execution time of scripts, in the browser this is an analogue of the $
function from the jQuery
library, and on the server, the code in this function will be executed immediately after loading all the scripts in the order of loading these files. Learn more about these variables and methods .mizzao:bootstrap-3
- it is the most popular among others, and I think when using it we should have no problems.head.jade
in the client/layouts
head.jade
. This will be the only file in our application that does not have a template format, in short, just create a page header, and later we will analyze what templates are. //- client/layouts/head.jade head meta(charset='utf-8') meta(name='viewport', content='width=device-width, initial-scale=1') meta(name='description', content='') meta(name='author', content='') title Meteor. TODO List.
iron:router
package ( repository ).client/config
directory, create a file router.coffee
, with the following content: # client/config/router.coffee Router.configure layoutTemplate: "application"
application
. Therefore, in the layouts
folder, create an application.jade
file. In this file we describe the template, some entity, which at the stage of assembling the application will turn into javascript
code. By the way, the meteor uses their own mustache spacebars
template and blaze
library.spacebars
templates spacebars
compiled into a blaze
library blaze
, which will later work directly with the DOM
. In the project description there is a comparison with other popular libraries:Ember
templates, the Blade renders only changes, there is no need for explicit data-baiting and descriptions of dependencies between the templates;angular
and polymer
templates, the Blaze has a clear and simple syntax, a lower entry threshold and is not positioned at all as a technology of the future, but simply works;React
has simple template description syntax and simple data management.ember
) and, in principle, agree with the authors of the library, from the minuses in the blaze I want to notice a tie on the meteor.blaze
or spacebars
. For jade
templates, the compilation process has the following sequence: jade
-> spacebars
-> blaze
.template
tag, where there should be an attribute with the name of the template. Remember, we specified layoutTemplate: "application"
in the settings of the router, here is the application
, which is exactly the name of the template. //- client/layouts/application.jade template(name='application') nav.navbar.navbar-default.navbar-fixed-top .container .navbar-header button.navbar-toggle.collapsed( type='button', data-toggle='collapse', data-target='#navbar', aria-expanded='false', aria-controls='navbar' ) span.sr-only Toggle navigation span.icon-bar span.icon-bar span.icon-bar a.navbar-brand(href='#') TODO List #navbar.collapse.navbar-collapse ul.nav.navbar-nav .container +yield .footer .container p.text-muted TODO List, 2014.
jade
is not quite familiar to us, with its mixins, javascript and includas. Jade
has to compile into a spacebars
template, and this imposes some features. From the jade
, we can say we will take only the syntax, the rest we just do not need. In this template, the +yield
construction is used, this construction means that the yield
template will be rendered instead, it is a iron:router
feature, it will automatically substitute the desired template depending on the path, we will deal with routers a little later, and now we can make cosmetic changes to the layout and look at the result. // client/styles/main.less html { position: relative; min-height: 100%; } body { margin-bottom: 60px; & > .container{ padding: 60px 15px 0; } } .footer { position: absolute; bottom: 0; width: 100%; height: 60px; background-color: #f5f5f5; .container .text-muted { margin: 20px 0; } }
iron:router
package, it is well documented , actively supported, has rich functionality and is also the most popular routing solution in the context of a meteor.Ruby on Rails
, and users do not need to think that they are two different applications and authorize them twice. In general, there are several popular approaches for server routing and creating a REST api for a meteor. //- client/layouts/application.jade //- ... #navbar.collapse.navbar-collapse ul.nav.navbar-nav li a(href='/') Home li a(href='/about') About
# client/routes/home.coffee Router.route '/', name: 'home' class @HomeController extends RouteController action: -> console.log 'Home Controller' super() # client/routes/about.coffee Router.route '/about', name: 'about' class @AboutController extends RouteController action: -> console.log 'About Controller' super()
Router.route
function, the first is the path, and the path can be a pattern (for example: /:user/orders/:id/info
), all parameters from the pattern will be available in the controller's object via the params
property. The second parameter is an object with options. To make all the logic separate from the simple description of the path and name, you can create controllers, in our case they are simple stubs, here we donβt explicitly specify controller names in the properties, because by default iron:router
tries to find a controller named <RouteName>Controller
, and of course, our controllers should be accessible globally, we do it in the censcript, binding the variable to the current context, in the usual js, it is enough just to declare the variable not through var
.By the way, the meteor is not used, for example,amd
to download the code, the files are simply loaded in a certain sequence. Therefore, all interaction between modules described in different files is carried out through global variables. Which, as for me, is quite convenient, and when using coffee, randomly declaring a global variable is quite difficult, and it will be immediately noticeable.
iron:router
will also try to automatically render the template with the name of the route (but the templates can be specified explicitly), create them //- client/components/home/home.jade template(name='home') h1 Home //- client/components/about/about.jade template(name='about') h1 About
In the course of the development of this lesson, I will try to make all changes in the code to the repository, in accordance with the sequence of presentation, so that you can follow the whole process, as in the post some things can be missed. Repository
google
and github
services. I used to set up a bunch of devise
and omniauth
in rails omniauth
pair of generators and a few lines in the config file. So, the meteor is not enough that it provides out of the box, so also the setting up of services is as simple as possible.accounts-base
- basic package for users of the application on a meteor;accounts-password
, accounts-github
, accounts-google
- we will add support for login / password authentication and github
and google
services;ian:accounts-ui-bootstrap-3
- a package to simplify the integration of accounts into the bootstrap application.ian:accounts-ui-bootstrap-3
package will allow us to add an authentication / registration form to the application in one line, as well as provide an interface to configure third-party services. The project itself , there is a small documentation and screenshots of how the form integration and customization of services look like. //- client/layouts/application.jade //- ... #navbar.collapse.navbar-collapse ul.nav.navbar-nav li a(href='/') Home li a(href='/about') About ul.nav.navbar-nav.navbar-right //- //- ian:accounts-ui-bootstrap-3 +loginButtons
$ meteor mongo MongoDB shell version: 2.4.9 connecting to: 127.0.0.1:3001/meteor meteor:PRIMARY> show collections meteor_accounts_loginServiceConfiguration meteor_oauth_pendingCredentials system.indexes users meteor:PRIMARY> db.meteor_accounts_loginServiceConfiguration.find() { "service" : "github", "clientId" : "<id>", "secret" : "<secret>", "_id" : "AjKrfCXAioLs7aBTN" } { "service" : "google", "clientId" : "<id>", "secret" : "<secret>", "_id" : "HaERjHLYmAAhehskY" }
smtp
, by the way, an email
package is used to send email
. It is not included in the standard set of meteors, so you must install it manually if you need to work with mail. # server/config/smtp/coffee smtp = username: "meteor-todo-list@yandex.ru" password: "meteor-todo-list1234" server: "smtp.yandex.ru" port: "587" # _(smtp).each (value, key) -> smtp[key] = encodeURIComponent(value) # url smtp url = "smtp://#{smtp.username}:#{smtp.password}@#{smtp.server}:#{smtp.port}" # , process.env.MAIL_URL = url
# server/config/accounts.coffee emailTemplates = from: 'TODO List <meteor-todo-list@yandex.ru>' siteName: 'Meteor. TODO List.' # _.deepExtend Accounts.emailTemplates, emailTemplates # Accounts.config sendVerificationEmail: true # Accounts.onCreateUser (options = {}, user) -> u = UsersCollection._transform(user) options.profile ||= {} # , # options.profile.emailHash = Gravatar.hash(u.getEmail() || "") # , options.service = _(user.services).keys()[0] if user.services # , # _.extend user, options
mondora:connect-with
, but it is still raw. You can try to merge accounts yourself, there is nothing complicated in this, and there are many examples and other solutions in the network: one , two , three .You shouldn't kick me hard, for having considered the system of accounts so superficially, I just wanted to show that there is nothing difficult in it. Detailed review will require a separate post. And we in the lesson have created the necessary basic functionality and can continue to go to the final result.
autopublish
and insecure
, and so now is the time to get rid of them, as they provide the user unlimited access to all the collections in the database, and can be used for prototyping only. Packages are removed by the command. $ meteor remove <package-name>
find
, insert
, update
, upsert
(the aggregation can be organized on the server using the package zvictor:mongodb-server-aggregation
). One of the collections we have already created and access to it can be obtained through Meteor.users
, for example, try running in the browser console Meteor.users.findOne()
. It is important to note here that all collection data is cached in the browser, and if you execute a million times in a loop on the client Meteor.users.find(options).fetch()
, you will not load anything except the browser. This is achieved by using a library minimongo
that is smart enough to make a selection depending on the parameters passed to the client._transform
the collection, into which objects are transferred after receiving them from the server and there they can be processed, however, so as not to delve into these subtleties , you can use a package dburles:collection-helpers
that adds a method to the collection helpers
where you can pass an object from which all data will be inherited. # collections/users.coffee Users = Meteor.users # _.extend Users, # allowFieldsForUpdate: ['profile', 'username'] # Users.helpers # , update: (data) -> Users.update @_id, data # , # set: (data) -> d = {} f = _(Users.allowFieldsForUpdate) for key, value of data when f.include(key) d[key] = value @update $set: d # , # # merge: (data) -> current = @get() @set _.deepExtend(current, data) # , , # get: -> r = {} r[key] = @[key] for key in _(@).keys() r # getEmails: -> p = [@profile?.email] s = _(@services).map (value, key) -> value?.email e = _(@emails).map (value, key) -> value?.address _.compact p.concat(e, s) # getEmail: -> @getEmails()[0] # getUsername : -> @username || @_id getName : -> @profile?.name || "Anonymous" getPublicEmail : -> @profile?.email urlData: -> id: @getUsername() # , # getAvatar: (size) -> size = Number(size) || 200 options = s: size d: 'identicon' r: 'g' hash = "00000000000000000000000000000000" if email = @getPublicEmail() hash = Gravatar.hash(email) else hash = @profile?.emailHash || hash Gravatar.imageUrl hash, options # isFromGithub: -> @service == 'github' isFromGoogle: -> @service == 'google' isFromPassword: -> @service == 'password' # # isEditable: -> @_id == Meteor.userId() # @UsersCollection = Users
$ meteor mongo meteor:PRIMARY> db.users.count() 3
autopublish
, now the process of publishing data needs to be done manually, this will allow us to control the data transmitted to the user. # server/publications/users.coffee Meteor.publish 'users', (limit = 20) -> UsersCollection.find {}, fields: service: 1 username: 1 profile: 1 limit: limit
autopublish
, only automatically and with all collections.profile
and user name, subscribers can not see. But for example, we want to provide access to e-mail addresses for the current user; we will create another publication. # server/publications/profile.coffee Meteor.publish 'profile', -> # , # if @userId # UsersCollection.find { _id: @userId }, fields: service: 1 username: 1 profile: 1 emails: 1 else # , @ready() return
Meteor.publish
is a function that should return the collection cursor. This function can take any number of arguments and it runs in the context of an object, in which some methods are available that allow you to notify the user about various changes in data and provide access to some properties of the connection. For example, in the profile publication we use the method ready
, in the case when the user is not authorized, it means that the data in the publication is ready, and a callback will be called upon the client when subscribing, but it will not receive any data. Read more about publications.autopublish
.iron:router
, and it will control the entire necessary process, since for manual control this process will have to follow many things, and this library solves all the problems. It is desirable to output some data page by page, so before creating a controller for users, we abstract a little and create a class that has the functionality to manage pages and which will inherit from the library controller iron:router
. # client/lib/0.pageable_route_controller.coffee varName = (inst, name = null) -> name = name && "_#{name}" || "" "#{inst.constructor.name}#{name}_limit" class @PagableRouteController extends RouteController pageable: true # , perPage: 20 # # limit: (name = null) -> Session.get(varName(@, name)) || @perPage # incLimit: (name = null, inc = null) -> inc ||= @perPage Session.set varName(@, name), (@limit(name) + inc) # resetLimit: (name = null) -> Session.set varName(@, name), null # ? loaded: (name = null) -> true
incLimit
, for the current controller, of course, if it supports this functionality. It could be done and endless scrolling, but it's easier. //- client/components/next_page_button/next_page_button.jade template(name='nextPageButton') unless loaded a.btn.btn-primary.btn-lg.NextPageButton(href = '#') | More
# client/components/next_page_button/next_page_button.coffee Template.nextPageButton.helpers loaded: -> ctrl = Router.current() if ctrl.pageable ctrl.loaded(@name) else true Template.nextPageButton.events 'click .NextPageButton': (event) -> ctrl = Router.current() if ctrl.pageable ctrl.incLimit(@name, @perPage)
Template
. Refer to the template we can through Template.<template-name>
. To describe the methods used in the template, you need to use the method helpers
to which the object is passed with the methods. In this example, we describe only one method loaded
that checks what the current controller is and gives a result indicating whether all the data has been loaded. In the template itself, we pull this method in the construction unless loaded
; also in the template, you can take data from the current context. Template helpers can be compared with the prototype of an object when using them in a template, but there are limitations within the function itself, since each helper is called something like this<helper-func>.apply(context, arguments)
, that is, we do not have the opportunity to apply to all template helpers, inside a function, which in general can sometimes interfere.events
to which the object is transferred, with keys of the following format <event> <selector>
. The event is passed to the handler, jQuery
and the template in which the event was triggered, since we can handle child events in the parent template, this can sometimes be useful.iron:router
. # client/routes/users.coffee Router.route '/users', name: 'users' class @UsersController extends PagableRouteController # perPage: 20 # , , # # # , iron:router # , # subscriptions: -> @subscribe 'users', @limit() # data: -> users: UsersCollection.find() # ? loaded: -> @limit() > UsersCollection.find().count() # , onRun: -> @resetLimit() @next()
subscriptions
is subscribed to the publication users
. There is still a practically similar method waitOn
, only in it the router will wait until all data is unloaded, and after it renders the page, until this point it will display a template that can be set via a property loadingTemplate
. The data returned by the method data
will be bound to the template, and we will be able to use it through the current context. UsersCollection.find()
returns the cursor, not the data itself, but the Blaze will do all the transformations for us, as if we are already working with ready data. Since we subscribe to a limited amount of data, the call UsersCollection.find().fetch()
will return us only the data uploaded to the client, that is, if we, for example, set a limit of 1, thenfind
will work only with the loaded selection (one record), and not all the data in the collection from the database. For example, here we redefine the method loaded
, I think its essence is clear, but it should be remembered that it count
will return us the number of local records, which means it will be equal limit
until all the data are unloaded, therefore the condition is strictly greater.iron:router
there are several hooks, for example, it would not be bad for us to reset the loaded limit every time you open a page with users. Otherwise, if we previously unloaded a large amount of data, the page can be rendered for a long time. Therefore, it is convenient to use the hook to reset the data limit.onRun
. It is executed once, when the page loads. There is only the moment that when hot replacing the code that the meteor performs, after saving the files with the code, this hook will not be executed, so manually refresh the page when the controller using the hook is debugged (there is no such problem with others). More about hooks and subscriptions .nextPageButton
will lead us to load a new piece of data, but all due to manipulations with the object Session
in PagableRouteController
. The data in this object are reactive, and iron:router
will automatically track changes in them. You can try to type in the browser console Tracker.autorun( function() { console.log( 'autorun test', Session.get('var') ); } )
Session.set('var', 'value')
, the result will not take long.iron:router
understands when a subscription needs to be updated, in the same way the data in the templates are updated automatically. More details about the reactive variables are well described in the official documentation , and in addition to the variables Session
there is an opportunity to create reactive objects , with methods set
and get
for managing the values ββthat will also be tracked by the tracker and templates. And the tracker is something like a listener, you can create a function that will not contain reactive variables, but will also be tracked by the tracker, for this you need to useTracker.Dependency . In general, this library has other possibilities, but in practice I did not have to use them, perhaps in vain. var depend = new Tracker.Dependency(); var reactFunc = function() { depend.depend(); return 42; } Tracker.autorun(function() { console.log( reactFunc() ); }); // 42 depend.changed() // 42 depend.changed() // 42
iron:router
, but this is not the only mechanism . The main thing to remember is that you need to use subscriptions carefully, otherwise you risk unloading a large amount of data and automatically tracking changes to them where there is no need. iron:router
provides us with a very simple way to manage subscriptions, it will turn off all unnecessary ones, connect the necessary ones, update the current ones if necessary, like, for example, this happens when we load the next page with us. //- client/components/users.jade template( name='users' ) h1 Users .row //- , +each users .col-xs-6.col-md-4.col-lg-3 //- //- each , //- +userCard //- +nextPageButton //- client/components/user_avatar/user_avatar.jade //- , template(name='userAvatar') img(src="{{user.getAvatar size}}", alt=user.getUsername, class="{{class}}") //- client/components/user_card.jade //- //- template(name='userCard') .panel.panel-default .panel-body .pull-left +userAvatar user=this size=80 .user-card-info-block ul.fa-ul //- li if isFromGithub i.fa.fa-li.fa-github else if isFromGoogle i.fa.fa-li.fa-google else i.fa.fa-li b= getName //- li i.fa.fa-li @ //- a(href="{{ pathFor route='users_show' data=urlData }}")= getUsername //- , if getPublicEmail li i.fa.fa-li.fa-envelope = getPublicEmail
mongo
- the changes will be displayed on the page, and you will not have to do anything to do this. # client/routers/home.coffee Router.route '/', name: 'home' class @HomeController extends PagableRouteController # ? isUserPresent: -> !!Meteor.userId() # # waitOn: -> if @isUserPresent() @subscribe 'profile' # , data: -> if @isUserPresent() { user: UsersCollection.findOne Meteor.userId() } # # action: -> if @isUserPresent() @render 'profile' else @render 'home'
# client/routers/user_show.coffee Router.route '/users/:id', name: 'users_show' class @UsersShowController extends PagableRouteController # template: 'profile' # waitOn: -> @subscribe 'user', @params.id # data: -> user: UsersCollection.findOneUser(@params.id)
# collections/users.coffee # ... _.extend Users, # ... findUser: (id, options) -> Users.find { $or: [ { _id: id }, { username: id } ] }, options findOneUser: (id, options) -> Users.findOne { $or: [ { _id: id }, { username: id } ] }, options
# server/publications/user.coffee Meteor.publish 'user', (id) -> UsersCollection.findUser id, fields: service: 1 username: 1 profile: 1 limit: 1
//- client/components/editable_field/editable_field.jade //- //- , //- //- //- this.<key> template(name='editableField') .form-group.EditableFiled if data.isEditable div(class=inputGroupClass) if hasIcon .input-group-addon if icon i.fa.fa-fw(class='fa-{{icon}}') else i.fa.fa-fw=iconSymbol input.Field.form-control(placeholder=placeholder, value=value, name=name) else if defaultValue span.form-control-static if hasIcon if icon i.fa.fa-fw(class='fa-{{icon}}') else i.fa.fa-fw=iconSymbol = defaultValue
class='fa-{{icon}}'
, icon
- it is a variable. # client/components/editable_field/editable_field.coffee Template.editableField.helpers value: -> ObjAndPath.valueFromPath @data, @path name: -> ObjAndPath.nameFromPath @scope, @path hasIcon: -> @icon || @iconSymbol inputGroupClass: -> (@icon || @iconSymbol) && 'input-group' || '' Template.editableField.events # , 'change .Field': (event, template) -> data = $(event.target).serializeJSON() $(template.firstNode).trigger 'changed', [data]
//- client/components/profile/profile.jade template(name='profile') //- , , //- +with user .profile-left-side .panel.panel-default .panel-body .container-fluid .row.row-bottom //- , //- <>=<>, //- userAvatar +userAvatar user=this size=200 class='profile-left-side-avatar' .row //- +editableField fieldUsername +editableField fieldName +editableField fieldEmail .profile-right-side h1 Boards
# client/components/profile/profile.coffee Template.profile.helpers fieldUsername: -> data: @ defaultValue: @getUsername() placeholder: 'Username' scope: 'user' path: 'username' iconSymbol: '@' fieldName: -> data: @ defaultValue: @getName() placeholder: 'Name' scope: 'user' path: 'profile.name' icon: 'user' fieldEmail: -> data: @ defaultValue: @getPublicEmail() placeholder: 'Public email' scope: 'user' path: 'profile.email' icon: 'envelope' Template.profile.events # # 'changed .EditableFiled': (event, template, data) -> user = template.data?.user return unless user data = data.user user.merge data
jade
quite semantic, you donβt need to think about many things and read a lot of documentation - everything is quite obvious. But if you have problems understanding the code above, I advise you to look through the documentation for the mquandalle package : jade and spacebars . Itβs just that when I got acquainted with the layout of templates in a meteor, there were no problems, I think that they, in fact, made them very comfortable.npm
on the server.aldeed:simple-schema
some new features to the package . # lib/simple_schema.coffee _.extend SimpleSchema, # # build: (objects...) -> result = {} for obj in objects _.extend result, obj return new SimpleSchema result # , # # timestamp: createdAt: type: Date denyUpdate: true autoValue: -> if @isInsert return new Date if @isUpsert return { $setOnInsert: new Date } @unset() updatedAt: type: Date autoValue: -> new Date
# collections/boards.coffee # boardsSchema = SimpleSchema.build SimpleSchema.timestamp, 'name': type: String index: true 'description': type: String optional: true # # 'owner': type: String autoValue: (doc) -> if @isInsert return @userId if @isUpsert return { $setOnInsert: @userId } @unset() # 'users': type: [String] defaultValue: [] 'users.$': type: String regEx: SimpleSchema.RegEx.Id # Boards = new Mongo.Collection 'boards' Boards.attachSchema boardsSchema # Boards.allow # insert: (userId, doc) -> userId && true # update: (userId, doc) -> userId && userId == doc.owner # _.extend Boards, findByUser: (userId = Meteor.userId(), options) -> Boards.find $or: [ { users: userId } { owner: userId } ] , options create: (data, cb) -> Boards.insert data, cb # Boards.helpers update: (data, cb) -> Boards.update @_id, data, cb addUser: (user, cb) -> user = user._id if _.isObject(user) @update $addToSet: users: user , cb removeUser: (user, cb) -> user = user._id if _.isObject(user) @update $pop: users: user , cb updateName: (name, cb) -> @update { $set: {name: name} }, cb updateDescription: (desc, cb) -> @update { $set: {description: desc} }, cb # joins getOwner: -> UsersCollection.findOne @owner getUsers: (options) -> UsersCollection.find $or: [ { _id: @owner } { _id: { $in: @users } } ] , options urlData: -> id: @_id # @BoardsCollection = Boards
aldeed:autoform
from the same author, you can generate forms that will immediately notify about errors when creating a record.Boards = new Mongo.Collection 'boards'
if it is not there, or we are connecting to the existing one. In principle, this is all the necessary functionality for creating new collections, there are a couple more options that you can specify when creating.allow
the collection, we can control access to change data in the collection. In the current example, we prohibit the creation of new entries in the collection for all unauthorized users, and allow only the creator of the board to change data. These checks will be carried out on the server and you can not worry that some cooler will change this logic on the client. Also at your disposal there is almost a similar method deny
, I think its essence is clear. Learn more about allow and deny . # server/publications/boards.coffee Meteor.publish 'boards', (userId, limit = 20) -> findOptions = limit: limit sort: { createdAt: -1 } if userId # cursor = BoardsCollection.findByUser userId, findOptions else # cursor = BoardsCollection.find {}, findOptions inited = false userFindOptions = fields: service: 1 username: 1 profile: 1 # addUser = (id, fields) => if inited userId = fields.owner @added 'users', userId, UsersCollection.findOne(userId, userFindOptions) # , # handle = cursor.observeChanges added: addUser changed: addUser inited = true # , # userIds = cursor.map (b) -> b.owner UsersCollection.find({_id: { $in: userIds }}, userFindOptions).forEach (u) => @added 'users', u._id, u # , @onStop -> handle.stop() return cursor
added
, changed
andremoved
in the context of the publication can manage the data transmitted to the client. If we return a collection cursor to a publication, then these methods will be called automatically depending on the state of the collection, which is why we return the cursor, but additionally in the publication itself we subscribe to changes to the data in the board collections, and send user data to the client as necessary.You can also use the mrt: reactive-publish package to create reactive publications .
npm
packages, you need to add meteorhacks:npm
, after that, all the necessary packages can be described in the file packages.json
. For example, I only need the gm package and mine packages.json
will look like this: { "gm": "1.17.0" }
npm
connected through meteorhacks:npm
I will turn around in one meteor package, so when you build an application through a command, meteor build
there will be no problems and all dependencies will be resolved automatically.npm
need to connect packages on the server through the command Meteor.npmRequire(<pkg-name>)
, it works as well as the function require
in the node. # server/lib/meteor.coffee Meteor.getUploadFilePath = (filename) -> "#{process.env.PWD}/.uploads/#{filename}" # server/methods/upload_board_image.coffee # gm = Meteor.npmRequire 'gm' # resizeAndWriteAsync = (buffer, path, w, h, cb) -> gm(buffer) .options({imageMagick: true}) .resize(w, "#{h}^", ">") .gravity('Center') .crop(w, h, 0, 0) .noProfile() .write(path, cb) # resizeAndWrite = Meteor.wrapAsync resizeAndWriteAsync # Meteor.methods uploadBoardImage: (boardId, data) -> board = BoardsCollection.findOne(boardId) if board.owner != @userId throw new Meteor.Error('notAuthorized', 'Not authorized') data = new Buffer data, 'binary' name = Meteor.uuid() # path = Meteor.getUploadFilePath name resizeAndWrite data, "#{path}.jpg", 1920, 1080 resizeAndWrite data, "#{path}_thumb.jpg", 600, 400 # BoardsCollection.update { _id: boardId }, $set: background: url: "/uploads/#{name}.jpg" thumb: "/uploads/#{name}_thumb.jpg" return
uploadBoardImage
we accept the identifier of the board to which the image is added and the string with the binary data of this image.Meteor.wrapAsync(<async-func>)
functions which last parameter accept kollbek turn around. In this callback, the first parameter must be an error, and the second is the result, such is the format of the parameters of all standard libraries in the node. If an error occurs, the wrapped function will throw an exception with this error, otherwise the second parameter returned from the function will be returned to the callback.I understand that for issuing statics from the server it is better to use ready-made and run-in solutions for many reasons, but here I am going to give statics to the node.
iron:router
. Similarly, as on the client, create a server route. # server/routes/uploads.coffee fs = Meteor.npmRequire 'fs' Router.route '/uploads/:file', where: 'server' action: -> try filepath = Meteor.getUploadFilePath(@params.file) file = fs.readFileSync(filepath) @response.writeHead 200, { 'Content-Type': 'image/jpg' } @response.end file, 'binary' catch e @response.writeHead 404, { 'Content-Type': 'text/plain' } @response.end '404. Not found.'
where: 'server'
, otherwise it will not work. In action, we are trying to read the specified file from disk, since in this directory there will be only images of the same format, I have simplified this method as much as possible.request
and response
available in the context of the route are class objects from the standard library of the http.IncomingMessage and http.ServerResponse nodes, respectively.Thereiron:router
is also an interface for creating a REST API.
Here I also created autocompet to add users to the board, it also uses RPC, more details on the implementation can be found in the repository .
//- client/components/new_board_form/new_board_form.jade template(name='newBoardForm') //- .panel.panel-default.new-board-panel(style='{{panelStyle}}') .panel-body h1 New board form(action='#') .form-group input.form-control(type='text',placeholder='Board name',name='board[name]') .form-group textarea.form-control(placeholder='Description',name='board[description]') .form-group //- , , label.btn.btn-default(for='newBoardImage') Board image .hide input#newBoardImage(type='file', accept='image/*') button.btn.btn-primary(type='submit') Submit
# client/components/new_board_form/new_board_form.coffee # currentImage = null currentImageUrl = null currentImageDepend = new Tracker.Dependency # resetImage = -> currentImage = null currentImageUrl = null currentImageDepend.changed() # uploadImage = (boardId) -> if currentImage reader = new FileReader reader.onload = (e) -> # Meteor.call 'uploadBoardImage', boardId, e.target.result, (error) -> if error alertify.error error.message else alertify.success 'Image uploaded' reader.readAsBinaryString currentImage # Template.newBoardForm.helpers # , # , panelStyle: -> currentImageDepend.depend() currentImageUrl && "background-image: url(#{currentImageUrl})" || '' # , Template.newBoardForm.rendered = -> resetImage() # Template.newBoardForm.events # , # , , # 'submit form': (event, template) -> event.preventDefault() form = event.target data = $(form).serializeJSON() BoardsCollection.create data.board, (error, id) -> if error alertify.error error.message else form.reset() alertify.success 'Board created' resetUsers() uploadImage(id) resetImage() # # 'change #newBoardImage': (event, template) -> files = event.target.files image = files[0] unless image and image.type.match('image.*') resetImage() return currentImage = image reader = new FileReader reader.onload = (e) => currentImageUrl = e.target.result currentImageDepend.changed() reader.readAsDataURL(image)
Meteor.call
. As you can see, the remote procedure call on the client is not much different from the usual function call, and all the data passed by the arguments will be uploaded to the server via a web socket. To read user files, I used the File API from the HTML5 specification.The example of downloading images may not be the best, but it demonstrates well the capabilities of the server part of the meteor. If you are writing for production, you can use the ready-made CollectionFS solution .
npm
;Source: https://habr.com/ru/post/242943/
All Articles