Думаю, все уже знают о том, как написать поиск по пользователям 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


Демо


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

Комментарии (8)


  1. IonianWind
    16.08.2018 12:56

    lodash.debounce можно отдельным пакетом поставить, будет оптимальнее.


    1. staticlab
      16.08.2018 15:06

      Оптимальнее только в расходовании места на диске разработчика. Если же в перспективе из лодеша понадобятся и другие функции, то способ автора будет оптимальнее в плане объёма бандла.


  1. nkozhevnikov
    16.08.2018 13:58

    Писать статьи о том, как сделать поиск пользователей по GitHub'у — тренд этого лета?


    1. Daar
      16.08.2018 15:05

      Ну Todo уже всех достал :)


      1. staticlab
        16.08.2018 15:09

        У Todo App главный недостаток, как мне кажется, — не демонстрируется асинхронное взаимодействие с сервером.


  1. PaulMaly
    16.08.2018 18:16
    +1

    Ништяк! Следующая статья будет: «Как сделать поиск пользователей по Github на React, не используя React». Ну и дальше серия статей, где React будет заменён на Vue и Angular))))


  1. stalkerg
    17.08.2018 05:26

    Неплохо, пока для меня на первом месте Svelte а потом реализация на Vue. Тупо за простоту.


    1. PaulMaly
      17.08.2018 09:26

      Хотя автор и постарался усложнить ее как только мог)))