habrahabr

Как сделать поиск пользователей по GitHub, используя Vue

  • пятница, 17 августа 2018 г. в 00:18:08
https://habr.com/post/420351/
  • VueJS
  • JavaScript


Думаю, все уже знают о том, как написать поиск по пользователям GitHub на React, Svelte, Angular, или вообще без них. Ну и как же тут обойтись без Vue? Самое время заполнить этот пробел.


image


Итак, сегодня мы создадим то самое приложение с использованием Vue, напишем для него тесты на Cypress и немного затронем Vue CLI 3.


В посте есть гифки


Подготовка


Для начала установим Vue CLI последней версии:


npm i -g @vue/cli

И запустим создание проекта:


vue create vue-github-search

Следуем по шагам генератора. Для нашего проекта я выбрал Manual mode и следующую конфигурацию:


image


Дополнительные модули


В качестве стилей будем использовать 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:


image


Как видим, у нас уже готова вся логика приложения! Мы вводим имя пользователя, выполняется запрос и данные профиля сохраняются в store.


Профиль пользователя


Создадим компонент User.vue и добавим логику для индикации загрузки, отображения профиля и ошибку, когда пользователь не найден. Также добавим анимацию переходов.


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 и насладимся результатом:


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>

image


Тесты


Напишем простые тесты для нашего приложения.


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 и видим, что тесты проходят:


image


Сборка


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

Как видим, новый метод действительно уменьшает размер сборки. Разница будет еще более заметна на крупных проектах, поэтому фича однозначно заслуживает внимания.


Код


GitHub


Демо


На этом все, спасибо за внимание!