Внимание! Рецепт написан для старой версии ExtJs и Django.
В этом разделе попытаюсь рассказать о создании интерфейсов с использованием ExtJS и Django. Тем, кто знает и ExtJS и Django, я думаю можно не читать, но среди знакомых таких мало. Так что для них будет полезно :)
Что такое ExtJs можно посмотреть здесь. Есть кое-какие туториалы, но я их не читал :) Изучал по коду примеров и вот этому замечательному сайту: examples.extjs.eu. В отличии от примеров на сайте ExtJs, здесь используется наследование от стандартных компонентов. Такой подход более практичный, если панелек, окошек и др. много — их проще переносить, можно переопределить встроенные методы, ну и в самом ExtJs всё сделано через наследование. Ещё понадобится API ExtJS.
Для взаимодействия сервера и клиента будем использовать Ext.Direct и самописный роутер-"велосипед" для Django. Ext.Direct — это реализация RPC на ExtJs. С таким подходом JS-код чище, легко добавить новый метод. Ознакомительная статья о Ext,Direct. Как видим для python роутера нет, поэтому пришлось написать свой велосипед. Можете использовать другие реализации, в гугле пару штук можно найти. Советую прочитать эту статью, чтобы понятно было, что будет происходить.
Код проекта выложил на github. Что не прокомментировано, постараюсь описать в следующих статьях.
Приступим
Создаем Django-проект, создаем приложение main. Структуру проекта можно посмотреть на github. Качаем последнюю версию ExtJs и кладём её в каталог static. В приложении main создаём представление index (Руслан в переводе назвал их представлениями), которое и будет возвращать страницу с нашим интерфейсом, то есть просто отдавать шаблон странички.
Базовый шаблон
Наш основной шаблон лежит в templates/main/base.html. Он наследуется от base.html, в котором кроме <head><body>
ничего нет. Здесь мы будем подключать все необходимые файлы и выполнять базовую инициализацию. Код шаблона с комментариями можно увидеть ниже.
{% extends 'base.html' %}
{% block head %}
<!-- Add styles for ExtJs -->
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}extjs/resources/css/ext-all.css" />
<!-- Add styles of theme. I like grey more then blue one -->
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}extjs/resources/css/xtheme-gray.css" />
<!-- Our custom styles -->
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}extjs/resources/css/custom.css" />
<!-- jQuery -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js" type="text/javascript"></script>
<!-- Adapter for jQuery -->
<script src="{{ MEDIA_URL }}extjs/adapter/jquery/ext-jquery-adapter.js"></script>
<!-- ExtJs -->
<script src="{{ MEDIA_URL }}extjs/ext-all.js"></script>
<!-- Extension for message display -->
<script src="{{ MEDIA_URL }}js/Ext.ux.msg.js"></script>
<!-- Our main viewport -->
<script src="{{ MEDIA_URL }}js/Ext.ux.MainViewport.js"></script>
<!-- Initialization of Ext.Direct provider -->
<script src="{% url 'main:api' %}"></script>
<script type="text/javascript">
//Initiliztion of QuickTips and Ext.BLANK_IMAGE_URL
Ext.BLANK_IMAGE_URL = '{{ MEDIA_URL }}extjs/resources/images/default/s.gif';
Ext.QuickTips.init();
//Show error message for failed request
Ext.Ajax.on('requestexception', function(){
Ext.ux.msg('Failure', 'Request failed', Ext.Msg.ERROR);
}, this);
//Show error message for RPC exceptions
Ext.Direct.on('exception', function(event){
Ext.ux.msg('RPC Error', event.result.error, Ext.Msg.ERROR);
});
</script>
{% endblock %}
{% block body %}
<!-- Loading mask -->
<div id="loading-mask" style=""></div>
<div id="loading">
<div class="loading-indicator">
<img src="{{ MEDIA_URL }}img/ajax-loader.gif" align="absmiddle"/>
</div>
</div>
{% block content %}{% endblock %}
<script type="text/javascript">
Ext.onReady(function(){
//Remove loading mask
setTimeout(function(){
Ext.get('loading').remove();
Ext.get('loading-mask').fadeOut({remove:true});
}, 250);
});
</script>
{% endblock %}
Стиль custom.css создали сами, сюда добавляем собственные стили. Можно его положить и в static/css/.
Используем jquery-adapter, можно использовать встроенные или prototype, но как-то привык работать с jQuery.
Ext.ux.msg.js — это утилита, которая красиво отображает сообщения. Нагло скопирастена с примеров ExtJS. Пример использования можно увидеть ниже. Может отображать сообщения типа ERROR, INFO, WARNING, QUESTION. В общем использует те же стили, что и Ext.MessageBox. Стили находятся в custom.css.
Инициализацию QuickTips и Ext.BLANK_IMAGE_URL требует ExtJs.
Определяем обработчики для ошибки AJAX-запроса. Ext.Ajax — это модуль понятно для чего.
Определяем обработчик для события exception в Ext.Direct. То есть, каждый раз, когда мы будем возвращать ответ сервера с типом "exception", у нас будет появляться окошко с сообщением, например "Запрошенный вами метод не найден". Подробнее об этом далее.
Далее у нас идет Loading mask, которая отображает индикатор загрузки, пока загружаются все скрипты (ext-all.js в 600кб, например). Также как и Ext.ux.msg.js скопирастено с примеров. Стили находятся в custom.css.
Ext.Direct и "роутер" для Django
Вот и подошли к самому интересному — интеграции интерфейса на ExtJs и Django через RPC. На стороне браузера используется вышеупомянутый Ext.Direct, про который вы должны были бы уже прочитать и про который поговорим далее. На стороне сервера запросы будет обрабатывать, так называемый, "роутер" про который и поговорим. Собственно нам нужно создать классы, методы которых будут вызываться на стороне клиента и добавить их в наш "роутер", который умеет обрабатывать запросы от Ext.Direct и отдавать правильно сформированный ответ. Еще он проверяет правильное ли количество параметров(и возвращает вышеупомянутый exception, это лучше чем 500), передает дополнительно в метод request.user (можно переопределить один метод и передавать что угодно). Про него постараюсь написать в следующей статье, а пока просто скопируйте utils.extjs.
Создадим модуль main/prc.py в котором и будут располагаться наши классы. Я их почему-то назвал actions (уже не помню почему), а сами классы с приставкой API. Так вот исторически сложилось, можете называть их как хотите.
#rpc.py
from utils.extjs import RpcRouter
class MainApiClass(object):
"""
Класс, методы которого и буду вызываться, все кроме тех, которые начинаются с _
"""
def hello(self, name, user):
"""
Обязательный параметр user, можете убрать переопределив RpcRouter.extra_kwargs
Метод просто получает name и возвращает сообщение. Можно возвращать просто msg, вместо словаря.
"""
return {
'msg': 'Hello %s!' % name
}
#количество принимаемых с клиента аргументов, требуется для работы Ext.Direct.
#Можно было бы как-то считать автоматически, но тогда нельзя было бы использовать
#параметры по умолчанию, *args и **kwargs
hello._args_len = 1
class Router(RpcRouter):
"""
Наш "роутер". Переопределяем метод __init__, хотя можно просто передать
параметрами в конструктор(смотреть extjs.py)
"""
def __init__(self):
#Имя линка на наш "роутер"
self.url = 'main:router'
#Наши классы, можете передать что-нибудь в конструктор. Это имя объекта в js,
#то есть вызывать будем MainApi.hello('Иван Петрович')
self.actions = {
'MainApi': MainApiClass()
}
#Устанавливаем enableBuffer для Ext.Direct провайдера
self.enable_buffer = 50
Цитата из API о enableBuffer:
true or false to enable or disable combining of method calls. If a number is specified this is the amount of time in milliseconds > to wait before sending a batched request (defaults to 10).
Calls which are received within the specified timeframe will be concatenated together and sent in a single request, optimizing > the application by reducing the amount of round trips that have to be made to the server.
То есть, если отключить, каждый вызов метода будет отсылаться отдельным запросом к серверу. В нашем случае все вызовы, которые происходят в течении 50 мс, будут отправляться одним запросом. Например при загрузке интерфейса, необходимо загрузить данные для панелей, форм, гридов и др, вместо 5+ запросов будет только один.
Добавляем наш "роутер" в urls.py:
from django.conf.urls.defaults import *
from rpc import Router
router = Router()
urlpatterns = patterns('main.views',
url(r'^$', 'index', name='index'),
url(r'^router/$', router, name='router'), #вот по-этому url = 'main:router'
#этот метод отдает параметры для Ext.Direct.addProvider
#в линках выше о Ext.Direct описано что за параметры
url(r'^router/api/$', router.api, name='api'),
)
В базовом шаблоне мы подключали <script src="{% url main:api %}"></script>
. С сервера вернется вот такой скиптик:
Ext.Direct.addProvider({
"url": "/router/",
"enableBuffer": 50,
"type": "remoting",
"actions": {
"MainApi": [{
"name": "hello",
"len": 1
}]
}
})
Что-то такое можно было увидеть в статье о Ext.Direct.
Ext.ux.MainViewport.js
А вот и наш интерфейс. Создаем две панели и кнопочку, при нажатии на которую, отображается окно для ввода имени. Это имя передается в метод MainApiClass.hello.
Ext.ux.MainViewport = Ext.extend(Ext.Viewport, {
public_attr: 1,
renderTo: Ext.getBody(),
initComponent: function(){
//initComponent это конструктор компонента.
//Сдесь может определить необходимыне нам параметры Ext.Viewport
var config = {
private_attr: 2,
layout: 'border',
defaults: {
frame: true
},
//центральная панель
items: [{
region: 'center',
html: '',
//тулбар с кнопкой и обработчиком this.sayHello
tbar: [{
text: 'Say Hello',
handler: this.sayHello,
scope: this
}]
},{
//еще одна панель
region: 'west',
html: 'West',
width: 350
}]
}
//В this.initialConfig находяться опции передаваемые при создании компонента.
//Тоесть здесь мы их просто затираем параметрами из config.
//Таким образом можно опредеть публичные и приватные атриубуты.
//Например public_attr по-умолчанию равен 1, но пользователь может его переопределить,
//передав в констурктор Ext.ux.MainViewport. private_attr же всегда будет
//установлен в 2.
Ext.apply(this, Ext.apply(this.initialConfig, config));
//Вызываем родительский метод, наследование все таки :)
Ext.ux.MainViewport.superclass.initComponent.apply(this, arguments);
},//initComponent
sayHello: function(){
//после нажатия кнопки показываем окошко для ввода имени
Ext.Msg.prompt('Name', 'Please enter your name:', function(btn, text){
if (btn == 'ok') {
//Вызываем метод hello класса на сервере.
MainApi.hello(text, function(response){
//callback, показываем сообщение с сервера.
Ext.ux.msg('Success', response.msg, Ext.Msg.INFO);
});
}
})
}
});
Более подробно о создании панелей, форм и др. постараюсь описать в следующих рецептах. Пишем в комментарии, критикуем и др.