Django 是一个 使用 Python 编程语言的全栈 web 开发框架。

Documentation: Writing your first Django app

下载和安装

pip install Django

初始化项目

在 Django 项目的目录下执行:

$ django-admin startproject <SITENAME>

启动项目

$ python manage.py runserver
# more specifically:
$ python manage.py runserver 0.0.0.0:8000

启动 app

$ python manage.py startapp polls

项目文件结构

mysite/  # 项目名,对 Django 不重要
|--- manage.py  # 命令行接口
|--- mysite/    # 项目包,用来 `import`
     |--- __init__.py
     |--- settings.py  # 设置,类似 Jekyll 中的 __config.yml
     |--- urls.py      # 网站的路由设定
     |--- wsgi.py      # 兼容 WSGI 的 web 服务器的接口
     |--- asgi.py      # Asynchronous Server Gateway Interface
     |--- templates/   # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#customizing-your-project-s-templates
          |--- admin/      # 魔改管理员页面
               |--- base_site.html  # 从 Django 源码的此路径复制粘贴而来,然后修改
               |--- index.html      # 魔改的管理员首页
django-polls
|--- README.rst  # 介绍,安装指南之类
|--- pyproject.toml  
|--- setup.cfg
|--- setup.py
|--- MANIFEST.in     # 指定需要一起打包的非 python 文件
|--- polls/          # 名为 polls 的 app,一个项目(project)中可以有多个app
     |--- __init__.py
     |--- apps.py       # 教程中没有,https://docs.djangoproject.com/en/5.0/ref/applications/
     |--- urls.py       # polls 这一 app 中的路由设定,相对于 mysite/urls.py 
     |--- views.py      # 指定页面渲染使用的模板
     |--- templates/    # 模板
          |--- polls/      # 帮助 Django 计算实际路径,防止和其他 app 撞车
               |--- index.html
               |--- detail.html  
               |--- results.html
     |--- migrations/
          |--- __init__.py
     |--- admin.py    # 将 app 中部分数据的读写权限赋予管理员
     |--- models.py   # 数据库的结构定义
     |--- tests.py    # 测试
     |--- static/     # 静态资源的文件夹
          |--- polls/      # 帮助 Django 计算实际路径,防止和其他 app 撞车
               |--- style.css
               |--- images/background.png
        

polls/apps.py

from django.apps import AppConfig

class PollsConfig(AppConfig):
    name = "polls"
    verbose_name = "<WHATEVER>"

前端:urls, views, templates, static

mysite/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("polls/", include("polls.urls")),
    path("admin/", admin.site.urls),
]

include() 用来引入其他 python 模块的 url,只有 admin.site.urls 不需要 include()

polls/urls.py

from django.urls import path

from . import views

app_name = "polls"
urlpatterns = [
		# ex: /polls/
    path("", views.IndexView.as_view(), name="index"),
    # ex: /polls/5/
    path("<int:question_id>/", views.DetailView.as_view(), name="detail"),
    # ex: /polls/5/results/
    path("<int:question_id>/results/", views.ResultsView.as_view(), name="results"),
    # ex: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

为防止不同的路由规则重名,用 app_name 进行区分,在模板部分使用。

polls/views.py

一个 view 就是一类网页,view + template 类似于 Jekyll 中的 layout。每个 view 由一个函数表示。

  • 简单 view,教学用,后面被 abstract views 取代

    # SIMPLE VIEWS REPLACED BY ABSTRACT ONES
    from .models import Question
    
    from django.http import HttpResponse
    from django.template import loader
    def index(request):
        latest_question_list = Question.objects.order_by("-pub_date")[:5]
        template = loader.get_template("polls/index.html")
        context = {
            "latest_question_list": latest_question_list,
        }
        return HttpResponse(template.render(context, request))
    # OR:
    from django.shortcuts import render
    def index(request):
        latest_question_list = Question.objects.order_by("-pub_date")[:5]
        context = {"latest_question_list": latest_question_list}
        return render(request, "polls/index.html", context)
    
    def detail(request, question_id):
        try:
            question = Question.objects.get(pk=question_id)
        except Question.DoesNotExist:
            raise Http404("Question does not exist")
        return render(request, "polls/detail.html", {"question": question})
    # OR:
    from Django.shortcuts import get_object_or_404
    def detail(request, question_id):
        question = get_object_or_404(Question, pk=question_id)
        return render(request, "polls/detail.html", {"question": question})
    
    def results(request, question_id):
        question = get_object_or_404(Question, pk=question_id)
        return render(request, "polls/results.html", {"question": question})
    

把有相似之处的 views 抽象成类:

# ABSTRACT VIEWS
from django.urls import reverse
from django.views import generic
from django.utils import timezone

class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

def get_queryset(self):
    """
    Return the last five published questions (not including those set to be
    published in the future).
    """
    # https://docs.djangoproject.com/en/5.0/intro/tutorial05/#id6
    return Question.objects.filter(
                                   pub_date__lte=timezone.now()
                                  ).order_by(
                                             "-pub_date")[
                                                          :5
                                                         ] 

class DetailView(generic.DetailView):
    model = Question
    template_name = "polls/detail.html"

class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"

vote 这个 view 不同,成功时用 HttpResponseRedirect 跳转到另一个 view

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
        # request.POST 是一个字典,各值均为 str
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(
            request,
            "polls/detail.html",
            {
                "question": question,
                "error_message": "You didn't select a choice.",
            },
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))

