跳转到主要内容

Django + React难题的缺失部分

项目描述

django-cra-helper

PyPI version

简介

django-cra-helper 是连接 Djangocreate-react-app 的缺失环节。通过将其添加到您的Django项目中,您可以几乎无缝地将React组件注入Django模板,并通过Django上下文变量初始化组件属性。

该软件包的最终目标是尽可能减少对通常在开发过程中使用的Django或create-react-app的工作流程的更改,以集成这两个项目。从 npm startpython manage.py collectstatic,您的命令应按预期工作,这样您就可以忘记实现细节并回到开发中!

注意:为了本README的目的,缩写 CRA 将用于指代 create-react-app

安装

此软件包可通过 pip 安装

pip install django-cra-helper

配置

1. settings.py

一旦安装了 django-cra-helper,则需要将 cra_helper 添加到 settings.py 中的 INSTALLED_APPS

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'cra_helper',
    'django.contrib.staticfiles',
]

注意: cra_helper 必须 在列表中位于 django.contrib.staticfiles 之上!

cra_helper.context_processors.static 添加到 TEMPLATES['OPTIONS']['context_processors']

TEMPLATES = [
    {
        # ...snip...
        'OPTIONS': {
            'context_processors': [
                # ...snip...
                'cra_helper.context_processors.static',
            ],
        },
    },
]

此外,必须将以下 STATICFILES_FINDERS 列表添加到 settings.py

STATICFILES_FINDERS = [
    # Required for CRAManifestFinder below to work
    'django.contrib.staticfiles.finders.FileSystemFinder',
    # A finder to pull in asset-manifest.json
    'cra_helper.finders.CRAManifestFinder',
]

最后必要的设置是包含CRA项目文件的文件夹名称,相对于Django 项目(包含manage.py的文件夹)的基准目录。

CRA_APP_NAME = 'cra-app'

如果由于某种原因,CRA liveserver不在localhost或端口3000上提供服务,可以将以下设置添加到settings.py中,以指定其实际的主机和端口

CRA_HOST = '0.0.0.0' # defaults to 'localhost'
CRA_PORT = 9999      # defaults to 3000

2. urls.py

可以通过首先将以下内容添加到您的项目或应用的urls.py文件中启用热重载支持

# import Django settings
from django.conf import settings
# add `re_path` import here
from django.urls import path, re_path
# ...other imports...

from cra_helper.views import proxy_cra_requests

# other existing urls
urlpatterns = [...]

# add a reverse-proxy view to help React in the Django view talk to Create-React-App
if settings.DEBUG:
    proxy_urls = [
        re_path(r'^__webpack_dev_server__/(?P<path>.*)$', proxy_cra_requests),
        re_path(r'^(?P<path>.+\.hot-update\.(js|json|js\.map))$', proxy_cra_requests),
    ]
    urlpatterns.extend(proxy_urls)

接下来,根据您的项目版本的react-scripts,遵循以下说明

对于使用react-scripts@<3.3.0的项目

将一个额外的url添加到上面的proxy_urls

proxy_urls = [
    # ...snip...
    re_path(r'^sockjs-node/(?P<path>.*)$', proxy_cra_requests),
]
对于使用react-scripts@>=3.3.0的项目

在Create-React-App文件夹的根目录下创建一个包含以下环境变量的.env文件

WDS_SOCKET_PORT=3000

3. asset-manifest.json

最后,运行CRA的npm run build命令一次以生成一个build/目录。django-cra-helper需要其中的build/asset-manifest.json文件来帮助加载可能用于任何React组件的非JS和非CSS资产。每次向项目中添加新的非JS或非CSS资产时,应重新运行此命令。

开发

如果CRA项目的liveserver通过npm start启动,然后再通过python manage.py runserver启动Django的开发服务器,React代码库中的代码更改将立即更新到Django视图中。

当CRA liveserver运行时,django-cra-helper添加一个bundle_js数组模板变量,该变量可以插入到Django视图的模板中,以加载包含所有当前JS和CSS的liveserver的各个文件。这些文件在编辑React代码时由liveserver即时重新编译。此文件可以按以下方式添加到Django模板中

{% if bundle_js %}
  {% for file_url in bundle_js %}
<script type="text/javascript" src="{{ file_url }}"></script>
  {% endfor %}
{% endif %}

注意:这里不要使用static模板标签!此文件需要从CRA liveserver加载。

生产

django-cra-helper还负责确保Django的collectstatic命令包含由CRA的npm run build命令构建的生产就绪的包。

首先,使用典型的CRA npm构建命令为React文件准备生产环境

npm run build

这将在CRA项目文件夹中的/build/文件夹输出捆绑、最小化的JavaScript和CSS以及资产。

此命令完成后,运行以下Django命令以收集静态文件,包括编译后的React资产

python manage.py collectstatic --no-input

React资产将与其他静态资产一起包含在settings.STATIC_ROOT目录中,在Django生产环境中按常规提供服务。还会拉入一个asset-manifest.json文件。此CRA生成的文件的内容由django-cra-helper需要,以帮助引用在构建过程中为其文件名添加了唯一哈希的React文件。

