2010年8月8日星期日

基于Django的Web应用设计

先呼吸一口气放松一下,把Django的文档在生成一份离线版本,然后去官方的wiki页逛逛:
http://code.djangoproject.com/wiki/DjangoResources。
本来打算是严肃的继续说明Django的功能的,不过现在又觉得来闲扯一会儿会更好。

之所以这么觉得,原因可以参看这篇《偶对django的rant》
http://www.javaeye.com/topic/361310
这篇文字表达对Django繁琐的url调度,晕人的import,别致的template等方面的不满。

其中的观点可以说是有理有据,确实如实反映了Django的设计,
所以才会流传一种类似于“Django只适合写Django风格的站点”的话,upadate
也有人因此就尝试通过替换路由和模板的方式来使用Django的功能。

个人对Django是刚有涉及的地步,并抱着一直的能黑则黑的态度,来表达不悦。
不过既然来使用Django了,也就有必要来体验一下Django方式的应用设计了。
下文便将从这个角度来尽量不跑题地,来扯一点点轻松的话题。

上面便是无趣的开场。


IDE

原本是在用Emacs写Python代码的,在Emacs里输入文本有一种洋洋洒洒行云流水畅畅快快的感觉,补全工具什么的也是安装好了随叫随到

不成问题。
不过仅以文本的方式来对待一个Django项目则显得视野不够开阔,因此就还是动用了启动更慢Eclipse。
按键选择Emacs绑定,快捷键什么的依旧好使。
安装好WebTools和PyDev,这样便可以建立Django项目了。

Eclipse的补全和重构工具总是让人用起来感觉一直不错。
补全除了用来补全对象的成员和方法,更重要的可以自动补上缺少的import(泪目啊~)。
而重构的话,对于Python代码Inline/ExtractLocalVariable可以让局部变量的存在变得清晰。

此外,Explorer支持了多级目录和py文件内部层次的展开,以及调用manage.py的菜单入口。

有好的工具让后面的事情变得舒心一些,毕竟算是好事吧。


History

Django最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的。
对这句的话的理解可以参考Guido van Rossum曾对Django发表的一段评价:
Guido:我是一个对 django 非常满意的用户,

并在项目中使用了一些 django 。我将 django 称为“第二代” python web 框架, 第一代是指 zope 和 twisted, django 是由两个

在堪萨斯新闻报社工作的小伙开发的,并非是一个很有名气的地方。
Chris:很奇怪,Zope, plone 也是来自报纸网站的
Leo :他们要流程化他们的工作流,这对他们可是很重要的事情。
Guido:也许是这个原因吧,堪萨斯的这家报社希望建立一个给当地人提供信息的本地网站,该网站必须对读者的响应非常及时,必须很

快地发布内容,并不是简单把文章发布到网站上这样谁都可以做的事情,它必须很容易更换整个网站的外观,添加一些新的创意,一些新

功能,增加一些新的应用。例如,发布本地体育赛事新闻,提供关于球队链接和照片等各种感兴趣的信息,他们希望这东西能很快运作。

我想他们做这个有两年了吧,这两个小伙子和一群编辑在一起工作,编辑为他们提供内容。在工作的同时,他们觉得有必要做一个框架,

他们从他们的第一个网站应用中提取了框架。 通过编辑对他们不断提出的对网站修改需求,他们对框架增加更好的灵活性,后来他们决

定说“我们开源吧”,他们的想法得到了报社的支持。
简单的说,Django的设计目的为了实现简化便捷的开发流程,通过

这样来快速地进行Web应用的设计与开发。
而它设计的原则是DRY,这代表Don't Repeat Yourself这一法则。
这意味着相比于Django已提供的功能而言,提供一个支持设计与模块重用的框架,是让Django的价值体现得更为显著的事情。


Model

在创建project和一个app之后,首先来进行的是Models部分设计。

参考一下django-simplecms,一个简单的内容管理系统的模型图:

href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifEroQXUBXJXK4jkOI44emzQ3Je8qhhdXPMHf_U-AP8eAAzku9WVNUpxXuOWdeaj_1NnHBGmlAqhyphenhyphen-jXrW1tzd27CpWRTem9NAdUwxsPG5W7v94-k61RZozYfv9Pfm-dzI89qqPfJM44g/s1600/cms.png">
src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifEroQXUBXJXK4jkOI44emzQ3Je8qhhdXPMHf_U-AP8eAAzku9WVNUpxXuOWdeaj_1NnHBGmlAqhyphenhyphen-jXrW1tzd27CpWRTem9NAdUwxsPG5W7v94-k61RZozYfv9Pfm-dzI89qqPfJM44g/s400/cms.png" alt=""

