Django + Vue. Как создать и обработать API. Часть 2

В предыдущей части урока мы написали бэкэнд для нашего приложения с заметками. В этом уроке мы продолжим, и напишем фронтэнд часть, используя фреймворк 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

Кроме стандартных зависимостей, нам еще в дальнейшем понадобится:

  1. HTML препроцессор pug
  2. Axios для отправки HTTP запросов к нашему API
  3. 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

8 Comments

  1. Антон March 4, 2019 at 11:46 am

    Привет! Спасибо за пособие!

    Тут опечатка:
    $ 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

    Reply
    1. apirobot March 4, 2019 at 12:04 pm

      Спасибо. Исправил 😉

      Reply
  2. Egor April 3, 2019 at 9:12 am

    Спасибо за отличное пособие, как использовать эту связку на простом примере. Круто 👍

    Reply
  3. corner May 15, 2019 at 12:17 pm

    Спасибо! Как же хочется научиться так делать самому…

    Reply
  4. eleroy August 5, 2019 at 10:57 am

    Спасибо за пособие! Может ещё сделаете пособие, как это всё задеплоить? Например на хероку.

    Reply
    1. apirobot August 5, 2019 at 11:49 am

      Привет. Может когда-нибудь напишу, но пока в планах нет.

      Reply
  5. tema May 7, 2021 at 5:09 am

    Необходима статья № 3 , с описанием аутентификации регистрации. В интернете полно статей на тему записных книжек и т.д. а вот как свести во едино вместе регистрацией аутентификацией и подтверждением.

    Reply
  6. GennadyS May 18, 2021 at 3:03 pm

    Да, tema верно написал.
    Если бы шаблонах джанго использовались методы регистрации и аутентификации, Админка,
    А в шаблонах “внешней стороны” подключался VUE через script, то все было бы круто,
    но webpack не хочем ребилдить проект заново в конечный JS после внесения изменений, как это реализовано в Laravel Mix. А руками все каждый раз пересобирать муть – какая-то .
    Напишите как это правильно делается, было бы отличная статья!

    Reply

Leave A Comment

Your email address will not be published. Required fields are marked *