В предыдущей части мы создали API, который отвечает за взаимодействие проекта Django и приложение реагирования. Третья часть руководства — создание одностраничного приложения с помощью React.
Вот ссылки на две предыдущие части этого урока:
Создать приложение React с нуля
Шаг 1: Настройка среды
(Примечание: если вы уже установили узел, вы можете пропустить эту часть)
Мы будем использовать Node backend для среды разработки. Поэтому нам нужно установить Node и Node менеджер пакетов npm. Чтобы предотвратить возможные проблемы с зависимостями, мне нужно создать чистую среду узлов. Я буду использовать NVM, который является менеджером версий Node, и он позволяет нам создавать изолированные среды Node.
Оболочка
xxxxxxxxxx
1
# install node version manager 
2
wget -qO- <https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh> | bash
3
4
# check installation
5
command -v nvm  #should prints nvm
6
7
# install node
8
nvm install node #"node" is an alias for the latest version
9
10
# use the installed version
11
nvm use node
12
# prints Now using node v13.1.0 (npm v6.12.1)
13
14
# Note:versions can be different
Шаг 2: Создание Frontend Director
Оболочка
xxxxxxxxxx
1
# go django root directory
2
3
# create frontend directory
4
mkdir FRONTEND
5
cd FRONTEND
6
7
# create a node project
8
npm init
9
# you may fill the rest
Вы также можете прочитать:
Как использовать GraphQL в React с использованием хуков
Шаг 3: Установите зависимости
питон
xxxxxxxxxx
1
# djr/FRONTEND
2
3
# add core react library
4
npm install react react-dom
5
6
# add graphql client-side framework of Apollo and parser 
7
npm install apollo-boost @apollo/react-hooks graphql
8
9
# add routing library for single page app
10
npm install react-router-dom  
11
12
# DEVELOPMENT PACKAGES
13
# add babel transpiler
14
npm install -D @babel/core @babel/preset-env @babel/preset-react
15
16
# add webpack bundler
17
npm install -D webpack webpack-cli webpack-dev-server
18
19
# add webpack loaders and plugins
20
npm install -D babel-loader css-loader style-loader html-webpack-plugin mini-css-extract-plugin postcss-loader postcss-preset-env
Шаг 4: Создайте необходимые файлы
Оболочка
xxxxxxxxxx
1
# djr/FRONTEND
2
3
# create source folder
4
mkdir src
5
6
#create webpack config file
7
touch webpack.config.js
8
9
# get into src folder
10
cd src
11
12
# djr/FRONTEND
13
14
# create html file for developing with react
15
touch index.html
16
17
# our react app's root file
18
touch index.js
19
20
# our app file and styling
21
touch App.js
22
touch App.css
23
24
# query file
25
touch query.js
Шаг 4: файл Package.Json
Ваш файл package.json должен выглядеть следующим образом.
JSON
xxxxxxxxxx
1
{
2
  "name": "frontend",
3
  "version": "1.0.0",
4
  "description": "",
5
  "main": "index.js",
6
  "scripts": {
7
    "start": "webpack-dev-server --open --hot --mode development",
8
    "build": "webpack --mode production"
9
  },
10
  "author": "",
11
  "license": "ISC",
12
  "babel": {
13
    "presets": [
14
      "@babel/preset-env",
15
      "@babel/preset-react"
16
    ]
17
  },
18
  "postcss": {
19
    "plugins": {
20
      "postcss-preset-env": {}
21
    }
22
  },
23
  "dependencies": {
24
    "@apollo/react-hooks": "^3.1.3",
25
    "apollo-boost": "^0.4.4",
26
    "graphql": "^14.5.8",
27
    "react": "^16.12.0",
28
    "react-dom": "^16.12.0",
29
    "react-router-dom": "^5.1.2"
30
  },
31
  "devDependencies": {
32
    "@babel/core": "^7.7.2",
33
    "@babel/preset-env": "^7.7.1",
34
    "@babel/preset-react": "^7.7.0",
35
    "babel-loader": "^8.0.6",
36
    "css-loader": "^3.2.0",
37
    "html-webpack-plugin": "^3.2.0",
38
    "mini-css-extract-plugin": "^0.8.0",
39
    "postcss-loader": "^3.0.0",
40
    "postcss-preset-env": "^6.7.0",
41
    "style-loader": "^1.0.0",
42
    "webpack": "^4.41.2",
43
    "webpack-cli": "^3.3.10",
44
    "webpack-dev-server": "^3.9.0"
45
  }
46
}
Шаг 4: Настройка Webpack
Что такое веб-пакет?
Webpack — это модуль-упаковщик и исполнитель задач. Мы свяжем все наши приложения javascript, включая стили CSS, в два файла javascript, если вы предпочитаете, чтобы вы могли выводить только один файл. Благодаря богатым плагинам, вы также можете делать много вещей с веб-пакетом, например, сжатие различными алгоритмами вашего файла, исключать неиспользуемый код CSS, извлекать ваш CSS из разных файлов, загружать ваш пакет в облако и т.д …
Это изображение ниже представляет собой наглядное представление окончательного процесса связывания с веб-пакетом.
Мы решили сделать две разные настройки веб-пакета в одном файле; один для производства и развития. Это минимальная конфигурация веб-пакета, и она не оптимизирована.
JavaScript
xxxxxxxxxx
1
const path = require("path");
2
const HtmlWebPackPlugin = require("html-webpack-plugin");
3
4
// checks if it is production bundling or development bundling 
5
const isEnvProduction = process.argv.includes("production")
6
7
// our root file
8
const entrypoint = './src/index.js'
9
10
const productionSettings = {
11
    mode: "production",
12
    entry: entrypoint,
13
    output: {
14
        // output directory will be the root directory of django
15
        path: path.resolve(__dirname, '../'),
16
        // this is the bundled code we wrote
17
        filename: 'static/js/[name].js',
18
        // this is the bundled library code
19
          chunkFilename: 'static/js/[name].chunk.js'
20
    },
21
    optimization: {
22
        minimize: true,
23
        splitChunks: {
24
          chunks: 'all',
25
          name: true,
26
        },
27
        runtimeChunk: false,
28
      },
29
    devServer: {
30
        historyApiFallback: true,
31
        stats: 'normal',
32
      },
33
    module: {
34
        rules: [
35
            {
36
                // for bundling transpiled javascript
37
                test: /\\.m?js$/,
38
                exclude: /(node_modules|bower_components)/,
39
                use: {
40
                    loader: "babel-loader",
41
                }
42
            },
43
            {
44
                test: /\\.css$/i,
45
                use: [
46
                  // IMPORTANT => don't forget `injectType`  option  
47
                  // in some cases some styles can be missing due to 
48
                  // inline styling. 
49
                  { loader: 'style-loader', options: { injectType: 'styleTag' } },
50
                  "css-loader"
51
                ],
52
            },
53
        ]
54
    },
55
    plugins: [
56
        new HtmlWebPackPlugin({
57
            // this is where webpack read our app for bundling
58
            template: "./src/index.html",
59
            // this is emitted bundle html file
60
            // django will use this as template after bundling
61
      filename:"./templates/index.html"
62
        }),
63
    ]
64
};
65
66
const devSettings = {
67
    mode: "development",
68
    entry: entrypoint,
69
    output: {
70
        path: path.resolve(__dirname, './build'),
71
        publicPath: "/",
72
        filename: 'static/js/bundle.js',
73
        chunkFilename: 'static/js/[name].chunk.js',
74
    },
75
    devtool: 'inline',
76
    devServer: {
77
        historyApiFallback: true,
78
        contentBase: './dist',
79
        stats: 'minimal',
80
      },
81
    module: {
82
        rules: [
83
            {   // using transpiled javascript
84
                test: /\\.m?js$/,
85
                exclude: /(node_modules|bower_components)/,
86
                include: path.resolve(__dirname, 'src'),
87
                use: {
88
                    loader: "babel-loader",
89
                    options: {
90
                        presets: ["@babel/preset-env"],
91
                        plugins: ["@babel/plugin-proposal-object-rest-spread"],
92
                        // for fast development environment
93
                        // enable caching transpilation
94
                        cacheDirectory: true
95
                    },
96
                }
97
            },
98
99
            {
100
                test: /\\.css$/i,
101
                use: [
102
                  // IMPORTANT => don't forget `injectType`  option  
103
                  // in some cases some styles can be missing due to 
104
                  // inline styling. 
105
                  { loader: 'style-loader', options: { injectType: 'styleTag' } },
106
                  "css-loader",
107
                  'postcss-loader'
108
                  //{ loader: 'sass-loader' },
109
                ],
110
            },
111
        ]
112
    },
113
    plugins: [
114
        new HtmlWebPackPlugin({
115
            template: "./src/index.html",
116
        })
117
    ]
118
};
119
120
121
122
module.exports = isEnvProduction ? productionSettings : devSettings;
Шаг 5: Создайте индексный HTML-файл
Когда мы разрабатываем интерфейс, наше приложение реагирования отображает весь наш код JavaScript в этот HTML-файл, расположенный в папке src. Также, когда мы создаем наш код для производства (связывания), веб-пакет будет использовать этот HTML-код в качестве шаблона.
Тем не менее, важно сказать, что Django не будет использовать этот HTML-файл в качестве шаблона. Это точка входа в HTML веб-пакета, и Django будет использовать вывод пакета.
HTML
xxxxxxxxxx
1
2
<html lang="en">
3
  <head>
