雖然能讓瀏覽器顯示“Hello World”是很有趣的一件事情,但是如果能讓用戶通過表單(form)向你的應用程序提交文本就更有趣了。這節(jié)習題中,我們將使用 form 改進你的 web 程序,并且將用戶相關的信息保存到他們的“會話(session)”中。
該學點無趣的東西了。在創(chuàng)建 form 前你需要先多學一點關于 web的工作原理。這里講并不完整,但是相當準確,在你的程序出錯時,它會幫你找到出錯的原因。另外,如果你理解了 form 的應用,那么創(chuàng)建 form 對你來說就會更容易了。
我將以一個簡單的圖示講起,它向你展示了 web 請求的各個不同的部分,以及信息傳遞的大致流程:
為了方便講述 HTTP 請求(request) 的流程,我在每條線上面加了字母標簽以作區(qū)別。
這段詳解中用到了一些術語。你需要掌握這些術語,以便在談論你的 web 應用時你能明白而且應用它們:
這個可以算是你能在網上找到的關于瀏覽器如何訪問網站的最快的快速課程了。這節(jié)課程應該可以幫你更容易地理解本節(jié)的習題,如果你還是不明白,就到處找資料多多了解這方面的信息,知道你明白為止。有一個很好的方法,就是你對照著上面的圖示,將你在《習題 50》中創(chuàng)建的 web 程序中的內容分成幾個部分,讓其中的各部分對應到上面的圖示。如果你可以正確地將程序的各部分對應到這個圖示,你就大致開始明白它的工作原理了。
熟悉“表單”最好的方法就是寫一個可以接收表單數(shù)據(jù)的程序出來,然后看你可以對它做些什么。先將你的 bin/app.py 修改成下面的樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import web
urls = (
'/hello', 'Index'
)
app = web.application(urls, globals())
render = web.template.render('templates/')
class Index(object):
def GET(self):
form = web.input(name="Nobody")
greeting = "Hello, %s" % form.name
return render.index(greeting = greeting)
if __name__ == "__main__":
app.run()
|
重啟你的 web 程序(按 CTRL-C 后重新運行),確認它有運行起來,然后使用瀏覽器訪問 http://localhost:8080/hello,這時瀏覽器應該會顯示“I just wanted to say Hello, Nobody.”,接下來,將瀏覽器的地址改成 http://localhost:8080/hello?name=Frank,然后你可以看到頁面顯示為“Hello, Frank.”,最后將 name=Frank 修改為你自己的名字,你就可以看到它對你說“Hello”了。
讓我們研究一下你的程序里做過的修改。
URL 中該還可以包含多個參數(shù)。將本例的 URL 改成這樣子: http://localhost:8080/hello?name=Frank&greet=Hola。然后修改代碼,讓它去獲取form.name 和 form.greet,如下所示:
greeting = "%s, %s" % (form.greet, form.name)
修改完畢后,試著訪問新的 URL。然后將 &greet=Hola 部分刪除,看看你會得到什么樣的錯誤信息。由于我們在 web.input(name="Nobody") 中沒有為 greet 設定默認值,這樣 greet 就變成了一個必須的參數(shù),如果沒有這個參數(shù)程序就會報錯。現(xiàn)在修改一下你的程序,在 web.input 中為 greet 設一個默認值試試看。另外你還可以設 greet=None,這樣你可以通過程序檢查 greet 的值是否存在,然后提供一個比較好的錯誤信息出來,例如:
form = web.input(name="Nobody", greet=None)
if form.greet:
greeting = "%s, %s" % (form.greet, form.name)
return render.index(greeting = greeting)
else:
return "ERROR: greet is required."
你可以通過 URL 參數(shù)實現(xiàn)表單提交,不過這樣看上去有些丑陋,而且不方便一般人使用,你真正需要的是一個“POST 表單”,這是一種包含了 <form> 標簽的特殊 HTML 文件。這種表單收集用戶輸入并將其傳遞給你的 web 程序,這和你上面實現(xiàn)的目的基本是一樣的。
讓我們來快速創(chuàng)建一個,從中你可以看出它的工作原理。你需要創(chuàng)建一個新的 HTML 文件, 叫做 templates/hello_form.html:
<html>
<head>
<title>Sample Web Form</title>
</head>
<body>
<h1>Fill Out This Form</h1>
<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">
<br/>
Your Name: <input type="text" name="name">
<br/>
<input type="submit">
</form>
</body>
</html>
然后將 bin/app.py 改成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import web
urls = (
'/hello', 'Index'
)
app = web.application(urls, globals())
render = web.template.render('templates/')
class Index(object):
def GET(self):
return render.hello_form()
def POST(self):
form = web.input(name="Nobody", greet="Hello")
greeting = "%s, %s" % (form.greet, form.name)
return render.index(greeting = greeting)
if __name__ == "__main__":
app.run()
|
都寫好以后,重啟 web 程序,然后通過你的瀏覽器訪問它。
這回你會看到一個表單,它要求你輸入“一個問候語句(A Greeting)”和“你的名字(Your Name)”,等你輸入完后點擊“提交(Submit)”按鈕,它就會輸出一個正常的問候頁面,不過這一次你的URL 還是 http://localhost:8080/hello,并沒有添加參數(shù)進去。
在 hello_form.html 里面關鍵的一行是 <form action="/hello" method="POST"> ,它告訴你的瀏覽器以下內容:
你可以看到兩段 <input> 標簽的名字屬性(name)和代碼中的變量是對應的,另外我們在 class index 中使用的不再只是 GET 方法,而是另一個 POST 方法。
這個新程序的工作原理如下:
作為練習,在 templates/index.html 中添加一個鏈接,讓它指向 /hello,這樣你可以反復填寫并提交表單查看結果。確認你可以解釋清楚這個鏈接的工作原理,以及它是如何讓你實現(xiàn)在 templates/index.html 和 templates/hello_form.html 之間循環(huán)跳轉的,還有就是要明白你新修改過的 Python 代碼,你需要知道在什么情況下會運行到哪一部分代碼。
在你下一節(jié)練習創(chuàng)建游戲的過程中,你需要創(chuàng)建很多的小 HTML 頁面。如果你每次都寫一個完整的網頁,你會很快感覺到厭煩的。幸運的 是你可以創(chuàng)建一個“布局模板”,也就是一種提供了通用的頭文件和腳注的外殼模板,你可以用它將你所有的其他網頁包裹起來。好程序員會盡可能減少重復動作,所以要做一個好程序員,使用布局模板是很重要的。
將 templates/index.html 修改成這樣:
$def with (greeting)
$if greeting:
I just wanted to say <em style="color: green; font-size: 2em;">$greeting</em>.
$else:
<em>Hello</em>, world!
然后把 templates/hello_form.html 修改成這樣:
<h1>Fill Out This Form</h1>
<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">
<br/>
Your Name: <input type="text" name="name">
<br/>
<input type="submit">
</form>
上面這些修改的目的,是將每一個頁面頂部和底部的反復用到的“boilerplate”代碼剝掉。這些被剝掉的代碼會被放到一個單獨的 templates/layout.html 文件中,從此以后,這些反復用到的代碼就由 layout.html 來提供了。
上面的都改好以后,創(chuàng)建一個 templates/layout.html 文件,內容如下:
$def with (content)
<html>
<head>
<title>Gothons From Planet Percal #25</title>
</head>
<body>
$:content
</body>
</html>
這個文件和普通的模板文件類似,不過其它的模板的內容將被傳遞給它,然后它會將其它 模板的內容“包裹”起來。任何寫在這里的內容多無需寫在別的模板中了。你需要注意$:content 的用法,這和其它的模板變量有些不同。
最后一步,就是將 render 對象改成這樣:
render = web.template.render('templates/', base="layout")
這會告訴 lpthw.web 讓它去使用 templates/layout.html 作為其它模板的基礎模板。重啟你的程序觀察一下,然后試著用各種方法修改你的 layout 模板,不要修改你別的模板,看看輸出會有什么樣的變化。
使用瀏覽器測試 web 程序是很容易的,只要點刷新按鈕就可以了。不過畢竟我們是程序員嘛,如果我們可以寫一些代碼來測試我們的程序,為什么還要重復手動測試呢?接下來你要做的,就是為你的 web 程序寫一個小測試。這會用到你在《習題 47》學過的一些東西,如果你不記得的話,可以回去復習一下。
為了讓 Python 加載 bin/app.py 并進行測試,你需要先做一點準備工作。首先創(chuàng)建一個 bin/__init__.py 空文件,這樣 Python 就會將 bin/ 當作一個目錄了。(在《習題 52》中你會去修改 __init__.py,不過這是后話。)
我還為 lpthw.web 創(chuàng)建了一個簡單的小函數(shù),讓你判斷(assert) web 程序的響應,這個函數(shù)有一個很合適的名字,就叫 assert_response。創(chuàng)建一個 tests/tools.py 文件,內容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from nose.tools import *
import re
def assert_response(resp, contains=None, matches=None, headers=None, status="200"):
assert status in resp.status, "Expected response %r not in %r" % (status, resp.status)
if status == "200":
assert resp.data, "Response data is empty."
if contains:
assert contains in resp.data, "Response does not contain %r" % contains
if matches:
reg = re.compile(matches)
assert reg.matches(resp.data), "Response does not match %r" % matches
if headers:
assert_equal(resp.headers, headers)
|
準備好這個文件以后,你就可以為你的 bin/app.py 寫自動測試代碼了。創(chuàng)建一個新文件,叫做 tests/app_tests.py,內容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from nose.tools import *
from bin.app import app
from tests.tools import assert_response
def test_index():
# check that we get a 404 on the / URL
resp = app.request("/")
assert_response(resp, status="404")
# test our first GET request to /hello
resp = app.request("/hello")
assert_response(resp)
# make sure default values work for the form
resp = app.request("/hello", method="POST")
assert_response(resp, contains="Nobody")
# test that we get expected values
data = {'name': 'Zed', 'greet': 'Hola'}
resp = app.request("/hello", method="POST", data=data)
assert_response(resp, contains="Zed")
|
最后,使用 nosetests 運行測試腳本,然后測試你的 web 程序。
$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.059s
OK
這里我所做的,是將 bin/app.py 這個模塊中的整個 web 程序都 import 進來,然后手動運行這個 web 程序。lpthw.web 有一個非常簡單的 API 用來處理請求,看上去大致是這樣子的:
app.request(localpart='/', method='GET', data=None, host='0.0.0.0:8080',
headers=None, https=False)
你可以將 URL 作為第一個參數(shù),然后你可以修改修改 request 的方法、form 的數(shù)據(jù)、以及 header 的內容,這樣你無須啟動 web 服務器,就可以使用自動測試來測試你的 web 程序了。
為了驗證函數(shù)的響應,你需要使用 tests.tools 中定義的 assert_response 函數(shù),用法屬下:
assert_response(resp, contains=None, matches=None, headers=None, status="200")
把你調用 app.request 得到的響應傳遞給這個函數(shù),然后將你要檢查的內容作為參數(shù)傳遞給誒這個函數(shù)。你可以使用 contains 參數(shù)來檢查響應中是否包含指定的值,使用status 參數(shù)可以檢查指定的響應狀態(tài)。這個小函數(shù)其實包含了很多的信息,所以你還是自己研究一下的比較好。
在 tests/app_tests.py 自動測試腳本中,我首先確認 / 返回了一個“404 Not Found”響應,因為這個 URL 其實是不存在的。然后我檢查了 /hello 在 GET 和 POST 兩種請求的情況下都能正常工作。就算你沒有弄明白測試的原理,這些測試代碼應該是很好讀懂的。
花一些時間研究一下這個最新版的 web 程序,重點研究一下自動測試的工作原理。確認你理解了將 bin/app.py 做為一個模塊導入,然后進行自動化測試的流程。這是一個很重要的技巧,它會引導你學到更多東西。