- PVSM.RU - https://www.pvsm.ru -
Многие знают про инфраструктуру от google под названием gae, некоторые считают её слишком проприетарной, другие слишком дорогой. Да она не дешевая, и мы попробуем написать оптимальное приложение для gae, которое жрало бы очень мало ресурсов и в идеале не выходило из бесплатных квот даже при хабраэффекте. Опишу мои ошибки, удачные технологические решения при написания сервиса японских кроссвордов [1]. Фишка сайта в том что он позволяет создавать свои кроссворды и из обычной картинки тоже и делиться ими с друзьями.
Для построения сайта используется след. технологии:
backbone.js [2] — фреймворк для обработки запросов на javascript'е. C его помощью будем надеяться, что уложимся в бесплатные квоты, так как весь код выполняется на клиенте, с сервера запрашиваются только данные о кроссвордах в json формате.
require.js [3] — библиотека для дозагрузки любых ресурсом(js, html), можно указать код, который выполнится после загрузки всех ресурсов. Идеальна если у вас есть на сайте javascript и он используется в 1% случаев, и вы не хотите включать js-файл в index.html, то она вам подойдет.
undescope.js [4] — всякие плюшки для слежения за изменением всего объекта или за конкретным его свойством. Очень большая и крутая библиотека, но я использую её как шаблонизатор.
bootstrap [5] — чтобы не заморачиться с дизайном.
less [6] — не ну, а почему б не использовать? (Потому что мы можем)
Ну и конечно же gae — на чем все это будет крутиться.
Весь наш MVC состоит из трех компонентов модель, вид, контроллер плюс роутер, который знает какой контроллер вызвать.
Наш роутер определяет все что идет после # в строке запроса и выбирает какому контроллеру передать управление. Поддерживаются также переменные которые будут переданы в функцию. В начале я использовал загрузку контроллера по требованию, то есть все было разбито на кучу js файлов и при отрисовки любой страницы получалась тройная задержка:
Предзагрузка контроллеров и, побочно, моделей, и видов для них убирает первые две задержки. Ну а от последней задержки никуда не деться(ну это в том случае, если странице нужны данные). Единственный минус такой системы что все файлы грузятся один за одним, что сильно замедляет процесс загрузки. Далее это будет оптимизировано.
Для меню я создал отдельный контроллер, для удобства. И как оказалось не зря, очень удобно иметь контроллер, который обрабатывают логику меню.
Примерная реализация роутера с комментариями:
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();
});
});
Контроллер, как и любой другой ресурс должен быть определен в стиле:
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;
});
Функцией define определяется ресурс и первый параметр это список зависимостей. В качестве возвращаемого значения должен быть сам ресурс. В моем случае это класс. Странная запись backbone/text!backbone/templates/start.html означает что модули грузятся не сразу с сервера а с помощью плагина text. У библиотеки require есть несколько полезных плагинов:
Модель всего лишь одна — это модель японского кроссворда(Puzzle), у нее есть такие поля как width, height, data, user_data, title. И куча методов для манипулирования ими. Сервис поддерживает не только разгадывание уже готовых кроссвордом, но и создания своих неповторимых. Модель приблизительно может быть представлена так:
define (function(){
var Puzzle = Backbone.Model.extend({
urlRoot : '/puzzle/',
defaults: {
title: '',
width: 5,
height: 5
},
// далее куча методов для обеспечения работы модели,
...
});
return Puzzle;
});
Вид это самый обычный html файл, в который можно вставлять данные так:
<div id="data"><%= data %></div>
А в тегах <% %> можно писать вообще любой javascript код, даже условия:
<% if (loaded) { %>
<div>Загружено</div>
<% } else { %>
<div>Загружаю...</div>
<% } %>
Данные в вид передаются при render контроллера:
var data = {
'loaded': true
};
$(this.el).html(this.template(data));
Проект достаточно долго грузится при первой загрузки страницы, но это легко побороть если собрать все ресурсы в один файл и заодно сжать его. Для этого нужен node.js, потому что собирающая программа написана на js. Создаем конфиг, который обрабатывает зависимости для одного файла и создает другой файл:
build.js
({
baseUrl: "../cross/static/js",
name: "common",
out: "../cross/static/js/common-pro.js"
})
и запуск нашего билдера:
node r.js -o build.js
В итоге получаем файл, в котором уже содержаться все наши мелкие файлы (модели, контроллеры, виды, файлы локализации), венегрет еще тот [7].
Обидно если мы не сможем определить по каким страницам гуляет пользователь, а будем видеть только первую загрузку сайта. Поэтому код ga пришлось немного изменить. Код был разбит на две части: первая устанавливает настройки аккаунта и загружает скрипты, а вторая дергает загрузку страниц.
1-я часть:
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);
})();
2-я часть, срабатывает когда срабатывает роутер. У меня есть одна функция которая рендерит контроллер (show_view) в ней просто добавляем:
_gaq.push(['_trackPageview', document.location.href]);
Несколько фактов:
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
Спасибо моему другу Rainum [9] за помощь с версткой и за логотип.
Я разрабатываю приложения на gae и очень хотел попробовать создать сервис, который будет обходиться достаточно дешево и при этом с отличной от нуля посещаемостью.
Автор: FerumFlex
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/bootstrap-2/3529
Ссылки в тексте:
[1] Image: http://japcrossword.appspot.com
[2] backbone.js: http://documentcloud.github.com/backbone/#
[3] require.js: http://requirejs.org/
[4] undescope.js: http://documentcloud.github.com/underscore/
[5] bootstrap: http://twitter.github.com/bootstrap/index.html
[6] less: http://lesscss.org/
[7] венегрет еще тот: http://japcrossword.appspot.com/static/js/common-pro.js
[8] ndb: http://code.google.com/p/appengine-ndb-experiment/
[9] Rainum: http://habrahabr.ru/users/rainum/
Нажмите здесь для печати.