Rails和Django的深度技術(shù)對比 難以取舍的公正
我想以一個(gè)免責(zé)聲明來開始下面的內(nèi)容。我使用Django開發(fā)網(wǎng)站已經(jīng)有三年了,眾所周知,我喜歡Django。我已經(jīng)寫了一個(gè)開源的應(yīng)用程序(app),并且我已經(jīng)將補(bǔ)丁發(fā)送到了Django.然而,我以盡可能以公正的態(tài)度寫了這篇文章,這篇文章對這個(gè)框架有稱贊,也有批評。
6個(gè)月以前我在大學(xué)用Ruby on Rails做了一個(gè)項(xiàng)目而且一直做到現(xiàn)在。我做地***件事就是仔細(xì)地學(xué)習(xí)了這兩個(gè)框架并對它們進(jìn)行了比較,但是我記得當(dāng)時(shí)我很泄氣的。當(dāng)我尋找這些問題(比如說:”對于這兩者來說,數(shù)據(jù)庫的遷移是如何操作的?“、”它們的語法有什么區(qū)別?“、”用戶認(rèn)證是如何做的“)的答案時(shí),我站在一個(gè)較高的角度比較了兩者,發(fā)現(xiàn)它們大部分是非常膚淺的。下面的評論將會回答這些問題并且比較每個(gè)web框架是如何操作模型、控制器、視圖、測試的。
簡要介紹
兩個(gè)框架都是為了更快的開發(fā)web應(yīng)用程序和更好的組織代碼這兩個(gè)需求應(yīng)運(yùn)而生的. 它們都遵循 MVC 原則, 這意味著域(模型層)的建模,應(yīng)用程序的展現(xiàn)(視圖層)以及用戶交互(控制層)三者之間都是相互分開的. 附帶說明一下, Django 實(shí)際上只考慮了讓框架做控制層的工作,因此Django 自己聲稱它是一個(gè)模型-模板-視圖(model-template-view)框架. Django 的模板可以被理解為視圖,而視圖則可以看做是MVC典型場景中的控制層. 本文中我將都是用標(biāo)準(zhǔn)的MVC術(shù)語.
Ruby on Rails
Ruby on Rails (RoR) 是一個(gè)用 Ruby 寫就的web開發(fā)框架,并且Ruby“famous”也經(jīng)常被認(rèn)為是歸功于它的. Rails 著重強(qiáng)調(diào)了約定大于配置和測試這兩個(gè)方面. Rails 的約定大于配置(CoC)意味著幾乎沒有配置文件, 只有實(shí)現(xiàn)約定好的目錄結(jié)構(gòu)和命名規(guī)則. 它的每一處都藏著很多小魔法: 自動引入, 自動向視圖層傳遞控制器實(shí)體,一大堆諸如模板名稱這樣的東西都是框架能自動推斷出來的. 這也就意味著開發(fā)者只需要去指定應(yīng)用程序中沒有約定的部分, 結(jié)果就是干凈簡短的代碼了.
Django
Django 是一個(gè)用 Python 寫成的web開發(fā)框架,并以吉他手 Django Reinhardt 命名. Django 出現(xiàn)的動機(jī)在于 "產(chǎn)品部密集的***期限和開發(fā)了它的有經(jīng)驗(yàn)的Web開發(fā)者他們的嚴(yán)格要求". Django 遵循的規(guī)則是 明確的說明要比深晦的隱喻要好 (這是一條核心的 Python 原則), 結(jié)果就是即使對框架不熟悉的人,代碼都是非常具有可讀性的. 項(xiàng)目中的Django是圍繞app組織的. 每一個(gè)app都有其自己的模型,控制器,視圖以及測試設(shè)置,從而像一個(gè)小項(xiàng)目一樣. Django 項(xiàng)目基本上就是一個(gè)小app的集合, 每一個(gè)app都負(fù)責(zé)一個(gè)特定的子系統(tǒng).
讓我們先從看看每個(gè)框架怎樣處理MVC原則開始. 模型描述了數(shù)據(jù)看起來是什么樣子的,并且還包含了業(yè)務(wù)邏輯. Rails 通過在終端中運(yùn)行一個(gè)命令來創(chuàng)建模型.模型(Model)
創(chuàng)建模型
該命令會自動生成一次遷移和一個(gè)空的模型文件,看起來像下面這樣:
- class Product < ActiveRecord::Base
- end
由于我有Django的技術(shù)背景, 令我很生氣的一個(gè)事實(shí)就是我不能只通過模型文件就了解到一個(gè)模型有哪些字段. 我了解到Rails基本上只是將模型文件用于業(yè)務(wù)邏輯,而把模型長什么樣存到了一個(gè)叫做 schemas.rb 的文件中. 這個(gè)文件會在每次有遷移運(yùn)行時(shí)被自動更新. 如果我們看看該文件,我們可以看到我們的 Product 模型長什么樣子.
- create_table "products", :force => true do |t|
- t.string "name",
- t.integer "quantity_in_stock",
- t.integer "category_id",
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
- end
在這個(gè)模型中你可以看到兩個(gè)額外的屬性. created_at 和 updated_at 是兩個(gè)會被自動添加到Rails中的每個(gè)模型中的屬性.
在 Django 中,模型被定義到了一個(gè)叫做models.py的文件中. 同樣的 Product 模型看起來也許會像下面這樣
- class Product(models.Model):
- name = models.CharField()
- quantity_in_stock = models.IntegerField()
- category = models.ForeignKey('Category')
- created_at = models.DateTimeField(auto_now_add=True) # set when it's created
- updated_at = models.DateTimeField(auto_now=True) # set every time it's updated
注意,我們必須在Django中明確的(也就是自己手動的)添加 created_at 和 updated_at 屬性. 我們也要通過auto_now_add 和 auto_now 兩個(gè)參數(shù)告訴 Django 這些屬性的行為是如何定義的.
模型(Model)字段默認(rèn)值和外鍵
Rails將默認(rèn)允許字段為空。你可以在上面的例子中看到,我們創(chuàng)建的三個(gè)字段都被允許為空。引用字段類別也將既不創(chuàng)建索引,也不創(chuàng)建一個(gè)外鍵約束。這意味著引用完整性是無法保證的。Django的字段默認(rèn)值是完全相反的。沒有字段是被允許為空的,除非明確地設(shè)置。Django的ForeignKey將自動創(chuàng)建一個(gè)外鍵約束和索引。盡管Rails這里的制定可能是出于性能的擔(dān)憂,但我會站在Django這邊,我相信這個(gè)制定可以避免(意外)糟糕的設(shè)計(jì)和意想不到的情況。舉例來說,在我們的項(xiàng)目中以前有一個(gè)學(xué)生沒有意識到他創(chuàng)建的所有字段都被允許空(null)作為默認(rèn)值。一段時(shí)間后,我們發(fā)現(xiàn)我們的一些表包含的數(shù)據(jù)是毫無意義的,如一個(gè)使用null作為標(biāo)題的輪詢。由于Rails不添加外鍵,在我們的例子中,我們可以刪除一個(gè)持續(xù)引用其他產(chǎn)品的類別,這些產(chǎn)品將會有無效引用。一種選擇是使用一個(gè)第三方應(yīng)用程序,增加對自動創(chuàng)建外鍵的支持。
遷移(Migrations)
遷移允許數(shù)據(jù)庫的模式(schema)在創(chuàng)建之后可以再次更改(實(shí)際上,在Rails中所有的內(nèi)容都使用遷移,即使是創(chuàng)建)。我不得不敬佩Rails長期以來支持這個(gè)特性。通過使用Rails的生成器(generator)即可完成工作。
- $ rails generate migration AddPartNumberToProducts part_number:string
這會向Product模型(model)添加一個(gè)名為part_number的新字段(field)。
然而Django只能通過名為South的第三方庫來支持遷移。我感覺South的方式更加簡潔和實(shí)用。上面對應(yīng)的遷移工作可以直接編輯Product模型的定義并添加新的字段
- class Product(models.Model):
- ... # 舊字段
- part_number = models.CharField()
然后調(diào)用
- $ python manage.py schemamigration products --auto
South會自動識別到一個(gè)新增字段添加到Product模型并創(chuàng)建遷移文件。隨后會調(diào)用下面命令完成同步(synced)
- $ python manage.py migrate products
Django最終在它的***版本(1.7) 將South整合進(jìn)來并支持遷移。
執(zhí)行查詢
感謝對象關(guān)系映射(object-relation mapping),你不需要在任何框架中寫一行SQL語句。感謝Ruby表達(dá)式,你能夠很優(yōu)雅的寫出范圍搜索查詢(range query)。.
- Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
這會查詢到昨天創(chuàng)建的Clients
。Python不支持像1.day這種極其可讀和簡潔的語法,也不支持..
范圍操作符。但是,有時(shí)在寫Rails時(shí)我感覺像是又在寫預(yù)聲明(prepared statement)一樣。比如為了選擇所有的某一字段大于某個(gè)值的行,你不得不像下面這樣
- Model.where('field >= ?', value)
Django完成的方式不是太好,但以我的觀點(diǎn),卻更加簡介。在Django對應(yīng)的代碼如下:
- Model.objects.filter(field__gt=value)
控制器(Controller)
控制器的工作就是利用請求返回準(zhǔn)確的應(yīng)答。網(wǎng)絡(luò)應(yīng)用程序典型工作是支持添加,編輯,刪除和顯示具體的資源,而RoR的便捷表現(xiàn)在使開發(fā)控制器的工作簡單而貼心??刂破鞅徊鸱譃槿舾蓚€(gè)方法(method),每個(gè)方法代表指定的動作(action)(show代表請求某個(gè)資源,new代表顯示創(chuàng)建資源的表單,create代表從new接收POST的數(shù)據(jù)并真正的創(chuàng)建資源)??刂破鞯膶?shí)例變量(以@為前綴)會自動被傳遞給視圖(view),Rails從方法名稱就會識別應(yīng)該把哪個(gè)模板(template)作為視圖。
- class ProductsController < ApplicationController
- # 自動渲染views/products/show.html.erb
- def show
- # params是包含請求變量的ruby hash
- # 實(shí)例變量會自動被傳遞給視圖
- @product = Product.find(params[:id])
- end
- # 返回空的product,渲染views/products/new.html.erb
- def new
- @product = Product.new
- end
- # 接收用戶提交的POST數(shù)據(jù)。多數(shù)來至于在'new'視圖中表單
- def create
- @product = Product.new(params[:product])
- if @product.save
- redirect_to @product
- else
- # 重寫渲染create.html.erb的默認(rèn)行為
- render "new"
- end
- end
- end
Django使用兩種不同的方式實(shí)現(xiàn)控制器。你可以使用一個(gè)方法來實(shí)現(xiàn)每個(gè)動作,與Rails做法非常相似,或者你可以為每個(gè)控制器動作創(chuàng)建一個(gè)類。 Django沒有區(qū)分new和create方法,資源的創(chuàng)建和空資源的創(chuàng)建發(fā)生在同一個(gè)控制器中。也沒有便捷的方法命名你的視圖。視圖變量需要從控制器顯式的傳遞,而使用的模板文件也需要顯式的設(shè)置。
- # django通常稱 'show' 方法為'detail'
- # product_id 參數(shù)由route傳遞過來
- def detail(request, product_id):
- p = Product.objects.get(pk=product_id) # pk 表示主鍵
- # 使用傳遞的第三個(gè)參數(shù)作為內(nèi)容渲染detail.html
- return render(request, 'products/detail.html', {'product': p})
- def create(request):
- # 檢查表單是否提交
- if request.method == 'POST':
- # 類似于RoR的 'create' 動作
- form = ProductForm(request.POST) # 綁定于POST數(shù)據(jù)的表單
- if form.is_valid(): # 所有的驗(yàn)證通過
- new_product = form.save()
- return HttpResponseRedirect(new_product.get_absolute_url())
- else:
- # 類似于RoR的 'new' 動作
- form = ProductForm() # 空的表單
- return render(request, 'products/create.html', { 'form': form })
在以上Django的例子中代碼數(shù)量與RoR相比很明顯。Django似乎也注意到這個(gè)問題,于是利用繼承和mixin開發(fā)出了第二種實(shí)現(xiàn)控制器的方法。第二種方法稱為基于類的視圖(class-based views) (注意, Django稱這個(gè)控制器為view),并且在Django 1.5中引入以提高代碼重用。很多常用的動作都存在可被用來繼承的類,比如對資源的顯示,列表,創(chuàng)建和更新等,這大大簡化了代碼開發(fā)。重復(fù)的工作比如指定將被使用的視圖文件名稱,獲取對象并向view傳遞該對象等工作也會被自動完成。上面相同的例子使用這種方式只有四行代碼。
- # 假設(shè)route傳遞了名為 'pk' 的參數(shù),包含對象的 id 并使用該id獲得對象。
- # 自動渲染視圖 /products/product_detail.html
- # 并將product作為上下文(context)變量傳遞給該視圖
- class ProductDetail(DetailView):
- model = Product
- # 為給定的模型生成表單。如果得到POST數(shù)據(jù)
- # 自動驗(yàn)證表單并創(chuàng)建資源。
- # 自動渲染視圖 /products/product_create.html
- # 并將表單作為上下文變量傳遞給視圖
- class ProductCreate(CreateView):
- model = Product
當(dāng)控制器比較簡單時(shí),使用基于類的視圖(class-based views)通常是***的選擇,因?yàn)榇a會變得緊密,具有可讀性。但是,取決于你的控制器的不標(biāo)準(zhǔn)(non-standard)程度,可能會需要重寫很多函數(shù)來得到想要的功能。常遇到的情況就是程序員想向視圖傳遞更多的變量,這時(shí)可以重寫get_context_data函數(shù)來完成。你是不是想按照當(dāng)前對象(模型實(shí)例)的特定的字段來渲染不同的模板?你只好重寫render_to_response函數(shù)。你想不想改變獲得對象的方式(默認(rèn)是使用主鍵字段pk)?你只好重寫get_object。例如,如果我們想要通過產(chǎn)品名稱選擇產(chǎn)品而不是id,也要把類似的產(chǎn)品傳遞給我們的視圖,代碼就有可能像這樣:
- class ProductDetail(DetailView):
- model = Product
- def get_object(self, queryset=None):
- return get_object_or_404(Product, key=self.kwargs.get('name'))
- def get_context_data(self, **kwargs):
- # 先調(diào)用基類函數(shù)獲取上下文
- context = super(ProductDetail, self).get_context_data(**kwargs)
- # 在相關(guān)產(chǎn)品(product)中添加
- context['related_products'] = self.get_object().related_products
- return context
視圖
Rails 視圖使用 內(nèi)置的Ruby 模板系統(tǒng),它可以讓你在你的模板里面編寫任意的Ruby代碼. 這就意味著它非常強(qiáng)大和快速, 而非常強(qiáng)大的同時(shí)就意味著非常大的責(zé)任. 你不得不非常小心的不去把表現(xiàn)層同任何其它類型的邏輯混在一起. 這里我需要再次提到涉及一位學(xué)生的例子. 一位新同學(xué)加入了我們的RoR項(xiàng)目,并且在學(xué)習(xí)一項(xiàng)新特性. 代碼審查的時(shí)間到了. 我們首先從控制器開始,***件令我吃驚的事情是他寫的控制器里面代碼非常少. 我轉(zhuǎn)而很快去看看他寫的視圖,看到了大塊混著HTML的ruby代碼. 誠然,Rails并不會嫌棄缺乏經(jīng)驗(yàn)的程序員,但我的觀點(diǎn)是框架可以幫助開發(fā)者避免一些壞的實(shí)踐. 例如 Django 就有一個(gè)非常簡潔的 模板語言. 你可以進(jìn)行if判斷以及通過for循環(huán)進(jìn)行迭代,但是沒有方法選擇沒有從控制器傳入的對象,因?yàn)樗⒉粫?zhí)行任意的Python表達(dá)式. 這是一個(gè)我認(rèn)為可以敦促開發(fā)者方向正確的設(shè)計(jì)決定. 這能讓我們項(xiàng)目中的新手找到組織他們代碼的正確方式.
資源: CSS, Javascript 以及 圖片
Rails 有一個(gè)很不錯(cuò)的內(nèi)置 資源管道. Rails 的資源管道具有對JavaScript和CSS文件進(jìn)行串聯(lián)、最小化和壓縮的能力. 不僅僅如此,它也還支持諸如 Coffeescript, Sass 和 ERB 等其它語言. Django 對資源的支持同Rails相比就顯得相形見絀了,它把要麻煩都丟給了開發(fā)者去處理. Django 唯一提供的就是所謂的 靜態(tài)文件, 這基本上就只是從每個(gè)應(yīng)用程序那里將所有的靜態(tài)文件集合到一個(gè)位置. 有一個(gè)叫做 django_compressor 的第三方app提供了一種類似于Rails的資源管道的解決方案.
單(Forms)
網(wǎng)絡(luò)應(yīng)用中的表單是用戶輸入(input)的界面。在Rails中的表單包含在視圖中直接使用的幫助方法(method)。
- <%= form_tag("/contact", method: "post") do %>
- <%= label_tag(:subject, "Subject:") %>
- <%= text_field_tag(:subject) %>
- <%= label_tag(:message, "Message:") %>
- <%= text_field_tag(:message) %>
- <%= label_tag(:subject, "Sender:") %>
- <%= text_field_tag(:sender) %>
- <%= label_tag(:subject, "CC myself:") %>
- <%= check_box_tag(:sender) %>
- <%= submit_tag("Search") %>
- <% end %>
像subject,message這樣的輸入字段可以在控制器中通過ruby哈希 (類似字典的結(jié)構(gòu))params來讀取,比如params[:subject]和params[:message]。Django通過另一種方式抽象了表單概念。表單封裝了字段并包含驗(yàn)證規(guī)則。它們看起來像是模型。
- class ContactForm(forms.Form):
- subject = forms.CharField(max_length=100)
- message = forms.CharField()
- sender = forms.EmailField()
- cc_myself = forms.BooleanField(required=False)
Django會將CharField解析為對應(yīng)HTML元素的文本輸入框,將BooleanField解析為單選框。你可以按照自己的意愿使用 widget 字段更換為其他輸入元素。Django的表單會在控制器中實(shí)例化。
- def contact(request):
- if request.method == 'POST':
- form = ContactForm(request.POST)
- if form.is_valid():
- return HttpResponseRedirect('/thanks/') # POST之后重定向
- else:
- form = ContactForm() # An unbound form
- return render(request, 'contact.html', { 'form': form })
Django會自動添加驗(yàn)證信息。默認(rèn)情況下所有的字段都是必須的,除非特意定義(比如cc_myself)。使用上面的代碼片段,如果表單驗(yàn)證失敗,帶有錯(cuò)誤信息的表單會自動重新顯示,已經(jīng)輸入的內(nèi)容也會顯示。下面的代碼在視圖中顯示顯示了一個(gè)表單。
- <form action="/contact/" method="post">
- {{ form.as_p }} <!-- 生成類似于rails的表單 -->
- <input type="submit" value="Submit" />
- </form>
URL 和 Route
Route 是將特定的URL匹配到指定控制器的工作。Rails建立REST網(wǎng)絡(luò)服務(wù)非常輕松,而route以HTTP的行為(verbs)來表達(dá)。
- get '/products/:id', to: 'products#show'
以上的例子顯示,向/products/any_id發(fā)起的GET請求會自動route到products控制器和show動作(action)。感謝慣例優(yōu)先原則(convention-over-configuration),對于建立包含所有動作(create,show,index等等)的控制器的這種常見任務(wù),RoR建立了一種快速聲明所有常用route的方法,叫做resources。如果你依照Rails的慣例(convention)命名了控制器的方法時(shí)這會很方便。
- # automatically maps GET /products/:id to products#show
- # GET /products to products#index
- # POST /products to products#create
- # DELETE /products/:id to products#destroy
- # etc.
- resources :products
Django不是通過HTTP的行為來決定route。而是使用更復(fù)雜的使用正則表達(dá)式來匹配URL和對應(yīng)的控制器。
- urlpatterns = patterns('',
- # 在products控制器中匹配具體方法
- url(r'^products/(?P\d+)/$', products.views.DetailView.as_view(), name='detail'),
- # 匹配index方法就獲得了主頁
- url(r'^products/$', products.views.IndexView.as_view(), name='index'),
- url(r'^products/create/$', products.views.CreateView.as_view(), name='create'),
- url(r'^products/(?P\d+)/delete/$', products.views.DeleteView.as_view(), name='delete'),
- )
由于使用了正則表達(dá)式,框架會自動使用單純的驗(yàn)證。請求/products/test/會因匹配不到任何route而返回404,因?yàn)閠est不是正確的數(shù)字。不同的哲學(xué)思想在這里又一次出現(xiàn)。Django在命名控制器動作方面確實(shí)方便一些,以至于Django就沒有像Rails的resource那樣方便的助手,而且每個(gè)route必須顯式的定義。這將導(dǎo)致每個(gè)控制器需要若干個(gè)route規(guī)則。
測試
在Rails中測試很輕松,與Django比較起來更需要著重強(qiáng)調(diào)。
Fixture
兩個(gè)框架以相似的方式都支持fixture(示例數(shù)據(jù))。我卻給Rails更高的評價(jià),因?yàn)樗鼘?shí)用,能從文件的名稱得知你在使用哪個(gè)模板。Rails使用YAML格式的fixture,這是人類可讀的數(shù)據(jù)序列化格式。
- # users.yml (Rails當(dāng)前知道我們在使用user的fixtures)
- john:
- name: John Smith
- birthday: 1989-04-17
- profession: Blacksmith
- bob:
- name: Bob Costa
- birthday: 1973-08-10
- profession: Surfer
所有的fixture會自動加載而且在測試中可以作為本地變量來訪問。
- users(:john).name # John Smith
Django也支持YAML格式的fixture但是開發(fā)人員更傾向于使用JSON格式。
- [
- {
- "model": "auth.user",
- "fields": {
- "name": "John Smith",
- "birthday": "1989-04-17",
- "profession": "Blacksmith",
- }
- },
- {
- "model": "auth.user",
- "fields": {
- "name": "Bob Costa",
- "birthday": "1973-08-10",
- "profession": "Surfer",
- }
- }
- ]
這沒什么吸引力,注意它有多啰嗦,因?yàn)槟惚仨氾@式的定義它屬于哪個(gè)模板,然后在 fields
下面列出每個(gè)字段。
#p#
測試模板
在單元測試模板時(shí)兩種框架的方式基本一致。使用一組不同類型的斷言來進(jìn)行確定,比如assert_equal
,assert_not_equal
,assert_nil
,assert_raises
等等。
- class AnimalTest < ActiveSupport::TestCase
- test "Animals that can speak are correctly identified" do
- assert_equal animals(:lion).speak(), 'The lion says "roar"'
- assert_equal animals(:cat).speak(), 'The cat says "meow"'
- end
- end
類似功能的代碼在Django非常相似。
- class AnimalTestCase(TestCase):
- def test_animals_can_speak(self):
- """Animals that can speak are correctly identified"""
- # no way of directly accessing the fixtures, so we have to
- # manually select the objects
- lion = Animal.objects.get(name="lion")
- cat = Animal.objects.get(name="cat")
- self.assertEqual(lion.speak(), 'The lion says "roar"')
- self.assertEqual(cat.speak(), 'The cat says "meow"')
測試控制器(controller)
Rails又因?yàn)樗攘Χ鼊僖换I。Rails 使用類名稱來決定哪個(gè)控制器正在被測試,而測試某個(gè)特定動作(action)就像調(diào)用http_verb :action_name
一樣簡單。我們看一下例子。
- class UsersControllerTest < ActionController::TestCase
- test "should get index" do
- get :index # 向index 動作發(fā)起GET請求
- assert_response :success # 請求返回200
- # assigns是包含所有實(shí)例變量的hash
- assert_not_nil assigns(:users)
- end
- end
上面的代碼很容易理解正在發(fā)生什么。***行測試模擬了向 User
控制器的 index
動作發(fā)起一個(gè)請求。第二行隨后檢查請求是否成功(返回代碼200-299)。 assigns
是一個(gè)hash,包含了傳遞到視圖(view)的實(shí)例變量。所以第三行檢查是否存在名為 users
的實(shí)例變量并且值不是 nil
。
也有一些類似于assert_difference
這樣方便的斷言幫助方法。
- # assert_difference檢查被測試的數(shù)字在開始和結(jié)束之間是否更改
- assert_difference('Post.count') do
- # 創(chuàng)建post
- post :create, post: {title: 'Some title'}
- end
在Django中測試控制器可以通過使用一個(gè)叫 Client
類來完成,它扮演著虛擬瀏覽器(dummy web browser)的角色。下面是Django中對應(yīng)的代碼。
- class UsersTest(unittest.TestCase):
- def setUp(self):
- self.client = Client()
- def test_index(self):
- """ should get index """
- response = self.client.get(reverse('users:index'))
- self.assertEqual(response.status_code, 200)
- self.assertIsNotNone(response.context['users'])
首先我們必須在測試設(shè)置時(shí)初始化 Client
。 test_index
的***行模擬了向 Users
控制器的 index
動作申請了一個(gè) GET
請求。 reverse
查找對應(yīng)index動作的URL。注意代碼是如此冗余并且沒有類似于 assert_response :success
的幫助方法。 response.context
包含我們傳遞給視圖的變量。
很顯然Rails的magic是相當(dāng)有幫助的。Rails/Ruby同時(shí)也擁有很多第三方app,比如factory_girl,RSpec,Mocha,Cucumber,這使得編寫測試是一種樂趣。
工具和其他特征
依賴性管理(Dependency management)
兩種框架都有出色的依賴性管理工具。Rails使用 Bundler 來讀取Gemfile文件并跟蹤文件中對應(yīng)ruby應(yīng)用程序運(yùn)行所依賴的gem。
- gem 'nokogiri'
- gem 'rails', '3.0.0.beta3'
- gem 'rack', '>=1.0'
- gem 'thin', '~>1.1'
簡單的在Gemfile文件中添加一個(gè)新行即可完成增加依賴( Dependency)。通過簡單調(diào)用如下命令即可安裝所有需要的gem:
- bundle install
Django強(qiáng)烈推薦使用 virtualenv 來隔離Python環(huán)境。 pip 則用來管理python包。單獨(dú)的python包的安裝可以通過以下命令完成:
- pip install django-debug-toolbar
而項(xiàng)目依賴文件可以通過以下集中起來:
- pip freeze > requirements.txt
管理命令
基本上每個(gè)項(xiàng)目完成時(shí)都有相同的管理工作要做,比如預(yù)編譯文件(precompiling assets),清理記錄(log)等等。Rails使用Rake來管理這些任務(wù)。Rake非常靈活而且將開發(fā)任務(wù)變得簡單,特別是依賴于其他任務(wù)的。
- desc "吃掉食物。在吃之前需要烹飪(Cooks)和設(shè)置表格(table)。"
- task eat: [:cook, :set_the_table] do
- # 在吃掉美味的食物之前, :cook和:set_the_table需要做完
- # 吃的哪部分代碼可以寫在這里
- end
Rake的任務(wù)可以有前提條件(prerequisite)。上面稱為 eat
的任務(wù),在執(zhí)行之前必須運(yùn)行任務(wù) cook
和任務(wù) set_the_table
。Rake也支持命名空間(namespace),能將相同的任務(wù)結(jié)合成組(group)來完成。執(zhí)行任務(wù)只需簡單的調(diào)用任務(wù)的名稱:
- rake eat
Django管理命令就沒那么靈活而且不支持前提條件和命名空間。雖然任務(wù)最終也會完成,但不是很出色。
- class Command(BaseCommand):
- help = '吃掉食物'
- def handle(self, *args, **options):
- call_command('cook') # 這里是如何在代碼中調(diào)用管理命令
- set_the_table() # 但子任務(wù)需要是常規(guī)的python函數(shù)
- # 這里是吃的那些代碼
如果我們將上面內(nèi)容保存到eat.py
中,我們可以如下調(diào)用它:
- python manage.py eat
國際化和本地化
在Rails中國際化有些偏弱。在文件夾config/locales,翻譯字符串在文件中作為ruby哈希來定義。
- # config/locales/en.yml
- en: # the language identifier
- greet_username: "Hello, %{user}!" # translation_key: "value"
- # config/locales/pt.yml
- pt:
- greet_username: "Olá, %{user}!"
通過函數(shù)t進(jìn)行翻譯。函數(shù)***個(gè)變量是決定哪個(gè)字符串需要使用的key(比如greet_username)。Rails會自動選擇正確的語言。
- t('greet_username', user: "Bill") # Hi, Bill or Olá, Bill
我發(fā)現(xiàn)處理本地語言文件中key名字和手動注冊這些內(nèi)容很繁瑣。Django將其打包進(jìn)非常便捷的gettext。翻譯也是通過一個(gè)幫助函數(shù)完成(ugettext),但是這次的key是不需要翻譯的字符串本身。
- ugettext('Hi, %(user)s.') % {'user': 'Bill'} # Hi, Bill 或者 Olá, Bill
Django會檢查所有的源代碼并調(diào)用以下命令自動收集將要翻譯的字符串:
- django-admin.py makemessages -a
上面的命令執(zhí)行后會為每一種你想要的翻譯的語言生成一個(gè)文件。文件內(nèi)容可能像這樣:
- # locale/pt_BR/LC_MESSAGES/django.po
- msgid "Hi, %(user)s." # key
- msgstr "Olá, %(user)s" # 值 (翻譯)
注意到我已經(jīng)在msgstr填充了翻譯內(nèi)容(那些本來是空的)。一旦翻譯完成,必須對它們進(jìn)行編譯。
- django-admin.py compilemessages
這種本地化項(xiàng)目的方法實(shí)際上更實(shí)用,因?yàn)椴恍枰紤]key的名字,在需要的時(shí)候也不需要現(xiàn)查找。
【譯注】即無需自定義key,django會將整句話作為key值代入
用戶授權(quán)
不得不說當(dāng)我知道RoR(Ruby on Rails)沒有打包任何形式的用戶授權(quán)時(shí)多少有點(diǎn)震驚。我想不出任何不需要授權(quán)和用戶管理的項(xiàng)目。在這方面***的gem是devise,毫無疑問也是Rails上***的,在Github上有Rails一半的得分。
盡管Django從最開始就將授權(quán)框架打包進(jìn)來,但直到一年之前這種授權(quán)方式的靈活性才有所改善,就是當(dāng)版本1.5發(fā)布并帶來可配置的用戶模型(user model)。之前,你會被強(qiáng)制要求使用Django的方式定義用戶,而不能任意更改字段或者添加字段(field)。如今這不再是問題,你可以用自己定義的用戶模型代替原有模型
第三方庫
這里沒什么好說的。這篇文章里已經(jīng)提到了很多二者可使用的第三方庫,而且都擁有太多的app。Django Packages是個(gè)非常好的網(wǎng)站,可以用來搜索Django的App。不過還未發(fā)現(xiàn)Rails有類似的網(wǎng)站。
社區(qū)
雖然我沒有更具體的數(shù)據(jù)來證明,但我相當(dāng)確定Rails的社區(qū)更大一些。在Github上RoR擁有Django兩倍的得分。在Stackoverflow上標(biāo)記為Rails的問題也有兩倍之多。而且似乎RoR比Django有更多的工作(在Stackoverflow職業(yè)中241對58)。Rails很龐大而且有非常多迷人的資源來供學(xué)習(xí),比如Rails Casts和Rails for Zombies。Django擁有Getting Started with Django但是沒有可比性。我知道Django使用The Django Book,但是已經(jīng)落后若干年了。不要以為我說錯(cuò)了,盡管有很多Django團(tuán)體而且如果你遇到問題,你很容易通過google找到答案,但Django就是沒有Rails龐大。
結(jié)論
Ruby on Rails和Django在網(wǎng)絡(luò)開發(fā)方面都是非常出色的框架。在開發(fā)模塊化的簡潔的代碼,在減少開發(fā)時(shí)間。我已經(jīng)離不開ORM框架,模板引擎和會話管理系統(tǒng)。那么問題是我如何選擇呢?
選擇任何一個(gè)都不會錯(cuò)。我的建議通常就是兩者都使用并選出你最合適的。最終的決定會取決于你傾向于哪種語言或者哪種原則:慣例優(yōu)先原則(convention-over-configuration,CoC)還是顯式優(yōu)先隱式原則(explicit is better than implicit)。使用CoC可以自動加載(import),控制器實(shí)例會自動傳遞給視圖以及便捷的編寫測試。使用顯式優(yōu)先隱式,會明確知道代碼在做什么,即使對那些不熟悉框架的人。
從我個(gè)人經(jīng)驗(yàn)來看我更喜歡Django。我喜歡Python的明確(explicitness),喜歡Django的表單以及此框架更有防御性(有限的模板語言,在model字段中null
默認(rèn)不可用)。但我也知道更多人離開了Rails的魔法和它優(yōu)秀的測試環(huán)境是沒法活的。