Думаю, все уже знают о том, как написать поиск по пользователям 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
Все изменения в коде будут мгновенно применяться в браузере без перезагрузки страницы.
Store
Начнём с того, что напишем 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
Как видим, новый метод действительно уменьшает размер сборки. Разница будет еще более заметна на крупных проектах, поэтому фича однозначно заслуживает внимания.
Код
На этом все, спасибо за внимание!
Комментарии (8)
nkozhevnikov
16.08.2018 13:58Писать статьи о том, как сделать поиск пользователей по GitHub'у — тренд этого лета?
PaulMaly
16.08.2018 18:16+1Ништяк! Следующая статья будет: «Как сделать поиск пользователей по Github на React, не используя React». Ну и дальше серия статей, где React будет заменён на Vue и Angular))))
IonianWind
lodash.debounce можно отдельным пакетом поставить, будет оптимальнее.
staticlab
Оптимальнее только в расходовании места на диске разработчика. Если же в перспективе из лодеша понадобятся и другие функции, то способ автора будет оптимальнее в плане объёма бандла.