使用反应逐渐增强Django应用程序
#react #python #django

介绍

在本教程中,您将使用django创建一个英雄构建器应用程序并进行反应。英雄构建器将允许用户构建角色并在诸如强度,敏捷和智能之类的属性上分发点。

Django是用Python编写的高级开源网络框架,可鼓励快速开发和清洁,务实的设计。它于2005年发行,遵循“电池”的理念,为开发人员提供了一套丰富的工具和公用事业,包括一个ORM(对象相关的映射),管理员界面和表单处理,以命名很少。 Django强调了组件的干燥(不要重复自己)原理和可重复使用性,从而使开发人员能够使用更少的代码构建可靠,可扩展和可维护的Web应用程序。

React通常称为React.js,是Facebook开发的开源JavaScript库,用于构建用户界面或UI组件。它在2013年推出,允许开发人员使用基于组件的体系结构构建复杂的交互式UI。 React中的每个组件都会管理其自己的状态并将其渲染到DOM,从而更容易开发应用程序的模块化件。多年来,React已成为现代网络开发的基础工具,为当今许多最受欢迎的网站和应用程序提供了动力。

在此应用程序中,Django框架将服务于前端,而React将接管需要动态交互的页面的特定部分。

先决条件

要构建此应用程序,您需要完成以下内容:

  • 安装node.js和npm
  • 安装python3

设置Django项目

在本节中,您将设置Django项目并验证所有先决条件是否就位。

首先使用 django-admin 工具来创建一个新的django项目。

django-admin startproject hero_builder

现在在 hero_builder 目录中创建一个新应用程序。

cd hero_builder
python3 manage.py startapp hero

运行SQLite开发数据库的初始迁移。

python3 manage.py migrate

启动开发服务器,以验证到目前为止的所有功能。

python3 manage.py runserver

您现在应该能够访问 http://localhost:8000 。 Django将为占位符页面提供。

注册英雄申请

您现在应该注册以较早的步骤创建的英雄应用程序。打开 hero_builder/settings.py 在编辑器中的文件,然后添加英雄 app installed_apps list。

INSTALLED_APPS = [ 
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "hero",
]

将更改保存到 settings.py 文件。

定义字符模型

英雄应用程序将具有一个字符模型,可以存储跨属性的点的分布。 Open HERIO/MODEL.PY 并添加以下内容。

from django.db import models

class Character(models.Model):
    strength = models.PositiveIntegerField(default=20)
    dexterity = models.PositiveIntegerField(default=20)
    health = models.PositiveIntegerField(default=20)
    intelligence = models.PositiveIntegerField(default=20)
    charisma = models.PositiveIntegerField(default=20)

    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name

现在您将添加并运行创建字符模型所需的迁移。

python manage.py makemigrations hero

运行迁移:

python manage.py migrate hero

设置django表格和视图

英雄应用程序视图将使用简单的Django表单类来验证字符数据。打开英雄/forms.py 并添加以下内容。

from django import forms

class CharacterForm(forms.Form):
    strength = forms.IntegerField(min_value=0, initial=20)
    dexterity = forms.IntegerField(min_value=0, initial=20)
    health = forms.IntegerField(min_value=0, initial=20)
    intelligence = forms.IntegerField(min_value=0, initial=20)
    charisma = forms.IntegerField(min_value=0, initial=20)

    def clean(self):
        cleaned_data = super().clean()
        total = (
            cleaned_data.get("strength", 0)
            + cleaned_data.get("dexterity", 0)
            + cleaned_data.get("health", 0)
            + cleaned_data.get("intelligence", 0)
            + cleaned_data.get("charisma", 0)
        )

        if total > 100:
            raise forms.ValidationError("The total attributes must not exceed 100.")
        return cleaned_data

表单类将验证每个属性是一个正数,总数不超过100点。

接下来,您将使用此表单类作为字符视图的一部分,浏览器将在其中提出表单发布请求以创建字符。打开英雄/views.py 文件并添加以下内容。

from django.shortcuts import render, redirect, get_object_or_404
from .models import Character
from .forms import CharacterForm

def home(request):
    form = CharacterForm()
    return render(request, "hero/home.html", {"form": form})

def character(request, character_id=None):
    if request.method == "POST":
        form = CharacterForm(request.POST)
        if form.is_valid():
            character = Character(
                strength=form.cleaned_data["strength"],
                dexterity=form.cleaned_data["dexterity"],
                health=form.cleaned_data["health"],
                intelligence=form.cleaned_data["intelligence"],
                charisma=form.cleaned_data["charisma"],
            )
            character.save()
            return redirect("character_with_id", character_id=character.pk)
    else:
        character = get_object_or_404(Character, id=character_id)

    return render(request, "hero/character.html", {"character": character})

主页视图呈现角色创建页面。字符页将在get请求期间在只读表中渲染创建字符的属性,并在邮政请求期间创建一个新字符。

定义Django模板

现在,您将定义英雄应用中使用的Django模板。

