Продолжаем создавать наше приложение на VueJS 2, начатое в предыдущем уроке.
Имея наши настройки, сделанные ранее, запустите npm run dev
из командной строки. Запустится наш сервер, скомпилирует наши активы, просмотрит наши JavaScript-файлы для внесения изменений. Теперь, когда все настроено, давайте приступим к созданию нашего Vue-приложения. Создайте файл по адресу src/app.js и поместите в него следующее:
import Vue from 'vue';
import ExclamationsViewer from './exclamations_viewer.vue';
new Vue({
el: '#app-container',
render(createElement) {
return createElement(ExclamationsViewer);
},
});
Здесь мы импортируем Vue и компонент ExclamationsViewer, который будет создан позже. Далее мы создаем новый экземпляр Vue, а потом передаем ему объект конфигурации. Этот объект имеет атрибут el, который принимает селектор запроса для контейнера нашего приложения (в данном случае это элемент с идентификатором app-container). Мы также передаем ему метод render. Это новый способ создания шаблонов в Vue. Раньше мы передали бы атрибуту template строку, представляющая собой шаблон. Теперь же мы можем программно создавать шаблоны, как в React (React.createElement
). render передает метод createElement родительскому компоненту. Мы используем его для создания компонентов внутри родителя. В нашем случае мы просто хотим создать экземпляр ExclamationsViewer.
Давайте создадим компонент ExclamationsViewer. Создайте файл src/exclamations_viewer.vue и поместите в него следующее:
<style>
.exclamations-viewer,
.add-form-container {
margin-top: 20px;
}
</style>
<template>
<div class="container">
<div class="row exclamations-viewer">
<div class="col-md-4">
<Exclamation-List :user='user' title='All Exclamations' :exclamations='exclamations'></Exclamation-List>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import ExclamationList from './exclamation_list.vue';
export default {
name: 'ExclamationsViewer',
data: () => ({
user: {
scopes: [],
},
exclamations: [],
}),
beforeMount() {
axios.all([
axios.get('/api/me'),
axios.get('/api/exclamations'),
]).then(([{ data: meData }, { data: exclamationData }]) => {
this.user = meData.user;
this.exclamations = exclamationData.exclamations;
});
},
components: {
ExclamationList,
},
};
</script>
Здесь у нас простой компонент Vue. Поскольку мы используем vueify, мы можем разделить CSS, шаблон и скрипт в нашем компоненте на три отдельные части в одном файле. CSS окружен тегами <style></style>
. Внутри нашего шаблона мы настраиваем обычную Bootstrap-сетку и вставляем в нее еще один настраиваемый компонент - Exclamation-List
. Чтобы передать «свойства» компоненту, мы добавляем к нему атрибуты с префиксом ввиде двоеточия. Затем мы передаем ему строку, которая представляет часть данных, которые мы хотим передать компоненту. Так :user='user'
означает, что мы передаем user
в данных нашего компонента в Exclamation-List
как пользовательское свойство.
Затем в наши теги <script></script>
мы добавляем axios
и наш компонент ExclamationList
. Мы инстанцируем данные нашего компонента, установив атрибут data
в функции, которая будет вызываться для установки данных. Здесь мы просто возвращаем объект, у которого есть пользовательский объект с пустым массивом scopes
и пустым массивом exclamations
. Очень важно, чтобы все данные, которые будут использоваться, сначала создавались в этом data-объекте. В противном случае Vue не сможет эффективно отслеживать изменения в этой части данных.
Затем мы используем метод жизненного цикла, чтобы воспользоваться API и получить данные о текущем пользователе и всех репликах, после чего сохраняем эти данные в нашем компоненте. Это заменит данные, которые мы создали в методе data и Vue легко их обработает. Наконец, мы рассказываем нашему компоненту о компоненте ExclamationList добавив его к объекту components
на нашем конфигурационном объекте. Без добавления VueJS ничего не узнает об этом. Обратите внимание: мы помещаем компонент в наш объект components в PascalCase или camelCase, но мы используем его в нашем шаблоне, используя lisp-case.
Следующее, что нам нужно сделать - это создать компонент ExclamationList. Создайте файл src/exclamation_list.vue и поместите в него следующее:
<style scoped>
.exclamation-list {
background-color: #FAFAFA;
border: 2px solid #222;
border-radius: 7px;
}
.exclamation-list h1 {
font-size: 1.5em;
text-align: center;
}
.exclamation:nth-child(2) {
border-top: 1px solid #222;
}
.exclamation {
padding: 5px;
border-bottom: 1px solid #222;
}
.user {
font-weight: bold;
margin-top: 10px;
margin-bottom: 5px;
}
</style>
<template>
<div class="exclamation-list">
<h1></h1>
<div class="exclamation" v-for='exclamation in exclamations' :key='exclamation.id'>
<p class="user"></p>
<p class="text"></p>
<button v-if='canDelete(exclamation.user)' class="btn btn-danger">Remove</button>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '',
},
exclamations: {
type: Array,
default: () => ([]),
},
user: {
default: {},
},
},
methods: {
canDelete(user) {
return this.user.scopes.includes('delete') || this.user.username === user;
},
},
};
</script>
В этом компоненте у нас уже побольше CSS, но ничего сверхъестественного. В нашем шаблоне мы вывоем каждую реплику, которую передаем как props. Директива v-for
повторяет каждое высказывание и обеспечивает использование их в элементах внутри div. Здесь мы выводим имя автора и текст высказывания. Также обратите внимание, что у нас есть свойство key, переданное div, которое используется вместо свойства track-by из предыдущей версии Vue. Оно помогает Vue оптимизировать изменения в DOM, которые необходимо сделать. У нас также есть кнопка удаления, которая будет отображаться только в том случае, когда у пользователя есть возможность удаления delete
. Мы используем директиву v-if
для показа кнопки удаления. Она настраивается одинаково с выводом функции canDelete когда мы передаем ей автора высказывания. Мы добавим это в следующий компонент.
In our script portion of the component, we export an object that designates the props that will be passed to our components. Мы ожидаем, что title
будет строкой, exclamations
- массивом, а user - объектом. Затем мы создаем объект методов с функцией canDelete. Этот метод выбирает пользователя и проверяет, он ли автор высказывания и может ли он ее удалять.
Если мы посмотрим наше приложение на VueJS 2 в браузере, то увидим все наши восклицания. Если мы войдем в систему как Rachel, кнопка удаления будет отображаться для всех из них, а вот Ross сможет видеть кнопку удаления только для тех высказываний, которые сделал он сам.
Раз у нас есть кнопка удаления, давайте сделаем чтобы она работала! Поскольку нашему родительскому компоненту принадлежат все данные, мы передадим функцию дочернему компоненту, который будет удалять реплику из API и локальные данные в родительском компоненте.
В ExclamationsViewer добавьте следующее:
methods: {
onExclamationRemoved(id) {
axios.delete(`/api/exclamations/${id}`)
.then(() => {
this.exclamations = this.exclamations.filter(e => e.id !== id);
});
},
},
Мы добавили объект methods, содержащий метод onExclamationRemoved, который принимает ID реплики. Он отправляет запрос DELETE
в API, а затем фильтрует реплики из локальных данных. Он автоматически обновится во всех компонентах с помощью этого отрывка данных. Теперь нам нужно передать этот метод нашему дочернему компоненту. Обновите шаблон:
<Exclamation-List
:user='user'
:onRemove='onExclamationRemoved'
title='All Exclamations'
:exclamations='exclamations'></Exclamation-List>
Мы передаем его как свойство onRemove
. Теперь давайте добавим это свойство в список в компоненте ExclamationList.
props: {
...
onRemove: {
default: () => {},
},
...
},
Мы также добавляем метод к нашему объекту methods:
methods: {
onRemoveClicked(id) {
this.onRemove(id);
},
...
}
Он автоматически возьмет свойство onRemove и передает ему идентификатор реплики. Теперь мы можем использовать его в нашем шаблоне ExclamationList.
<button v-on:click='onRemoveClicked(exclamation.id)'
v-if='canDelete(exclamation.user)'
class="btn btn-danger">Remove</button>
Здесь мы добавили директиву v-on
с модификатором click. Это очень хороший способ сказать Vue удалить выражение, когда этот элемент кликнут. Мы вызываем onRemoveClicked и передаем ей реплику. Она, в свою очередь, передается родительскому элементу, который удалит ее через API, а затем из локальных данных.
Теперь, если вы попытаетесь удалить реплику, она исчезнет из списка. Если вы обновите страницу, она не вернется!
Чтобы в полной мере оценить все фишки, создадим еще один список. На этот раз это будет список, который содержит только те высказывания, которые принадлежат пользователю. Добавьте в компонент ExclamationsViewer этот код:
<div class="col-md-4">
<Exclamation-List
:user='user'
:onRemove='onExclamationRemoved'
title='Your Exclamations'
:exclamations='userExclamations'>
</Exclamation-List>
</div>
Здесь мы добавляем другой список, но обратите внимание, что мы только передаем userExclamations как свойство высказываний. userExclamations является вычисленным свойством. Это концепция Vue позволяющая запускать функцию, используемую как обычная переменная в шаблоне, но вычисляемую лишь один раз, если данные, которые она использует, не меняются. Мы вычислим userExclamations отфильтровав текущий список реплик. Это позволит нам работать с обоими списками по отдельности и нам не нужно будет вообще изменять исходный список. В конфигурацию объекта компонента ExclamationsViewer добавьте следующий фрагмент кода:
computed: {
userExclamations() {
return this.exclamations.filter(exc => exc.user === this.user.username);
},
},
Теперь, если вы просмотрите наше приложение в браузере, вы увидите, что у нас есть два списка. Новый показывает только реплики, принадлежащие зарегистрированному пользователю.
Вся сила нашего кода - в компонентах, поскольку мы можем легко повторно использовать каждый компонент в нескольких местах.
Когда реплик много, очень сложно найти нужную. Давайте добавим список, в котором также есть окно поиска. Мы реализуем это в новом компоненте. Этот новый компонент будет повторно использовать текущий ExclamationList
. Создайте файл src/exclamation_search_list.vue и напишите в нем следующее:
<template>
<div>
<div class="input-container">
<div class="form-group">
<label for='searchTerm'>Search:</label>
<input v-model='searchTerm' type="text" class='form-control' placeholder="Search term">
</div>
</div>
<Exclamation-List :user='user' :onRemove='onRemove' title='Filtered Exclamations' :exclamations='exclamationsToShow'></Exclamation-List>
</div>
</template>
<script>
import ExclamationList from './exclamation_list.vue';
export default {
data() {
return {
searchTerm: '',
};
},
props: {
exclamations: {
type: Array,
default: () => ([]),
},
onRemove: {
default: () => {},
},
user: {
default: {},
},
},
computed: {
exclamationsToShow() {
let filteredExclamations = this.exclamations;
this.searchTerm.split(' ')
.map(t => t.split(':'))
.forEach(([type, query]) => {
if (!query) return;
if (type === 'user') {
filteredExclamations = filteredExclamations.filter(e => e.user.match(query));
} else if (type === 'contains') {
filteredExclamations = filteredExclamations.filter(e => e.text.match(query));
}
});
return filteredExclamations;
},
},
components: {
ExclamationList,
},
};
</script>
Здесь у нас есть шаблон, который состоит из формы и ExclamationList. В форме нет особого особенного. Это всего лишь ярлык label и input для текстового ввода. Однако, обратите внимание на атрибут v-model
в input. Строка, переданная ему (в данном случае «searchTerm»), соответствует части данных, которые мы хотим сохранить в синхронизации со значением этого ввода. Если мы изменим значение ввода, введя его, Vue будет обновлять часть данных по этому компоненту. Затем мы можем использовать этот фрагмент данных для фильтрации отображаемых реплик.
Чтобы отфильтровать реплики, мы будем использовать другое вычисленное свойство. Фильтрация в текстовом поле позволяет фильтровать данные по пользователю или текстовому содержимому. Если мы хотим отфильтровать данные по пользователям, мы пишем user:searchTerm
, где searchTerm
- пользователь, для которого мы фильтруем реплики. Если мы хотим фильтровать по текстовому контенту, мы пишем contains:searchTerm
. Мы разбиваем searchTerm
на подстроки, используя пробел в качестве разделителя, раз мы можем использовать несколько фильтров, а затем разделяем каждую подстроку на новые подстроки с помощью двоеточия. Так мы получаем тип и запрос. Наконец, мы перебираем каждый тип, отфильтровываем реплики и возвращаем результат.
Посмотрите в шаблон, куда мы добавили ExclamationList
. Обратите внимание, как мы pass the same props to it as we passed to this component except for passing the exclamationsToShow
as the exclamations. Поскольку мы фильтруем их в этом компоненте, они будут меняться в ExclamationList
.
Взгляните на нашу работу в браузере! Хм.. ничего нет! Дело в том, что мы создали компонент, но не добавили его в ExclamationsViewer
. Откройте этот файл и добавьте ExclamationSearchList
в шаблон, импортируйте его в сценарий, а затем добавьте его в объект компонентов.
<template>
...
<div class="col-md-4">
<Exclamation-Search-List :user='user' :onRemove='onExclamationRemoved' :exclamations='exclamations'></Exclamation-Search-List>
</div>
...
</template>
<script>
...
import ExclamationSearchList from './exclamation_search_list.vue';
...
components: {
ExclamationList,
ExclamationSearchList,
},
...
</script>
Теперь просмотрите еще раз в браузер! У нас появился третий столбец со списком реплик со строкой ввода, которая позволяет нам фильтровать то, что мы видим. Попробуйте ввести текст user:rac
в нем. При этом должно остаться только реплика Rachel.
Осталось только одно: форма добавления высказывания. Создайте файл src/exclamation_add_form.vue и напишите в нем следующее:
<template>
<form class="form-inline" v-on:submit.prevent='onFormSubmit'>
<div class="form-group">
<label for='exclamationText'>Exclamation</label>
<textarea cols="30" rows="2" class="form-control" placeholder="Enter exclamation here." v-model='exclamationText'></textarea>
</div>
<input type="submit" value="Submit" class="btn btn-success">
</form>
</template>
<script>
export default {
data() {
return {
exclamationText: '',
};
},
props: ['onAdd'],
methods: {
onFormSubmit() {
this.onAdd(this.exclamationText);
this.exclamationText = '';
},
},
};
</script>
Это довольно простой компонент. Шаблон в нем - обычная форма с текстовым полем и кнопкой отправки. В этой форме есть директива v-on
. v-on
- это способ добавления слушателей событий к элементам в Vue. Мы используем :submit
для прослушивания события submit. .prevent
будет автоматически запускать preventDefault
на событии, поэтому не нужно делать это вручную. В нашем скрипте мы отслеживаем текст в текстовом поле exclamationText piece of data. Мы принимаем от родителя функцию onAdd и метод, который вызывается при отправке формы. При этом функция onAdd вызывается, передавая его к exclamationText, а затем опустошая exclamationText.
Мы должны добавить к нашему ExclamationsViewer следующее:
<template>
<div class="container">
<div class="row add-form-container" v-if='canAdd()'>
<div class="col-md-12">
<Exclamation-Add-Form :onAdd='onExclamationAdded'></Exclamation-Add-Form>
</div>
</div>
<div class="row exclamations-viewer">
...
</template>
<script>
import ExclamationAddForm from './exclamation_add_form.vue';
...
methods: {
onExclamationAdded(text) {
axios.post('/api/exclamations', { text }).then(({ data }) => {
this.exclamations = [data.exclamation].concat(this.exclamations);
});
},
canAdd() {
return this.user.scopes.includes('add');
},
onExclamationRemoved(id) {
...
components: {
...
ExclamationAddForm,
...
},
</script>
Мы добавили форму добавления в наш шаблон и передали его свойству onAdd. Мы также используем условие v-if
для показа формы. Она будет показана, только если пользователь имеет возможность добавления. Мы добавили методы onExclamationAdded и canAdd в наш конфигурационный объект, наряду с добавлением компонента в объект components. onExclamationAdded отправляет текст как POST-запрос API. Затем он добавляет реплику, которая возвращается в массив с репликами. К счастью, все остальное обновляется, чтобы показать нашу новую реплику. Ура!
Если мы посмотрим наше приложение в браузере, то увидим, что теперь мы можем добавить реплику. Если вы обновите страницу после добавления, реплика все равно останется!
Дополнительно: Использование Auth0 с вашим приложением на Vue.js
Auth0 выпускает JSON Web Tokens на каждый логин для ваших пользователей. Это означает, что у вас может быть Инфраструктура идентификации, включающая single sign-on(разовый вход), управление пользователем, поддержку соц.сетей (Facebook, Github, Twitter и прочие), протоколов аутентификации (Active Directory, LDAP, SAML и прочие) и вашу собственную базу данных пользователей с помощью всего нескольких строк кода.
Мы можем легко настроить аутентификацию в наших Vue.js приложениях, используя Lock Widget.
Шаг 1: Добавляем Auth0 Виджет Блокировки
<!-- index.html -->
...
<!-- Auth0 Lock script -->
<script src="http://cdn.auth0.com/js/lock/10.0/lock.min.js"></script>
...
Шаг 2: Запускаем блокировку в index.js
// src/index.js
...
// Создание экземпляра блокировки
export var lock = new Auth0Lock(YOUR_CLIENT_ID, YOUR_CLIENT_DOMAIN)
...
Шаг 3: Вызвов Lock Widget из компонента Vue.js
<!-- src/components/Login.vue -->
<template>
<div class="col-sm-4 col-sm-offset-4">
<h2>Log In</h2>
<p>Log In with Auth0's Lock Widget.</p>
<button class="btn btn-primary" @click="login()">Log In</button>
</div>
</template>
<script>
// Импорт экземпляра блокировки
import {lock} from '../index'
export default {
ready() {
lock.on("authenticated", function(authResult) {
lock.getProfile(authResult.idToken, function(error, profile) {
if (error) {
// Ошибка обработки
return;
}
localStorage.setItem('profile', JSON.stringify(profile))
localStorage.setItem('id_token', authResult.idToken)
});
});
},
methods: {
login() {
// Показать блокировку виджета
lock.show();
},
logout() {
// Удалить профиль и токен из localStorage
localStorage.removeItem('profile');
localStorage.removeItem('id_token');
}
}
}
</script>
Мы рассмотрели лишь малую толику того, что и как можно делать с помощью Vue 2. Следите за нашими уроками по Vue.js