4
    <meta charset="utf-8" />
5
    <meta name="viewport" content="width=device-width, initial-scale=1" />
6
    <meta name="theme-color" content="#000000" />
7
    <meta name="description" content="Django-React Integration Tutorial"/>
8
    <title>Django React Integration</title>
9
  </head>
10
  <body>
11
    <div id="root"></div>
12
  </body>
13
</html>
Шаг 6: Создание корневого файла приложения React: Index.Js
Индексный файл является корневым файлом нашего приложения, что означает, что весь наш код будет связан с этим корневым файлом. Другие учебные пособия или шаблоны реакции обычно используют этот файл только для рендеринга функции ReactDOM и оставляют его очень маленьким и понятным. Написание этого индексного файла как есть, это личный выбор.
Что мы сделаем, это создадим компонент Init, который будет инициализировать структуру API и библиотеку маршрутизации.
Мы обернем наш файл приложения с API-фреймворком, чтобы все наши компоненты были в контексте нашего API. Провайдер Apollo ожидает, что клиент Apollo, у которого есть информация запрошенного адреса, будет адресом нашего сервера Django.
После этого мы снова обернем наш файл приложения компонентом маршрутизатора, а именно Browser Router. Это позволит нам выполнять маршрутизацию без рендеринга всей страницы при изменении URL-адреса адресной строки.
В конце файла вы увидите функцию рендеринга ReactDOM, которая принимает наш корневой компонент, который в нашем случае является компонентом Init, и элемент DOM, в котором наше приложение будет отображаться там.
JavaScript
xxxxxxxxxx
1
// djr/FRONTEND/src/index.js
2
import React from 'react';
3
import ReactDOM from 'react-dom';
4
import App from './App';
5
6
import { BrowserRouter } from "react-router-dom"
7
8
import ApolloClient from 'apollo-boost';
9
import { ApolloProvider } from '@apollo/react-hooks';
10
11
12
13
/*
14
    our api client will make request to thils adress.
15
    at      ~/Blog/djr/djr/urls.py
16
*/
17
const apiclient = new ApolloClient({
18
    uri: '<http://127.0.0.1:8000/graphql>',
19
  });
