⬆️ ⬇️

Writing a Japanese crossword service on gae, backbone, underscore, require and also knows what the hell

Introduction



Many people know about the infrastructure from google called gae, some consider it too proprietary, others too expensive. Yes, it is not cheap, and we will try to write an optimal application for gae, which would eat very few resources and ideally did not come out of free quotas even with habraeffekt. I will describe my mistakes, successful technological solutions when writing a service of Japanese crosswords . The point of the site is that it allows you to create your own crossword puzzles and also share them with your friends from a regular image.

To build the site uses the following. technology:

backbone.js is a javascript request processing framework. With its help, we hope that we will be in free quotas, since all the code is executed on the client, only crossword data in json format is requested from the server.

require.js is a library for uploading any resource (js, html), you can specify the code that will run after all resources are loaded. Ideal if you have javascript on the site and it is used in 1% of cases, and you do not want to include the js-file in index.html, then it will suit you.

undescore.js - all sorts of buns to monitor the change of the entire object or its specific property. A very large and cool library, but I use it as a template engine.

bootstrap - not to get stuck with the design.

less - well, why not use it? (Because we can)

And of course, gae - what will it all be spinning on?







Wow, this is MVC !!!



Our entire MVC consists of three components: a model, a view, a controller plus a router that knows which controller to call.

')

Router


Our router determines everything that comes after # in the query string and selects which controller to transfer control to. Variables that will be passed to the function are also supported. At the beginning, I used the on-demand controller loading, that is, everything was broken into a bunch of js files and when drawing any page, a triple delay was obtained:



Preloading controllers and, incidentally, models, and views for them removes the first two delays. Well, there's no way to get away from the last delay (well, this is if the page needs data). The only drawback of such a system is that all files are loaded one by one, which greatly slows down the download process. Further it will be optimized.

For the menu, I created a separate controller for convenience. And as it turned out not in vain, it is very convenient to have a controller that processes the logic of the menu.

Approximate implementation of a router with comments:

require ( [ //        DOM ,  $(document).ready 'backbone/domReady', //   'backbone/views/menu', 'backbone/views/start', 'backbone/views/create', 'backbone/views/image', 'backbone/views/view_puzzle', 'backbone/views/list', 'backbone/views/mylist', 'backbone/views/search', 'backbone/views/edit_puzzle', ], function(domReady, MenuView, StartView, CreateView, ImageView, PuzzleViewView, ListView, MyListView, SearchView, EditPuzzleView){ domReady(function () { //       var menu = new MenuView(); menu.render(); var Router = Backbone.Router.extend({ //        , //        routes: { "": "start", "!/": "start", "!/create": "create", "!/image": "image", "!/list": "list", "!/list/:page": "list", "!/search/:query": "search", "!/search/:query/:page": "search", "!/mylist": "mylist", "!/mylist/:page": "mylist", "!/puzzle/:id": "view_puzzle", "!/puzzle/edit/:id": "edit_puzzle" }, start: function () { this.show_view(StartView, 'start'); }, create: function () { this.show_view(CreateView, 'create'); }, image: function () { this.show_view(ImageView, 'image'); }, view_puzzle: function(id) { this.show_view(PuzzleViewView, '', id); }, edit_puzzle: function(id) { this.show_view(EditPuzzleView, '', id); }, search: function(query, page) { this.show_view(SearchView, '', query, page); }, list: function(page) { this.show_view(ListView, 'list', page); }, mylist: function(page) { this.show_view(MyListView, 'mylist', page); }, show_spinner: function() { menu.show_spinner(); }, hide_spinner: function() { menu.hide_spinner(); }, show_view: function(View, view_name, arg1, arg2) { this.current_view = new View(arg1, arg2); $('.navbar li').removeClass('active'); if (view_name) { $('#'+view_name+'_item').addClass('active'); } this.current_view.render(); } }); window.router = new Router(); Backbone.history.start(); }); }); 




Controller


The controller, like any other resource, must be defined in the style:

 define(['backbone/text!backbone/templates/start.html'], function(template){ var StartView = Backbone.View.extend({ el: "#block", template: _.template(template), //       #block render: function () { //      var data = {}; $(this.el).html(this.template(data)); } }); return StartView; }); 


The define function defines a resource and the first parameter is a list of dependencies. The return value should be the resource itself. In my case, this is a class. The strange recording of backbone / text! Backbone / templates / start.html means that the modules are loaded not directly from the server but using the text plugin. The require library has several useful plugins:

  1. text - to download templates from the server. The plus is that the templates are rendered on the client, and the server gives them static, i.e. No load on our instance. By the way, in the free quota it is only one (if.
  2. i18n - for downloading localization files.
  3. domReady - to run the code after the DOM is ready.




Model


The model is only one - a model of the Japanese crossword puzzle (Puzzle), it has fields such as width, height, data, user_data, title. And a bunch of methods to manipulate them. The service supports not only solving the ready-made crossword puzzles, but also creating your own unique ones. The model can be approximately represented as follows:



 define (function(){ var Puzzle = Backbone.Model.extend({ urlRoot : '/puzzle/', defaults: { title: '', width: 5, height: 5 }, //       , ... }); return Puzzle; }); 




View


The view is the most common html file into which you can insert data like this:



 <div id="data"><%= data %></div> 


And in the tags <%%> you can write any javascript code at all, even the conditions:

 <% if (loaded) { %> <div></div> <% } else { %> <div>...</div> <% } %> 


The data in the view is transferred during the render controller:

 var data = { 'loaded': true }; $(this.el).html(this.template(data)); 




Optimization


The project is loaded for a long time when the page is first loaded, but it is easy to overcome if you collect all the resources into one file and compress it at the same time. This requires node.js, because the build program is written in js. Create a config that handles dependencies for one file and creates another file:



build.js

 ({ baseUrl: "../cross/static/js", name: "common", out: "../cross/static/js/common-pro.js" }) 


and launch our builder:

node r.js -o build.js





As a result, we obtain a file that already contains all of our small files (models, controllers, types, localization files), the same one is still there .



Google Analytics Integration


It's a shame if we can't determine which pages the user is walking through, but we will only see the first download of the site. Therefore, the ga code had to change a little. The code was broken into two parts: the first one sets the account settings and loads the scripts, and the second one pulls the pages loading.



1st part:

  var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA--']); (function () { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); 




2nd part, it works when the router is triggered. I have one function that renders the controller (show_view) in it just add:

  _gaq.push(['_trackPageview', document.location.href]); 




Server part



Some facts:

  1. The ndb library is used (help for which is already available in the official gae help). It is chosen because it can pack several requests to the database in one request and send it to the server (although I don’t think it happens somewhere in a simple server), and also for the fact that there is a built-in cache.
  2. Data received from the server are in json format.
  3. There is a small search implementation that creates an index while saving the crossword. Of course, she does not understand the end. Just parse the words from the heading and put the list which is indexed (maybe someone can use the example):



     class PuzzleIndex(db.Model): keywords = db.StringProperty(repeated=True) update_date = db.DateTimeProperty(auto_now=True) @classmethod def index(cls, puzzle): index = PuzzleIndex.get_or_insert(str(puzzle.key.id())) index.keywords = cls.stemming(puzzle.title) index.put() @classmethod def delete_index(cls, puzzle): db.Key(PuzzleIndex, str(puzzle.key.id())).delete() @classmethod def stemming(cls, text): words = set(re.split(ur'\s+', text.lower(), re.U)) return list(filter(None, words)) @classmethod def search(cls, text, limit=10, offset=0): puzzles = [] words = cls.stemming(text) query = PuzzleIndex.query()\ .order(-PuzzleIndex.update_date)\ .filter(PuzzleIndex.keywords.IN(words)) indexes = query.fetch(limit + 1, offset=offset, keys_only=True) if len(indexes): keys = [db.Key(Puzzle, int(key.id())) for key in indexes] puzzles = db.get_multi(keys) return puzzles 






Acknowledgments



Thanks to my friend Rainum for help with the layout and for the logo.



Conclusion



I am developing applications on gae and really wanted to try to create a service that would be quite cheap and at the same time with non-zero attendance.

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



All Articles