base.html 模板将充当基础,以便所有常见的脚本和样式导入发生在一个地方。

开放英雄/模板/英雄/base.html 并添加以下内容。

<!DOCTYPE html>
<html lang="en">
{% load static %}
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hero Builder</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

    {% block extra_js %}{% endblock extra_js %}
    {% block extra_css %}{% endblock extra_css %}
</head>
<body>
    {% if messages %}
        {% for message in messages %}
            <div class="alert alert-danger alert-bottom alert-dismissible fade show" role="alert">
                {{ message }}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>
        {% endfor %}
    {% endif %}
    <nav class="navbar navbar-expand-lg">
        <div class="container-fluid">
            Hero Builder
        </div>
    </nav>

    <div>
        {% block content %}{% endblock content %}
    </div>
</body>
</html>

此模板加载了React和Reactdom库的缩小生产源代码。稍后,当您将要在接下来的几个步骤中构建的React Web组件时,这将很重要。

现在您将编写 home.html 模板,其中包含代表英雄编辑器的反应组件。

{% extends "hero/base.html" %}
{% load static %}

{% block extra_js %}
<script src="{% static 'hero/js/main.js' %}"></script>
{% endblock %}

{% block extra_css %}
<link href="{% static 'hero/css/main.css' %}" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="w-75 p-4 mb-4 border">
        <hero-attribute-editor></hero-attribute-editor>
        <form method="POST" action="{% url 'character' %}">
            {% csrf_token %}

            {{ form.strength.as_hidden }}
            {{ form.dexterity.as_hidden }}
            {{ form.health.as_hidden }}
            {{ form.intelligence.as_hidden }}
            {{ form.charisma.as_hidden }}

            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</div>
{% endblock %}

如果您现在要查看浏览器中呈现的此页面,则 hero-attribute-editor 标签将无法使用。您需要在接下来的几个步骤中使用React创建此Web组件。 main.js main.css 文件将包含英雄编辑器标签的React构建输出。

最后,您将创建 cartem.html 模板。此模板只允许用户查看他们创建的英雄的属性。 OPEN 英雄/模板/英雄/角色。

{% extends "hero/base.html" %}
{% load static %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="w-75 p-4 mb-4 border">
        <table class="table">
            <thead>
                <tr>
                    <th>Attribute</th>
                    <th>Value</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Strength</td>
                    <td>{{ character.strength }}</td>
                </tr>
                <tr>
                    <td>Dexterity</td>
                    <td>{{ character.dexterity }}</td>
                </tr>
                <tr>
                    <td>Health</td>
                    <td>{{ character.health }}</td>
                </tr>
                <tr>
                    <td>Intelligence</td>
                    <td>{{ character.intelligence }}</td>
                </tr>
                <tr>
                    <td>Charisma</td>
                    <td>{{ character.charisma }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
{% endblock %}

设置React项目

在本节中,您将设置React项目并创建一个用于Django模板中的Web组件。

首先创建您将用于包含项目的目录。

mkdir hero-attribute-editor

现在创建一个 package.json 列出项目依赖项并构建命令。

{
  "name": "hero-attribute-editor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@r2wc/react-to-web-component": "^2.0.2",
    "rc-slider": "^10.2.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.22.10",
    "@babel/preset-env": "^7.22.10",
    "@babel/preset-react": "^7.22.5",
    "babel-loader": "^9.1.3",
    "css-loader": "^6.8.1",
    "style-loader": "^3.3.3",
    "webpack-cli": "^5.1.4"
  }
}

这里最重要的依赖性是 r2wc/react to-web-component 库。该库充当反应组件与Web Component API之间的桥梁。例如,在Web组件介质中工作时,React Prop的概念需要特殊处理。

接下来创建 index.js package.json 文件中定义的入口点。 index.js 文件应包含以下内容。

import React from 'react';

import r2wc from '@r2wc/react-to-web-component';
import AttributeEditor from './components/AttributeEditor';

const wcAttributeEditor = r2wc(AttributeEditor, { props: {} });

customElements.define("hero-attribute-editor", wcAttributeEditor);

r2wc 库将 attributeEditor 组件包装并创建Web组件兼容对象。然后,您使用自定义功能向浏览器注册该Web组件。 customElements函数是Web组件API的一部分。

下一个文件使用React实现了 AttributeEditor 组件。该组件呈现一系列代表每个字符属性的滑块。每个属性最多可以分配给它100点,但是只有100点可用。当使用所有点时,分配给属性的任何其他点将自动从其他属性中扣除。

该组件将在每个属性更改上发布自定义事件。稍后,我们将在Django模板中看到这些事件是如何截获的。

将以下内容放在组件/attributeeditor.jsx 文件中。

import React, { useState } from "react";

import Slider from "rc-slider";
import "rc-slider/assets/index.css";

import "./AttributeEditor.css";

const TOTAL_POINTS = 100;

function AttributeEditor() {
  const [attributes, setAttributes] = useState({
    Strength: 20,
    Dexterity: 20,
    Health: 20,
    Intelligence: 20,
    Charisma: 20,
  });

  const remainingPoints =
    TOTAL_POINTS - Object.values(attributes).reduce((a, b) => a + b, 0);

  const handleSliderChange = (attribute, value) => {
    setAttributes((prev) => {
      const newAttributes = { ...prev, [attribute]: value };
      let totalUsed = Object.values(newAttributes).reduce((a, b) => a + b, 0);

      if (totalUsed > TOTAL_POINTS) {
        let excess = totalUsed - TOTAL_POINTS;
        let attributesToAdjust = Object.keys(newAttributes).filter(
          (key) => key !== attribute
        );

        while (excess > 0) {
          for (let key of attributesToAdjust) {
            if (newAttributes[key] > 0 && excess > 0) {
              newAttributes[key] -= 1;
              excess -= 1;
            }
          }
        }
      }

      window.dispatchEvent(
        new CustomEvent("heroAttributesChange", {
          detail: newAttributes,
          bubbles: true,
          composed: true,
        })
      );

      return newAttributes;
    });
  };

  return (
    <div>
      <div className="total">Total Points Left: {remainingPoints}</div>
      {Object.entries(attributes).map(([attribute, value]) => (
        <div className="attribute" key={attribute}>
          <label htmlFor={attribute}>{attribute}</label>
          <Slider
            value={value}
            min={0}
            max={100}
            onChange={(val) => handleSliderChange(attribute, val)}
          />
        </div>
      ))}
    </div>
  );
}

export default AttributeEditor;

attributeEditor 包含一些对CSS类的引用。在组件/attributeeditor.css 上创建一个新文件,然后插入以下内容。

.attribute {
  margin: 65px 0;
}

.attribute label {
  display: block;
  margin-bottom: 10px;
}

.total {
  font-weight: bold;
  text-align: center;
  margin-bottom: 30px;
}

babel需要一个名为 .babelrc 的配置文件,其中您将定义预设反应是已加载的预设之一。 React预设加载了能够将JSX转换为JavaScript的最终构建输出的插件集合。

{
  "presets": [
    "@babel/preset-env", 
    ["@babel/preset-react", {"runtime": "automatic"}]
  ]
}

最后,您将创建一个 webpack.config.js 文件来指示WebPack如何构建项目。

配置将 index.js 定义为构建入门点,并设置了WebPack来编译其找到的任何JSX或CSS文件。配置还从编译的输出中剥离了React运行时,因为Django模板将使用脚本标签从CDN下载React Runtime。

const path = require('path');

module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

您现在应该能够构建React项目。您可以使用 npm 命令。

npm run build

这应该导致 dist 目录,其中包含 index.js 构建文件。整个React组件及其CSS被编译到此文件中。

现在,您应该将此输出文件复制到适当的Django静态目录中。

cp dist/index.js ../hero_builder/hero/static/hero/js/main.js

启动Django dev服务器,如果它尚未运行,并再次访问 http://localhost:8000 再次查看应用程序。

在Django模板中拦截自定义事件

难题的最后一部分是从 Hero-attribute-editor 中收集数据,并将其发布到Django。

django对Web组件一无所知,因此您需要构造隐藏表单以发布数据。每当收到自定义事件时

打开 hero_builder/hero/hero/segplates/hero/home.html 模板并使用以下内容进行更新。

{% extends "hero/base.html" %}
{% load static %}

{% block extra_js %}
<script src="{% static 'hero/js/main.js' %}"></script>
{% endblock %}

{% block extra_css %}
<link href="{% static 'hero/css/main.css' %}" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="w-75 p-4 mb-4 border">
        <hero-attribute-editor></hero-attribute-editor>
        <form method="POST" action="{% url 'character' %}">
            {% csrf_token %}

            {{ form.strength.as_hidden }}
            {{ form.dexterity.as_hidden }}
            {{ form.health.as_hidden }}
            {{ form.intelligence.as_hidden }}
            {{ form.charisma.as_hidden }}

            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</div>
<script>
const attributes = [
    "Strength",
    "Dexterity",
    "Health",
    "Intelligence",
    "Charisma"
];

window.addEventListener("heroAttributesChange", (e) => {
    for (const attribute of attributes) {
        const lowercasedAttribute = attribute.toLowerCase();
        const value = e.detail[attribute];

        const inputElement = document.getElementById(`id_${lowercasedAttribute}`);
        if (inputElement) {
            inputElement.value = value;
        }
    }
});
</script>
{% endblock %}

模板中的最终脚本标签将从Web组件中收听 hereatributeschange 事件,并更新隐藏的表单。在表单提交中,Django将处理表格,因为它将以任何其他形式。

GitHub存储库

您可以找到Django application on GitHub here的代码。英雄属性编辑器组件can also be found on GitHub的代码。

结论

您现在应该有一个工作示例,即在Django应用程序中包括基于React的Web组件。这种方法仍然使用Django的模板渲染,但可以在需求需要更多互动性的情况下进行逐步增强。