polls/templates/polls/index.html

{% raw %}
<!-- https://docs.djangoproject.com/en/5.0/intro/tutorial06/#id2 -->
{% load static %}
<link rel="stylesheet" href="{% static 'polls/style.css' %}">

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
{% endraw %}

在真实的项目中 HTML 应当完整,包括 <head /> 之类

模板中的 url 应当使用 {% raw %} {% url %} {% endraw %}, 该语句的第一个参数是 polls/url.py 中定义的各个 path 中的 name 参数,第二个参数是路由中的变量。

polls/templates/polls/detail.html

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %} <!-- against Cross Site Request Forgeries -->
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

polls/templates/polls/results.html

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

polls/static/polls/style.css

不是由 Django 生成的网页文件不能使用 {% raw %}{% static %}{% endraw %}

/* https://docs.djangoproject.com/en/5.0/intro/tutorial06/#id1 */
li a {
    color: green;
}
/* https://docs.djangoproject.com/en/5.0/intro/tutorial06/#id3 */
body {
    background: white url("images/background.png") no-repeat;
}

后端:数据库

https://docs.djangoproject.com/en/5.0/intro/tutorial02/

mysite/settings.py

文档写的不清楚,据说 polls/app.py 中应该定义一个名为 PollsConfig 的类,但是文档中完全没有这个内容。

在命令行中运行 python manage.py makemigrations polls 可以更新对数据库的修改

在命令行中运行 python manage.py sqlmigrate polls 0001 可以看到 python 语句对应的 SQL 命令

在命令行运行 python manage.py migrate,会把 INSTALLED_APPS 中的数据建成数据表。

TIME_ZONE = 'America/Chicago'

INSTALLED_APPS = [
    "polls.apps.PollsConfig",      # 开发者自己创建的数据 table
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

# SQLite
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": "mydatabase",
    }
}
# Postgresql
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydatabase",
        "USER": "mydatabaseuser",
        "PASSWORD": "mypassword",
        "HOST": "127.0.0.1",
        "PORT": "5432",
    }
}

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "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",
            ],
        },
    },
]

polls/models.py

建立数据 model,用的是 python 的类

from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)  # 每个类变量都是一个 field
    pub_date = models.DateTimeField("date published") # 可以赋予人类可读的名称
    def __str__(self):
        return self.question_text
    @admin.display(
        boolean=True,
        ordering="pub_date",
        description="Published recently?",
    ) # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id8
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE) # 外键,每个 Choice 对应一个 Question
    choice_text = models.CharField(max_length=200)    # 有些 field 有必填参数
    votes = models.IntegerField(default=0)
    def __str__(self):
        return self.choice_text

命令行中的 Django 数据库 API

在命令行中运行 python manage.py shell

# =========================================================

>>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=datetime.timezone.utc)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

# =========================================================

# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith="What")
<QuerySet [<Question: What's up?>]>

# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>

# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. **Django creates
# a set to hold the "other side" of a ForeignKey relation**
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)

# Display any choices from the related object set -- none so far.
>>> q.**choice_set**.all()
<QuerySet []>

# Create three choices.
>>> q.choice_set.create(choice_text="Not much", votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text="The sky", votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text="Just hacking again", votes=0)

# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith="Just hacking")
>>> c.delete()
# =========================================================

polls/admin.py

admin.site.register 的第二个参数是 ModelAdmin,用来指定第一个参数显示哪些列

admin.TabularInline 将本数据插到外键的下面,以表格的形式。

QuestionAdmin.list_disply 选择按行展示时显示的属性

from django.contrib import admin
from .models import Question
from .models import Choice # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id3

# https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id4:
class ChoiceInline(admin.TabularInline): # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id5
    model = Choice
    extra = 3

# https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id1
class QuestionAdmin(admin.ModelAdmin):
    # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id2
    fieldsets = [
        (None, {"fields": ["question_text"]}),      
        ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
    ]
    inlines = [ChoiceInline]
    list_display = ["question_text", "pub_date", "was_published_recently"] # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id6
# admin.site.register(Choice)

admin.site.register(Question,QuestionAdmin)

在命令行运行 python manage.py createsuperuser,根据提示输入信息,建立管理员账户

在命令行运行 python manage.py runserver,启动开发服务器

在浏览器输入 http://127.0.0.1:8000/admin/,可以看到管理员登陆界面


测试

polls/tests.py

测试用例继承自 django.test.TestCase

测试方法的名字开头必须是 test_