类似于前面提到的bundle_js模板变量,当CRA liveserver没有运行时,django-cra-helper还包括许多其他模板变量

对于使用react-scripts@>=3.2.0的项目

react-scripts@3.2.0开始,可以在asset-manifest.json中找到新的entrypoints属性。它包含一个文件数组,这些文件由django-cra-helper在模板中提供,以便更容易通过新的entrypoints.cssentrypoints.js数组注入这些文件

{% for file in entrypoints.css %}
<link href="{% static file %}" rel="stylesheet">
{% endfor %}
{% for file in entrypoints.js %}
<script type="text/javascript" src="{% static file %}"></script>
{% endfor %}

注意:这些JavaScript和CSS文件应该按网站加载所需的顺序排列;最终顺序是从asset-manifest.json中存在的顺序导出的。

对于使用react-scripts@>=3.0.0 to react-scripts@<3.2.0的项目

在较晚版本的react-scripts中引入了代码拆分,将main_js拆分为多个文件。需要添加额外的<script>标签以启用React项目加载。根据项目的大小,除了上面提到的main_js之外,您还需要按特定顺序添加至少另外两个标签

{% if main_js %}
<script type="text/javascript" src="{% static runtime_main_js %}"></script>
<script type="text/javascript" src="{% static static_js_2_9a95e042_chunk_js %}"></script>
<script type="text/javascript" src="{% static main_js %}"></script>
{% endif %}

上述 static_js_2_9a95e042_chunk_js 的命名会因项目而异。遗憾的是,您需要在项目的 asset-manifest.json 中手动确认此值并进行相应的更新。不过,这似乎不会在不同构建之间改变,因此可能不是您需要定期更新的值...

对于使用 react-scripts@<=2.1.8 的旧项目

最重要的两个变量是 main_jsmain_css。这些可以通过模板中的典型调用 {% static %} 注入到页面中

{% if main_css %}
<link href="{% static main_css %}" rel="stylesheet">
{% endif %}
{% if main_js %}
<script type="text/javascript" src="{% static main_js %}"></script>
{% endif %}

注意:尝试使用 react-scripts@2.1.8 构建一个全新的 CRA 项目,但未能重建只允许一个 main_js 的 SPA。使用 npm run build 生成的工件与下面详细说明的 react-scripts@3.1.2 生成的工件几乎相同。

可能存在 react-scripts 的子依赖项,使得无法启动按照上述说明工作的应用程序。在这种情况下,请尝试下一节中的说明。

支持CRA的相对路径

CRA 允许开发者通过 package.json 中的 "homepage" 属性指定一个相对子文件夹,以便从该文件夹托管其站点

{
  "name": "cra-app",
  "version": "0.1.0",
  "homepage": "/frontend",
  ...
}

设置此值后,npm run build 将输出带有路径前缀的资源和 asset-manifest.json

Before: /static/js/main.319f1c51.chunk.js
After:  /frontend/static/js/main.319f1c51.chunk.js

为了确保 React 导入/资产等...可以在通过 Django 托管时找到,还需要更新 Django 的 settings.py 中的 STATIC_URL 以包含路径前缀

STATIC_URL = '/frontend/static/'
CRA_PACKAGE_JSON_HOMEPAGE = '/frontend'

上面设置的 CRA_PACKAGE_JSON_HOMEPAGE 值应与 package.json 中的 "homepage" 值匹配,以便 django-cra-helper 可以找到 CRA liveserver 并相应地重定向

一旦做出这些更改,React 应用程序应该能够找到它需要的所有内容。

React在Django模板中

通过模板上下文指定React组件

CRA 项目需要进行一点小的重构,以便在 Django 服务器视图时能够接受通过上下文传递的输入值。以下是一个示例,说明如何通过修改 CRA 项目的 src/index.js 文件,在 src/index.js 文件中进行一些小的调整,从而为 Django 与打包的 React 代码库之间的通信建立一个简单的 API

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

/**
 * Maintain a simple map of React components to make it easier
 * for Django to reference individual components
 */
const pages = {
  App,
}

/**
 * If Django hasn't injected these properties into the HTML
 * template that's loading this script then we're viewing it
 * via the create-react-app liveserver
 */
window.component = window.component || 'App';
window.props = window.props || { env: 'Create-React-App' };
window.reactRoot = window.reactRoot || document.getElementById('root');

/**
 * React the component as usual
 */
ReactDOM.render(
  React.createElement(pages[window.component], window.props),
  window.reactRoot,
);

基本上,index.js 被更新为读取 window.componentwindow.propswindow.reactRoot 中设置的值,并使用这些值来渲染组件。这三个“输入”中的每一个都将允许 Django 容易地指定每个视图初始化哪个组件

  • window.component:指向 pages 中的组件条目的 字符串
  • window.props:包含要传递给组件的属性的 对象
  • window.reactRoot:一个 document.getElementById实例

注意:设置这些值是可选的。模板中指定的默认值使得组件在从 CRA liveserver 观看时能够按预期渲染。

“API”就绪后,Django 视图可以通过传递给模板的上下文包括这些输入值

