📜 ⬆️ ⬇️

Lovers of Ruby and Coffeescript - another bike?

image

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:


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:


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


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?


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


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:


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:


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.


Let's start their consideration with the template, its file after generation is empty, fill it

 <!-- app/client/templates/photo/preview.html --> <div class="name">{ @name }</div> <!--   --> <div class="photo">{ +photoTag }</div> <!--   -    photoTag --> <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> <!--  { yield }  { preview of @photos } --> <!--      preview     @photos --> 

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:


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

file 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


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:

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


All Articles