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