def index(request):
    context = {
        'component': 'App',
        'props': {
            'env': 'Django',
        },
    }
    return render(request, 'index.html', context)

以下是 Django 应用程序视图的 index.html 模板,它可以渲染多个版本的 react-scripts(仅用于与新的 CRA 应用程序进行演示目的)

{% load static %}
<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    {% if entrypoints %}
      {% for file in entrypoints.css %}
    <link href="{% static file %}" rel="stylesheet">
      {% endfor %}
    {% elif main_css %}
    <link href="{% static main_css %}" rel="stylesheet">
    {% endif %}
    <title>Django + React Project</title>
  </head>

  <body>
    <div id="react">Loading...</div>

    {{ props | json_script:"react-props" }}

    <script>
      window.component = '{{ component }}';
      window.props = JSON.parse(
        document.getElementById('react-props').textContent
      );
      window.reactRoot = document.getElementById('react');
    </script>
    {% if bundle_js %}
      {% for file in bundle_js %}
    <script type="text/javascript" src="{{ file }}"></script>
      {% endfor %}
    {% elif entrypoints %}
      {% for file in entrypoints.js %}
    <script type="text/javascript" src="{% static file %}"></script>
      {% endfor %}
    {% elif main_js %}
    <script type="text/javascript" src="{% static runtime_main_js %}"></script>
    {# make sure to update this accordingly according to asset-manifest.json #}
    <script type="text/javascript" src="{% static static_js_2_597565cd_chunk_js %}"></script>
    <script type="text/javascript" src="{% static main_js %}"></script>
    {% endif %}
  </body>

</html>

上下文的 componentprops 分别绑定到 window.componentwindow.props

注意设置 windows.props 时使用 json_script 过滤器。Django 提供了这个过滤器,作为一种将 Python dict 转换为 Javascript Object 的简单方法。注入的 <script> 标签的内容可以通过 JSON.parse() 运行,以安全地将其分配给变量。

window.reactRoot 指定了 React 组件应渲染到的容器元素。只有在容器的 id 不是 "root"(与 CRA 项目的 index.html 中分配给容器的 <div> 的 ID 相同)时,才需要设置此值。

合并Django和React路由

在某些场景中,可能会希望利用React客户端路由和典型的Django路由。幸运的是,定义一个额外的“通配符”Django路由来优雅地处理应由React应用程序的路由处理的URL非常简单。

在一个典型的客户端路由设置中,React应用程序可能定义了以下路由:

import { BrowserRouter as Router, Switch, Route, NavLink, Redirect } from 'react-router-dom';

const App = () => (
  <Router>
    <ul>
      <li><NavLink to='/'>To Home</NavLink></li>
      <li><NavLink to='/foo'>To Foo</NavLink></li>
      <li><NavLink to='/bar'>To Bar</NavLink></li>
    </ul>
    <Switch>
      <Route path='/foo'>foo</Route>
      <Route path='/bar'>bar</Route>
      <Route exact path='/'>home</Route>
      <Route path='*'>
        <Redirect to='/' />
      </Route>
    </Switch>
  </Router>
);

在Django方面,可以在“frontend”应用程序的frontend/urls.py中定义以下路由,这些路由都指向一个渲染frontend/templates/index.htmlindex视图

from django.urls import path, re_path, include

from .views import index

urlpatterns = [
    # Default index template to render index.html
    path('', index),
    # Helps Django pass unknown routes to the client-side router
    re_path(r'^.*/$', index)
]

然后,可以将这些URL包含到项目的根urls.py

from django.urls import path, include

urlpatterns = [
    # ...other routes...
    path('', include('frontend.urls')),
]

注意:如果您的路由不起作用,请确保已将'frontend'添加到settings.py中的INSTALLED_APPS

有了这两个路径,当Django尝试处理只定义为客户端路由的路径时,现在将加载React应用程序。

引用React静态文件

CRA捆绑的其他资产,包括图像资产,可以通过将文件路径中的/.~-替换为_来在模板中访问。 django-cra-helperasset-manifest.json中的每个条目添加到基本上下文,使用这些替换规则来适应Django的static标签。

例如,CRA项目中的一个logo.svg文件可以按如下方式包含在Django模板中:

<!-- This file is located at `/build/static/media/logo.svg` -->
<img src="{% static static_media_logo_svg %}" height="40" width="40">

注意:这是可选的!静态资产仍然可以包含在Django应用程序的/static/目录中,并按常规加载。上述特殊替换仅在重用React资产而不仅限于特定组件时需要。

收益揭晓

到此为止,React组件现在应该在CRA liveserver和通过Django提供时都渲染并可查看。以下是一个略微修改的CRA App组件在CRA(左)和Django(右)中显示的示例

Comparison Shot

项目详情


下载文件

下载适用于您的平台文件。如果您不确定要选择哪一个,请了解更多关于安装软件包的信息。

源分发

django-cra-helper-2.0.1.tar.gz (24.2 kB 查看散列)

上传时间

构建分发

django_cra_helper-2.0.1-py3-none-any.whl (21.5 kB 查看散列)

上传时间 Python 3

支持者