id="BLOGGER_PHOTO_ID_5503062974169040690" border="0" />


我这里只是一个helloworld般的例子"posts":
class Post(models.Model):
....title = models.CharField(max_length=255)
....author = models.ForeignKey(User)
....content = models.TextField()
....pub_date = models.DateTimeField(auto_now_add=True)
....last_update = models.DateTimeField(auto_now=True)
....def __unicode__(self):
........return self.title
....class Meta:
........ordering = ['-pub_date']
这部分代码放在models.py文件中或者建立一个名为model目录。
此外这里依照习惯,引用名使用复数小写,模型名为首字母大写的单数形式。
Django中对变量的命名采用小写字母加下划线分割的风格。

模型设置好后,便可将所创建的应用添加到INSTALLED_APPS中并执行syncdb。
如果设置好Django的admin模块的话,就已经可以正确地来编辑模型的数据了。


more about Model

这部来对Model进行一些额外的说明,阅读时可以暂时跳过。

建立模型需要而模型添加字段,用于表示数据,而Django中也提供了字段用以建立模型之间的关系:
Many-to-one关系,使用ForeignKey字段,
例如从属关系一个投票对多个选项,选项有指向投票的ForeignKey。
Many-to-many关系,使用ManyToManyField,会额外建立一张关系模型(含两个ForeignKey),
例如小组和成员之间,一个小组拥有若干成员,每个成员属于若干小组。
One-to-one关系 类似于unique=True的ForeignKey但为两个模型都添加了成员,
例如用于表示模型的派生关系,每个餐厅都对应一个地点,某些地点对应一个餐厅。

声明完模型的字段有,将可以使用Django已经提供了的属性,例如objects,save()。
也可按需要添加自己的方法供调用,或者使用修饰模式来扩展已有的方法。

Django的Authentication系统中,在django.contrib.auth.models有名为User的模型,用来表示站点的一个用户。
使用系统提供的模型的好处是可以在不同的Apps中有一个一致使用的对象,使得一个用户可以在不同的第三方组件中使用。
User的字段有username等,还有groups和user_permissions两个many-to-many字段。
为给用户增加信息,会建立AUTH_PROFILE_MODULE,用一个OneToOne指向User的Profile模型。

ContentType是Django中表示应用和模型的一个模型,并以此提供了GenericForeignKey,可以用来任意模型的某个数据。
一个实例便是Comments模块,用以给任意模型添加评论。
有时Bookmark模型也以这样的机制来实现。


URL

根据上面完成的Models部分,便可对应的进行RESTful URLs的设计。
参照http://microformats.org/wiki/rest/urls,并参照Django的自身模块的设计,来针对模型展开CRUD操作。

例如pages有一个叫Page的简单Model,那么对应的URL可以是:
/pages/ #page_list
/pages/new/ #page_create
/pages/1/ #page_detail
/pages/1/edit #page_upadate
/pages/1/del #page_delete
对有副作用的URL,GET方式为显示编辑框,POST才确认提交。
对于模型中的Relationship,如Comment有ForeignKey指向Page:
/pages/1/comments/ #page_comment_list
/pages/1/comments/new/ #page_comment_create
/pages/1/comments/1/del/ #page_comment_delete
在上面URL中对应模型的名称,使用小写的复数形式。
并以"APP_MODEL_VIEW"的形式(要避免撞车)为每则URL设定一个name以方便调用。
查询条目可以用object_id或者slug,当条目不存在时需要正确的返回404状态码。

通过模型来设计URL,看起来是件很自然称心的事情。

为了使上面的URL可以工作,在项目的urls.py中添加:
(r'^pages/', include('demo.pages.urls')),
然后在

我们的App中建立一个urls.py的副本供编辑。

这里为使用方便,可以上面名为Page的Model中添加:
@models.permalink
def get_absolute_url(self):
....return ('page_detail', [str(self.id)])
另一种更通用与广泛的做法是使用
reverse('page_detail',{"object_id": self.pk})
或者模

板中的
{% url page_detail object_id %}
AND
<a href="{{ object.get_absolute_url }}">{{ object.name
}}</a>
这里要注意的是,Django的URL调度有序列和字典两种形式。

在其他的Web框架中还有一种常见的URL形式:
/{controller}/{action}/{id}
其中controller支持多层的结构


所以有时也会使用到
/pages/
/pages/new/
/pages/view/1/
/pages/edit/1/
/pages/del/1/
这样的URL模式。

在用户接触站点的界面中,URL是一个首先会接触到的内容。
这也是为下一步设计站点的显示效果的设计,做一个基础。

