# Django_web_ask **Repository Path**: lihaowen2017/Django_web_ask ## Basic Information - **Project Name**: Django_web_ask - **Description**: 在线知识分享平台,包含问答模块,文章模块,私信模块,新闻模块等。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 2 - **Created**: 2020-02-08 - **Last Updated**: 2024-05-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Django高级实战——问答平台 ### 代码规范 1. 需要返回查询集的逻辑写在QuerySetModel中 利用条件筛选过后对象集合 2. 模型类中的数据库处理的逻辑写在models中 具有实体含义且不存储在数据库的字段 3. 业务相关逻辑的处理写在View中 ### 大纲 1. django高级用法 2. Channels实时消息推送(websocket编程) 3. TestCase测试用例 4. 数据库设计&网站优化 5. 算法/设计模式+融合项目 6. 云计算服务 ### 重点内容 1. 更加Pythonic的编码风格 2. 使用Pipenv管理项目开发环境 3. Django开发生态,不局限于框架,探索项目开发最佳实践 4. 学会为Models和Views编写测试用例,提升代码质量 5. 使用Cookiecutter火速搭建具有高完成度Django项目 6. MySQL数据库设计,安全和权限管理,SQL优化 7. redis缓存设计,缓存数据,celery中间人,Channels通道 8. 通用类视图源码 9. python的MRO与C3线性化算法 10. mixin中的组合模式,Signal机制的观察者模式 11. Django Template Language 12. channels实战WebSocket编程实现消息推送 13. haystack+elasticsearch实现全站搜索 14. Django应用的主流部署方式 15. 使用阿里云的ECS+RDS部署项目 ### 特殊问题 HTTP 方法: head方法: 类似于get方法,不返回数据,只得到请求头 场景:判断某一资源是否存在,得到200状态码即可!节省网络流量 options方法:获取服务器支持哪些http方法,没有响应正文。 #### django templetes重写问题 虽然这种场景很少见,但希望能够在 admin 窗口中使用自己的表单。根据模板的载入顺序,从 Django 目录中的 forms/templates/django/forms/widgets/textarea.html 复制一份到项目的模板目录 : templates/django/forms/widgets/textarea.html 。给新模板添加了写修改后,重启 django。 发现这并不起作用。 解决方法 在 settings.py,把 django.forms 加入到 INSTALLED_APPS 中; 在 settings.py,设置 FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' 。 ## Django项目的一些最佳实践 ### 1. pipenv管理项目环境 pip3 install pipenv 新建文件夹 移动到新文件夹目录下 pipenv --python 3.7 ### 2. 自定义用户模型 1. 继承BaseUserManager和AbstractBaseUser 指定AUTH_USER_MODEL 2. 指定登录字段不是username字段 3. 用户权限伪代码 ```python class Meta: ... username_filed = 'nickname' def has_perm(self, perm, obj=None): """用户具体的权限""" pass def has_module_perms(self, app_label): """用户具有访问哪些应用的权限""" pass def is_staff(self): """管理员权限""" pass # 实例化用户创建管理类 objects = MyUserManager() ``` 4. 自定义用户创建,伪代码 user.models.py中,usermodel中必须实例化 ```python class MyUserManager(BaseUserManager): def create_user(self, username, password=None): pass return user def create_superuser(self, username, password): pass return user ``` ### 3. 优先使用通用类视图(Class-based Generic Views) django视图学习网址 http://ccbv.co.uk/ ### 4. 在系统环境变量中保存敏感信息 依据Twelve-Factor方法论为Django应用配置环境变量 Twelve-Factor ### 5. 为不同环境(生产环境,开发环境,测试环境)分别配置settings.py文件 使用环境变量存储敏感信息的优势: 1. 方便版本控制,代码里没有密码信息部署上线不需要更改配置信息(heroku平台) 2. 配置文件写入到.env文件中,应用配置代码分离,部署时,.env文件写入.ignore中 3. 使用virtualenv 创建虚拟环境时,为每个环境写requirements.txt文件(课程使用pipenv Cookiecutter) ### 6. 测试用例编写 单元测试的最佳实践:理论上,视图,路由,模型类,表单均要写测试用例;测试每一个方法。 提交分支必须要跑测试用例 测试覆盖度: 安装coverage包 跑测试用例: pipenv run coverage run manage.py test -v 2 # 运行过程中输出具体信息 生成测试报告 pipenv run coverage html ## 需求分析,功能设计,技术选型 ### 需求规格说明书 1. 修订页 2. 项目概述 : 产品描述 产品功能 3. 业务需求:总体需求,业务需求一,业务需求二 ### 问题: 1. 网站所有内容用户登录后才能访问,是否先开发个人中心模块, 个人中心中用户信息统计来源于其它模块是否先要开发其它模块 2. 文章模块,收到用户评论,触发消息通知,消息通知写在每个模块中还是写在消息通知模块中。 ### 功能设计 #### 原则:低耦合,高内聚 低耦合:不同模块间降低依赖性。 高内聚:在同一个模块中,相关性要强 ### 技术选型 前端:html/css/JQuery/DTL/Bootstrap/websocket 后端:python37+django2.1+Cookiecutter+haystack+elasticsearch+channels+常用django包(django-taggit文章打标签,django-environ环境变量,django-markdownx markdown预览,django-crispy-form表单,awesome-slugify文章状态,sorl-tgumbnail图片,django-contrib-comments评论,django-allth三方登录) 部署运维:阿里云ESC,RDS;Nginx+WSGI+django(http请求)+Daphne(socket) 数据库:MySQL,redis 网站优化(包括数据库优化,前端优化,算法优化):Celery异步任务 django-compressor静态文件压缩 ## Cookiecutter搭建项目 ### 远程pycharm 开发 全局安装pip install cookiecutter 在文件夹中 使用cookiecutter https://github.com/pydanny/cookiecutter-django.git 下载django模板 pycharm linux 远程开发 使用 cookiecutter https://github.com/pydanny/cookiecutter-django.git 下载django模板 创建项目 在项目文件夹下制定虚拟环境 pipenv -- python 3.7,记录解释器地址,pycharm ssh解释器使用该地址的/bin/python解释器 pycharm创建空文件, 解释器使用远程,设置空文件为django项目,设置中搜索django,配置django项目地址,setting文件为项目的config/settings/local.py pipenv --py 查看虚拟环境路 pipenv install -r requirements/local.txt 安装包 ### Cookiecutter 文件夹说明 config:配置文件夹 docs :文档 locale:django国际化翻译文件 requirements:包文件 utility:本项目需要的工具或脚本 ### Django-Allauth ,Django Social Auth ,Python-Social-Auth 区别 邮箱注册 配置 DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 原生邮件的BACKEND 第一个集成了django的登录;后两个只集成了三方登录 三方登录使用Django-Allauth 使用 OAuth 2.0 协议原理流程: 1. 向三方登录应用,请求授权 2. 授权同意 3. 利用授权向认证服务器获取token 4. 返回accesstoken 5. 使用accesstoken向资源服务器申请获取信息,例如邮箱,用户名 6. 将资源信息返回给客户端 ### 测试用例编写(网址,视图,模型类) 使用django-test-plus #### django-test 单元测试的坑: 方法1:: 自定义test测试数据库的字符编码 DATABASES["default"]["TEST"] = { 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci', } 方法2: 在第一次跑测试用例的时候,会自动创建test前缀的测试数据库,然而自动生成的数据库没有设定字符集utf8编码;导致测试数据无法插入。 解决方案:修改mysql数据库默认字符集编码为utf-8; 以阿里云linux centos系统为例:修改my.cnf 文件 搜索 my.cnf文件 find / -name my.cnf 我的my.cnf地址 /etc/my.cnf 末行添加 [client] default-character-set=utf8 [mysqld] character-set-server=utf8 collation-server=utf8_general_ci 重启mysql service mysqld restart 输入 status 显示: Server characterset: utf8 Db characterset: utf8 Client characterset: utf8 Conn. characterset: utf8 此时测试用例可以成功跑通。 ### 网址路由测试 测试内容 (正反向解析): 1. 命名的url解析到真正的url网址; 2. 网址解析到命名的路由; ### 通用类视图ListView源码理解 ```python """使用通用类视图必须制定model和queryset,否则报错""" def get_queryset(self): """ Return the list of items for this view. The return value must be an iterable and may be an instance of `QuerySet` in which case `QuerySet` specific behavior will be enabled. """ if self.queryset is not None: queryset = self.queryset if isinstance(queryset, QuerySet): queryset = queryset.all() elif self.model is not None: queryset = self.model._default_manager.all() else: raise ImproperlyConfigured( "%(cls)s is missing a QuerySet. Define " "%(cls)s.model, %(cls)s.queryset, or override " "%(cls)s.get_queryset()." % { 'cls': self.__class__.__name__ } ) ordering = self.get_ordering() if ordering: if isinstance(ordering, str): ordering = (ordering,) queryset = queryset.order_by(*ordering) return queryset # get_context_data def get_context_data(self, *, object_list=None, **kwargs): """Get the context for this view.""" queryset = object_list if object_list is not None else self.object_list # 获取对象列表 page_size = self.get_paginate_by(queryset) # 对对象列表分页 context_object_name = self.get_context_object_name(queryset) # 获取上下文对象的名字 if page_size: # 对查询集进行分页 paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size) context = { # 前端中直接使用 'paginator': paginator, 'page_obj': page, 'is_paginated': is_paginated, 'object_list': queryset } else: context = { 'paginator': None, 'page_obj': None, 'is_paginated': False, 'object_list': queryset } if context_object_name is not None: context[context_object_name] = queryset context.update(kwargs) return super().get_context_data(**context) ``` ```python """view""" class NewsListView(LoginRequiredMixin, ListView): """首页动态""" model = News # queryset = News.objects.all() paginate_by = 20 # 分页 url自带?page= # page_kwarg = 'p' # 分页查询参数别名 # context_object_name = 'news_list' # 查询集在模板视图中的别名,默认是模型类名_list # ordering = 'created_at' # 排序字段,多字段时('x','y') 使用元组, 也可以在model中实现 template_name = 'news/news_list.html' # 如果不写会自动关联到,模型类名_list.html def get_ordering(self): """自定义排序,重构复杂排序,例如热度推荐等""" pass def get_paginate_by(self, queryset): """自定义分页""" pass def get_queryset(self): """返回查询集""" return News.objects.filter(reply=False) # def get_context_data(self, *, object_list=None, **kwargs): # """添加额外的上下文;添加前端显示的额外信息""" # context = super().get_context_data() # context['views'] = 100 # return context ``` ### python多继承-MRO MRO 方法解析顺序(Method Resolution Order)。定义了python中多继承存在的情况下,解释器查找函数解析的具体顺序。 函数解析顺序 经典类(Old-style Class)vs新式类(New-style Class) python 2.1 经典类 DFS算法 (深度优先搜索) python2.3 引入新式类,DFS和BFS算法(深度优先,广度优先) python2.3 经典类与新式类共存,DFS和C3算法(C3线性算法) python3 新式类,C3算法 #### 理解经典类的MRO ```python class A(): def who_am_i(self): print("I am A") class B(A): pass class C(A): def who_am_i(self): print("I am C") class D(B, C): pass d = D() print(d.who_am_i()) ``` python 2 按照从左到右的顺序深度优先遍历类的继承图,从而确定类中函数的调用顺序: 1. 检查当前的类里面是否有该函数,如果有则直接调用。 2. 检查·1当类的第一个父类里面是否有该函数,如果没有则检查父类的第一个父类是否有该函数,以此递归深度遍历。 3. 如果没有则回溯以此检查下一个父类里面是否有该函数并按照2中的方式递归。 D -> B -> A -> C 深度优先遍历存在的问题: C类作为A的子类如果重写了A类的方法则无法访问。 #### 理解新式类的MRO ```mermaid graph TD A --> F B --> F Y --> A Y --> B X --> A X --> B ``` C3线性算法(基于DFS算法)(从左至右深度优先) 新算法与基于深度遍历的算法类似,但是不同在于型算法会对深度优先遍历得到的搜索路径进行额外的检查。 其从做到有扫描得到的搜索路径,对于每一个节点解释器都会判断该结点是不是好的节点。 如果不是好的节点,那么将其从当前的搜索路径中移除。 好的节点定义: N是一个好的节点当且仅当搜索路径中N之后的节点都不继承自N。 搜索路径 F --> A --> Y --> X --> B --> Y --> X 第一次出现的X, Y都不是好节点,后面出现过 所以C3算法得到的搜索路径 F --> A --> B --> Y --> X self.方法,如果实例化搜索路径的一个类后,后续类的self.方法都是实例化的那个对象为self super, self 继承重写的问题视频讲解(django 高级实战,赞乎,7-7 CreateView那一节中) ##### 实例化一个类后,搜索路径形成不可改变,继承关系按搜索路径关系而非自身的继承关系!!! #### C3 线性化算法 左类和右类继承同一个父类,从左至右遍历,应为继承相同父类,所以搜索路径深度优先时先删除左类继承的父类。 维基百科 https://en.wikipedia.org/wiki/C3_linearization ### 通用类视图DeleteView源码理解 #### slug和taggit,python两个三方库 python-slugify(uuslug)和django-taggit slug: url 后资源的别名 python-slugify: 将汉字词组变为url能识别的拼音; 局限性:资源名不能重复,否则导致url重复 #### markdown文档的编辑和实时预览 django-markdownx使用 https://pypi.org/project/django-markdownx/1.2.1/ 1. 安装依赖库 * Markdown * Pillow * Django * jQuery 2. models.py中导入如下语句 ```python from markdownx.models import MarkdownxField from markdownx.utils import markdownify # 将文本字段修改为MarkdownxField content = MarkdownxField(verbose_name="内容") def get_markdown(self): """将markdown文本转换成HTML""" return markdownify(self.content) ``` 4. forms.py 中 ```python from markdownx.fields import MarkdownxFormField class ArticleForm(forms.ModelForm): content = MarkdownxFormField() class Meta: model = Article fields = ["title", "content", "image", "tags"] ``` 5. urls.py中 path("markdownx/", include('markdownx.urls')), 6. setting中添加应用 "markdownx" 7. makemigrations ,migrate 8. python manage.py collectstatic # 将markdownx的css,js文件引入static_root 路径下 #### django-contrib-comments 实现评论文章 #### django template 模板语法(重点) json_script Django2.1版本新增 js 分离时可以接受后端的值 {{ value|json_script:"hello-data"}} value 为 {'hello':'world'} 前端为js代码 ```javascript 然后js里可以接收这个值 var value = JSON.parse(document.getElementById('hello-data').textContent) ``` ### 问答模块 #### 用户-问题-回答-点赞/踩-采纳 逻辑关系梳理 1. 用户与问题的关系为一对多,与回答的关系为一对多 2. 问题与回答的关系为一对多 ##### 根据需求讨论是否将问题和回答放入一张表 1. 需求: 用户可以对问题,回答进行投票,问题提出者只能选择一答案进行采纳。 2. 一张表的问题:若放在一张表中时,将设计多个字段,如判断是问题还是答案,还要设计一个字段表明哪个答案是被采纳的答案。多个字段放入一张表中属于宽表设计模式,增加数据库查询压力,对于用户的操作可能既要读该表又要写该表,叫难进行读写分离。 #### Django中的ContentTypes框架 Django ContentTypes是由Django框架提供的一个核心功能,它对当前项目中所有基于Django驱动的model提供了更高层次的抽象接口 1. Django权限管理中的Permission借助ContentType实现了对任意models的权限操作 2. ContentType的通用类型 - GenericRelation(通用关联) ##### ContentType 的巧妙用法 - Timeline设计 Timeline: 不同模块的内容在首页按时间顺序展示 ##### 什么是GenericRelation和GenericForeignKey ```python # 通用类外键 class Comment(models.Model): """ 评论关联三个类型外键, 也就是一条记录三个类型只有一个有值, 新增模块时代码扩展性较弱, 目标comment类变得通用 """ author = models.ForeignKey(to=User, on_delete=models.CASCADE) body = models.TextField(blank=True, null=True) # pic = models.ForeignKey(to=Picture, on_delete=models.CASCADE, null=True) # post = models.ForeignKey(to=Post, on_delete=models.CASCADE, null=True) # article = models.ForeignKey(to=Article, on_delete=models.CASCADE, null=True) # 通用外键的三个字段 content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.IntegerField() # 其他表的主键,下面使用该通用类外键表的主键 content_object = GenericForeignKey("content_type", "object_id") class Post(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) body = models.TextField(blank=True, null=True) comments = GenericRelation(Comment) # 默认是删除级联 ``` ### 赞踩业务及代码逻辑 1. 用户首次操作:赞一下或踩一下,创建.create() 2. 用户已经赞过,要取消赞:删除.delete() 3. 用户已经赞过,要踩一下:更新.update() 4. 用户已经踩过,要取消踩:删除.delete() 5. 用户已经踩过,要赞一下:更新.update() ## 单元测试RequestFactory ## 用户私信功能 1. 使用Ajax异步请求(必须刷新才能看到) Ajax轮询模拟长连接的缺陷,HTTP协议携带请求头,数据较为冗余,经过TCP三次握手占用较大的网络资源,而且轮询时后端无数据接收时仍然会进行网络资源的占用,多用户进行长连接模拟时存在较大问题。 2. 使用WebSocket 登录时发送一次HTTP请求建立WebSocket连接 ### WebSocket协议的概念和原理 1. WebSocket和Http协议均属于应用层协议。 2. 通过http协议建立传输层的Tcp连接,先通过http发送get请求, 3. WebSocket 请求头中重要的字段 Connection和Upgrade: 表示客户端发起的是WebSocket请求 在Connection字段中发送Upgrade进行升级,在Upgrade字段中发送WebSocket,表示将当前的http请求升级为WebSocket请求。 Sec-WebSocket-Version: 客户端所使用的WebSocket协议版本号,服务端进行校验确认是否支持该版本号。 Sec-WebSocket-Key: 一个Base64编码值,由浏览器随机生成,用于升级request,服务端拿到该值后,将HTTP协议升级成WebSocket协议,计算response的字段 Sec-WebSocket-Extensions: 客户端向要表达的协议级的扩展 4. WebSocket允许服务器主动向客户端推送数据。在WebSocket协议中,客户端浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。 5. WebSocket 响应头中重要的字段 HTTP/1.1 101 Switching Protocols: 切换协议,WebSocket协议通过HTTP协议来建立运输层的TCP连接。服务端返回值包含Upgrade字段说明协议转换成功 Connection和Upgrade: 表示服务端返回的是WebSocket响应 Sec-WebSocket-Accept: 表示服务器接受了客户端的请求,由Sec-WebSocket-Key计算得来 Sec-WebSocket-Version: 服务端端所支持的WebSocket协议版本号 6. 协议建立的简单流程 6-1. 客户端通过HTTP协议发送GET请求,通过在请求头中携带Connection和Upgrade字段,指定要将通信协议升级为WebSocket 6-2. 服务器返回给客户端状态码101 Switching Protocols 表示协议切换成功 6-3. 服务端与客户端进行通信 ### WebSocket协议的优缺点及应用场景 #### WebSocket协议的优点 1. 支持双向通信,实时性更强 2. 数据格式比较轻量,性能开销较小,通信高效。(一般2-10个字节,客户端向服务端传递需要额外携带4个字节的掩码)只携带报头和数据包,不像HTTP携带数据冗余。 3. 支持扩展。用户可以扩展协议或者实现自定义的子协议(比如支持自定义压缩算法等)Sec-WebSocket-Extensions字段用于扩展协议 #### WebSocket协议的缺点 1. 少部分浏览器不支持,浏览器支持的程度与方式有区别。 2. 长连接对后端处理业务的代码稳定性要求更高,后端推送功能相对复杂。 3. 成熟的HTTP生态下有大量的组件可以复用,WebSocket较少。 #### WebSocket协议的应用场景 1. 即时聊天通信,网站消息通知 2. 在线协同编辑,如腾讯文档 3. 多玩家在线游戏,视频弹幕、股票基金实时报价 #### Django中如何实现WebSocket编程 ##### Django中实现WebSocket编程需要解决的问题 1. 如何分别路由HTTP请求和WebSocket请求 同一端口的访问如何区分请求的协议 2. 如何兼容Django的认证系统 私信和消息通知功能,需要登录,发送给正确的接收方,如何让WebSocket请求使用Django的认证模块 3. 如何接受和推送WebSocket消息 4. 如何通过ORM保存和获取数据 #### Django Channels的原理 Django Channels 是一个为Django提供异步扩展的库,通常主要用来提供WebSocket支持和后台任务 ##### Channels中文件和配置的含义 asgi.py: 介于网络协议服务和Python应用之间的标准接口,能够处理多种通用协议类型,包括HTTP、HTTP2和WebSocket channel_layers: 在settings.py中配置,类似于一个通道,发送者(producer)在一端发送消息,消费者(consumer)在另外一端监听 routings.py: 相当于Django中的urls.py consumers.py: 相当于Django中的views.py ##### WSGI和ASGI的区别 1. WSGI(Python Web Server Gateway Interface): 为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口 Nginx/Apache + WSGI(uWSGI符合WSGI的软件) + Django/Flask/Python (只使用了HTTP/HTTP2协议) 2. ASGI(Asynchronous Server Gateway Interface): 异步服务网关接口,一个介于网络协议服务和Python应用之间的标准接口,能够处理多种通用的协议类型,包括HTTP,HTTP2,WebSocket Nginx/Apache + ASGI(Daphne) + Django/Flask/Python 3. WSGI和ASGI的区别: WSGI是基于HTTP协议模式的,不支持WebSocket,而ASGI就是为了支持PYthon常用的WSGI所不支持的新协议标准,即ASGI是WSGI的扩展。而且能通过asyncio异步运行。 ##### Channels_layer配置和使用 在应用中添加channels asgi.py文件(与wsgi同级,一定要写应用查找路径否则部署后出错) ```python """ ASGI entrypoint. Configures Django and then runs the application defined in the ASGI_APPLICATION setting. """ import os import sys import django from channels.routing import get_default_application # 查找应用的路径 app_path = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) ) sys.path.append(os.path.join(app_path, "web_ask")) # ..web_ask/web_ask os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") django.setup() application = get_default_application() ``` setting.py 文件 ```python # channels频道层缓存 CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [f'{env("REDIS_URL", default="redis://:123456@127.0.0.1:6379")}/3', ], # channels layers缓存使用3库 }, }, } ``` Channels_layer存在组的概念。建立连接即为同一组 ##### consumer使用见项目study_consumer.py文件 ##### 前后端分离项目中使用WebSocket可以使用JsonWebSocketConsumer以及AsyncJsonWebsocketConsumer #### 前端WebSocket 使用 reconnecting websocket包 ```javascript // WebSocket连接,使用wss(https)或者ws(http) const ws_scheme = window.location.protocol === "https:" ? "wss" : "ws"; const ws_path = ws_scheme + "://" + window.location.host + "/ws/" + currentUser + "/"; const ws = new ReconnectingWebSocket(ws_path); // 监听后端发送过来的消息 ws.onmessage = function (event) { const data = JSON.parse(event.data); console.log(data); if (data.sender === activeUser) { // 发送者为当前选中的用户 $(".send-message").before(data.message); // 将接收到的消息插入到聊天框 scrollConversationScreen(); // 滚动条下拉到底 } } }); /* // WebSocket构造函数,用于新建WebSocket实例 var ws = new WebSocket('ws://ip:80', 'websocket'); //实现重连 // 返回实例对象当前的状态 //ws.readyState //CONNNECTING: 值为0, 表示正在连接 //OPEN: 值为1, 表示连接成功, 可以通信了 //CLOSING: 值为2, 表示连接正在关闭 //CLOSED: 值为3, 表示连接已关闭, 或者打开连接失败 switch (ws.readyState) { case ws.CONNECTING: // XOXO break; case ws.OPEN: // break; case ws.CLOSING: // break; case ws.CLOSED: // break; default: // ... break; } // ws.onopen 用于指定连接成功后的回调函数 ws.onopen = function () { ws.send('连接成功!') }; // ws.onclose 用于指定连接关闭后的回调函数 // ws.onmessage 用于指定收到服务器数据后的回调函数 ws.onmessage = function (event) { if (typeof event.data === String) { console.log("received string") } else { console.log("xxx") } }; // ws.send() // ws.onerror 指定报错时的回调函数 ws.onerror = function (event) { // }; */ ``` ## Channels实现WebSocket消息通知 ### 通知处理器设计与实现 ```python def notification_handler(actor, recipient, verb, action_object, **kwargs): """ 通知处理器,点赞用,评论用 :param actor: request.user对象 :param recipient: User Instance 接受者实例,可以是一个或者多个接收者 :param verb: str 通知类别 :param action_object: Instance 动作对象的实例 :param kwargs: key, id_value 等(前端传递的) :return: None """ if actor.username != recipient.username and recipient.username == action_object.user.username: # 只通知接收者 recipient == 动作对象的作者 key = kwargs.get("key", "notification") id_value = kwargs.get("id_value", "notification") # 记录通知内容 Notification.objects.create( actor=actor, recipient=recipient, verb=verb, action_object=action_object ) channel_layer = get_channel_layer() payload = { "type": "receive", "key": key, # 传递一个参数,js响应用的,key是前端case里的参数 "actor_name": actor.username, "id_value": id_value } async_to_sync(channel_layer.group_send)("notifications", payload) # payload 是consumer中receive的text_data参数 ``` 在对应的model中使用 #### django-comment使用信号量来完成websocket 见官方文档https://django-contrib-comments.readthedocs.io/en/latest/ #### WebSocket消息通知排错 第一步:WebSocket是否连接成功,检查请求头,响应头,状态码 建立连接失败的可能原因: 1. 前端WebSocket API 使用错误,未正确发送连接,常见的的错误:连接地址写错,JS语法错误,未正确的引用Reconnecting-WebSocket.js文件 2. Consumers.py 中接收WebSocket连接的代码错误,检查connect方法,或者routing.py中的路由是否匹配 第二步:WebSocket连接成功,接收不到数据,前端是否接收到WebSocket消息,使用chrome浏览器,选中WS,点击建立的连接,看Frames是否能收到数据,用其它浏览器和其它用户登录,触发消息通知,看是否有数据。 可能的错误原因: 1. 模型类或视图中调用get_channle_layers的时候,payload有没有传递给consumers.py中对应的方法(WebSocket连接对应的Consumer类) 2. consumers.py中self.send()方法,数据发送给前端时出错 3. 前端WebSocket API中onmessage方法,没有正确解析event.data 第三步: Frames能看到WebSocket消息,就是没有消息通知 可能的错误原因: 1. js对html标签操作的错误,对于点赞数和评论数的更新,看下update_social_activity是否发送了post请求 第四步:查看django routing.py 文件路由解析顺序 ### Haystack+Elasticsearch实现全站搜索 #### 全站搜索的实现思路 ##### Elasticsearch(全文检索框架,面向文档型数据库) 介绍 Elasticsearch 属于CS架构 Elasticsearch是分布式可扩展的实时搜索和分析引擎 同类型的框架(Whoosh, Solr, Xapian) django中要使用搜索框架必须要添加对应的客户端 比如python/django es client(django-haystack) ##### Elasticsearch(全文检索框架) 特性 1. 实时分析的分布式搜索引擎 2. 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索 3. 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据 ##### Elasticsearch安装 1. 服务器安装java环境 yum install java ##### 2. Elasticsearch服务不允许使用root账户启动 3. Haystack 只支持Elasticsearch2+的版本 2.4.6 下载https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.4.6/elasticsearch-2.4.6.tar.gz 4. 启动Elasticsearch ./elasticsearch-2.4.6/bin/elasticsearch 默认启动端口为9200 5. 后端运行 新建文件 running.log 名字任意;./elasticsearch-2.4.6/bin/elasticsearch > running.log 2>&1 & 输出正确的日志 6. 查看进程 ps - ef | grep java ##### Elasticsearch三种python客户端比较 django-haystack/ elasticsearch-py/elasticsearch-dsl-py django-haystack: 可以直接使用django的ORM直接连接到elasticsearch elasticsearch-py: 原生操作 elasticsearch-dsl-py: 高级封装 #### django中使用Elasticsearch和django-haystack 安装对应版本的Elasticsearch pip install Elasticsearch==2.4.1 新建search应用 新建索引文件,search_index.py ```python # 建立索引 索引字段:客户端搜索关键字可能包含的内容 import datetime from haystack import indexes from web_ask.news.models import News from web_ask.articles.models import Article from web_ask.qa.models import Question from django.contrib.auth import get_user_model from taggit.models import Tag # 标签 class ArticleIndex(indexes.SearchIndex, indexes.Indexable): """对Article模型类中部分字段上建立索引,use_template=True使用""" text = indexes.CharField(document=True, use_template=True, template_name="search/articles_text.txt") def get_model(self): return Article def index_queryset(self, using=None): """当Article模型类中的索引有更新的时候调用, 检索已发表的文章""" return self.get_model().objects.filter(status="P", updated_at__lte=datetime.datetime.now()) class NewsIndex(indexes.SearchIndex, indexes.Indexable): """对News模型类中部分字段上建立索引,use_template=True使用""" text = indexes.CharField(document=True, use_template=True, template_name="search/news_text.txt") def get_model(self): return News def index_queryset(self, using=None): """当News模型类中的索引有更新的时候调用, 检索已发表的文章""" return self.get_model().objects.filter(reply=False, updated_at__lte=datetime.datetime.now()) class QuestionIndex(indexes.SearchIndex, indexes.Indexable): """对Question模型类中部分字段建立索引""" text = indexes.CharField(document=True, use_template=True, template_name='search/questions_text.txt') def get_model(self): return Question def index_queryset(self, using=None): return self.get_model().objects.filter(updated_at__lte=datetime.datetime.now()) class UserIndex(indexes.SearchIndex, indexes.Indexable): """对User模型类中部分字段建立索引""" text = indexes.CharField(document=True, use_template=True, template_name='search/users_text.txt') def get_model(self): return get_user_model() def index_queryset(self, using=None): return self.get_model().objects.filter(updated_at__lte=datetime.datetime.now()) class TagsIndex(indexes.SearchIndex, indexes.Indexable): """对Tags模型类中部分字段建立索引""" text = indexes.CharField(document=True, use_template=True, template_name='search/tags_text.txt') def get_model(self): return Tag def index_queryset(self, using=None): return self.get_model().objects.all() ``` 在setting文件中引入 "web_ask.search.apps.SearchConfig", "haystack", ```python HAYSTACK_CONNECTIONS = { 'default': { # 使用的Elasticsearch搜索引擎 'ENGINE': 'haystack.backends.elasticsearch2_backend.Elasticsearch2SearchEngine', # Elasticsearch连接地址,部署在服务器所以使用127.0.0.1 'URL': 'http://127.0.0.1:9200/', # 默认的索引名 'INDEX_NAME': 'web_ask', }, } HAYSTACK_SEARCH_RESULTS_PER_PAGE = 20 # 分页结果 # 实时信号量处理器,当模型类中数据增加、更新、删除时自动更新索引 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' ``` python manage.py rebuild_index 创建索引 templete文件中写入索引字段 ```txt {{ object.title }} {{ object.content }} ``` 路由配置 path('search/', include('haystack.urls')), #### 前端 后端用request.GET.get('q'),接受 ```html
``` ### 网站优化与Django Channels应用部署 #### 1. Django-degub-toolbar 使用 #### 2. Bug修复,功能优化 2-1. 测试环境启动在django editor中将配置文件修改为test 2-2. 限制图片上传的大小(markdownx): ```python # 使用markdown上传图片大小限制 允许上传的最大图片大小为5M MARKDOWNX_UPLOAD_MAX_SIZE = 5 * 1024 * 1024 MARKDOWNX_IMAGE_MAX_SIZE = { "size": (1000, 1000), # 加载的大小 "quality": 100 # 加载是否压缩,100为不压缩 } ``` #### 3. 使用django-compressor压缩静态文件 静态文件压缩手动生成 python manage.py compress --force #### 4. Celery异步发送邮件 安装django-celery-email 三方app中添加 djcelery_email local配置文件中 ```python # True为同步执行没有异步效果,要想异步执行改为False或者删除 CELERY_TASK_ALWAYS_EAGER = False CELERY_TASK_EAGER_PROPAGATES = False ``` env配置文件中添加 DJANGO_EMAIL_BACKEND=djcelery_email.backends.CeleryEmailBackend celery启动任务执行单元命令 celery -A config.celery_app:app worker -l info #### 5. Django缓存优化之redis缓存 django 缓存类别 per-site cache:整站缓存,页面内容不会改变 per-view cache:视图缓存,针对某一个视图返回的结果进行缓存 Template fragment caching:模板片段的缓存,导航栏之类的 low-level cache:用户自定义的缓存,缓存某个函数中计算的值,然而又不是函数,或某一视图返回的结果,使用低级别的api缓存 Downstream caches:isp服务商,浏览器缓存 ##### 配置缓存 使用本地内存作为缓存 ```python CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "", } } ``` SESSION缓存 setting base.py ```python SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # SESSION缓存到redis中 # SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 数据库存储session # SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 本地内存存储session ``` 缓存整个视图,缓存视图的所有数据(文章详情页缓存5分钟) article url.py ```python from django.views.decorators.cache import cache_page path("