20
21
22
const Init = () => (
23
    <ApolloProvider client={apiclient}>
24
        <BrowserRouter>
25
            <App ></App>
26
        </BrowserRouter>
27
    </ApolloProvider>
28
)
29
30
ReactDOM.render( <Init ></Init>, document.getElementById('root'))
Информация о приложении
Теперь мы готовы создать наше простое приложение для просмотра фильмов.
Наше приложение имеет два разных экрана; На главной странице, где перечислены все фильмы в базе данных с небольшой информацией, а на странице фильма будет показан конкретный фильм с дополнительной информацией.
Техническое объяснение
Когда пользователь впервые откроет нашу страницу, компонент переключателя из response-router-dom будет смотреть URL. Затем попытайтесь сопоставить путь компонентов маршрута с этим URL-адресом, если таковой имеется, тогда сопоставленный компонент в маршруте будет отображен.
В идеальном случае, когда пользователь открывает нашу домашнюю страницу, функция переключения будет соответствовать компоненту главной страницы. Затем запрос на главной странице сделает запрос к серверу. Если запрос будет выполнен успешно, главная страница отобразит данные, а пользователь увидит маленькие карточки с фильмами. Когда пользователь щелкает по любой из этих карт, компонент ссылки изact-router-dom перенаправляет пользователя на страницу фильма этого конкретного фильма. URL будет изменен. Затем переключите внешний вид функции и сопоставьте этот URL с компонентом страницы фильма. Этот временной запрос на странице фильма запросит сервер с заданным аргументом слага, который был захвачен из URL. Сервер посмотрит на этот аргумент и проверит свою базу данных, если совпадение найдено, тогда информация о фильме будет отправлена обратно клиенту. Наконец, страница фильма отображает информацию о фильме с этими данными.
Примечание: лучше сначала загрузить всю информацию, чем визуализировать страницу фильма с этими данными. Это не хороший вариант сделать второй запрос с этими небольшими данными. Из-за необходимости объяснения этот подход был выбран.
Шаг 7: Создайте файл App.Js
JavaScript
xxxxxxxxxx
1
// djr/FRONTEND/src/App.js
2
import React from "react";
3
import { Route, Switch, Link } from "react-router-dom"
4
5
import "./App.css"
6
7
const App = () => {
8
    return (
9
        <div className="App">
10
            <Switch>
11
                <Route exact path="/" component={MainPage} ></Route>
12
13
                // colon before slug means it is a dynamic value
14
                // that makes slug parameter anything
15
                // like: /movie/the-matrix-1999   or /movie/anything
16
                <Route exact path="/movie/:slug" component={MoviePage} ></Route>
17
            </Switch>
18
        </div>
19
    )
20
}
21
export default App
Шаг 7: Написать клиентские запросы
Перед созданием нашей главной страницы и компонентов страницы фильма мы должны сначала создать наши запросы API.
JavaScript
xxxxxxxxxx
1
// djr/FRONTEND/src/query.js
2
3
//import our graph query parser
4
import gql from "graphql-tag";
5
6
// our first query will requests all movies
7
// with only given fields
8
// note the usage of gql with jsvascript string literal
9
export const MOVIE_LIST_QUERY = gql`
10
    query movieList{
11
        movieList{
12
            name, posterUrl, slug
13
        }
14
    }
15
`
16
// Note the usage of argument.
17
// the exclamation mark makes the slug argument as required
18
// without it , argument will be optional
19
export const MOVIE_QUERY = gql`
20
    query movie($slug:String!){
21
        movie(slug:$slug){
22
            id, name, year, summary, posterUrl, slug
23
        }
24
    }
25
`
Шаг 7: Создание компонентов страницы
Как правило, лучше создать другую страницу для компонентов. Однако, поскольку этот проект небольшой, запись в них в файле приложения не составит проблемы.
Импортируйте запрос Apollo и наши запросы в файл приложения.
JavaScript
xxxxxxxxxx
1
// djr/FRONTEND/src/App.js
2
3
// import Apollo framework query hook
4
import { useQuery } from '@apollo/react-hooks'; // New
5
6
// import our queries previously defined
7
import { MOVIE_QUERY, MOVIE_LIST_QUERY } from "./query" //New
Компонент главной страницы
JavaScript
xxxxxxxxxx
1
// djr/FRONTEND/src/App.js
2
const MainPage = (props) => {
3
    const { loading, error, data } = useQuery(MOVIE_LIST_QUERY);
4
    
5
    // when query starts, loading will be true until the response will back.
6
    // At this time this will be rendered on screen
7
    if (loading) return <div>Loading</div>
8
    
9
    // if response fail, this will be rendered
10
    if (error) return <div>Unexpected Error: {error.message}</div>
11
12
    //if query succeed, data will be available and render the data
13
    return(
14
        <div className="main-page">
15
            {data && data.movieList &&
16
                data.movieList.map(movie => (
17
                    <div className="movie-card" key={movie.slug}>
18
                        <img 
19
                            className="movie-card-image"
20
                            src={movie.posterUrl} 
21
                            alt={movie.name + " poster"} 
22
                            title={movie.name + " poster"} 
23
                        />
24
                        <p className="movie-card-name">{movie.name}</p>
25
                        <Link to={`/movie/${movie.slug}`} className="movie-card-link" />
26
                    </div>
27
                ))
28
            }
29
        </div>
30
    )
31
}
Компонент страницы фильма
JavaScript
xxxxxxxxxx
1
// djr/FRONTEND/src/App.js
2
const MoviePage = (props) => {
3
    // uncomment to see which props are passed from router
4
    //console.log(props)
5
6
    // due to we make slug parameter dynamic in route component,
7
    // urlParameters will look like this { slug: 'slug-of-the-selected-movie' }
8
    const urlParameters = props.match.params
9
10
    const { loading, error, data } = useQuery(MOVIE_QUERY, { 
11
        variables:{slug:urlParameters.slug}
12
    });
13
14
    if (loading) return <div>Loading</div>
15
    if (error) return <div>Unexpected Error: {error.message}</div>
16
  
17
    return (
18
        <div className="movie-page">
19
        <Link to="/" className="back-button" >Main Page</Link>
20
            {data && data.movie && 
21
                <div className="movie-page-box">
22
                    <img 
23
                        className="movie-page-image"
24
                        src={data.movie.posterUrl} 
25
                        alt={data.movie.name + " poster"} 
26
                        title={data.movie.name + " poster"} 
27
                    />
28
                    <div className="movie-page-info">
29
                        <h1>{data.movie.name}</h1>
30
                        <p>Year: {data.movie.year}</p>
31
                        <br />
32
                        <p>{data.movie.summary}</p>
33
                    </div>
34
                </div>
35
            }
36
37
        </div>
38
    )
39
}
Шаг 8: добавление стилей
Вы можете скопировать их в App.css
CSS
xxxxxxxxxx
1
/* djr/FRONTEND/src/App.css  */
2
3
html, body {
4
    width:100vw;
5
    overflow-x: hidden;
6
    height:auto;
7
    min-height: 100vh;
8
    margin:0;
9
}
10
11
.App {
12
    position: absolute;
13
    left:0;
14
    right:0;
15
    display: flex;
16
    min-width: 100%;
17
    min-height: 100vh;
18
    flex-direction: column;
19
    background-color: #181818;
20
    /*font-family: "Open Sans", sans-serif;*/
21
    font-size: 16px;
22
    font-family: sans-serif;
23
}
24
25
/* MAIN PAGE */
26
.main-page {
27
    position: relative;
28
    display: flex;
29
    flex-wrap: wrap;
30
    min-height: 80vh;
31
    background-color: #3f3e3e;
32
    margin:10vh 5vw;
33
    border-radius: 6px;
34
}
35
36
/* MOVIE CARD */
37
.movie-card {
38
    position: relative;
39
    width:168px;
40
    height:auto;
41
    background: #f1f1f1;
42
    border-radius: 6px;
43
    margin:16px;
44
    box-shadow: 0 12px 12px -4px rgba(0,0,0, 0.4);
45
}
46
.movie-card:hover {
47
    box-shadow: 0 12px 18px 4px rgba(0,0,0, 0.8);
48
49
}
50
.movie-card-image {
51
    width:168px;
52
    height:264px;
53
    border-top-left-radius: 6px;
54
    border-top-right-radius: 6px;
55
}
56
.movie-card-name {
57
    text-align: center;
58
    margin: 0;
59
    padding: 8px;
60
    font-weight: bold;
61
}
62
.movie-card-link {
63
    position: absolute;
64
    top:0;
65
    left:0;
66
    right: 0;
67
    bottom: 0;
68
}
69
70
/* MOVIE PAGE */
71
.back-button {
72
    position: absolute;
73
    left:10px;
74
    top:10px;
75
    width:120px;
76
    padding: 8px 16px;
77
    text-align: center;
78
    background: #f1f1f1;
79
    color:black;
80
    font-weight: bold;
81
    cursor:pointer;
82
}
83
84
.movie-page {
85
    position: relative;
86
    display: flex;
87
    flex-direction: column;
88
    justify-content: center;
89
    align-items: center;
90
    flex-wrap: wrap;
91
    min-height: 80vh;
92
    margin:10vh 10vw;
93
    border-radius: 6px;
94
}
95
96
.movie-page-box {
97
    position: relative;
98
    display: flex;
99
    height:352px;
100
    background-color: #f1f1f1;
101
}
102
.movie-page-image {
103
    width:280px;
104
    height:352px;
105
}
106
.movie-page-info {
107
    position: relative;
108
    display: flex;
109
    flex-direction: column;
110
    height:352px;
111
    width: auto;
112
    max-width: 400px;
113
    padding: 16px 32px;
114
}
Шаг 9: Запустите среду разработки
Откройте два разных экрана терминала.
Оболочка
xxxxxxxxxx
1
# in root directory of django project     djr/
2
3
# make ready server for client requests.
4
python manage.py runserver
5
6
# in FRONTEND directory   ~/Blog/djr/FRONTEND
7
8
# run react dev environment
9
npm run start
10
# this will probably open a browser page
11
# <http://localhost:8080></div>
Вуаля
Когда мы нажмем любой из фильмов, вы увидите, что URL-адрес будет изменен. Давайте нажмем
Мы создали простое одностраничное приложение. Теперь в последней части этого урока это приложение будет работать без проблем с нашим проектом Django.
Теперь вы можете остановить сервер веб-пакетов на экране соответствующего терминала.
Шаг 10: Создайте производственную среду
Теперь мы можем построить наше приложение для производственной среды.
Оболочка
xxxxxxxxxx
1
# in djr/FRONTEND
2
npm run build
По завершении процесса связывания Django будет использовать новый файл index.html в качестве шаблона, который содержит наше клиентское приложение. При успешной сборке у вас будет два связанных файла javascript в статической папке корневого каталога, файл index.html в каталоге шаблонов.
( Изменить : последняя строка в urls.py должна начинаться с re_path вместо пути)
В начале этой серии я говорил, что мы будем разрабатывать этот проект на двух серверах, но в производственной среде будет только один сервер.
Теперь давайте проверим это.
Пожалуйста, закройте открытые терминальные сессии и заново откройте сервер Django.
Оболочка
xxxxxxxxxx
1
# in root directory
2
python manage.py runserver
3
4
# then open <http://127.0.0.1:8000></div> on your browser.
Это работает.
Эта серия уроков окончена. Надеюсь это кому-нибудь пригодится. Критика, отзывы и вопросы приветствуются.
Наконец, вы можете найти весь код этого урока здесь.
Дальнейшее чтение
Интегрировать приложение React Native с GraphQL и клиентом Apollo