
Vorwort
Nuxt ist ein "Framework over the Vue Framework" oder eine beliebte Konfiguration von Vue-basierten Anwendungen unter Verwendung der besten Vue-Entwicklungsmethoden. Darunter: Organisation von Anwendungsverzeichnissen; Aufnahme und Vorkonfiguration der beliebtesten Tools in Form von Nuxt-Modulen; Standardmäßige Aufnahme von Vuex in jede Konfiguration; fertige und vorkonfigurierte SSR mit Hot-Reloading
Django — - — Python. "- ". " " MVP -.
GraphQL — Facebook. , Apollo graphene .
dev- SPA server side , Django Nuxt, GraphQL API.
, .
, (, , ), .
, , , , .
, , , , .
, .
, node.js python. : 13.9 3.7 .
python pipenv.
bash. Windows, cd
, mv
, mkdir
, - python node, .
Sqlite, .
. , .
Python
Javascript
Django
. pipenv. :
pip install pipenv
. pipenv .
pipenv
. ~/Documents/projects/todo-list
. .
mkdir ~/Documents/projects/todo-list
cd ~/Documents/projects/todo-list
django
graphene_django
:
pipenv install django==2.2.10 graphene_django
graphene_django
GraphQL API Django ORM. , , .
pipenv. , .
pipenv shell
Django.
django-admin createapp backend
manage.py
todo- , . Django manage.py
, backend
.
, :
mv backend/manage.py .
manage.py
.
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.backend.settings")
...
backend/backend/settings.py
:
ROOT_URLCONF = 'backend.backend.urls'
WSGI_APPLICATION = 'backend.backend.wsgi.application'
graphene_django
backend/backend/settings.py
INSTALLED_APPS
graphene_django
:
INSTALLED_APPS = [
...,
'graphene_django',
]
python manage.py runserver
- 8000
. http://localhost:8000/, :
graphene
http://localhost:8000/
. backend/backend/urls.py
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django.conf import settings
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', GraphQLView.as_view(graphiql=settings.DEBUG))
]
, , backend/backend/api.py
import graphene
schema = graphene.Schema()
GRAPHENE
, :
GRAPHENE = {
'SCHEMA': 'backend.backend.api.schema',
}
. runserver
:
python manage.py runserver
http://localhost:8000/graphql/. "IDE" GrapiQL:
, . - , . .
todo_list
todo_list . , pipenv:
cd backend
django-admin startapp todo_list
django-admin
, backend/todo_list/apps.py
, :
from django.apps import AppConfig
class TodoListConfig(AppConfig):
name = 'backend.todo_list'
INSTALLED_APPS
, settings.py
:
INSTALLED_APPS = [
...,
'backend.todo_list',
...
]
Todo
Category
backend/todo_list/models.py
:
models.py
from datetime import timedelta
from django.db import models
from django.utils import timezone
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
class Meta:
verbose_name = ''
verbose_name_plural = ''
def __str__(self):
return self.name
def get_due_date():
""" - """
return timezone.now() + timedelta(days=1)
class Todo(models.Model):
title = models.CharField(max_length=250)
text = models.TextField(blank=True)
created_date = models.DateField(auto_now_add=True)
due_date = models.DateField(default=get_due_date)
category = models.ForeignKey(Category, related_name='todo_list', on_delete=models.PROTECT)
done = models.BooleanField(default=False)
class Meta:
verbose_name = ''
verbose_name_plural = ''
def __str__(self):
return self.title
, , :
, :
python manage.py makemigrations
:

