В предыдущей части мы создали 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