通过使用urls.py文件,就像一个.h一样,可以为整个站点提供一个清晰的结构。


View

URL调度的作用是以形式:
url(r'^posts/$', views.index,name="post_index"),
url(r'^posts/(?P\d+)/$', views.detial,name="post_detial"),
为每个URL模式指定对应的View。

View是一个形为:
def detail(request, object_id):
return HttpResponse("You're looking at object %s." % object_id)
的函数,其中的object_id便是URL的正则表达

式中捕获的部分。

依据习惯,我们会将所编写的View从urls.py中取出,放在App的views.py文件或view目录下。
而且在Django已经提供了若干实用的generic views,这就可以根据需要直接使用,或者只做一下简单的修饰。

继续以一个简单的CRUD为例:
info_dict = {
  'queryset': Post.objects.all(),
}
info_dict2 = {
  'post_save_redirect':'/posts/%(id)s',
#通常不设,默认用get_absolute_url的值
  'model':Post,
}
info_dict3 = {
  'post_delete_redirect':'/posts/',
  'model':Post,
}

urlpatterns = patterns('',
url(r'^posts/$',
'django.views.generic.list_detail.object_list',
info_dict,name="post_list"),

url(r'^posts/(?P<object_id>\d+)/$',
'django.views.generic.list_detail.object_detail',
info_dict,name="post_detail"),

url(r'^(?P<topic_id>\d+)/new/$',
'django.views.generic.create_update.create_object',
info_dict2,name="post_create"),

url(r'^(?P<topic_id>\d+)/edit/(?P<object_id>\d+)/$',
'django.views.generic.create_update.update_object',
info_dict2,name="post_update"),

url(r'^(?P<topic_id>\d+)/del/(?P<object_id>\d+)/$',
'django.views.generic.create_update.delete_object',
info_dict3,name="post_delete"),

)
这里的info_dict中可以再放名为extra_context的字典,用于传入额外需要的值或函数。
url也可以简单的只按照顺序使用正则的捕获来向View传入参数。

对于不能直接使用generic views的,可以来自己编写view:
url(r'^(?P\d+)/$',views.list,

name=""),
然后在views,py中添加
def list(request,user_id):
    author = get_object_or_404(User, pk=user_id)
    queryset = author.post_set.all()
    return list_detail.object_list(request,queryset,
paginate_by=3,extra_context={'author':author})
或者就用更加直接的写法:
def index(request):
  latest_post_list = Post.objects.all().order_by('-pub_date')[:5]
  return render_to_response('posts/index.html', {'latest_post_list': latest_post_list},

context_instance=RequestContext(request)))
自行调用模板来渲染出返回的结果。
generic views的代码也是为自行编写View一个很好的参考


Template

从分工的角度说,模板的设计属于设计师的职责。
Django给它的模板系统很大的限制,使得其中蕴含的应用的逻辑依然能够保留到View部分的代码中。
对于上节末的render_to_response代码,模板里可以使用的内容包括模板地址和传入的dict,
还包括RequestContext中的信息,例如变量 user, messages, perms供使用。
也可自行编写tags and filters供{% load **** %}。

所以说呢,设计模板的内容是和对应的View相承接的,直接由Views所传入的内容所决定。
在创建模板文件时会先考虑其了路径和名称,以及其中可以使用的变量或函数。

下面为一组CRUD的GenericView简单的template示例:
这里应用名为posts,有一个叫Post的模型,模板名为默认。

post_list.html
<a href="./new">new</a>
{% if object_list %}
<table>
  {% for post in object_list %}
  <tr>
    <td><a href="{{post.get_absolute_url }}">
{{ post.title }}</a></td>
  </tr>
  {% endfor %}
</table>
{% else %}
<p>no post</p>
{% endif %}
post_detail.html
<div>
  <div>title:{{ object.title }}</div>
  <div>{{ object.pub_data }}</div>
  <div>{{ object.content }}</div>
<a href="../{{ object.id }}/edit/">edit</a>
<a href="../{{ object.id }}/del/">del</a>
<a href="../">back</a>
post_form.html
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
post_confirm_delete.html
title:{{ object.title }}
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Delete" />
</form>
这里只是一个让CRUD可以工作的示例。

通常的模板文件中,会使用到继承结构
{% extends "base_site.html" %}
{% block content %}
...
{% endblock %}
也会再有base.html建立两层关系,为让页面继承栏目的再继承站点的模板。

为显示messages,可在base.html中添加:
{{% if messages %}<ul class="messagelist">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}</ul>
{% endif %}
以及为user不同的登录状态显示login或logout。

tag 中有linebreaksbr换行转br,也有markup可使用markdown来HTML化文本。


