只看代码请直接往下拖。
环境
Django 2.1.1
Python 3.6.6
Ubuntu
提出问题
进行网页微调(字体大小、布局微调)时,使用前端 js 或 css 自然优于后端处理;但在 移动/ PC 端间切换时,往往涉及到较大的布局更改,用 js 一个一个调显然不太现实,不如后端重新来过。市面上大多也是这样,前端加个 js 判断,再根据设备类型直接定向到不同的链接页面。
但是,对于个人小站或是调整并不大的网站,为移动与 PC 写两套不同的页面,是一件难以忍受的事情。
解决思路
利用 Django 页面模板的继承特性,将页面框架与内容解耦。
也就是说,只需要为 移动端/PC端 分别制作不同的底层模板,并为内容部分提供好接口;内容部分大家都一样,只管往底层的接口里塞就行了。
所以,问题简化成如下:
所有页面均继承自 base_mobile.html 或 base_pc.html
base_*.html 只提供页面框架模板,留下 block 供内容接入
解决过程
查阅资料发现,一个模板只能继承一个父模板,并且没有办法进行条件判断。所以如要不走死胡同,我们只能把所有的页面全部继承到 base.html,再在 base.html 里进行分类讨论。
继承到 base.html 简单,只要加一行 {% extends 'base.html' %}
就完了,但分类讨论呢?如何在模板层中完成?
仿照 js 的判断方法,我们需要对浏览器的 UserAgent 进行处理。在 Django 中,UserAgent 作为数据包头的一部分被存放在字典 request.META
中,key 为'HTTP_USER_AGENT'
。之后就可以仿照 js 进行一下关键词搜索,这里列举一个非常非常潦草的解决方案.
def get_device(request):
''' Get client device type.''''
agent = request.META['HTTP_USER_AGENT'].lower()
mobile = ['iphone','android','symbianos','windows phone']
if any([agent.find(name) + 1 for name in mobile]):
return 'mobile'
else:
return 'pc'
因为只是搞个人小站,就懒得再使用正则,并区分安卓苹果平板等等了,有更仔细需求的,请自行查阅或对着各大网站 F12 ,此处仅做抛砖引玉。
但这是 Python 函数,是不能直接写到 Django 模板里的,Django 模板只认 context 参数里的内容(其实把模板引擎换成 jinjia2 就搞定这个问题了,不过换引擎貌似得不偿失…),总不能每个 render 函数前全加一串代码吧?
这个时候我想到了一个细节,就是在渲染模板时,虽然没有在 context 里添加 request 对象,但是写 {{request.***}}
却能正常工作。看起来 request 对象是被 Django 自动添加进 render 的 context 参数里的,那我们能不能手动再添加一条自己的呢?查阅果然发现了这么一条——
class DjangoTemplates[source]
… …
DjangoTemplates engines accept the following OPTIONS:
‘context_processors’:
a list of dotted Python paths to callables that are used to populate the context when a template is rendered with a request. These callables take a request object as their argument and return a dict of items to be merged into the context.It defaults to an empty list.
Django官方文档
那问题就解决了,我们可以在 mysite/myapp 下新建一个 context_processors.py,把刚才的函数稍作改动,让它把设备类型作为 device_type 塞进一个字典并返回。现在只需把这个函数注册到项目的 settings.py 里,Django 就会帮我们在 render 的 context 中加一个存有设备类型的 device_type 变量。我们在 base.html 中,只需用 {% if device_type == 'mobile' %}
这样的条件标签进行编写即可。
总结
这种在后端进行判断响应、解耦框架内容的方式,发挥出了模板引擎相较于前端 DOM 操作的优势,相较于写两套页面系统也减少了很多工作量,利于维护。
缺点自然也有,它虽然只制作了框架,但本质还是为不同的设备穷举页面,仍然不能与真正意义上的‘自适应网页’相当。
只不过相较于考验布局水平的自适应,再相较于繁琐的前端 DOM,此方案为小型网站提供了一个更优的平衡点。
于我个人,在解决这次问题的过程中,对 Django 整个系统又有了一个更深的认识,对其工作流程更加清晰,还熟悉了几乎整个 settings.py(笑)
代码
项目目录 (只放出相关文件)
mysite
├── mysite
│ ├── context_processors.py
│ ├── settings.py
│ └── urls.py
├── manage.py
├── myapp
│ ├── templates
│ │ └── myapp
│ │ └── home.html
│ └── views.py
└── templates
└── base.html
mysite/settings.py(只放出相关内容,以下文件同)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['/...../mysite/templates'], # 此处自行添加路径
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'mysite.context_processors.device', # 前四个是 startproject 时自带的,这个是我们自己加的
],
},
},
]
mysite/context_processors.py
def device(request):
'''Add client device type into template contexts.'''
agent = request.META['HTTP_USER_AGENT'].lower()
mobile = ['iphone','android','symbianos','windows phone']
if any([agent.find(name) + 1 for name in mobile]):
return {'device_type':'mobile'}
else:
return {'device_type':'pc'}
templates/base.html
要啥 js 啊 css 啊仿照下面自己加就行了。
<!DOCTYPE HTML>
<html>
<head>
<meta charset='utf8'>
<title>
{% block title %}
{% endblock %}
</title>
</head>
<body>
{% if device_type == 'mobile' %}
<p>我是手机版框架</p>
{% else %}
<p>我是电脑版框架</p>
{% endif %}
</p>我是两个框架的相同点</p>
<div>
{% block body %}
{% endblock %}
</div>
</body>
</html>
myapp/templates/myapp/home.html
同理,要啥自己加。
{% extends 'base.html' %}
{% block title %} 测试界面 {% endblock %}
{% block body %}
<p>我是与它们无关的页面内容</p>
{% endblock %}
myapp/views.py
from django.shortcuts import render
# Create your views here.
def home(request):
return render(request,'myapp/home.html')
mysite/urls.py
from django.contrib import admin
from django.urls import path
from myapp import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.home, name='home'),
]