TestCase 自带 client,所以不需要“前端测试”一节中的 from django.test import Client

import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question

class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)
    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)
    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)

# Test View
# https://docs.djangoproject.com/en/5.0/intro/tutorial05/#id8
def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        If no questions exist, an appropriate message is displayed.
        """
        response = self.client.get(reverse("polls:index"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_past_question(self):
        """
        Questions with a pub_date in the past are displayed on the
        index page.
        """
        question = create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question],
        )

    def test_future_question(self):
        """
        Questions with a pub_date in the future aren't displayed on
        the index page.
        """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse("polls:index"))
        self.assertContains(response, "No polls are available.")
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_future_question_and_past_question(self):
        """
        Even if both past and future questions exist, only past questions
        are displayed.
        """
        question = create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question],
        )

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        question1 = create_question(question_text="Past question 1.", days=-30)
        question2 = create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question2, question1],
        )
# https://docs.djangoproject.com/en/5.0/intro/tutorial05/#id10
class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text="Future question.", days=5)
        url = reverse("polls:detail", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text="Past Question.", days=-5)
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

运行测试

$ python manage.py test polls

后端:用命令行发现 bug

$ python manage.py shell
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True

前端

在命令行中进行.

$ python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

setup_test_environment() 并不会建立一个测试数据库,前端测试使用的是后端已经建好的数据库。

>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
>>> # get a response from '/'
>>> response = client.get("/")
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse("polls:index"))
>>> response.status_code
200
>>> response.content
b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
>>> response.context["latest_question_list"]
<QuerySet [<Question: What's up?>]>

第三方工具

先安装 debug tools

python -m pip install django-debug-toolbar

剩下的工作看各个第三方工具自己的文档。

第三方包的集合站:https://djangopackages.org/

对 app 进行打包(选做)

而不是对整个 project 进行打包

django-polls/README.rst

=====
Polls
=====

Polls is a Django app to conduct web-based polls. For each question,
visitors can choose between a fixed number of answers.

Detailed documentation is in the "docs" directory.

Quick start
-----------

1. Add "polls" to your INSTALLED_APPS setting like this::

    INSTALLED_APPS = [
        ...,
        "polls",
    ]

2. Include the polls URLconf in your project urls.py like this::

    path("polls/", include("polls.urls")),

3. Run ``python manage.py migrate`` to create the polls models.

4. Start the development server and visit http://127.0.0.1:8000/admin/
   to create a poll (you'll need the Admin app enabled).

5. Visit http://127.0.0.1:8000/polls/ to participate in the poll.

django-polls/pyproject.toml

[build-system]
requires = ['setuptools>=40.8.0']
build-backend = 'setuptools.build_meta'

django-polls/setup.cfg

[metadata]
name = django-polls
version = 0.1
description = A Django app to conduct web-based polls.
long_description = file: README.rst
url = https://www.example.com/
author = Your Name
author_email = y[email protected]
license = BSD-3-Clause  # Example license
classifiers =
    Environment :: Web Environment
    Framework :: Django
    Framework :: Django :: X.Y  # Replace "X.Y" as appropriate
    Intended Audience :: Developers
    License :: OSI Approved :: BSD License
    Operating System :: OS Independent
    Programming Language :: Python
    Programming Language :: Python :: 3
    Programming Language :: Python :: 3 :: Only
    Programming Language :: Python :: 3.10
    Programming Language :: Python :: 3.11
    Programming Language :: Python :: 3.12
    Topic :: Internet :: WWW/HTTP
    Topic :: Internet :: WWW/HTTP :: Dynamic Content

[options]
include_package_data = true
packages = find:
python_requires = >=3.10
install_requires =
    Django >= X.Y  # Replace "X.Y" as appropriate

django-polls/setup.py

from setuptools import setup
setup()

django-polls/MANIFEST.in

include LICENSE
include README.rst
recursive-include polls/static *
recursive-include polls/templates *

命令行操作

打包:在 django-polls/ 执行:

python setup.py sdist

使用打包好的 app:

python -m pip install --user django-polls/dist/django-polls-0.1.tar.gz

卸载:

python -m pip uninstall django-polls

部署项目

两种 interface: WSGI 和 ASGI,后者支持异步请求,似乎更加先进,先只学这一个吧……

mysite/asgi.py 里面有个名为 application 的 callable, ASGI 服务器需要调用这个callable,要使用中间件,需要在这个文件 import,然后 application = Middleware(application)

环境变量 DJANGO_SETTINGS_MODULE 设定设置模块的路径

三种 ASGI 的服务器可供选择:Daphne, Hypercorn, Uvicorn, (Gunicorn )

ASGI Server安装命令运行命令
Daphnepython -m pip install daphnedaphne myproject.asgi:application
Hypercornpython -m pip install hypercornhypercorn myproject.asgi:application
Uvicornpython -m pip install uvicornpython -m uvicorn myproject.asgi:application
Gunicornpython -m pip install uvicorn gunicornpython -m gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker

本文收录于以下合集: