關於本文
本文主要總結網站編寫以來在傳遞 JSON 資料方面遇到的一些問題以及目前採用的解決方案。網站資料庫採用 MongoDB,後端是 Python,前端採用“半分離”形式的 Riot.js,所謂半分離,是說第一頁資料是通過伺服器端的模板引擎直接渲染到 HTML 中,從而避免首頁兩次載入的問題,而其它動態內容則採用 Ajax 載入。整個流程中資料都是通過 JSON 格式傳遞的,但是在不同的環節中需要採用不同的方式並遇到一些不同的問題,本文主要做記錄、總結。
1. What is JSON?
JSON(JavaScript Object Notation) 是一種由道格拉斯·克羅克福特構想設計、輕量級的資料交換語言,它的前輩 XML 可能更早被人們所熟知。當然 JSON 並不是為了取代 XML 而存在的,只是相比於 XML 它更小巧、更適合在網頁開發中用作資料傳遞(JSON 之於 JavaScript 就像 XML 之於 Lisp)。從名字上可以看出,JSON 的格式符合 JavaScript 語言中“對象”的文法格式,除了 JavaScript 之外,很多其他語言中也具有類似的類型,例如 Python 中的字典( dict ),除了程式設計語言之外,一些基於文檔儲存的 NoSQL 非關係型資料庫也選擇 JSON 作為其資料存放區格式,例如 MongoDB。
總的來說,JSON 定義一種標記格式,可以非常方便地在程式設計語言中的變數資料與字串文本資料之間相互轉換。JSON 描述的資料結構包括以下這幾種形式:
對象: {key: value}
列表: [obj, obj,...]
字串: "string"
數字:數字
布爾值: true / false
瞭解了 JSON 的基本概念之後,下面分別針對上圖中的幾個資料互動環節進行總結。
2. Python <=> MongoDB
Python 與 MongoDB 之間的互動主要由現有的驅動庫提供支援,包括 PyMongo、Motor 等,而這些驅動所提供的介面都是非常友好的,我們不需要瞭解任何底層的實現,只要對 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 ,因此在查詢時需要以此對象的執行個體進行:
from bson.objectid import ObjectId user = db.user.find_one(dict( _id = ObjectId("572df0b78a83851d5f24e2c1")))
3. Python <=> Ajax
前端與後端之間的資料交流比較常用的是通過 Ajax 完成,這時遇到了第一個不大不小的坑。在之前的一篇文章中,我總結了 一次 Python 編碼的坑 ,我們知道 HTTP 傳遞過程中肯定不存在 JSON/XML ,一切都是位元據,但是我們可以選擇讓前端用什麼樣的方式解讀這些資料,即通過設定 Header 中的 Content-Type ,一般傳遞 JSON 資料時將其設定為 Content-Type: application/json ,在 Tornado 最新版本中,只需要直接寫入字典類型即可:
# Handlerasync 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)# Handlerasync 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。當然還有進一步的彌補方案,那就是前端再進行一次轉換:
$.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 變換的鏈條向上追溯,看有沒有更好的解決方案。很簡單, 遵循傳統規則,出現特例的時候,改變自身適應規則,而不是改變規則 :
# Handlerasync def post(self): user = await self.db.user.find_one({})user['_id'] = str(user['_id'])self.write(user)
當然,如果是多條資料的列表形式,還需要進一步改造:
# DBasync 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
4. Python <=> HTML+Riot.js
如果上面的問題可以通過 遵守規則 來解決,那麼接下來這個問題就是一個挑戰規則的故事。除去 Ajax 動態載入部分,網頁上的其他資料是通過後端模板引擎渲染得來的,也就是說是 Hard-coding 為 HTML 的。在瀏覽器載入並解析這個 HTML 檔案之前它們只是純文字檔案,而我們需要的是直接將資料塞僅 <script> 標籤在瀏覽器運行 JavaScript 時直接可用。嚴格意義上來說這並不算是 JSON 的應用,而是 Python 的 dict 與 JavaScript 的 Object 之間的直接轉換,常規的方法應該這樣寫:
# Handlerasync 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 遇到錯誤之前):
# Handlerasync 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 是很好用的資料格式,但是在不同語言環境之間切換還是有很多細節問題需要注意。此外, 遵循傳統規則,出現特例的時候,改變自身適應規則,而不是試圖改變規則 ,這一條不一定適應所有問題,但對於那些已被公認的規則,請勿輕易挑戰。
以上所述是小編給大家介紹的JSON 的正確用法探討:Pyhong、MongoDB、JavaScript與Ajax的相關知識,希望對大家有所協助,如果大家想瞭解更多資訊敬請關注云棲社區網站!