migrate
. .. migrate
, Django:
python manage.py migrate
GraphQL API
, . todo_list
schema.py
, :
schema.py
import graphene
from graphene_django import DjangoObjectType
from backend.todo_list.models import Todo, Category
class CategoryNode(DjangoObjectType):
class Meta:
model = Category
class TodoNode(DjangoObjectType):
class Meta:
model = Todo
class Query(graphene.ObjectType):
""" """
todo_list = graphene.List(TodoNode)
categories = graphene.List(CategoryNode)
def resolve_todo_list(self, info):
return Todo.objects.all().order_by('-id')
def resolve_categories(self, info):
return Category.objects.all()
class Mutation(graphene.ObjectType):
""" ( ),
"""
add_todo = graphene.Field(TodoNode,
title=graphene.String(required=True),
text=graphene.String(),
due_date=graphene.Date(required=True),
category=graphene.String(required=True))
remove_todo = graphene.Field(graphene.Boolean, todo_id=graphene.ID())
toggle_todo = graphene.Field(TodoNode, todo_id=graphene.ID())
def resolve_add_todo(self, info, **kwargs):
category, _ = Category.objects.get_or_create(name=kwargs.pop('category'))
return Todo.objects.create(category=category, **kwargs)
def resolve_remove_todo(self, info, todo_id):
try:
Todo.objects.get(id=todo_id).delete()
except Todo.DoesNotExist:
return False
return True
def resolve_toggle_todo(self, info, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.done = not todo.done
todo.save()
return todo
, . , , api.py
:
import graphene
from backend.todo_list.schema import Query, Mutation
schema = graphene.Schema(query=Query, mutation=Mutation)
, , Graphene (.).
API
ID ID .
runserver
:
python manage.py runserver
http://localhost:8000/graphql/. graphiql
. , , .
.
addTodoAnfrage
mutation(
$title: String!
$text: String
$dueDate: Date!
$category: String!
) {
addTodo(
title: $title
text: $text
dueDate: $dueDate
category: $category
) {
todo {
id
title
text
done
createdDate
dueDate
category {
id
name
}
}
}
}
Variablen
{
"title": "First Todo",
"text": "Just do it!",
"dueDate": "2020-10-17",
"category": ""
}
Ergebnis

Als Ergebnis dieser Mutation haben wir zwei Einträge erstellt:
Todo
da die Mutation selbst ist zu diesem Zweck geschrieben;Category
, .. "", get_or_create
.
todoList categories
{
todoList {
id
title
text
createdDate
dueDate
category {
id
name
}
}
categories {
id
name
}
}
:

toggleTodo
mutation ($todoId: ID) {
toggleTodo(todoId: $todoId) {
id
title
text
createdDate
dueDate
category {
id
name
}
done
}
}
{
"todoId": "1"
}
:

removeTodo
mutation ($todoId: ID) {
removeTodo(todoId: $todoId)
}
.

GraphQL, .
Nuxt
Nuxt
Nuxt
:
npx create-nuxt-app frontend
, . , , "Custom UI Framework: vuetify", .. , "Rendering mode: Universal", .. SSR.
:

. . :
cd frontend
npm run dev
http://localhost:3000. Nuxt + Vuetify:
, , . :
cd frontend
mv node_modules ..
mv nuxt.config.js ..
mv .gitignore ..
mv package-lock.json ..
mv package.json ..
mv .prettierrc ..
mv .eslintrc.js ..
mv .editorconfig ..
rm -rf .git
nuxt.config.js
:
export default {
...,
rootDir: 'frontend',
...
}
, dev- :
npm run dev
v-
UI-toolkit'a Vuetify. .
, , v-component-name
. , vuetify 1.5.
frontend/layouts/default.vue
:
<template>
<v-app>
<v-content>
<v-container>
<nuxt />
</v-container>
</v-content>
</v-app>
</template>
Todo
frontend/components/NewTodoForm.vue
:
NewTodoForm.vue
<template>
<v-form ref="form" v-model="valid">
<v-card>
<v-card-text class="pt-0 mt-0">
<v-layout row wrap>
<v-flex xs8>
<v-text-field
v-model="newTodo.title"
:rules="[nonEmptyField]"
label=""
prepend-icon="check_circle_outline"
/>
</v-flex>
<v-flex xs4>
<v-menu
ref="menu"
v-model="menu"
:close-on-content-click="false"
:nudge-right="40"
:return-value.sync="newTodo.dueDate"
lazy
transition="scale-transition"
offset-y
full-width
min-width="290px"
>
<template v-slot:activator="{ on }">
<v-text-field
v-model="newTodo.dueDate"
:rules="[nonEmptyField]"
v-on="on"
label=" "
prepend-icon="event"
readonly
/>
</template>
<v-date-picker
v-model="newTodo.dueDate"
no-title
scrollable
locale="ru-ru"
first-day-of-week="1"
>
<v-spacer />
<v-btn @click="menu = false" flat color="primary"></v-btn>
<v-btn
@click="$refs.menu.save(newTodo.dueDate)"
flat
color="primary"
></v-btn
>
</v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12>
<v-textarea
v-model="newTodo.text"
:rules="[nonEmptyField]"
label=""
prepend-icon="description"
hide-details
rows="1"
class="py-0 my-0"
/>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions>
<v-combobox
v-model="newTodo.category"
:rules="[nonEmptyField]"
:items="categories"
hide-details
label=""
class="my-0 mx-2 mb-2 pt-0"
prepend-icon="category"
/>
<v-spacer />
<v-btn :disabled="!valid" @click="add" color="blue lighten-1" flat
></v-btn
>
</v-card-actions>
</v-card>
</v-form>
</template>
<script>
export default {
name: 'NewTodoForm',
data() {
return {
newTodo: null,
categories: ['', '', '', ''],
valid: false,
menu: false,
nonEmptyField: text =>
text ? !!text.length : ' '
}
},
created() {
this.clear()
},
methods: {
add() {
this.$emit('add', this.newTodo)
this.clear()
this.$refs.form.reset()
},
clear() {
this.newTodo = {
title: '',
text: '',
dueDate: '',
category: ''
}
}
}
}
</script>
Todo
, :
TodoItem.vue
<template>
<v-card>
<v-card-title class="pb-1" style="overflow-wrap: break-word;">
<b>{{ todo.title }}</b>
<v-spacer />
<v-btn
@click="$emit('delete', todo.id)"
flat
small
icon
style="position: absolute; right: 0; top: 0"
>
<v-icon :disabled="$nuxt.isServer" small>close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="py-1">
<v-layout row justyfy-center align-center>
<v-flex xs11 style="overflow-wrap: break-word;">
{{ todo.text }}
</v-flex>
<v-flex xs1>
<div style="text-align: right;">
<v-checkbox
v-model="todo.done"
hide-details
class="pa-0 ma-0"
style="display: inline-block;"
color="green lighten-1"
/>
</div>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions>
<span class="grey--text">
<v-icon small>event</v-icon> {{ todo.dueDate }} |
<v-icon small>calendar_today</v-icon> {{ todo.createdDate }}
</span>
<v-spacer />
<span class="grey--text">
<v-icon small>category</v-icon>: {{ todo.category }}
</span>
</v-card-actions>
</v-card>
</template>
<script>
export default {
name: 'TodoItem',
props: {
todo: {
type: Object,
default: () => ({})
}
}
}
</script>
index.vue
, :
index.vue
<template>
<v-layout row wrap justify-center>
<v-flex xs8 class="pb-1">
<new-todo-form @add="addTodo" />
</v-flex>
<v-flex v-for="todo of todoList" :key="todo.id" xs8 class="my-1">
<todo-item :todo="todo" @delete="deleteTodo" />
</v-flex>
</v-layout>
</template>
<script>
import NewTodoForm from '../components/NewTodoForm'
import TodoItem from '../components/TodoItem'
export default {
components: { TodoItem, NewTodoForm },
data() {
return {
todoList: [
{
id: 1,
title: 'TODO 1',
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
dueDate: '2020-10-16',
createdDate: '2020-03-09',
done: false,
category: ''
},
{
id: 2,
title: 'TODO 2',
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
dueDate: '2020-10-16',
createdDate: '2020-03-09',
done: false,
category: ''
},
{
id: 3,
title: 'TODO 3',
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
dueDate: '2020-10-16',
createdDate: '2020-03-09',
done: false,
category: ''
},
{
id: 4,
title: 'TODO 4',
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
dueDate: '2020-10-16',
createdDate: '2020-03-09',
done: false,
category: ''
},
{
id: 5,
title: 'TODO 5',
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
dueDate: '2020-10-16',
createdDate: '2020-03-09',
done: false,
category: ''
},
{
id: 6,
title: 'TODO 6',
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
dueDate: '2020-10-16',
createdDate: '2020-03-09',
done: false,
category: ''
}
]
}
},
methods: {
addTodo(newTodo) {
const id = this.todoList.length
? Math.max.apply(
null,
this.todoList.map(item => item.id)
) + 1
: 1
this.todoList.unshift({
id,
createdDate: new Date().toISOString().substr(0, 10),
done: false,
...newTodo
})
},
deleteTodo(todoId) {
this.todoList = this.todoList.filter(item => item.id !== todoId)
}
}
}
</script>
. dev
http://localhost:3000/, :
CSRF- Django + Apollo
Django - CSRF .
(middleware) — CsrfViewMiddleware
. settings.py
MIDDLEWARE
.
: POST- Django CSRF-. , .
CSRF- django GET-, .
, , Apollo , - POST. Apollo , Query
GET, Mutation
— POST, , graphene .
: CsrfViewMiddleware
, GraphQL , .
CSRF, , api.py
api.py
import json
import graphene
from django.middleware.csrf import CsrfViewMiddleware
from backend.todo_list.schema import Query, Mutation
schema = graphene.Schema(query=Query, mutation=Mutation)
class CustomCsrfMiddleware(CsrfViewMiddleware):
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
if getattr(callback, 'csrf_exempt', False):
return None
try:
body = request.body.decode('utf-8')
body = json.loads(body)
except (TypeError, ValueError, UnicodeDecodeError):
return super(CustomCsrfMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
if isinstance(body, list):
for query in body:
if 'mutation' in query:
break
else:
return self._accept(request)
else:
if 'query' in body and 'mutation' not in body:
return self._accept(request)
return super(CustomCsrfMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
, settings.py
"" CsrfViewMiddleware
, :
MIDDLEWARE = [
...,
'backend.backend.api.CustomCsrfMiddleware',
...,
]
CSRF- Django + Nuxt + Apollo, .
.. dev , Django , , . django-cors-headers
:
pipenv install "django-cors-headers>=3.2"
settings.py
:
from corsheaders.defaults import default_headers
INSTALLED_APPS = [
...,
'graphene_django',
'backend.todo_list',
'corsheaders',
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = default_headers + ('cache-control', 'cookies')
CORS_ORIGIN_ALLOW_ALL = True
MIDDLEWARE = [
...,
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...,
]
Apollo
Nuxt apollo
, vue-apollo
( Apollo). :
npm install --save @nuxtjs/apollo graphql-tag cookie-universal-nuxt
Apollo
cookie-universal-nuxt
.
nuxt.config.js
. . vuetify
:
export default {
...,
modules: [
...,
'@nuxtjs/vuetify',
'@nuxtjs/apollo',
'cookie-universal-nuxt'
],
apollo: {
clientConfigs: {
default: '~/plugins/apollo-client.js'
}
},
...
}
Apollo . . :
Apollo
, .
apollo-client.js
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { from, concat } from 'apollo-link'
import { InMemoryCache } from 'apollo-cache-inmemory'
export default ctx => {
const ssrMiddleware = setContext((_, { headers }) => {
if (process.client) return headers
return {
headers: {
...headers,
connection: ctx.app.context.req.headers.connection,
referer: ctx.app.context.req.headers.referer,
cookie: ctx.app.context.req.headers.cookie
}
}
})
const csrfMiddleware = setContext((_, { headers }) => {
return {
headers: {
...headers,
'X-CSRFToken': ctx.app.$cookies.get('csrftoken') || null
}
}
})
const httpLink = new HttpLink({
uri: 'http://localhost:8000/graphql/',
credentials: 'include'
})
const link = from([csrfMiddleware, ssrMiddleware, httpLink])
const cache = new InMemoryCache()
return {
link,
cache,
defaultHttpLink: false
}
}
, Nuxt , dev .
.
- , . frontend/graphql.js
:
graphql.jsimport gql from 'graphql-tag'
const TODO_FRAGMENT = gql`
fragment TodoContents on TodoNode {
id
title
text
done
createdDate
dueDate
category {
id
name
}
}
`
const ADD_TODO = gql`
mutation(
$title: String!
$text: String
$dueDate: Date!
$category: String!
) {
addTodo(
title: $title
text: $text
dueDate: $dueDate
category: $category
) {
...TodoContents
}
}
${TODO_FRAGMENT}
`
const TOGGLE_TODO = gql`
mutation($todoId: ID) {
toggleTodo(todoId: $todoId) {
...TodoContents
}
}
${TODO_FRAGMENT}
`
const GET_CATEGORIES = gql`
{
categories {
id
name
}
}
`
const GET_TODO_LIST = gql`
{
todoList {
...TodoContents
}
}
${TODO_FRAGMENT}
`
const REMOVE_TODO = gql`
mutation($todoId: ID) {
removeTodo(todoId: $todoId)
}
`
export { ADD_TODO, TOGGLE_TODO, GET_CATEGORIES, GET_TODO_LIST, REMOVE_TODO }
. .
Vue :
index.vue
<template>
<v-layout row wrap justify-center>
<v-flex xs8 class="pb-1">
<new-todo-form />
</v-flex>
<v-flex v-for="todo of todoList" :key="todo.id" xs8 class="my-1">
<todo-item :todo="todo" />
</v-flex>
</v-layout>
</template>
<script>
import NewTodoForm from '../components/NewTodoForm'
import TodoItem from '../components/TodoItem'
import { GET_TODO_LIST } from '../graphql'
export default {
components: { TodoItem, NewTodoForm },
data() {
return {
todoList: []
}
},
apollo: {
todoList: { query: GET_TODO_LIST }
}
}
</script>
NewTodoForm.vue
<template>
<v-form ref="form" v-model="valid">
<v-card>
<v-card-text class="pt-0 mt-0">
<v-layout row wrap>
<v-flex xs8>
<v-text-field
v-model="newTodo.title"
:rules="[nonEmptyField]"
label=""
prepend-icon="check_circle_outline"
/>
</v-flex>
<v-flex xs4>
<v-menu
ref="menu"
v-model="menu"
:close-on-content-click="false"
:nudge-right="40"
:return-value.sync="newTodo.dueDate"
lazy
transition="scale-transition"
offset-y
full-width
min-width="290px"
>
<template v-slot:activator="{ on }">
<v-text-field
v-model="newTodo.dueDate"
:rules="[nonEmptyField]"
v-on="on"
label=" "
prepend-icon="event"
readonly
/>
</template>
<v-date-picker
v-model="newTodo.dueDate"
no-title
scrollable
locale="ru-ru"
first-day-of-week="1"
>
<v-spacer />
<v-btn @click="menu = false" flat color="primary"></v-btn>
<v-btn
@click="$refs.menu.save(newTodo.dueDate)"
flat
color="primary"
></v-btn
>
</v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12>
<v-textarea
v-model="newTodo.text"
:rules="[nonEmptyField]"
label=""
prepend-icon="description"
hide-details
rows="1"
class="py-0 my-0"
/>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions>
<v-combobox
v-model="newTodo.category"
:rules="[nonEmptyField]"
:items="categories"
hide-details
label=""
class="my-0 mx-2 mb-2 pt-0"
prepend-icon="category"
/>
<v-spacer />
<v-btn
:disabled="!valid"
:loading="loading"
@click="add"
color="blue lighten-1"
flat
></v-btn
>
</v-card-actions>
</v-card>
</v-form>
</template>
<script>
import { ADD_TODO, GET_CATEGORIES, GET_TODO_LIST } from '../graphql'
export default {
name: 'NewTodoForm',
data() {
return {
newTodo: null,
categories: [],
valid: false,
menu: false,
nonEmptyField: text =>
text ? !!text.length : ' ',
loading: false
}
},
apollo: {
categories: {
query: GET_CATEGORIES,
update({ categories }) {
return categories.map(c => c.name)
}
}
},
created() {
this.clear()
},
methods: {
add() {
this.loading = true
this.$apollo
.mutate({
mutation: ADD_TODO,
variables: {
...this.newTodo
},
update: (store, { data: { addTodo } }) => {
const todoListData = store.readQuery({ query: GET_TODO_LIST })
todoListData.todoList.unshift(addTodo)
store.writeQuery({ query: GET_CATEGORIES, data: todoListData })
const categoriesData = store.readQuery({ query: GET_CATEGORIES })
const category = categoriesData.categories.find(
c => c.name === addTodo.category.name
)
if (!category) {
categoriesData.categories.push(addTodo.category)
store.writeQuery({ query: GET_CATEGORIES, data: categoriesData })
}
}
})
.then(() => {
this.clear()
this.loading = false
this.$refs.form.reset()
})
},
clear() {
this.newTodo = {
title: '',
text: '',
dueDate: '',
category: ''
}
}
}
}
</script>
TodoItem.vue
<template>
<v-card>
<v-card-title class="pb-1" style="overflow-wrap: break-word;">
<b>{{ todo.title }}</b>
<v-spacer />
<v-btn
@click="remove"
flat
small
icon
style="position: absolute; right: 0; top: 0"
>
<v-icon :disabled="$nuxt.isServer" small>close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="py-1">
<v-layout row justyfy-center align-center>
<v-flex xs11 style="overflow-wrap: break-word;">
{{ todo.text }}
</v-flex>
<v-flex xs1>
<div style="text-align: right;">
<v-checkbox
:value="todo.done"
@click.once="toggle"
hide-details
class="pa-0 ma-0"
style="display: inline-block;"
color="green lighten-1"
/>
</div>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions>
<span class="grey--text">
<v-icon small>event</v-icon> {{ todo.dueDate }} |
<v-icon small>calendar_today</v-icon> {{ todo.createdDate }}
</span>
<v-spacer />
<span class="grey--text">
<v-icon small>category</v-icon>: {{ todo.category.name }}
</span>
</v-card-actions>
</v-card>
</template>
<script>
import { GET_TODO_LIST, REMOVE_TODO, TOGGLE_TODO } from '../graphql'
export default {
name: 'TodoItem',
props: {
todo: {
type: Object,
default: () => ({})
}
},
methods: {
toggle() {
this.$apollo.mutate({
mutation: TOGGLE_TODO,
variables: {
todoId: this.todo.id
}
})
},
remove() {
const todoId = this.todo.id
this.$apollo.mutate({
mutation: REMOVE_TODO,
variables: {
todoId
},
update(store, { data: { removeTodo } }) {
if (!removeTodo) return
const data = store.readQuery({ query: GET_TODO_LIST })
data.todoList = data.todoList.filter(todo => todo.id !== todoId)
store.writeQuery({ query: GET_TODO_LIST, data })
}
})
}
}
}
</script>
,
Django Nuxt GraphQL API, . - , .
GitHub.