本文主要總結網站編寫以來在傳遞 JSON 數據方面遇到的一些問題以及目前採用的解決方案。網站數據庫採用 MongoDB,後端是 Python,前端採用「半分離」形式的 Riot.js,所謂半分離,是說第一頁數據是經過服務器端的模板引擎直接渲染到 HTML 中,從而避免首頁兩次加載的問題,而其它動態內容則採用 Ajax 加載。整個流程中數據都是經過 JSON 格式傳遞的,可是在不一樣的環節中須要採用不一樣的方式並遇到一些不一樣的問題,本文主要作記錄、總結。javascript
JSON(JavaScript Object Notation) 是一種由道格拉斯·克羅克福特構想設計、輕量級的數據交換語言,它的前輩 XML 可能更早被人們所熟知。固然 JSON 並非爲了取代 XML 而存在的,只是相比於 XML 它更小巧、更適合在網頁開發中用做數據傳遞(JSON 之於 JavaScript 就像 XML 之於 Lisp)。從名字上能夠看出,JSON 的格式符合 JavaScript 語言中「對象」的語法格式,除了 JavaScript 以外,不少其餘語言中也具備相似的類型,例如 Python 中的字典(dict
),除了編程語言以外,一些基於文檔存儲的 NoSQL 非關係型數據庫也選擇 JSON 做爲其數據存儲格式,例如 MongoDB。html
總的來講,JSON 定義一種標記格式,能夠很是方便地在編程語言中的變量數據與字符串文本數據之間相互轉換。JSON 描述的數據結構包括如下這幾種形式:前端
{key: value}
[obj, obj,...]
"string"
true
/false
瞭解了 JSON 的基本概念以後,下面分別針對上圖中的幾個數據交互環節進行總結。java
Python 與 MongoDB 之間的交互主要由現有的驅動庫提供支持,包括 PyMongo、Motor 等,而這些驅動所提供的接口都是很是友好的,咱們不須要了解任何底層的實現,只要對 Python 原生的字典類型進行操做便可:python
import motor client = motor.motor_tornado.MotorClient() db = client['test'] user_col = db['user'] user_col.insert(dict( name = 'Yu', is_admin = True, ))
惟一須要注意的是 MongoDB 中的索引項 _id
是經過 ObjectId("572df0b78a83851d5f24e2c1")
存儲的,而對應的 Python 對象爲 bson.objectid.ObjectId
,所以在查詢時須要以此對象的實例進行:ajax
from bson.objectid import ObjectId user = db.user.find_one(dict( _id = ObjectId("572df0b78a83851d5f24e2c1") ))
前端與後端之間的數據交流比較經常使用的是經過 Ajax 完成,這時遇到了第一個不大不小的坑。在以前的一篇文章中,我總結了一次 Python 編碼的坑,咱們知道 HTTP 傳遞過程當中確定不存在 JSON/XML ,一切都是二進制數據,可是咱們能夠選擇讓前端用什麼樣的方式解讀這些數據,即經過設定 Header 中的 Content-Type
,通常傳遞 JSON 數據時將其設定爲 Content-Type: application/json
,在 Tornado 最新版本中,只須要直接寫入字典類型便可:mongodb
# Handler async def post(self): user = await self.db.user.find_one({}) self.write(user)
因而迎來了第一個錯誤:TypeError: ObjectId('572df0b58a83851d5f24e2b1') is not JSON serializable
。追溯緣由,雖然 Tornado 幫咱們簡化了操做,但在像 HTTP 中寫入字典類型時仍然須要經歷一次 json.dumps(user)
操做,而對於 json.dumps
來講,ObjectId
類型是非法的。因而我選擇了最直觀的解決方案:數據庫
import json from bson.objectid import ObjectId class JSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, ObjectId): return str(obj) return super().default(self, obj) # Handler async def post(self): user = await self.db.user.find_one({}) self.write(JSONEncoder.encode(user))
此次不會再出錯了,咱們本身的 JSONEncoder
能夠應對 ObjectId
了,但另外一個問題也出現了:編程
JSONEncoder.encode
以後字典類型被轉換成字符串,寫入 HTTP 以後 Content-Type
變爲text/html
,這時前端將認爲接收的數據爲字符串而不是可用的 JavaScript Object。固然還有進一步的彌補方案,那就是前端再進行一次轉換:json
$.post(API, {}, function(res){ data = JSON.parse(res); console.log(data._id); })
問題暫時解決了,在整個過程當中 JSON 的變換是這樣的:
Python ==> json.dumps ==> HTTP ==> JavaScript ==> JSON.parse dict ==> str ==> binary ==> string ==> Object
結果第二個問題來了,當數據中存在一些特殊字符時,JSON.parse
將出現錯誤:
JSON.parse("{'abs': '\n'}"); // VM536:1 Uncaught SyntaxError: Unexpected token ' in JSON at position 1(…)
這就是在遇到問題是隻着眼解決眼前錯誤致使後續一連串改動所帶來的弊病。咱們沿着上面 JSON 變換的鏈條向上追溯,看有沒有更好的解決方案。很簡單,遵循傳統規則,出現特例的時候,改變自身適應規則,而不是改變規則:
# Handler async def post(self): user = await self.db.user.find_one({}) user['_id'] = str(user['_id']) self.write(user)
固然,若是是多條數據的列表形式,還須要進一步改造:
# DB async def get_top_users(self, n = 20): users = [] async for user in self.db.user.find({}).sort('rank', -1).limit(n): user['_id'] = str(user['_id']) users.append(user) return users
若是上面的問題能夠經過遵照規則來解決,那麼接下來這個問題就是一個挑戰規則的故事。除去 Ajax 動態加載部分,網頁上的其餘數據是經過後端模板引擎渲染得來的,也就是說是 Hard-coding 爲 HTML 的。在瀏覽器加載並解析這個 HTML 文件以前它們只是純文本文件,而咱們須要的是直接將數據塞僅 <script>
標籤在瀏覽器運行 JavaScript
時直接可用。嚴格意義上來講這並不算是 JSON 的應用,而是 Python 的 dict
與 JavaScript 的 Object
之間的直接轉換,常規的方法應該這樣寫:
# Handler async def get(self): users = self.db.get_top_users() render_data = dict( users = users ) self.render('users.html', **render_data)
<!-- HTML + Riot.js --> <app></app> <script> riot.mount('app', { users: [ {% for user in users %} { name: "{{ user['name']}}", is_admin: "{{ user['is_admin']}}" }, {% end %} ], }) </script>
這樣寫是對的,可是要解決上面提到的 ObjectId()
問題仍是須要一些額外的處理(尤爲是引號問題)。另外爲了解決 ObjectId
的問題我還嘗試了一種比較蠢的方法(在上面的 JSON.parse
遇到錯誤以前):
# Handler async def get(self): users = self.db.get_top_users() render_data = dict( users = JSONEncoder.encode(users) ) self.render('users.html', **render_data)
<!-- HTML + Riot.js --> <app></app> <script> riot.mount('app', { users: JSON.parse('{{ users }}'), }) </script>
其實跟第 3 小節的問題同樣,模板引擎渲染過程與 HTTP 傳輸過程是相似的,不一樣的是在模板中字符串變量就是純粹的值(沒有引號),所以徹底能夠用生成 JavaScript 腳本文件的形式渲染變量而無需顧慮特殊字符(下面的 {% raw ... %}
是 Tornado 模板用於防止特殊符號被 HTML 編碼的語法):
<!-- HTML + Riot.js --> <app></app> <script> riot.mount('app', { users: {% raw users %}), }) </script>
JSON 是很好用的數據格式,可是在不一樣語言環境之間切換仍是有不少細節問題須要注意。此外,遵循傳統規則,出現特例的時候,改變自身適應規則,而不是試圖改變規則,這一條不必定適應全部問題,但對於那些已被公認的規則,請勿輕易挑戰。