第七章 书籍详情页面的构建



一、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来操作用视图函数操作

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 第七章 书籍详情页面的构建


Stop chasing money, and start chasing the solutions to the problem.