一、ViewModel的基本概念
把数据缓存进数据库中,要写很多代码,但都只是简单的逻辑判断。学习的重要知识点很少。
所以,本项目直接从web API获取数据。
1.1 审视search()视图函数所提供的数据
1.search()视图函数所提供的数据,到底满不满足客户端的要求,合不合适?
(1)全不全面?
- 搜索页面的结构:大概分为3部分:①②③
真实项目下,没有已经做好的页面作为目标参考,但是会有设计师给你画的设计图或者产品经理给你的需求文档,作为参考。
search()视图函数所返回的数据结构有两种类型:一种是普通关键字
缺少②
另一种是isbn
同样缺少②,也缺少③。其中③要么是1,要么是0。
(2)客户端好不好用?
不好用。
因为返回的是两种不同数据结构的数据。一种是集合,另一种是单本。
基于上述两个标准,下一步应该调整search()视图函数所返回的结果:
- 补充返回结果缺少的信息,比如关键字②;
- 将两种不同的数据结构,合并为一种。
1.2 ViewModel层
由下图可以看出:
页面需要显示的最终数据,并不是与原始数据一一对应的。
因此,需要对原始数据进行改变、修饰。位置就是在ViewModel。
即,为每一个页面,适配一个ViewModel。所有的ViewModel合在一起,就是web里面的一个分层:ViewModel层。
见下图:
总结ViewModel的作用:
裁剪 ✂、修饰、合并。有可能综合使用。
二、使用ViewModel层处理书籍数据
利用上节的ViewModel层,改写下search()视图函数所返回的结果:
将单本视为集合的一种特例,所以可以将单本包装为集合的形式。
新建目录:
2.1 改写下search()视图函数所返回的结果数据
搜索结果的书籍展示:需要以下6个信息。
在app|api|viewmodels|book.py中:编写代码
class BookViewModel:
'''
对`search()`视图函数所返回的结果:补充关键字、合并数据结构
'''
@classmethod # 很明显,没有用到实例变量,所以是静态的
def package_single(cls, data, keyword): # data原始数据 # 因为缺少关键字,所以传进来作为一个参数
'''
处理单本数据
'''
returned = {
'book': [],
'total': 0,
'keyword': keyword
}
if data: # 当原始数据存在,总记录数才为1
returned['total'] = 1
returned['book'] = [cls.__cut_book_data(data)] # 调用下面新定义的函数 # 因为上面字典中的键值对,其值是列表形式
return returned
@classmethod
def package_collection(cls, data, keyword):
pass
@classmethod
def __cut_book_data(cls, data): # 新单独定义一个函数,上面两个函数都可以调用
'''
用于裁剪原始数据
'''
book = {
'title': data['title'],
'publisher': data['publisher'],
'pages': data['pages'],
'author': '、'.join(data['author']), # 取出把列表中的每一项,用顿号连接在一起,称为str
'price': data['price'],
'summary': data['summary'],
'image': data['image']
}
return book下面api中的author比较特殊,是列表的形式。可以将其转换为上面的字符串形式。
2.2 处理原始数据的位置
数据在哪里处理,说下普适的情况:
单页面前后端分离的应用程序:
建议author直接以列表的形式,返回到客户端去,让客户端用JavaScript来解析这个列表。
因为JavaScript处理,非常方便。
做网站:
建议在ViewModel层中处理。
因为用模板渲染的方式,渲染HTML的话,把数据处理好后直接往模板中填充会更方便。
2.3 经典的编程思维:编写package_collection()函数
1.API中的原始数据
2.一个经典的编程思维:
对单项数据,编写一个处理函数。然后,对于一组多项数据,就可以复用上面单项数据的处理函数了。
class BookViewModel:
'''
对`search()`视图函数所返回的结果:补充关键字、合并数据结构
'''
@classmethod
def package_single(cls, data, keyword):
'''
处理单本数据
'''
returned = {
'book': [],
'total': 0,
'keyword': keyword
}
if data:
returned['total'] = 1
returned['book'] = [cls.__cut_book_data(data)]
return returned
@classmethod
def package_collection(cls, data, keyword): # 编写package_collection()函数
'''
处理一组书籍
'''
returned = { # 定义一个要返回的数据结构字典,以接受有效数据
'books': [],
'total': 0,
'keyword': keyword
}
if data: # 将原始数据,存放到上面新定义的字典中
returned['total'] = data['total']
returned['books'] = [cls.__cut_book_data(book) for book in data['books']] # 列表推导式
return returned
@classmethod
def __cut_book_data(cls, data):
'''
用于裁剪原始数据
'''
book = {
'title': data['title'],
'publisher': data['publisher'],
'pages': data['pages'],
'author': '、'.join(data['author']),
'price': data['price'],
'summary': data['summary'],
'image': data['image']
}
return book如果不具备上述编程思维,在处理一组数据时,很容易写成下面的代码:把集合当作整体来处理
@classmethod
def __cut_books_data(cls, data):
books = [] # 定义一个空列表,以接受对原始数据处理好的数据
for book in data['books']: # 每一本书,数据结构形式是一个小字典
r = {
'title': book['title'],
'publisher': book['publisher'],
'pages': book['pages'],
'author': '、'.join(book['author']), # 把列表中的每一项,用顿号连接在一起
'price': book['price'],
'summary': book['summary'],
'image': book['image']
}
books.append(r) # 追加到之前定义的空列表中
return books在视图函数app|web|book.py中,调用上面刚编写好的BookViewModel类:
from flask import request, jsonify
from app.libs.helper import is_isbn_or_key
from app.spider.yushu_book import YuShuBook
from . import web
from app.forms.book import SearchForm
from ..api.view_models.book import BookViewModel # 导入刚才定义的类
@web.route('/book/search')
def search():
form = SearchForm(request.args)
if form.validate():
q = form.q.data.strip()
page = form.page.data
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
result = BookViewModel.package_single(result, q) # 调用package_single(),裁剪数据、统一数据结构
else:
result = YuShuBook.search_by_keyword(q, page)
result = BookViewModel.package_collection(result, q) # 调用package_collection(),裁剪数据、统一数据结构
return jsonify(result) # 把数据用json格式,返回到客户端去,这就是制作api
else:
return jsonify(form.errors)postman中isbn搜索的返回结果:
postman中普通关键字搜索的返回结果:
3.下图,为什么不是None?
因为,None是Python中的数据类型。
null是json对象,不是Pyhton中的数据结构,已经脱离了python的语言范畴。
最好将null转换成空字符串,否则,后续在html模板渲染时,可能会出出现None。
利用逻辑运算符or的运算性质。
class BookViewModel:
'''
对`search()`视图函数所返回的结果:补充关键字、合并数据结构
'''
@classmethod
def package_single(cls, data, keyword):
'''
处理单本数据
'''
returned = {
'book': [],
'total': 0,
'keyword': keyword
}
if data:
returned['total'] = 1
returned['book'] = [cls.__cut_book_data(data)]
return returned
@classmethod
def package_collection(cls, data, keyword):
'''
处理一组书籍
'''
returned = {
'books': [],
'total': 0,
'keyword': keyword
}
if data:
returned['total'] = data['total']
returned['books'] = [cls.__cut_book_data(book) for book in data['books']]
return returned
pass
@classmethod
def __cut_book_data(cls, data):
'''
用于裁剪原始数据
'''
book = {
'title': data['title'],
'publisher': data['publisher'],
'pages': data['pages'] or '', # 如果前者不为空,就取前者;否则,就取后者
'author': '、'.join(data['author']),
'price': data['price'],
'summary': data['summary'] or '', # 如果前者不为空,就取前者;否则,就取后者
'image': data['image']
}
return book最终,结果如下:
三、伪面向过程:披着面向对象外衣的面向过程
3.1 审视之前的类代码
审视之前编写的代码:
- BookViewModel类
- YuShuBook类
从函数的角度看,类下面封装了几个方法,方法意义明确、代码短小,还不错。
但是,从面向对象看,完全没有理解OOP的内涵。
3.2 面向对象里,两个最重要的性质
面向对象里,有两个最重要的性质:描述特征、描述行为。
类里面,描述特征的是:类变量
类里面,描述行为的是:方法
3.3 初学者常犯的错误
很多的初学者犯的错误是,写面向对象时,只写方法,不写类变量、实例变量。
如何审视自己写的面向对象的类,是不是一个伪面向对象的类?
如果一个类下面,有大量的可以被标为staticmethod、classmethod这样的静态方法,那么这个类封装的就不是面向对象。
所以,BookViewModel类、YuShuBook类,是披着面向对象外衣的面向过程
四、重构鱼书核心对象:YuShuBook
用面向对象的方式,重构和改写上面这两个类。
4.1 重构YuShuBook类:
from app.libs.httper import HTTP
from flask import current_app
class YuShuBook:
'''
类:用于描述书籍。比较抽象,隐藏了具体的操作。
'''
isbn_url = 'http://t.talelin.com/v2/book/isbn/{}'
keyword_url = 'http://t.talelin.com/v2/book/search?q={}&count={}&start={}'
def __init__(self):
self.total = 0 # 定义一些实例的变量,以描述特征:书籍的数据
self.books = [] # 保存数据 + 将两种不同形式的数据,统一成集合形式
def search_by_isbn(self, isbn):
'''
描述行为1:用isbn查询
'''
url = self.isbn_url.format(isbn) # 在实例方法里,推荐用self来访问类变量
result = HTTP.get(url)
self.__fill_single(result) # 鱼书数据就记录在类YuShuBook的内部,不用return给客户端
def __fill_single(self, data): # 被上面调用
if data:
self.total = 1
self.books.append(data)
def __fill_collection(self, data): # 被下面调用
self.total = data['total']
self.books = data['books']
def search_by_keyword(self, keyword, page=1):
'''
描述行为2:用普通关键字查询
'''
url = self.keyword_url.format(keyword, current_app.config['PER_PAGE'], self.calculate_start(page))
result = HTTP.get(url)
self.__fill_collection(result)
def calculate_start(self, page):
return (page - 1) * current_app.config['PER_PAGE']五、从json序列化看代码的解释权反转
5.1 有意义的空行
可以用空行来分隔代码片段,便于阅读。
用在函数里面,语法不强制,但建议用。
上述结果报错。是因为:
python可以序列化一个字典,但是不能序列化一个对象。books = BookCollection()是个对象。
5.2 将特殊对象转换为字典
1.如何将变量books = BookCollection()转换成一个字典?
__dict__:对象下面都有一个内置的属性,用于取得对象下面所有的属性数据
即,对于普通的对象,只用__dict__上述就可以解决序列化的问题。
但books = BookCollection()比较特殊:因为对象里面还有对象。
有点像《盗梦空间》里的梦中梦 :)
转换图示:
2.编写代码:
调用dumps()方法即可。
from flask import request, jsonify
from app.libs.helper import is_isbn_or_key
from app.spider.yushu_book import YuShuBook
from . import web
from app.forms.book import SearchForm
from ..api.view_models.book import BookViewModel, BookCollection
import json # 导入json模块
@web.route('/book/search')
def search():
form = SearchForm(request.args)
books = BookCollection()
if form.validate():
q = form.q.data.strip()
page = form.page.data
yushu_book = YuShuBook()
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
yushu_book.search_by_isbn(q)
else:
yushu_book.search_by_keyword(q, page)
books.fill(yushu_book, q)
return json.dumps(books, default=lambda o: o.__dict__) # lambda表达式,函数定义的简化写法
# 将不能序列化的object,转换为可以序列化的字典
else:
return jsonify(form.errors)5.3 经典的编程思维:函数的调用方决定函数内部实现
编程设计的经典思维:
代码解释权的反转,并不是由函数编写方来定义,而是由函数的调用方来定义,即将内部实现的逻辑交给调用方。
json.dumps(books, default=lambda o: o.__dict__)例如,sorted()函数、filter函数等。
如果在本行长久发展,就不能简单停留在调用上,而是学习这种思维。
当你自己编写函数时,运用函数式编程的思维,把代码的解释权交给函数的调用方,以增大函数的灵活性。
六、详解单页面与网站的区别
做服务器最主要的时数据。但是,如果不把数据展现在页面上,本项目后续会容易理不清业务逻辑。
所以,一个萝卜一个坑,视图函数写完就立马渲染到页面上。
但是,强烈建议大家,在真正做项目、团队项目中,不要太关注页面。
关注的应该是视图函数中数据的返回,不管是做api,还是做网站
1.梳理普通的网站,与单页面应用程序之间的流程的区别?
(1)对于多页面普通网站:
数据的填充,是在服务器。
(2)对于单页面:
数据的填充,是在客户端。
在JS里发送http请求,去服务器加载数据的技术,被称为AJAX。
用途:用于改善客户端用户的体验
2.适合场景:
画图:适合单页面
腾讯视频:适合多页面
3.总结两者的区别
| 单页面 | 多页面普通网站 | |
|---|---|---|
| 页面数量 | 只有一个页面 | 有多个页面 |
| 数据的渲染、模板的填充的位置 | 在客户端进行 | 在服务器进行 |
| 业务逻辑 | 用js来操作 | 用视图函数操作 |






















Comments | NOTHING