В предыдущей части урока мы написали бэкэнд для нашего приложения с заметками. В этом уроке мы продолжим, и напишем фронтэнд часть, используя фреймворк vue.js для Javascript.
Исходный код урока: https://github.com/apirobot/django-vue-simplenote
После предыдущего урока, структура вашего приложения должна выглядить примерно так:
django-vue-simplenote
└── backend
├── db.sqlite3
├── manage.py
├── notes
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
└── simplenote
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
Настройка фронтэнда и установка зависимостей
Давайте начнем с создания шаблона с помощью коммандной утилиты vue-cli:
$ cd django-vue-simplenote $ vue init webpack simplenote

Коммандная утилита создала папку simplenote. Переименуем эту папку:
$ mv simplenote frontend
Устанавливаем зависимости и запускаем сборку:
$ cd frontend $ npm install $ npm run dev
Если все прошло успешно, то теперь ваш Vue проект доступен по адресу localhost:8080
Кроме стандартных зависимостей, нам еще в дальнейшем понадобится:
- HTML препроцессор pug
- Axios для отправки HTTP запросов к нашему API
- Vuex для хранения наших заметок в хранилище
Установим все это:
$ npm install pug --save-dev $ npm install axios vue-axios --save $ npm install vuex --save
Чтобы не писать самим свои стили, мы будем использовать CSS фреймворк spectre.css. Включим его в наш проект. Для этого достаточно добавить ссылку на cdn в файле frontend/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>simplenote</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.2.14/spectre.min.css">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
API
Прежде чем отобразить заметки на нашей странице, их нужно сначала получить. Для этого создадим несколько функций с запросами к нашему API.
Так как все наши запросы будут обращаться к одному и тому же URL (http://localhost:8000/api/v1/), то мы создадим отдельный экземпляр axios, который будем позже использовать:
// frontend/src/api/common.js
import axios from 'axios'
export const HTTP = axios.create({
baseURL: 'http://localhost:8000/api/v1/'
})
В нашем приложении мы должны иметь возможность создать заметку, получить список всех заметок, и удалить заметку. Создадим функции для каждого запроса:
// frontend/src/api/notes
import { HTTP } from './common'
export const Note = {
create (config) {
return HTTP.post('/notes/', config).then(response => {
return response.data
})
},
delete (note) {
return HTTP.delete(`/notes/${note.id}/`)
},
list () {
return HTTP.get('/notes/').then(response => {
return response.data
})
}
}
Если вы попытаетесь отправить запрос к API, вызвав одну из функций выше, то вы должны получить ошибку:

Проблема в том, что по умолчанию, мы можем отправлять запросы только в пределах своего хоста (localhost:8080). Для того, чтобы решить эту проблему, нужно добавить специальные response headers на нашем сервере. Делается это просто. Устанавливаем django-cors-headers:
$ pip install django-cors-headers
Обновляем настройки проекта django:
# backend/simplenote/settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
CORS_ORIGIN_ALLOW_ALL = True
Хранилище
Давайте создадим хранилище для заметок с помощью Vuex. Начнем с простого:
$ mkdir store $ touch store/index.js $ touch store/mutation-types.js
Добавим хранилище в файл main.js, чтобы в дальнейшем мы могли его использовать в наших компонентах:
// frontend/src/main.js
import Vue from 'vue'
import App from './App'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
template: '<App/>',
components: { App }
})
Добавим типы мутаций:
// frontend/src/store/mutation-types.js export const ADD_NOTE = 'ADD_NOTE' export const REMOVE_NOTE = 'REMOVE_NOTE' export const SET_NOTES = 'SET_NOTES'
Реализуем хранилище:
// frontend/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { Note } from '../api/notes'
import {
ADD_NOTE,
REMOVE_NOTE,
SET_NOTES
} from './mutation-types.js'
Vue.use(Vuex)
// Состояние
const state = {
notes: [] // список заметок
}
// Геттеры
const getters = {
notes: state => state.notes // получаем список заметок из состояния
}
// Мутации
const mutations = {
// Добавляем заметку в список
[ADD_NOTE] (state, note) {
state.notes = [note, ...state.notes]
},
// Убираем заметку из списка
[REMOVE_NOTE] (state, { id }) {
state.notes = state.notes.filter(note => {
return note.id !== id
})
},
// Задаем список заметок
[SET_NOTES] (state, { notes }) {
state.notes = notes
}
}
// Действия
const actions = {
createNote ({ commit }, noteData) {
Note.create(noteData).then(note => {
commit(ADD_NOTE, note)
})
},
deleteNote ({ commit }, note) {
Note.delete(note).then(response => {
commit(REMOVE_NOTE, note)
})
},
getNotes ({ commit }) {
Note.list().then(notes => {
commit(SET_NOTES, { notes })
})
}
}
export default new Vuex.Store({
state,
getters,
actions,
mutations
})
Компоненты
Наше приложение будет состоять из одной страницы, которая включает в себя форму с добавлением новой заметки, и список заметок, отсортированных по дате добавления. Поэтому мы создаем два Vue компонента: CreateNote (форма с добавлением заметки) и NoteList (список всех заметок):
$ touch src/components/CreateNote.vue $ touch src/components/NoteList.vue
Добавим эти компоненты в наш основной компонент src/App.vue вместе с небольшим html и css кодом:
<template lang="pug">
#app
section.container.grid-960
.columns
.column.col-2
.column.col-8.col-md-12
header.text-center
h2 Create note
create-note
header.text-center
h2 List of notes
note-list
.column.col-2
</template>
<script>
import CreateNote from './components/CreateNote'
import NoteList from './components/NoteList'
export default {
name: 'app',
components: {
'create-note': CreateNote,
'note-list': NoteList
}
}
</script>
<style>
@import url(https://fonts.googleapis.com/css?family=Eczar);
@import url(https://fonts.googleapis.com/css?family=Work+Sans);
body {
font-family: "Work Sans", "Segoe UI", "Helvetica Neue", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Eczar", sans-serif;
}
</style>
Реализуем компонент frontend/src/components/CreateNote.vue:
<template lang="pug">
form.form-horizontal(@submit="submitForm")
.form-group
.col-3
label.form-label Title
.col-9
input.form-input(type="text" v-model="title" placeholder="Type note title...")
.form-group
.col-3
label.form-label Body
.col-9
textarea.form-input(v-model="body" rows=8 placeholder="Type your note...")
.form-group
.col-3
.col-9
button.btn.btn-primary(type="submit") Create
</template>
<script>
export default {
name: 'create-note',
data () {
return {
'title': '',
'body': ''
}
},
methods: {
submitForm (event) {
this.createNote()
// Т.к. мы уже отправили запрос на создание заметки строчкой выше,
// нам нужно теперь очистить поля title и body
this.title = ''
this.body = ''
// preventDefault нужно для того, чтобы страница
// не перезагружалась после нажатия кнопки submit
event.preventDefault()
},
createNote () {
// Вызываем действие `createNote` из хранилища, которое
// отправит запрос на создание новой заметки к нашему API.
this.$store.dispatch('createNote', { title: this.title, body: this.body })
}
}
}
</script>
Реализуем компонент frontend/src/components/NoteList.vue:
<template lang="pug">
#app
.card(v-for="note in notes")
.card-header
button.btn.btn-clear.float-right(@click="deleteNote(note)")
.card-title {{ note.title }}
.card-subtitle {{ note.created_at }}
.card-body {{ note.body }}
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'note-list',
computed: mapGetters(['notes']),
methods: {
deleteNote (note) {
// Вызываем действие `deleteNote` из нашего хранилища, которое
// попытается удалить заметку из нашех базы данных, отправив запрос к API
this.$store.dispatch('deleteNote', note)
}
},
beforeMount () {
// Перед тем как загрузить страницу, нам нужно получить список всех
// имеющихся заметок. Для этого мы вызываем действие `getNotes` из
// нашего хранилища
this.$store.dispatch('getNotes')
}
}
</script>
<style>
header {
margin-top: 50px;
}
</style>
Запуск
На этом все. Фронтэнд и бэкэнд готов. Осталось только запустить это все вместе, введя команду в папке с проектом:
$ npm run --prefix frontend dev & python backend/manage.py runserver
Либо откройте два терминала и в одном из них запустите django, а в другом vue:
$ python backend/manage.py runserver $ cd frontend && npm run dev
Привет! Спасибо за пособие!
Тут опечатка:
$ pip install django-cors-heAders
и тут отсутствует вывод {{ note.title }}:
notelist.vue
#app
.card(v-for=”note in notes” v-bind:key=”note.id”)
.card-header
button.btn.btn-clear.float-right(@click=”deleteNote(note)”)
.card-title {{ note.title }}
.card-subtitle {{ note.body }}
.card-body
Спасибо. Исправил 😉
Спасибо за отличное пособие, как использовать эту связку на простом примере. Круто 👍
Спасибо! Как же хочется научиться так делать самому…
Спасибо за пособие! Может ещё сделаете пособие, как это всё задеплоить? Например на хероку.
Привет. Может когда-нибудь напишу, но пока в планах нет.
Необходима статья № 3 , с описанием аутентификации регистрации. В интернете полно статей на тему записных книжек и т.д. а вот как свести во едино вместе регистрацией аутентификацией и подтверждением.
Да, tema верно написал.
Если бы шаблонах джанго использовались методы регистрации и аутентификации, Админка,
А в шаблонах “внешней стороны” подключался VUE через script, то все было бы круто,
но webpack не хочем ребилдить проект заново в конечный JS после внесения изменений, как это реализовано в Laravel Mix. А руками все каждый раз пересобирать муть – какая-то .
Напишите как это правильно делается, было бы отличная статья!