Как сделать поиск пользователей по GitHub, используя Vue
- пятница, 17 августа 2018 г. в 00:18:08
Думаю, все уже знают о том, как написать поиск по пользователям GitHub на React, Svelte, Angular, или вообще без них. Ну и как же тут обойтись без Vue? Самое время заполнить этот пробел.
Итак, сегодня мы создадим то самое приложение с использованием Vue, напишем для него тесты на Cypress и немного затронем Vue CLI 3.
В посте есть гифки
Для начала установим Vue CLI последней версии:
npm i -g @vue/cli
И запустим создание проекта:
vue create vue-github-search
Следуем по шагам генератора. Для нашего проекта я выбрал Manual mode и следующую конфигурацию:
В качестве стилей будем использовать Stylus, поэтому нам понадобятся stylus и stylus-loader. Ещё нам понадобится Axios для запросов в сеть и Lodash, из которого мы возьмем функцию debounce.
Перейдем в папку проекта и установим необходимые пакеты:
cd vue-github-search
npm i stylus stylus-loader axios lodash
Запускаем проект и убеждаемся что все работает:
npm run serve
Все изменения в коде будут мгновенно применяться в браузере без перезагрузки страницы.
Начнём с того, что напишем vuex store, где будут все данные приложения. Хранить нам нужно всего-то ничего: поисковый запрос, данные пользователя и флаг процесса загрузки.
Откроем store.js
и опишем изначальное состояние приложения и необходимые мутации:
...
const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
const SET_LOADING = 'SET_LOADING';
const SET_USER = 'SET_USER';
const RESET_USER = 'RESET_USER';
export default new Vuex.Store({
state: {
searchQuery: '',
loading: false,
user: null
},
mutations: {
[SET_SEARCH_QUERY]: (state, searchQuery) => state.searchQuery = searchQuery,
[SET_LOADING]: (state, loading) => state.loading = loading,
[SET_USER]: (state, user) => state.user = user,
[RESET_USER]: state => state.user = null
}
});
Добавим action'ы для загрузки данных с GitHub API и для изменения поискового запроса (понадобиться нам для поисковой строки). В итоге наш store примет такой вид:
store.js
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
const SET_LOADING = 'SET_LOADING';
const SET_USER = 'SET_USER';
const RESET_USER = 'RESET_USER';
export default new Vuex.Store({
state: {
searchQuery: '',
loading: false,
user: null
},
mutations: {
[SET_SEARCH_QUERY]: (state, searchQuery) => state.searchQuery = searchQuery,
[SET_LOADING]: (state, loading) => state.loading = loading,
[SET_USER]: (state, user) => state.user = user,
[RESET_USER]: state => state.user = null
},
actions: {
setSearchQuery({commit}, searchQuery) {
commit(SET_SEARCH_QUERY, searchQuery);
},
async search({commit, state}) {
commit(SET_LOADING, true);
try {
const {data} = await axios.get(`https://api.github.com/users/${state.searchQuery}`);
commit(SET_USER, data);
} catch (e) {
commit(RESET_USER);
}
commit(SET_LOADING, false);
}
}
});
Создадим новый компонент Search.vue
в папке components
. Добавим computed-свойство, чтобы связать компонент со store. При изменениях поискового запроса будем вызывать поиск с debounce.
Search.vue
<template>
<input v-model="query" @input="debouncedSearch" placeholder="Enter username" />
</template>
<script>
import {mapActions, mapState} from 'vuex';
import debounce from 'lodash/debounce';
export default {
name: 'search',
computed: {
...mapState(['searchQuery']),
query: {
get() {
return this.searchQuery;
},
set(val) {
return this.setSearchQuery(val);
}
}
},
methods: {
...mapActions(['setSearchQuery', 'search']),
debouncedSearch: debounce(function () {
this.search();
}, 500)
}
};
</script>
<style lang="stylus" scoped>
input
width 100%
font-size 16px
text-align center
</style>
Теперь подключим нашу строку поиска в главный компонент App.vue
и попутно удалим лишние строки, созданные генератором.
App.vue
<template>
<div id="app">
<Search />
</div>
</template>
<script>
import Search from './components/Search';
export default {
name: 'app',
components: {
Search
}
};
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
font-smoothing antialiased
margin 10px
</style>
Посмотрим результат в браузере, убедившись что всё работает с помощью vue-devtools:
Как видим, у нас уже готова вся логика приложения! Мы вводим имя пользователя, выполняется запрос и данные профиля сохраняются в store.
Создадим компонент User.vue
и добавим логику для индикации загрузки, отображения профиля и ошибку, когда пользователь не найден. Также добавим анимацию переходов.
<template>
<div class="github-card">
<transition name="fade" mode="out-in">
<div v-if="loading" key="loading">
Loading
</div>
<div v-else-if="user" key="user">
<div class="background" :style="{backgroundImage: `url(${user.avatar_url})`}" />
<div class="content">
<a class="avatar" :href="`https://github.com/${user.login}`" target="_blank">
<img :src="user.avatar_url" :alt="user.login" />
</a>
<h1>{{user.name || user.login}}</h1>
<ul class="status">
<li>
<a :href="`https://github.com/${user.login}?tab=repositories`" target="_blank">
<strong>{{user.public_repos}}</strong>
<span>Repos</span>
</a>
</li>
<li>
<a :href="`https://gist.github.com/${user.login}`" target="_blank">
<strong>{{user.public_gists}}</strong>
<span>Gists</span>
</a>
</li>
<li>
<a :href="`https://github.com/${user.login}/followers`" target="_blank">
<strong>{{user.followers}}</strong>
<span>Followers</span>
</a>
</li>
</ul>
</div>
</div>
<div v-else key="not-found">
User not found
</div>
</transition>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
name: 'User',
computed: mapState(['loading', 'user'])
};
</script>
<style lang="stylus" scoped>
.github-card
margin-top 50px
padding 20px
text-align center
background #fff
color #000
position relative
h1
margin 16px 0 20px
line-height 1
font-size 24px
font-weight 500
.background
filter blur(10px) opacity(50%)
z-index 1
position absolute
top 0
left 0
right 0
bottom 0
background-size cover
background-position center
background-color #fff
.content
position relative
z-index 2
.avatar
display inline-block
overflow hidden
background #fff
border-radius 100%
text-decoration none
img
display block
width 80px
height 80px
.status
background white
ul
text-transform uppercase
font-size 12px
color gray
list-style-type none
margin 0
padding 0
border-top 1px solid lightgray
border-bottom 1px solid lightgray
zoom 1
&:after
display block
content ''
clear both
li
width 33%
float left
padding 8px 0
box-shadow 1px 0 0 #eee
&:last-of-type
box-shadow none
strong
display block
color #292f33
font-size 16px
line-height 1.6
a
color #707070
text-decoration none
&:hover
color #4183c4
.fade-enter-active, .fade-leave-active
transition opacity .5s
.fade-enter, .fade-leave-to
opacity 0
</style>
Подключим наш компонент в App.vue
и насладимся результатом:
<template>
<div id="app">
<Search />
<User />
</div>
</template>
<script>
import Search from './components/Search';
import User from './components/User';
export default {
name: 'app',
components: {
User,
Search
}
};
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
font-smoothing antialiased
margin 10px
</style>
Напишем простые тесты для нашего приложения.
tests/e2e/specs/test.js
describe('Github User Search', () => {
it('has input for username', () => {
cy.visit('/');
cy.get('input');
});
it('has "User not found" caption', () => {
cy.visit('/');
cy.contains('User not found');
});
it("finds Linus Torvalds' GitHub page", () => {
cy.visit('/');
cy.get('input').type('torvalds');
cy.contains('Linus Torvalds');
cy.get('img');
cy.contains('span', 'Repos');
cy.contains('span', 'Gists');
cy.contains('span', 'Followers');
});
it("doesn't find nonexistent page", () => {
cy.visit('/');
cy.get('input').type('_some_random_name_6m92msz23_2');
cy.contains('User not found');
});
});
Запустим тесты командой
npm run test:e2e
В открывшемся окне нажимаем кнопку Run all specs и видим, что тесты проходят:
Vue CLI 3 поддерживает новый режим сборки приложения, modern mode. Он создает 2 версии скриптов: облегченную для современных браузеров, которые поддерживают последние фичи JavaScript, и полную версию со всеми необходимыми полифилами для более старых. Главная прелесть заключается в том, что нам абсолютно не нужно заморачиваться с деплоем такого приложения. Это просто работает. Если браузер поддерживает <script type="module">
, он сам подтянет облегченную сборку. Как это работaет, можно подробнее почитать в этой статье.
Добавим в package.json
флаг modern к команде сборки:
"build": "vue-cli-service build --modern"
Собираем проект:
npm run build
Посмотрим на размеры итоговых скриптов:
8.0K ./app-legacy.cb7436d4.js
8.0K ./app.b16ff4f7.js
116K ./chunk-vendors-legacy.1f6dfb2a.js
96K ./chunk-vendors.a98036c9.js
Как видим, новый метод действительно уменьшает размер сборки. Разница будет еще более заметна на крупных проектах, поэтому фича однозначно заслуживает внимания.
На этом все, спасибо за внимание!