JSON 的正确用法:Python、MongoDB、JavaScript与AjaxJSON 的正确用法:Python、MongoDB、JavaScript与Ajax

本文主要总结网站编写以来在传递 JSON 数据方面遇到的一些问题以及目前采用的解决方案。网站数据库采用 MongoDB,后端是 Python,前端采用“半分离”形式的 Riot.js,所谓半分离,是说第一页数据是经过服务器端的模板引擎直接渲染到 HTML 中,从而避免首页两次加载的问题,而其它动态内容则采用 Ajax 加载。整个流程中数据都是经过 JSON 格式传递的,可是在不一样的环节中须要采用不一样的方式并遇到一些不一样的问题,本文主要作记录、总结。javascript

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。html

总的来讲,JSON 定义一种标记格式,能够很是方便地在编程语言中的变量数据与字符串文本数据之间相互转换。JSON 描述的数据结构包括如下这几种形式:前端

  1. 对象:{key: value}
  2. 列表:[obj, obj,...]
  3. 字符串:"string"
  4. 数字:数字
  5. 布尔值:true/false

了解了 JSON 的基本概念以后,下面分别针对上图中的几个数据交互环节进行总结。java

2. Python <=> MongoDB

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") )) 

3. Python <=> Ajax

前端与后端之间的数据交流比较经常使用的是经过 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 了,但另外一个问题也出现了:编程

html

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 

4. Python <=> HTML+Riot.js

若是上面的问题能够经过遵照规则来解决,那么接下来这个问题就是一个挑战规则的故事。除去 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 是很好用的数据格式,可是在不一样语言环境之间切换仍是有不少细节问题须要注意。此外,遵循传统规则,出现特例的时候,改变自身适应规则,而不是试图改变规则,这一条不必定适应全部问题,但对于那些已被公认的规则,请勿轻易挑战。

相关文章
相关标签/搜索