像专业人士一样设置 Django 项目
wemake-django-模板
在本文中,我将向您展示如何布局一个典型的中型到复杂的django项目,该项目使用git进行源代码控制,使用pyenv和pipenv来处理包,使用celery和redis来运行任务,使用pytest进行测试,使用flake8和jsbeautify进行代码检查,使用sentry来管理日志,使用webpack来编译静态文件,使用 docker compose来本地提供服务,使用postgresql来存储数据,所有这些都尝试遵循十二因素应用程序配置方法。
如果您希望直接得出结论,请查看此 repo,我已经为您准备好了一切,您可以克隆并立即开始编写新项目的代码!
注意:我并不声称对这方面拥有最终决定权,这是一个不断发展的主题,所以如果您认为我可以做得更好,请告诉我:)
Python 版本
每次发布 Python 新版本,都会添加、弃用或删除一些功能,所以你需要确保自己清楚自己正在使用哪个版本的 Python。最好的方法是使用pyenv,它是一个 Python 版本管理器,可以让你为每个项目安装和指定不同的 Python 版本。要指定一个版本,请在项目根目录下运行
pyenv local 3.7.2
并将创建名为 的文件, 其中.python-version
包含文本。现在,当您键入 时,您将获得,如果您修改文件并写入,则同一命令将输出。3.7.2
python --version
Python 3.7.2
3.6.8
Python 3.6.8
包管理器和虚拟环境
对解释器的版本有信心后,我们就可以放心地考虑如何下载库以及将它们放在何处。如果是几年前,我会谈论pip、virtualenv甚至标准库的模块venv,但现在最酷的是pipenv,至少pypa是这么说的。Pipenv 可以同时处理虚拟环境和包管理。只需运行
pipenv install
pipenv 将创建一个虚拟环境 (vm) 和两个文件:Pipfile
和Pipfile.lock
。如果您使用 JavaScript,它与package.json
和非常相似package-lock.json
。要查看 vm 的创建位置,请输入pipenv --venv
和 。这将为您提供有关 pipenv 工作原理的线索:它会自动将项目目录映射到其特定的 vm。
因为我们正在创建一个 Django 项目,所以我们将下载软件包并使用命令 将其添加到我们的虚拟机中pipenv install django
。要访问虚拟机的库,您必须pipenv run
在运行的每个命令前面添加此命令。例如,要检查已安装的 Django 版本,请输入pipenv run python -c "import django; print(django.__version__)"
。
项目布局
下图展示了我喜欢如何布局我的目录结构。
我不喜欢默认的 Django 布局,它提示我们输入一个像awesome这样的项目名称(这恰好也是我们仓库的名称),最后我们输入了像 这样的内容~/code/awesome/awesome/settings.py
,这简直糟透了。我觉得把所有配置文件放在一个名为 的目录中更合理conf
。你的项目名称已经是根目录的名称了。所以,让我们通过
pipenv run django-admin.py startproject conf .
mkdir {apps,assets,logs,static,media,templates,tests}
输入 后,开发服务器应该可以正常工作了pipenv run python manage.py runserver
,事实也确实如此,但有些地方不对劲,这个命令写起来非常痛苦。幸运的是,我们可以Pipfile
像这样在 config.json 文件中为 pipenv 创建别名。
[scripts]
server = "python manage.py runserver"
再试一次,这次用pipenv run server
,好多了,对吧?
源代码控制
现在我们已经准备好了应用程序的骨架,它是开始跟踪我们的更改的好地方,确保我们不会跟踪秘密代码、开发媒体和日志文件等。创建一个名为的文件,.gitignore
其中包含以下内容,您就可以安全地初始化您的 repo git init
,并使用创建您的第一个提交git commit -am "Initialize project"
。
# Python bytecode
__pycache__
# Django dynamic directories
logs
media
# Environment variables
.env
.env.*
# Static files
node_modules
# Webpack
assets/webpack-bundle.dev.json
assets/bundles/style-dev-main.css
assets/bundles/style-dev-main.css.map
assets/bundles/script-dev-main.js
assets/bundles/script-dev-main.js.map
设置和环境变量
你读过《十二要素应用》吗?那就读读吧。太懒了?我来帮你总结一下,至少是关于配置的部分:你应该将连接信息存储在你的环境中,例如数据库、外部存储、API 和凭证等外部服务。这样可以很容易地在代码运行的不同环境(例如开发、预发布、持续集成、测试和生产)之间切换。
有一个很棒的包叫django-environ,可以帮到我们。安装它并修改pipenv install django-environ
,conf/settings.py
以便从环境中读取所有外部服务配置和机密值。
import environ
env = environ.Env()
root_path = environ.Path(__file__) - 2
ENV = env('DJANGO_ENV')
DEBUG = env.bool('DEBUG', default=False)
SECRET_KEY = env('SECRET_KEY')
DATABASES = {'default': env.db('DATABASE_URL')}
...
如果像在这个例子中,我们决定使用postgresql,我们需要确保安装一个像psycopg2这样的适配器pipenv install pyscopg2-binary
。
日志记录
日志记录是项目中很少被关注的事情之一,但如果它做得好并且存在,那就真的很棒了。让我们从正确的方向开始,修改我们的conf/settings.py
文件
LOGS_ROOT = env('LOGS_ROOT', default=root_path('logs'))
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'console_format': {
'format': '%(name)-12s %(levelname)-8s %(message)s'
},
'file_format': {
'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
}
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'console_format'
},
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOGS_ROOT, 'django.log'),
'maxBytes': 1024 * 1024 * 15, # 15MB
'backupCount': 10,
'formatter': 'file_format',
},
},
'loggers': {
'django': {
'level': 'INFO',
'handlers': ['console', 'file'],
'propagate': False,
},
'apps': {
'level': 'DEBUG',
'handlers': ['console', 'file'],
'propagate': False,
}
}
}
我们现在需要做的就是将所需的环境变量添加到我们的.env
文件中
...
LOGS_ROOT=./logs
USE_SENTRY=on
SENTRY_DSN=https://<project-key>@sentry.io/<project-id>
请注意,如果我们决定使用 来打开哨兵USE_SENTRY=on
,我们首先需要在sentry.io上创建一个新项目pipenv install sentry-sdk
并获取我们的秘密网址。
测试
测试代码是个好主意。发现了 bug?那就写一个失败的测试,修复代码,通过测试并提交。这样,你就不用担心这个 bug 会再次出现,即使其他人(很可能就是未来的你)触碰了完全不相关的部分,通过类似戈德堡过程的影响,导致 bug 再次出现。只要你写一些测试,就不会出现这种情况。
你可以使用 DjangoTestCase
或其他框架,但我喜欢Pytest,它我最喜欢的功能之一是参数化测试。我们继续吧
pipenv install pytest pytest-django --dev
请注意--dev
,这告诉 pipenv 仅将某些依赖项作为开发进行跟踪。
我们可以配置 pytest 的文件有很多名称,但我更喜欢使用一个名为 的文件.setup.cfg
,因为这个文件名与其他工具共享,这有助于减少文件数量。以下是一个可能的配置
[tool:pytest]
testpaths = tests
addopts = -p no:warnings
tests
现在您可以在目录中创建测试并使用以下方式运行它们
pipenv run pytest
代码检查
代码 linting 是指运行某种软件来分析你的代码。我只会关注代码风格一致性工具,但你应该知道还有很多其他工具可以考虑,比如微软的pyright。
我要提到的第一个工具是你的 IDE 本身。无论是 VIM、VSCode、Sublime 还是其他,都有一个叫做EditorConfig的项目,它是一个标准规范,可以告诉你的 IDE 缩进应该有多大,你喜欢用哪种字符串引号等等。只需在项目根目录下添加一个名为 EditorConfig 的文件,内容.editorconfig
如下:
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
indent_size = 4
combine_as_imports = true
max_line_length = 79
multi_line_output = 4
quote_type = single
[*.js]
indent_size = 2
[*.{sass,scss,less}]
indent_size = 2
[*.yml]
indent_size = 2
[*.html]
indent_size = 2
接下来,我们将添加flake8来强制执行 PEP8 规则,并添加 isort来获得对导入进行排序的标准方法。
pipenv install flake8 isort --dev
两者都可以在.setup.cfg
我们为 pytest 创建的同一个文件中进行配置。这是我喜欢的方式,但你也可以选择按照自己的意愿进行配置。
[flake8]
exclude = static,assets,logs,media,tests,node_modules,templates,*/migrations/*.py,urls.py,settings.py
max-line-length = 79
ignore =
E1101 # Instance has not member
C0111 # Missing class/method docsting
E1136 # Value is unsubscriptable
W0703 # Catching too general exception
C0103 # Variable name doesnt conform to snake_case naming style
C0330 # Wrong hanging indentation
W504 # Too few public methods
[isort]
skip = static,assets,logs,media,tests,node_modules,templates,docs,migrations,node_modules
not_skip = __init__.py
multi_line_output = 4
pipenv run flake8
我们可以分别使用和运行这两个程序pipenv run isort
。
我们将要使用的最后一个工具是Js Beautifier,它将帮助我们维护 html、js 和样式表的有序性。对于这个工具,你可以选择为你的 IDEpipenv install jbbeautifier
安装一个插件,让它显示错误信息(我就是这样做的)。要配置它,请在项目根目录中创建一个文件,内容如下(由于篇幅过大,本篇文章无法完整展示)。.jsbeautifyrc
静态文件
我们所有的 JavaScript、样式表、图片、字体和其他静态文件都位于此assets
目录中。我们将使用 Webpack 将 SASS 样式表编译成 CSS,并将 ES6 编译成浏览器 JS(ES5?)。Webpack 会提取assets/index.js
这些文件并将其用作所有静态文件的入口点。我们将从此文件导入所有 JavaScript 和样式表,然后 Webpack 会编译、最小化并将其打包成一个合适的文件包。我的典型assets/index.js
示例如下:
import './sass/main.sass'
import './js/main.js'
当然,我们至少需要安装 webpack、babel 和 sass 编译器。我们需要创建一个名为 的文件,package.json
内容如下:
{
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production"
},
"devDependencies": {
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"babel-loader": "^8.0.6",
"css-loader": "^2.1.1",
"file-loader": "^3.0.1",
"mini-css-extract-plugin": "^0.6.0",
"node-sass": "^4.12.0",
"sass-loader": "^7.1.0",
"webpack": "^4.32.0",
"webpack-bundle-tracker": "^0.4.2-beta",
"webpack-cli": "^3.3.2"
}
}
然后运行npm install
,它将下载中指定的所有依赖项package.json
到名为的目录中node_modules
。
现在该配置 Webpack 了。这是我使用的配置链接(篇幅过长,不适合这篇文章),长话短说,它定义了一系列规则,当你看到这类文件时,应该执行这些操作;当你看到其他文件时,应该执行这些操作。请将此配置放入webpack.config.js
目录根目录下名为 的文件中。
现在是时候使用我发现的最好的工具django-webpack-loader将 webpack 的输出连接到 django 模板了。你可能已经习惯了,我们需要pipenv install django-webpack-loader
安装它,然后修改conf/settings.py
INSTALLED_APPS = [
...
'webpack_loader',
]
filename = f'webpack-bundle.{ENV}.json'
stats_file = os.path.join(root_path('assets/'), filename)
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': not DEBUG,
'BUNDLE_DIR_NAME': 'bundles/', # must end with slash
'STATS_FILE': stats_file,
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
'IGNORE': ['.+\.hot-update.js', '.+\.map']
请注意,我们正在做的是读取一个文件,该文件包含有关在哪里找到 webpack 的编译文件(包)的信息。
我们现在可以使用模板标签在模板中添加 webpack 编译的脚本和样式表
{% load render_bundle from webpack_loader %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block page_title %}{% endblock %}</title>
<meta name="description" content="{% block page_description %}{% endblock %}">
{% block page_extra_meta %}{% endblock %}
{% render_bundle 'main' 'css' %}
</head>
<body>
{% block body %}{% endblock %}
{% render_bundle 'main' 'js' %}
</body>
</html>
芹菜
Celery 是一个任务队列,它非常方便地卸载那些不应该阻塞用户请求(例如发送电子邮件)的工作。使用以下命令安装 Celerypipenv install celery
并添加以下代码
conf/celery.py
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings')
app = Celery('conf')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
conf/settings.py
...
CELERY_BROKER_URL = env('CELERY_BROKER_URL')
.env
...
CELERY_BROKER_URL=redis://localhost:6379/0
现在你可以开始运行异步代码了pipenv run celery
,它应该会运行成功,或者提示你 Redis 不可用。如果提示不可用也不用担心,反正我们也不会使用你系统的 Redis。
Docker compose
要使所有项目组件上线,我们必须启动 Web 服务器、Celery、Redis、Postgres 和 Webpack。打开多个终端并输入所有必要的命令可能相当麻烦。为了解决这个问题,我们将使用 Docker Compose,这是一个Docker 容器编排工具。
简而言之,docker 的工作原理是将你的代码及其所有依赖项“编译”成所谓的镜像。然后,为了运行我们的应用程序,我们可以将此镜像实例化为一个容器,容器是镜像的运行版本。我们还会将代码挂载到容器中,这样我们就可以在本地机器上进行更改,并在容器中实时查看更改,而无需重新构建镜像。
为了构建我们需要的图像,我们将创建一个名为的文件Dockerfile
,它将作为构建 Web 和工作图像的脚本。
Dockerfile
# Pull base image
FROM python:3.7.2-slim
# Instal system dependencies
RUN apt-get update
RUN apt-get install git -y
# Set environment varibles
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Create dynamic directories
RUN mkdir /logs /uploads
# Set work directory
WORKDIR /code
# Install pipenv
RUN pip install --upgrade pip
RUN pip install pipenv
# Install project dependencies
COPY Pipfile Pipfile.lock ./
RUN pipenv install --dev --ignore-pipfile --system
现在我们将创建一个名为的文件,docker-compose.yml
我们将在其中设置项目使用的所有服务
version: '3'
services:
web:
image: dev_server
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/code
- ./logs/web:/logs
- ./media:/uploads
command: python manage.py runserver 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- .env
- .env.docker
environment:
- BROKER_URL=redis://cache
- MEDIA_ROOT=/uploads
- LOGS_ROOT=/logs
links:
- redis:cache
worker:
image: dev_worker
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/code
- ./logs/worker:/logs
- ./media:/uploads
command: python manage.py celery
env_file:
- .env
- .env.docker
environment:
- BROKER_URL=redis://cache
- MEDIA_ROOT=/uploads
- LOGS_ROOT=/logs
links:
- redis:cache
redis:
image: redis
expose:
- 6379
webpack:
image: dev_webpack
build:
context: .
dockerfile: Dockerfile-webpack
volumes:
- ./assets:/code/assets
command: npm run dev
关于这个配置文件的几点
- 请注意,我们将三个环境变量文件传递给 web 和 worker 服务。我们这样创建是为了在 Docker 中运行时覆盖某些设置。具体来说,我们需要将数据库地址更改为指向主机(你的机器),而不是 localhost(Docker 容器内)。为此,请创建
.env.docker
并添加以下行
DATABASE_URL=postgres://<user>@host.docker.internal:4321/<db-name>
- 我们将这些文件挂载
./media
到./logs
您的容器内部,以便您轻松读取日志并在本地机器上检查已上传的文件。不用担心,它们会被 git 忽略。
我们一直在努力构建的时刻……tum tum tum
docker-compose up
耶!现在一切都应该上线了,虽然还没到喝啤酒的时间,但你可以开始考虑了。
当我们在机器上更改代码时,服务器将像 Django 的开发服务器一样自动重新加载。但是,当我们使用 添加新库时pipenv install
,我们需要使用 重新构建镜像。
docker-compose build
最后一件事是,我们将通过在安装代码时忽略一些文件来使我们的图像更轻量,文件名称.dockerignore
如下
.env
.env.*
.git
.gitignore
.dockerignore
.editorconfig
.gitignore
.vscode
Dockerfile*
docker-compose*
node_modules
logs
media
static
README.md
额外:自定义用户模型
我还没有做过一个不需要修改 Django 内置身份验证应用的项目,无论是添加/修改字段、添加自定义行为还是重命名 URL。为了让所有操作都触手可及,而不是深陷于 Django 依赖的泥潭,我们将创建第一个名为 的应用users
,并添加一个自定义用户模型。
应用程序/用户/模型.py
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from apps.users.managers import UserManager
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True, null=True, db_index=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(default=timezone.now)
REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
objects = UserManager()
应用程序/用户/managers.py
from django.contrib.auth.models import BaseUserManager
class UserManager(BaseUserManager):
def create_user(self, email, password, **extra_fields):
if not email:
raise ValueError('The Email must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_active', True)
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(email, password, **extra_fields)
应用程序/用户/urls.py
from django.contrib.auth import views as auth_views
from django.urls import path
urlpatterns = [
path('login/',
auth_views.LoginView.as_view(),
name='login'),
path('logout/',
auth_views.LogoutView.as_view(),
name='logout'),
path('password-change/',
auth_views.PasswordChangeView.as_view(),
name='password_change'),
path('password-change/done/',
auth_views.PasswordChangeDoneView.as_view(),
name='password_change_done'),
path('password-reset/',
auth_views.PasswordResetView.as_view(),
name='password_reset'),
path('password-reset/done/',
auth_views.PasswordResetDoneView.as_view(),
name='password_reset_done'),
path('reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(),
name='password_reset_confirm'),
path('reset/done/',
auth_views.PasswordResetCompleteView.as_view(),
name='password_reset_complete'),
]
conf/settings.py
AUTH_USER_MODEL = 'users.User'
INSTALLED_APPS = [
...
'apps.users',
]
conf/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('', include('apps.users.urls')),
path('admin/', admin.site.urls),
]
一切就绪后,我们现在可以完全控制用户的身份验证流程了。如果您想将注册和自定义模板添加到我的代码库中,请查看我的代码库,我已经在其中设置好了所有与django-registration-redux相关的设置。
最后的想法
我希望本指南能像我撰写指南一样,对你的阅读有所帮助。如果你觉得有任何可以改进的地方,我鼓励你克隆我的 GitHub 仓库并提交拉取请求。
祝您编码愉快!
这篇文章最初发表在medium上。
文章来源:https://dev.to/fceruti/setting-up-a-django-project-like-a-pro-353