Html & Css

由于Template的继承结构,根据Django的理念在编写View直接使用的模板代码时,
是直接进行网页代码的编写,以分割开显示效果相关以关注其中的逻辑。
所以这一段选择性地说一说站点的前端代码。

xhtml
<h1>标题</h1>
<span>行文本</span>
<strong>加粗<strong>
<em>斜体</em>

<a href="url">链接</a>
<img src="url" alt="text" />图像
<br />换行
<hr />水平线

<p>段落 </p>
<pre>预格式文本</pre>
<div>层</div>

<table border="0">
<tr>
<td>1,1</td>
<td>1,2</td>
</tr>
<tr>
<td>2,1</td>
<td>2,2</td>
</tr>
</table>
表格

<ul>
<li>first</li>
<li>second</li>
</ul>
无序列表
这些代码可以用来组织页面结构。

然后是css
<link rel="stylesheet" type="text/css" href="style.css" /> 引入css文件

strong {font-weight:bold;} 标签选择器
#red {color:red;} id选择器
.center {text-align: center} class选择器

Box
* {
margin: 0;
padding: 0;
}
#box {
border-style: none;
padding: 10px;
}

定位
.box {
float: left;
}
.clear {
clear: both;
}
网页同时使用div的浮动来布局,样式也在css中设定。

可以嵌入JavaScript
<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
这里以Jquery为例。

可以使用浏览器中的开发工具来调试站点的各种代码。

然后再考虑一下配色(声音渐低中...


App

根据以上的步骤,以及可以建立一个简单的站点了,比如说一个私人博客。
不过对于有一定复杂度的站点,在设计模型之前,需要为项目进行App的划分。

多App的方式可以使站点的结构变得清晰,也使得App的复用变得方便。

例如:

Pinax是一组用以建立SNS的Apps。
可以创建Pinax项目,或者使用其中的应用,或者阅读其中的代码。
需要注意的是其中Apps之间存在依赖关系。
django-notification就被其中大大多数App可选的使用,
用以向用户发送通知。
django-groups被tribes以及projects使用,
用以建立用户小组。

在Debian的包中,也含了一些Django的Apps
http://packages.debian.org/search?keywords=django
如django-registration django-tagging django-tinymce app-plugins
使用这些组件,除了可以减少重复,以为增加了Apps间相一致的地方。

为了复用方便(Be Portable),需要在编写App时尊崇一些习惯
http://ericholscher.com/projects/reusable-app-docs/

还有一个叫GoFlow的工作流App看起来很有趣。

除了App的复用,也有提供整个项目来进行开发的。
例如Satchmo一个WebShop的App,
django-cms一个CMS的App。

最后说明一下,虽然Django有较好的向后兼容,
但随着版本变化会有不同的内置Apps,所以在第三方代码的实现上也会有不同。
依旧来提官方Wiki,那里始终是个很全的资料收集之处。

这些第三方Apps的存在也是为自己的项目的Apps划分提供一个参照。

像PIL那样的Python库是也会在Django项目中使用的,
更多的可复用apps也可以在Pypi找到若干。

在Django的文件中使用import可以使用Python中通用的相对路径,
也可用以当前应用所在项目名称为开始的绝对路径来引用。


Csrf

这是与安全相关的一个模块,是用于对抗Cross Site Request Forgeries的保护机制。

MIDDLEWARE_CLASSES中添加
'django.middleware.csrf.CsrfResponseMiddleware',
render_to_response时添

context_instance=RequestContext(request))
模板中
<form action=""

method="post">{% csrf_token %}
Form依旧按照
def contact(request):
    if request.method == 'POST': # If the form has been

 submitted...
        form = ContactForm(request.POST)

 # A form bound to the POST data
        if form.is_valid

(): # All validation rules pass
            # Process the data in f

orm.cleaned_data
            # ...
            return HttpResponseRedirect('/thanks/')

 # Redirect after POST
    else:
        form = ContactForm() # An unbound form

    return render_to_response('contact.html', {
        'form': form,
    })
另一种form的形式是从ModelForm派生并在Meta中指定model属性。
在构造时指定instance属性,并提供save(commit=True)用于将form的属性赋予模型对象。

其他

django-admin.py inspectdb用于从已有的db生成model.py代码

若干模块可以在不影响应用设计的情况下被添加,例如:
Admin
i18n
Cache
Syndication
以及一些第三方的分页插件机制。


End
练手中...


http://www.indexofire.com/blog/?p=774
http://komunitasweb.com/2010/02/django-tutorial-simple-notes-application/
http://github.com/vicalloy/LBForum

没有评论: