
I have always been attracted to responsive, dynamic interfaces created in Javascript, but every time I tried to immerse myself in learning this language, I turned my brain into a mess and
ate it left for “better” times, returning to static pages on the client and PHP on the server . As time went.
A year ago, stumbling through the pages of the network, I came across an article about Coffeescript. Hmm, interesting ... Couples of code samples were enough to get the idea to apply it somewhere, but something bothered me - I wanted some kind of framework that would take care of compiling coffee in js myself. So I found Rails, and with it ruby, gems, sass, and a bunch of things that brought me into
ecstasy, the critical point of no return ...
')
Good day, gentlemen! My name is Denis, and in this article I want to share with you my views on the development of the front-end and a small history of the invention of one bicycle, but you can judge whether it is next or not.
Why the invention of the bicycle? Probably because no one has ever taught me programming and methods of learning languages, and the desire to “get behind the wheel” always triumphs over the desire to “get rights”, given the absence of a direct threat to the life and health of developers at the oncoming “counter” with the slogan - "we will understand on the way."
By myself knowing that only by practice, by typing and googleing, I manage to learn the theory and understand in general what I started to develop, namely to create a Javascript MVC framework the way I see it, and by passing the languages ​​I need for it.
Dreams come true, fairy tales come true
Since Javascript is a prototype-oriented language, I decided to abandon the class-oriented approach and master the power of the prototypes.
Long thinking about the intricacies of the implementation of the client side, I came to the following conclusions:
- each request to the application is a work with a certain collection of models, which ends with the display of the necessary representation of the form of these models on the page;
- the controller in its action must have a ready-made, selected collection of models and parameters for its work;
- Collections are sets of models filtered by certain parameters, constantly maintaining their set up to date;
- models are closely related to the species, all changes to the model should immediately be displayed in the view of the species;
- a view is an object that knows how to draw a template into the html representation in accordance with the parameters of its model, as well as where to show it, besides this, each piece of html page code must belong to some type;
- view is a view template drawn in html-code, intended for display on the page and user interaction;
- the user interacts with the views through views, views with models, and vice versa - models with views, views through views with the user.
After my thoughts, I began to develop and started it with the implementation of
models . Inspired by the capabilities of the ActiveRecord models, I tried to make client models similar to them. As a result, validations, callbacks, relationships to, has one, has many, has one through, has many through, search methods, as well as creating, updating, deleting and saving to the server and many other things were implemented.
I gave the
views the ability to monitor the state of the models and respond quickly to their changes, created templates for them with preliminary compilation on the server, gave them a small client template engine and taught to redraw not completely, but only in those places (nodes) where it is necessary, depending on the state of models, their collections, or other elements of the system capable of generating change events. In addition to the above, the binding of these form fields to model properties, direct calling of view methods or its model when clicking on a link or sending a form, as well as wrapping views of views into layouts was implemented.
I tried to make the
collections look like ordinary arrays with extended functionality for working with models, added in them the possibilities of sorting, filtering models and proxying their methods.
I implemented the
controllers so that the names of their actions are at the same time routes with the selection parameters of the displayed models, and also added the ability to define before and after filters.
In addition to the above, a small object was created to work with cookies, another one that is responsible for working with the server via Websockets and a router for routing requests to controller actions using the Browser's History API. To work with the DOM, the small
jBone library
fits perfectly . Inside, of course, everything is maximally implemented by native means, it is used only for associating view methods with DOM events and for further developer convenience, if necessary, can be easily replaced with the beloved jQuery, since has a similar syntax.
As time went on, the basic client-side functionality was implemented, but I didn’t like the server room anymore - Rails did not suit me, well, it wasn’t for that, it needed something of its own ...
Coffee, google, habr, articles, google -
Sinatra ! What you need! Add here sinarta-websockets, sprockets, active-record, thin, pinch of pepper settings, a bit of magic, mix thoroughly, dance with a tambourine and voila - your own synchronization server.
Without thinking for a long time, I decided to pack this into a ruby ​​gem, I found an article here, a couple of trial attempts, and here it is
nali-0.0.1.gem .
Even at the stage of creating a heme, the idea arose of adding a generator of a new application to it, as well as models and types, which was immediately implemented.
And a little later, I developed:
- browser tab recognition system on the server, which allows you to find out, for example, which of the connected clients are different browsers, and which tabs are the same;
- the system of client access rights to model properties, which allows to easily break clients into groups by roles and strictly specify each interaction parameter;
- a model synchronization system that allows you to synchronize a model with dependent models, track customers following the model and promptly inform them about all its movements;
- a system of model selectors that allows you to easily download the necessary models to the client;
- the possibility of calling methods from the client on the server, and vice versa, from the server on the client with data transfer;
- a notification model that allows you to easily set up user notification with informational messages, launching them both from the client and from the server side.
- and much more…
The more the framework began to look like something independent and complete, the more I was filled with pride for the work done and the desire to share it with the public. For this, a repository was created on
Github , and documentation was published in the
Wiki section, and this article became the cry of the soul.
What is the result?
And in my opinion, it turned out to be a rather interesting thing that allows you to quickly, with the help of several console commands, deploy a working environment and start writing code, for example
$ gem install nali # Nali $ nali new gallery && cd gallery # $ bundle install # $ bundle exec thin start # - thin
After that, you can open the browser, go to
http: // localhost: 3000 / and see the first prepared
Welcome to Nali page .
The structure of the created files and directories is described in the
documentation , all application files are clearly divided into client and server. A built-in generator makes it easy to generate new models, controllers and views, for example the following command
$ nali model Photo
create client and server files of model and control controller
- app / client / javascript / models / photo.js.coffee - client model
- app / client / javascript / controllers / photos.js.coffee - client controller
- app / server / models / photo.rb - server model
- app / server / controllers / photos_controller.rb - server controller
and also changes the
app / server / models / access.yml file , adding to it a preset for setting client access levels to the model properties.
Consider the client controller
After generation, its code looks like this.
# app/client/javascripts/controllers/photos.js.coffee Nali.Controller.extend Photos: actions: {}
We define in it a method showing photos of a certain album.
Nali.Controller.extend Photos: actions: before: # -, @stop @redirect # preview: -> # , @redirect 'home' unless @collection.length # @collection - , album_id 'preview/album_id': -> # preview - , album_id - @Model.Photo.select album: id: @filters.album_id # @collection.order by: 'created', desc: true #
What have we written here?
- First, we defined a router with a new preview / album_id route , which is the same as / photos / preview / 1 , where 1 is the album id, and preview is an action and at the same time the name of the type that the collection models should display;
- secondly, we defined a before-filter that runs before the action itself and, if the collection of selected photos is empty, will interrupt the further execution of the request and send the router to the home page;
- thirdly, in the action itself, we determined the sorting order of photos and sent a request to the server to receive them.
After performing the action, the controller will give the collection a command to display the
preview view.
Server controller
Its code after generation looks like this
# app/server/controllers/photos_controller.rb class PhotosController < ApplicationController include Nali::Controller end
In it we need to implement a selector for fetching photos of the album.
class PhotosController < ApplicationController include Nali::Controller before_only :album do check_album end # album selector :album do @album.photos end # private def check_album stop unless @album = Album.find_by_id( params[ :id ] ) # stop , end end
In the body of the selector, we just need to return the collection of models, Nali takes over their synchronization with the client.
The
Nali :: Controller module extends the controller class with its own methods, here are some
- params - a hash of parameters received from the client
- client - current client
- clients - a list of all clients connected to the server
Client model
Its preparation, as well as the others, looks minimal.
# app/client/javascripts/models/photo.js.coffee Nali.Model.extend Photo: attributes: {}
Add the necessary attributes, validations, links and callbacks.
Nali.Model.extend Photo: belongsTo: [ 'user', 'album' ] # - hasMany: [ 'comments' # commentators: through: 'comments', key: 'user' # - "" ] attributes: user_id: presence: true format: 'number' # , album_id: presence: true format: 'number' # , name: presence: true length: in: [5..50] # , 5 50 format: presence: true inclusion: [ 'jpg', 'gif', 'png' ] # , onCreate: -> # onUpdate: ( changed ) -> # # changed - onUpdateName: -> # name onDestroy: -> #
Immediately give some examples of working with models
photo = Nali.Model.Photo.new user_id: 1, album_id: 2, name: 'test', format: 'jpg' # photo.save() # photo = Nali.Model.Photo.create user_id: 1, album_id: 2, name: 'test', format: 'jpg' # photo.update name: 'test_update_name' # photo.save() # photo.upgrade name: 'test_upgrade_name' # photo.remove() # photo.destroy() # , Nali.Model.Photo.find 1 # id == 1 Nali.Model.Photo.where id: [1..10], name: /test/ # id 1 10 "test" Nali.Model.Photo.all() #
Server model
To connect the model with the database, it is necessary to create an appropriate table, this can be done using migrations. You can create a migration with the command
$ bundle exec rake db:create_migration NAME=create_photos
As a result, we get the file
# db/migrate/20150119080311_create_photos.rb class CreatePhotos < ActiveRecord::Migration def change end end
in which you need to add the code that creates the table
class CreatePhotos < ActiveRecord::Migration def change create_table :photos do |t| t.belongs_to :user # t.belongs_to :album # t.string :name, limit: 50 # t.string :format, limit: 3 # end end end
Then you need to start the migration command
$ bundle exec rake db:migrate
Now you can begin to configure the server model by opening its file, we will see
# app/server/models/photo.rb class Photo < ActiveRecord::Base include Nali::Model def access_level( client ) :unknown end end
This is the usual
ActiveRecord model, extended by the
Nali :: Model module, which adds several methods to it, some of which are:
- sync (* watches) - synchronizes the model with all clients following it, plus those that are passed as arguments;
- call_method (name, params) - calls the name method with params parameters in all of its client copies;
- clients - returns an array of all clients connected to the server.
The
access_level method
is special, it is called by the model at the time of the attempt of a client to create, update, read, or delete it. The object of this client is passed as an argument. In the body of this method, you need to implement a mechanism, the essence of which is to determine the attitude of the client to the model and “tell” who it is - “master”, “my host’s friend” or “I don’t know him” (or something else). ). We realize it
class Photo < ActiveRecord::Base include Nali::Model # belongs_to :user belongs_to :album has_many :comments has_many :commentators, through: :comments, source: :user # validates :user_id, numericality: { only_integer: true } validates :album_id, numericality: { only_integer: true } validates :name, length: { in: 5..50 } validates :format, inclusion: { in: %w(jpg gif png) } # def access_level( client ) return :owner if client[ :user ] and self.user == client[ :user ] # owner (), User (, # , ) return :commentator if client[ :user ] and self.commentators.include?( client[ :user ] ) # commentator (), User, # :unknown # , end end
Why do we need it? We need this in order to determine which clients can create / receive / update / delete a model and with which properties and which cannot. Open the file
app / server / models / access.yml , here the generator has already prepared a pig for us
Photo: create: read: update: destroy:
In it, we need to define access parameters
Photo: create: owner: [ user_id, album_id, name, format ] read: owner: [ user_id, album, name, format, comments ] commentator: [ +owner ] update: owner: [ name ] destroy: owner:
And now in order:
- In the create section, we determined that creating a photo, or rather saving the client model to the server by calling new then save or immediately create, can only be a client with an owner level. The list [user_id, album_id, name, format] defines the fields to be taken to create the model;
- In the read section, we determined that only owners and commentators can retrieve the model from the server, the [user_id, album, name, format, comments] list defines the properties to be received. The sync feature is that if the property returns a model or models, they will also be synchronized with the client, moreover, if the model is a belongs_to connection, then the key for the connection will be automatically transferred, i.e. the owner will receive an album model, models of all comments, and the model of the photo itself with fields [user_id, album_id, name, format] . Another feature is mixing access levels, i.e. if the commentators have the same synchronization parameters as the owners, then you do not need to “copy-paste” them, you just need to add them like this [+ owner] ;
- In the update section, we determined that only the owners (by calling update on the client, then save or immediately upgrade ) can change the properties of the model while being saved on the server, and only the name property can be saved;
- In the destroy section, we indicated that only the owner can delete a photo from the server by calling the destroy method on the client model.
Kinds
A view is an object that parses the template in the html-code (view), fills it with data and places it at the destination on the application page. The view is responsible for the timely rendering of the view when the data displayed in it changes, and also carries the logic of user interaction with the view. The view always belongs to a particular model.
Any view always has a reference to its model in the @model property (for brevity there is an alias property @my), and the
element property contains a view in the form of a jBone object (or jQuery if the application uses it as a library for DOM manipulations).
You can create a new view through the terminal using the command
$ nali view PhotoPreview
where
Photo is the model that owns the view, and
Preview is the name of the view. As a result of the command, we will get the following files.
- app / client / javascripts / views / photo / preview.js.coffee - the view itself
- app / client / stylesheets / photo / preview.css.sass - his styles
- app / client / templates / photo / preview.html - template
Let's start their consideration with the template, its file after generation is empty, fill it
<div class="name">{ @name }</div> <div class="photo">{ +photoTag }</div> <div class="autor">: { @user.name }</div> <div class="album">: { @album.name }</div> <a href="/comments/{ @id }">: { @comments.length }</a>
On the client, this template will automatically turn into a
div.PhotoPreview and will look like this.
<div class="PhotoPreview"> <div class="name">{ @name }</div> </div>
In Nali, views of the views are wrapped in a tag that has a class recorded in the CamelCase style, thanks to which they can be easily distinguished during debugging when viewing the source code of the page. More information about the instructions of the templates and wrapping can be found on the
documentation page.
If you need to precompile the template on the server before returning to the client, you can do this using the ERB template engine by simply renaming the template extension from
preview.html to
preview.html.erb and adding the appropriate instructions to it. By the way, you can use other template engines, such as haml or slim, these features are provided by gem sprockets.
When inserting data directly into the template, using the
{ name } instruction, the viewpoint is the point of reference, i.e. @ Is a model,
name is the name of a photo,
@album is her album,
@ album.name is his name, etc., the chain can be as long as you want, when it is parsed, the view will get to the final property, insert its value into the view and subscribes to the event of its change.
Default styling is handled by the SASS preprocessor and in our case will look something like this.
# app/client/stylesheets/photo/preview.css.sass .PhotoPreview .name # ... .photo # ... .autor # ... .album # ...
To use the SCSS preprocessor, it is necessary, by analogy with the template, to rename the style file extension in
preview.css.scss , and for regular CSS in
preview.css .
The view itself after generation looks like this.
# app/client/javascripts/views/photo/preview.js.coffee Nali.View.extend PhotoPreview: events: [] # helpers: {} # , onDraw: -> # , onShow: -> # , onHide: -> # ,
Edit it according to our requirements.
Nali.View.extend PhotoPreview: events: 'openFullSize on click at div.photo' # div.photo openFullSize, # : '[, ... ] on [, ...] at [ ]' # helpers: photoTag: -> '<img src="/photos/' + @my.id + '.' + @my.format + '" alt="" />' # openFullSize: ( event ) -> #
Now, to see the thumbnails of all the photos of the album, just go to the browser at the address, for example,
/ photos / preview / 1 , as a result, the page (by default, the body tag) will be inserted into the page
<body> <div class="PhotoPreview">...</div> <div class="PhotoPreview">...</div> <div class="PhotoPreview">...</div> </body>
To change the destination where thumbnails are inserted, it is enough to add a simple method in the form of
PhotoPreview , which returns the selector of the DOM element or the element itself (at the moment of insertion it must exist in the DOM tree)
Nali.View.extend PhotoPreview: insertTo: -> 'div.photosContainer'
then the result of the insertion will look like this
<body> <div class="photosContainer"> <div class="PhotoPreview">...</div> <div class="PhotoPreview">...</div> <div class="PhotoPreview">...</div> </div> </body>
Moreover, views can be inserted into layouts, i.e. creating a simple view, for example
AlbumIndex Nali.View.extend AlbumIndex: insertTo: -> 'div.photosContainer'
with template
<div class="name">: { @name }</div> <div class="photos">{ yield }</div>
and replacing the
insertTo method with the
layout method in the
PhotoPreview Nali.View.extend PhotoPreview: layout: -> @my.album.viewIndex() # index # ...
we will see thumbnails in the album view and on the page it will look like this
<body> <div class="photosContainer"> <div class="AlbumIndex"> <div class="name">: { @name }</div> <div class="photos"> <div class="PhotoPreview">...</div> <div class="PhotoPreview">...</div> <div class="PhotoPreview">...</div> </div> </div> </div> </body>
The key statement
{yield} defines the place where they will be inserted. I will also add that the nesting of views in layouts is not limited, because the layout is a normal view, so you can also define a parent layout for it - this allows you to conveniently split the html code of the page into components.
Here, using examples, I described only some of the capabilities of the Nali species, in fact there are many more of them and they are described in detail on the
documentation page.
Another way
The task described above can be solved in a slightly different way, namely, having examined the photos from albums, you can add an action route to the
Albums client controller
Nali.Controller.extend Albums: actions: default: 'index' # , /albums/index/1 /albums/1 'index/id': -> # index - , id - @Model.Photo.select album: id: @filters.id #
then add attributes to the
Album client model, link to photos and sort them before showing the album
index view
Nali.Model.extend Album: hasMany: 'photos' attributes: name: null beforeShow: index: -> @photos.order by: 'created', desc: true
then slightly change the template of the
AlbumIndex view <div class="name">: { @name }</div> <div class="photos">{ preview of @photos }</div>
and get the same result at
/ albums / 1 (by the way, the router will understand
/ album / 1 ).
Collections
A collection is a collection of models selected by a specific filter. Whenever we use the
where method to search for models, we get their collection. The collection methods are described in detail in the
documentation .
The features of the Nali collections are:
- relevance - a once created collection will always carry only the current set of models in itself, if the model has changed and ceased to meet the requirements of the collection, it disappears from the set and vice versa, if the model begins to meet the requirements of the collection, it falls into the set;
- sorting - if the collection has sorting parameters, then the models in it are always ordered according to these parameters, in addition, if the collection was given the command to show views, then their views are arranged on the page according to the sorting, and if the collection is re-sorted, they will also be re-sorted;
- adaptation mechanism - the collection remembers what actions were carried out with its models and, when a new model is added to the collection, it will automatically undergo these actions in the order in which they were carried out, and if you remove it from the collection, the model will be reversed (let’s say the collection shows views, a new model, once in it, will also show the required view, if you remove a model from the collection, its view will disappear from the page);
- proxying model methods — if a method is defined in a model, the name of which begins with a double underscore, then the collection of such models will have a method of the same name (without underscores) that calls it on all models.
A small example:
# Nali.Model.extend User: {} # Nali.View.extend UserIndex: {}
view template
<div>User: { @name }, Age: { @age }</div>
create local instances of models
Nali.Model.User.new( name: 'Ted', age: 20 ).write() Nali.Model.User.new( name: 'Bob', age: 25 ).write() Nali.Model.User.new( name: 'Dan', age: 30 ).write() # name age , # write()
let's sample the collection of models and show on the screen a representation of their type index
users = Nali.Model.User.where( age: [ 20..30 ] ).order( by: 'age' ).show 'index'
in the end we get
<body> <div class="UserIndex"> <div>User: Ted, Age: 20</div> </div> <div class="UserIndex"> <div>User: Bob, Age: 25</div> </div> <div class="UserIndex"> div>User: Dan, Age: 30</div> </div> </body>
change the age of the last
users.last().update age: 23
the collection is automatically re-sorted, views also
<body> <div class="UserIndex"> <div>User: Ted, Age: 20</div> </div> <div class="UserIndex"> div>User: Dan, Age: 23</div> </div> <div class="UserIndex"> <div>User: Bob, Age: 25</div> </div> </body>
change the age of the first one to the value beyond the collection filter (age: [20..30])
user1 = users.first().update age: 19
the model disappears from the collection, hiding the view
<body> <div class="UserIndex"> <div>User: Dan, Age: 23</div> </div> <div class="UserIndex"> <div>User: Bob, Age: 25</div> </div> </body>
return it a value suitable for the collection filter
user1.update age: 27
the model appears again in the collection and shows a view of the index view according to the sorting
<body> <div class="UserIndex"> <div>User: Dan, Age: 23</div> </div> <div class="UserIndex"> <div>User: Bob, Age: 25</div> </div> <div class="UserIndex"> <div>User: Ted, Age: 27</div> </div> </body>
re-sort the entire collection and get an array of usernames
users.order( by: 'name', desc: true ).pluck 'name' # > [ 'Ted', 'Dan', 'Bob' ] # users.hide 'index' #
Customers
When a new client connects to the server, an object of the class
EventMachine :: WebSocket :: Connection is created for it, to which Nali adds additional features. In any controller, the current client can be obtained by the
client method, and the list of all connected clients by the
clients method.
Each client has a personal repository, work with which is quite simple and fits into a small example.
client[ :user ] = user # client[ :user ] # client[].delete( :user ) # client[] #
To synchronize models with the client, there is a sync method (* models) , which is quite simple to use. user.update city: 'Moskow' client.sync user
For getting other clients, which are tabs of one browser, there are methods all_tabs and other_tabs client.all_tabs # , , client.other_tabs # , ,
This is convenient in cases when, for example, a user has several tabs with a site, then when you click "exit" on one of them, you can easily make an "exit" on the others, or when the user launches the player on one tab, and on others need to stop. Each of these methods takes a block of code that runs separately for each client in the array. Three methods are defined in the app / server / clients.rbfile that allow you to configure server responses to incoming messages and enable / disable clients. module Nali::Clients def client_connected( client ) # , # client end def on_message( client, message ) # , # client message - # end def client_disconnected( client ) # , # client end end
Notifications
Nali has a built-in Notice model that has 3 types of info , warning and error , responsible for displaying notifications to the user. The type and design of notifications is easily customized to the needs of the developer. The display of notifications is elementary: @Notice.info 'Hello World' # info, Hello World # 3 # @Notice.info message: 'Hello World'
It is also easy to create your own type of notification, this process is described in detail in the documentation .You can initiate the notification from the server side as follows. client.info message: 'Hello World' # @Notice.info message: 'Hello World'
Deployment
Nali allows you to compile, minify and merge client-side files with the command $ bundle exec rake client:compile
This team will collect- all your javascript files in public / client / application.js
- all your stylesheets files in public / client / application.css
- all your html templates in public / index.html
that will allow in the production environment to give them ready. In order to compile client files for the production environment, while being in the development environment , you must explicitly set this environment in the command call $ bundle exec rake client:compile RACK_ENV=production
This is useful when the file system on the destination server is in read-only mode , like on Heroku .In addition, if you think about it, you can conclude that by placing these files in PhoneGap or CocoonJS, you can easily and quickly get a cross-platform application client.Let's sum up
Looking back and remembering the moments of the past year, I believe that it was not for nothing that I spent this time, albeit idle, financially, but the baggage of the knowledge gained is much more valuable to me. I cannot assess myself in terms of how high my level of knowledge and qualifications in the field of web development is now - I will draw these conclusions for myself based on your comments, in which, I hope, you answer the main question - is this another bike or is there anything worthy of attention? I will be glad to your criticism, any help and just support. To all readers who have found time to read this article, I want to say thank you and take a bow at this.Battery links: