码上有坑
本文最后更新于:2020年7月3日 早上
暑假这几天,还真拿Django + Hexo 搭建出了一个博客(准确说是DjangoBlog + Hexo-Matery主题,然后不知道重构了多少代码,30%+?),这下应该不会换了,毕竟也是自己鼓搞半天的成果(谁知道我已经换了多少博客了呢),总之一句,处处有坑,希望这些坑对阅读本文的你有些帮助。
第一坑:Hexo中的ejs模板如何变成Django中的Jinja2模板?
闪烁之狐做的这个响应式的Matery主题挺棒的,有很多人也在使用,可是爱鼓搞的我并不想用Hexo(当然我已经在几年前就用过了), 又看到了且听风吟的DjangoBlog,于是我就想把它们结合起来,所以……就有了这个问题。
最开始我并不是使用的Jinja2,而是Django自带的模板,但当我改动了一些后,发现ejs模板文件里那些功能用Django模板并不好实现时,果断换上了Jinja2。(用过了Jade, ejs, Django template, 还是Jinja2好用,抱歉,没用过Mako)
Hexo ejs中的partial是局部模板,其实差不多相当于Jinja2中的include
<%- partial('_partial/post-cover') %>
{% include '_partial/post-cover.html' %}
Hexo ejs中的url_for,其实可以用Django的static解决(什么,你问我怎么让Jinja2兼容,且看下一坑)
<%- url_for(theme.libs.css.fontAwesome) %>
{{ static(theme.libs.css.fontAwesome) }}
Hexo ejs默认都会继承layout这个布局,所以我们也要让Jinja2继承,没错,就是extends,当然了,还有巧用block
{% extends 'layout.html' %}
{% block header %}
{% endblock %}
Hexo为了国际化,使用了gettext这个i18n工具,Jinja2怎么办?Jinja2的扩展也不多,却刚好有i18n插件,不过网上说的过程都有些简单,我尝试了许久才成功(且看下一坑)。
<%= __('friends') %>
{{ gettext('friends') }}
Hexo还有个_config.yml
怎么办,当然是用pyyaml load后,注册为模板的全局变量了(先保证最小改动)。
第二坑:Django与Jinja2的兼容,Jinja2的扩展
Jinja2本是Flask的模板,当时语法仿的Django模板,但最后却成了一个比Django模板更强大、更快的模板,所以,现在Django也可以用Jinja2模板,但是配置有点稍微麻烦。
首先在你的Django的[app]目录下建立一个jinja2_env.py
文件。
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from django.utils.translation import gettext, ngettext
from jinja2 import Environment
from babel.support import Translations
from django.conf import settings
from compressor.contrib.jinja2ext import CompressorExtension
def jinja2_environment(**options):
translations = Translations.load(settings.LOCALE_PATH, settings.LIST_OF_DESIRED_LOCALES)
# LOCALE_PATH 存放翻译对应文件的文件夹路径, LIST_OF_DESIRED_LOCALES 支持的语言列表
env = Environment(extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape', 'jinja2.ext.with_', , CompressorExtension], **options)
# 加载i18n插件(gettext支持),with_插件(with标签),autoescape插件(autoescape标签), CompressorExtension(compress 标签,自动压缩css,js)
env.install_gettext_callables(gettext = gettext, ngettext = ngettext, newstyle = True)
# 指定gettext,ngettext的方法
env.install_gettext_translations(translations)
# 从LOCALE_PATH的mo语言文件获取翻译
env.globals.update({
'static': staticfiles_storage.url,
'url': reverse,
}) #确保可以使用Django模板引擎中的{% url('') %} {% static('') %}这类的语句
env.filters['datetimeformat'] = datetimeformat
# ... 还可以自定义很多filter
return env
当然也可以使JInja2实现cache标签,这个可以看JInja2官方文档。
接着是app中settings.py
中加上Jinja2
CONTEXT_PROCESSORS = [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
'environment': 'DjangoBlog.jinja2_env.jinja2_environment',
},
},
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
},
}
]
# Use pybabel to finish i18n
LIST_OF_DESIRED_LOCALES = ["zh_Hans"]
LOCALE_PATH = os.path.join(BASE_DIR, 'locale')
也别把默认的删了,不然Django Admin
就会不能访问。
当你写好了{{ gettext('friends') }}
这类的语句后,怎么从模板里抽取出来呢?答案:Babel (Django自带的只把Py文件里的抽了出来)
先写好配置文件babel.cfg
[jinja2: **/templates/**/**/*.html ]
extensions=jinja2.ext.i18n,jinja2.ext.autoescape,jinja2.ext.with_
(文件夹名前不要有_
,文件夹最多有多少层就写多少层星号匹配)!
1. pybabel -v extract -F babel.cfg -o ./locale/messages.pot ./
2. pybabel init -i ./locale/messages.pot -d ./locale/ -l zh_Hans
3. pybabel compile -d ./locale/
4. pybabel update -i ./locale/messages.pot -d ./locale/
首次建立,$1 \rightarrow 2 \rightarrow 3 $,先1抽取模板文件得到messages.pot文件,2生成对应语言po文件,手动输入翻译,3编译为mo文件。
模板文件内有更新,$1 \rightarrow 4 \rightarrow 3 $,先1抽取模板文件得到messages.pot文件,4更新对应语言po文件,手动输入翻译,3编译为mo文件。
第三坑:Docker环境的建立
我还是比较喜欢用docker的,不过每次这环境配置起来都坑人。
DjangoBlog给了docker支持,不过好像用的是CPython,不行,我得用PyPy3,一搜,果然有jamiehewland/alpine-pypy:3.6-alpine3.11
这个镜像,就在这基础上build吧
不过一定要记得,要用国内镜像,不然会慢的……Timeout……
RUN echo "http://mirrors.ustc.edu.cn/alpine/v3.11/main/" > /etc/apk/repositories
RUN pip3 install --upgrade pip -i https://mirrors.cloud.tencent.com/pypi/simple
由于gevent
安装一直报错,最后我决定用uWSGI来优化Django(由于我用的PyPy3,于是又有了下一坑),不用gevent
了……
还有,容器间怎么通信,主容器可以加--link
命令链接其它容器,就可以用容器名ping通了(这个过了半天才想起)
比如建立django_blog容器时link
了mysql容器,就可以直接在blog配置里写mysql:3306
第四坑:uWSGI与PyPy3的“爱恨情仇”,外加Nginx一手
总所周知,Django的runserver性能实在不高,稳定性也一般,所以一般用uWSGI+Nginx部署Django项目,但遇上了PyPy3,这……好像又是一个坑……
uWSGI的安装,由于要用PyPy3,还是用pip安装,不过在alpine
系统环境下,记得先装两个依赖外加gcc
apk --no-cache add gcc libc-dev linux-headers
然后Pip就行了……然后是最重要的配置
建立uwsgi.ini
[uwsgi]
socket=:8000
#与Nginx通信端口,用socket
chdir=/app
#工作目录 docker时已经映射到本机
processes=4
threads=2
master=True
pidfile=/app/uwsgi/uwsgi.pid
buffer-size=65536
#daemonize=/app/uwsgi/uwsgi.log
module=[YourAppName].wsgi:application
pypy-home=/usr/local/bin/pypy3
pypy-lib=/usr/local/bin/libpypy3-c.so
pypy-wsgi=[YourAppName].wsgi:application
pypy-wsgi-file=/app/[YourAppName]/wsgi.py
pypy-pp=/app
#pythonPath目录,当然要写工作目录
pypy-setup=/app/uwsgi/pypy_setup.py
#uWSGI 有pypy插件,但是不支持pypy3, 幸好github上有dalao写了这个
Python 3 PyPy support for uwsgi
修改Nginx配置文件
upstream django {
server 127.0.0.1:8000; #和上面端口一样
}
server {
root /app/;
listen 80;
server_name localhost;
charset utf-8;
location /static/ {
expires max;
alias /app/collectedstatic/;
}
location / {
uwsgi_pass django;
include /etc/nginx/uwsgi_params;
}
}
最后用下面命令应该就能跑起来了(首先保证你的项目在runserver时能跑起来)
uwsgi --ini ./uwsgi/uwsgi.ini
第五坑:Markdown与Mathjax(Latex)又打架,打酱油的代码高亮
Markdown真是一个好用的文本标记语言,Mathjax也是个好数学公式渲染引擎,但自从Markdown出来后,各种解析器就各立潮头,多少有些标准不同,但有两点是差不多都有的,\
的转义和_
转换成了<em></em>
标签,于是,当你用上Mathjax时,有些公式就出大问题了……
我用的是mistune
解析器,本来作者也写了math支持扩展,但是并没有给exmaple,而且照着网上的一些教程,发现作者的math支持扩展解析自定义块级元素,运行时会报错,最后翻了半天网站,终于又在国外网站找到了解决方法。
附上我稍微改动的代码:
#!/usr/bin/python3
# Modify from https://blog.depado.eu/post/mistune-parser-syntax-mathjax-centered-images
import re, mistune
class MathBlockGrammar(mistune.BlockGrammar):
block_math = re.compile(r"^\$\$(.*?)\$\$", re.DOTALL)
latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", re.DOTALL)
class MathBlockLexer(mistune.BlockLexer):
default_rules = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_rules
def __init__(self, rules=None, **kwargs):
if rules is None:
rules = MathBlockGrammar()
super(MathBlockLexer, self).__init__(rules, **kwargs)
def parse_block_math(self, m):
"""Parse a $$math$$ block"""
self.tokens.append({
'type': 'block_math',
'text': m.group(1)
})
def parse_latex_environment(self, m):
self.tokens.append({
'type': 'latex_environment',
'name': m.group(1),
'text': m.group(2)
})
class MathInlineGrammar(mistune.InlineGrammar):
math = re.compile(r"^\$(.+?)\$", re.DOTALL)
block_math = re.compile(r"^\$\$(.+?)\$\$", re.DOTALL)
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~\$]|https?://| {2,}\n|$)')
class MathInlineLexer(mistune.InlineLexer):
default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules
def __init__(self, renderer, rules=None, **kwargs):
if rules is None:
rules = MathInlineGrammar()
super(MathInlineLexer, self).__init__(renderer, rules, **kwargs)
def output_math(self, m):
return self.renderer.inline_math(m.group(1))
def output_block_math(self, m):
return self.renderer.block_math(m.group(1))
class MathRendererMixin(mistune.Renderer):
def block_code(self, code, lang=None):
code = code.rstrip('\n')
if not lang:
lang = 'text'
code = mistune.escape(code, quote=True, smart_amp=False)
return '<pre class="language-%s"><code class="language-%s">%s\n</code></pre>\n' % (lang, lang, code)
def block_math(self, text):
return '$$%s$$' % text
def latex_environment(self, name, text):
return r'\begin{%s}%s\end{%s}' % (name, text, name)
def inline_math(self, text):
return '$%s$' % text
class MarkdownWithMath(mistune.Markdown):
def __init__(self, renderer, **kwargs):
if 'inline' not in kwargs:
kwargs['inline'] = MathInlineLexer
if 'block' not in kwargs:
kwargs['block'] = MathBlockLexer
super(MarkdownWithMath, self).__init__(renderer, **kwargs)
def output_block_math(self):
return self.renderer.block_math(self.token['text'])
def output_latex_environment(self):
return self.renderer.latex_environment(self.token['name'], self.token['text'])
像下面这么用:
mk = MarkdownWithMath(renderer=MathRendererMixin())
content = mk(r"{}".format(content))
记住,一定要防止传入content时就被python转义了(就这又坑了好一会儿)
一些可能会出错的例子:
The entries of $C$ are given by the exact formula:
but there are many ways to _implement_ this computation. $\approx 2mnp$ flops
$m$
$x^m$
$r=\overline{1,n}$
i.e. the $i^{th}$
至于block_code
, 由于我用的CodeBlock.js
和 Prism.js
进行代码高亮,识别的代码块应该是<pre class="language-%s"><code class="language-%s">%s\n</code></pre>\n
这种形式,所以重新实现了Renderer
的block_code
函数。
第六坑:materialize与 editormd 的暗中冲突,也别忘了matery
materialize是个好前端响应式框架,editormd 是个好前端markdown编辑器,但是它们的css冲突……
/* 在materialize.css里注释下面语句 */
select {
/* display: none; */
}
select {
background-color: rgba(255, 255, 255, 0.9);
/*width: 100%;*/
padding: 5px;
border: 1px solid #f2f2f2;
border-radius: 2px;
height: 3rem;
}
.divider {
height: 1px;
/*overflow: hidden;
background-color: #e0e0e0;*/
}
matery 也是个好主题,但是还是冲突……
/* 在matery.css里注释并修改下面语句 */
/* Resolve conflict with editormd
pre {
padding: 2.5rem 1.5rem 1.5rem 1.5rem !important;
margin: 1rem 0 !important;
background: #272822;
overflow: auto;
border-radius: 0.35rem;
tab-size: 4;
}*/
pre[class*="language-"] {
/*padding: 1.2em;
margin: .5em 0;*/
padding: 2.5rem 1.5rem 1.5rem 1.5rem !important;
margin: 1rem 0 !important;
background: #272822;
overflow: auto;
border-radius: 0.35rem;
tab-size: 4;
}
第n坑:总结
总之,这个用Nginx反代,uWSGI开多线程,PyPy3加速,Docker集成,Django + Jinja2驱动的博客总算能跑动了……
解决问题,一定要多看日志log,比上网查效率更高,解决更准确。
同时,也要多去查查些国外的网站,说不定有解决方法。
也要结合多个教程,去做出自己真正想要的结果。
就是要多角度思考,才能很好解决问题。
(最重要的是不清楚就别乱删文件,否则……)
(等把代码重构了再开源吧)