Создание приложения, отображающего данные из API New York Times.
В Vue.js. есть отличная поддержка Материального дизайна. Одна из библиотек, доступных для Vue.js, - Vuetify. Его легко включить в приложение Vue.js, и результат будет привлекательным для пользователей.
В этой части мы создадим приложение, которое отображает данные из New York Times API. Вы можете зарегистрироваться для получения ключа API на https://developer.nytimes.com/. После этого мы можем приступить к созданию приложения.
Чтобы начать сборку приложения, мы должны установить Vue CLI. Мы делаем это, запустив:
npm install -g @vue/cli
Для запуска Vue CLI требуется Node.js 8.9 или новее. Мне не удалось заставить Vue CLI работать с версией Node.js. для Windows. У Ubuntu не было проблем с запуском Vue CLI.
Затем запускаем:
vue create vuetify-nyt-app
Создать папку проекта и создать файлы. В мастере вместо использования параметров по умолчанию мы выбираем Выбрать функции вручную. Мы выбираем Babel, Router и Vuex из списка опций, нажимая пробел на каждой. Если они зеленые, это означает, что они выбраны.
Теперь нам нужно установить несколько библиотек. Нам нужно установить HTTP-клиент, библиотеку для форматирования дат, одну для генерации GET
строк запроса из объектов, а другую - для проверки формы.
Также нам необходимо установить саму библиотеку Vue Material. Мы делаем это, запустив:
npm i axios moment querystring vee-validate
axios
- наш HTTP-клиент, moment
- для управления датами, querystring
- для генерации строк запроса из объектов, а vee-validate
- дополнительный пакет для Vue.js для проверки.
Затем нам нужно добавить шаблон для vuetify
. Мы делаем это, запустив vue add vuetify
. Это добавляет библиотеку и ссылки на нее в нашем приложении в соответствующие места в нашем коде.
Теперь, когда у нас установлены все библиотеки, мы можем приступить к созданию нашего приложения.
Сначала мы создаем некоторые компоненты. В папке views
создаем Home.vue
и Search.vue
. Это файлы кода для наших страниц. Затем создайте папку mixins
и файл с именем nytMixin.js
.
Миксины - это фрагменты кода, которые можно включать непосредственно в компоненты Vue.js и использовать, как если бы они были непосредственно в компоненте. Затем мы добавляем несколько фильтров.
Фильтры - это код Vue.js, который отображает одно на другое. Создаем папку filters
и добавляем capitalize.js
и formatDate.js
.
В папке components
мы создаем файл с именем SearchResults.vue
. Папка components
содержит компоненты Vue.js, которые не являются страницами.
Чтобы упростить и упорядочить передачу данных между компонентами, мы используем Vuex для управления состоянием. Поскольку мы выбрали Vuex при запуске vue create
, у нас должен быть store.js
в папке нашего проекта. Если нет, создайте его.
В store.js
мы помещаем:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { searchResults: [] }, mutations: { setSearchResults(state, payload) { state.searchResults = payload; } }, actions: { } })
В объекте state
хранится состояние. Объект mutations
- это то место, где мы можем управлять своим состоянием.
Когда мы вызываем this.$store.commit(“setSearchResults”, searchResults)
в нашем коде, учитывая, что searchResults
определено, state.searchResults
будет установлено в searchResults
.
Затем мы можем получить результат, используя this.$store.state.searchResults
.
Нам нужно добавить шаблонный код в наше приложение. Сначала мы добавляем наш фильтр. В capitalize.js
мы помещаем:
export const capitalize = (str) => { if (typeof str == 'string') { if (str == 'realestate') { return 'Real Estate'; } if (str == 'sundayreview') { return 'Sunday Review'; } if (str == 'tmagazine') { return 'T Magazine'; } return `${str[0].toUpperCase()}${str.slice(1)}`; } }
Это позволяет нам использовать заглавные буквы в названиях разделов New York Times, перечисленных на страницах для разработчиков New York Times. Затем в formatDate.js
мы помещаем:
import * as moment from 'moment'; export const formatDate = (date) => { if (date) { return moment(date).format('YYYY-MM-DD hh:mm A'); } }
Чтобы отформатировать наши даты в удобочитаемый формат.
В main.js
мы помещаем:
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import { formatDate } from './filters/formatDate'; import { capitalize } from './filters/capitalize'; import VeeValidate from 'vee-validate'; import Vuetify from 'vuetify/lib' import vuetify from './plugins/vuetify'; import '@mdi/font/css/materialdesignicons.css' Vue.config.productionTip = false; Vue.use(VeeValidate); Vue.use(Vuetify); Vue.filter('formatDate', formatDate); Vue.filter('capitalize', capitalize); new Vue({ router, store, vuetify, render: h => h(App) }).$mount('#app')
Обратите внимание, что в приведенном выше файле мы должны зарегистрировать библиотеки, которые мы используем, с Vue.js, вызвав для них Vue.use
, чтобы их можно было использовать в наших шаблонах приложений.
Мы вызываем Vue.filter
для наших функций фильтрации, чтобы мы могли использовать их в наших шаблонах, добавляя канал и имя фильтра справа от нашей переменной.
Затем в router.js
мы помещаем:
import Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue'; import Search from './views/Search.vue'; Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home }, { path: '/search', name: 'search', component: Search } ] })
Чтобы мы могли переходить на страницы, когда вводим перечисленные URL-адреса.
mode: ‘history’
означает, что у нас не будет знака решетки между базовым URL и нашими маршрутами.
Если мы развернем наше приложение, нам нужно настроить наш веб-сервер так, чтобы все запросы перенаправлялись на index.html
, чтобы у нас не было ошибок при перезагрузке приложения.
Например, в Apache мы делаем:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
А в NGINX мы помещаем:
location / {
try_files $uri $uri/ /index.html;
}
См. Документацию к вашему веб-серверу, чтобы узнать, как сделать то же самое на вашем веб-сервере.
Теперь мы пишем код для наших компонентов. В SearchResult.vue
мы помещаем:
<template> <v-container> <v-card v-for="s in searchResults" :key="s.id" class="mx-auto"> <v-card-title>{{s.headline.main}}</v-card-title> <v-list-item> <v-list-item-content>Date: {{s.pub_date | formatDate}}</v-list-item-content> </v-list-item> <v-list-item> <v-list-item-content> <a :href="s.web_url">Link</a> </v-list-item-content> </v-list-item> <v-list-item v-if="s.byline.original"> <v-list-item-content>{{s.byline.original}}</v-list-item-content> </v-list-item> <v-list-item> <v-list-item-content>{{s.lead_paragraph}}</v-list-item-content> </v-list-item> <v-list-item> <v-list-item-content>{{s.snippet}}</v-list-item-content> </v-list-item> </v-card> </v-container> </template> <script> export default { computed: { searchResults() { return this.$store.state.searchResults; } } }; </script> <style scoped> .title { margin: 0 15px !important; } #search-results { margin: 0 auto; width: 95vw; } </style>
Здесь мы получаем результаты поиска из магазина Vuex и отображаем их.
Мы возвращаем this.$store.state.searchResults
в функции свойства computed
в нашем приложении, поэтому результаты поиска будут автоматически обновляться при обновлении состояния searchResults
магазина.
md-card
- карточный виджет для отображения данных в поле. v-for
предназначен для зацикливания записей массива и отображения всего. md-list
- виджет списка для аккуратного отображения элементов в списке на странице. {{s.pub_date | formatDate}}
- это то место, где применяется наш formatDate
фильтр.
Далее мы пишем наш миксин. Мы добавим код для наших HTTP-вызовов в наш миксин.
В nytMixin.js
мы помещаем:
const axios = require('axios'); const querystring = require('querystring'); const apiUrl = 'https://api.nytimes.com/svc'; const apikey = 'your api key'; export const nytMixin = { methods: { getArticles(section) { return axios.get(`${apiUrl}/topstories/v2/${section}.json?api-key=${apikey}`); }, searchArticles(data) { let params = Object.assign({}, data); params['api-key'] = apikey; Object.keys(params).forEach(key => { if (!params[key]) { delete params[key]; } }) const queryString = querystring.stringify(params); return axios.get(`${apiUrl}/search/v2/articlesearch.json?${queryString}`); } } }
Мы возвращаем обещания для HTTP-запросов на получение статей в каждой функции. В функции searchArticles
мы сообщаем объекту, который передаем, в строку запроса, которую мы передаем в наш запрос.
Убедитесь, что вы поместили свой ключ API в свое приложение в константу apiKey
и удалили все, что не определено, с помощью:
Object.keys(params).forEach(key => { if (!params[key]) { delete params[key]; } })
В Home.vue
мы помещаем:
<template> <div> <div class="text-center" id="header"> <h1>{{selectedSection | capitalize}}</h1> <v-spacer></v-spacer> <v-menu offset-y> <template v-slot:activator="{ on }"> <v-btn color="primary" dark v-on="on">Sections</v-btn> </template> <v-list> <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)"> <v-list-item-title>{{ s | capitalize}}</v-list-item-title> </v-list-item> </v-list> </v-menu> <v-spacer></v-spacer> <v-spacer></v-spacer> </div> <v-spacer></v-spacer> <v-card v-for="a in articles" :key="a.id" class="mx-auto"> <v-card-title>{{a.title}}</v-card-title> <v-card-text> <v-list-item> <v-list-item-content>Date: {{a.published_date | formatDate}}</v-list-item-content> </v-list-item> <v-list-item> <v-list-item-content> <a :href="a.url">Link</a> </v-list-item-content> </v-list-item> <v-list-item v-if="a.byline"> <v-list-item-content>{{a.byline}}</v-list-item-content> </v-list-item> <v-list-item> <v-list-item-content>{{a.abstract}}</v-list-item-content> </v-list-item> <v-list-item> <v-list-item-content> <img v-if="a.multimedia[a.multimedia.length - 1]" :src="a.multimedia[a.multimedia.length - 1].url" :alt="a.multimedia[a.multimedia.length - 1].caption" class="image" /> </v-list-item-content> </v-list-item> </v-card-text> </v-card> </div> </template> <script> import { nytMixin } from "../mixins/nytMixin"; export default { name: "home", mixins: [nytMixin], computed: {}, data() { return { selectedSection: "home", articles: [], sections: `arts, automobiles, books, business, fashion, food, health, home, insider, magazine, movies, national, nyregion, obituaries, opinion, politics, realestate, science, sports, sundayreview, technology, theater, tmagazine, travel, upshot, world` .replace(/ /g, "") .split(",") }; }, beforeMount() { this.getNewsArticles(this.selectedSection); }, methods: { async getNewsArticles(section) { const response = await this.getArticles(section); this.articles = response.data.results; }, selectSection(section) { this.selectedSection = section; this.getNewsArticles(section); } } }; </script> <style scoped> .image { width: 100%; } .title { color: rgba(0, 0, 0, 0.87) !important; margin: 0 15px !important; } .md-card { width: 95vw; margin: 0 auto; } #header { margin-bottom: 10px; } </style>
В этом компоненте страницы мы получаем статьи для выбранного раздела, по умолчанию это раздел home
. У нас также есть меню для выбора раздела, который мы хотим видеть, добавив:
<v-menu offset-y> <template v-slot:activator="{ on }"> <v-btn color="primary" dark v-on="on">Sections</v-btn> </template> <v-list> <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)"> <v-list-item-title>{{ s | capitalize}}</v-list-item-title> </v-list-item> </v-list> </v-menu>
Обратите внимание, что мы используем ключевые слова async
и await
в нашем коде обещаний вместо использования then
.
Он намного короче, а функциональные возможности между then
, await
и async
эквивалентны. Однако он не поддерживается в Internet Explorer. В блоке beforeMount
мы запускаем this.getNewsArticles
, чтобы получать статьи по мере загрузки страницы.
Обратите внимание, что библиотека Vuetify широко использует слоты функций Vue.js. Элементы с вложенностью, такие как опора v-slot
, находятся в:
<v-menu offset-y> <template v-slot:activator="{ on }"> <v-btn color="primary" dark v-on="on">Sections</v-btn> </template> <v-list> <v-list-item v-for="(s, index) in sections" :key="index" @click="selectSection(s)"> <v-list-item-title>{{ s | capitalize}}</v-list-item-title> </v-list-item> </v-list> </v-menu>
См. Подробности в Руководстве по Vue.js.
В Search.vue
мы помещаем:
<template> <div> <form> <v-text-field v-model="searchData.keyword" v-validate="'required'" :error-messages="errors.collect('keyword')" label="Keyword" data-vv-name="keyword" required ></v-text-field> <v-menu ref="menu" v-model="toggleBeginDate" :close-on-content-click="false" transition="scale-transition" offset-y full-width min-width="290px" > <template v-slot:activator="{ on }"> <v-text-field v-model="searchData.beginDate" label="Begin Date" prepend-icon="event" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="searchData.beginDate" no-title scrollable :max="new Date().toISOString()" > <v-spacer></v-spacer> <v-btn text color="primary" @click="toggleBeginDate = false">Cancel</v-btn> <v-btn text color="primary" @click="$refs.menu.save(searchData.beginDate); toggleBeginDate = false" >OK</v-btn> </v-date-picker> </v-menu> <v-menu ref="menu" v-model="toggleEndDate" :close-on-content-click="false" transition="scale-transition" offset-y full-width min-width="290px" > <template v-slot:activator="{ on }"> <v-text-field v-model="searchData.endDate" label="End Date" prepend-icon="event" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="searchData.endDate" no-title scrollable :max="new Date().toISOString()" > <v-spacer></v-spacer> <v-btn text color="primary" @click="toggleEndDate = false">Cancel</v-btn> <v-btn text color="primary" @click="$refs.menu.save(searchData.endDate); toggleEndDate = false" >OK</v-btn> </v-date-picker> </v-menu> <v-select v-model="searchData.sort" :items="sortChoices" label="Sort By" data-vv-name="sort" item-value="value" item-text="name" > <template slot="selection" slot-scope="{ item }">{{ item.name }}</template> <template slot="item" slot-scope="{ item }">{{ item.name }}</template> </v-select> <v-btn class="mr-4" type="submit" @click="search">Search</v-btn> </form> <SearchResults /> </div> </template> <script> import { nytMixin } from "../mixins/nytMixin"; import SearchResults from "@/components/SearchResults.vue"; import * as moment from "moment"; import { capitalize } from "@/filters/capitalize"; export default { name: "search", mixins: [nytMixin], components: { SearchResults }, computed: { isFormDirty() { return Object.keys(this.fields).some(key => this.fields[key].dirty); } }, data: () => { return { searchData: { sort: "newest" }, disabledDates: date => { return +date >= +new Date(); }, sortChoices: [ { value: "newest", name: "Newest" }, { value: "oldest", name: "Oldest" }, { value: "relevance", name: "Relevance" } ], toggleBeginDate: false, toggleEndDate: false }; }, methods: { async search(evt) { evt.preventDefault(); if (!this.isFormDirty || this.errors.items.length > 0) { return; } const data = { q: this.searchData.keyword, begin_date: moment(this.searchData.beginDate).format("YYYYMMDD"), end_date: moment(this.searchData.endDate).format("YYYYMMDD"), sort: this.searchData.sort }; const response = await this.searchArticles(data); this.$store.commit("setSearchResults", response.data.response.docs); } } }; </script>
Здесь у нас есть форма для поиска статей. У нас также есть два средства выбора даты, чтобы пометить пользователей, чтобы установить даты начала и окончания. Мы ограничиваем даты только сегодняшним днем и ранее, чтобы поисковый запрос имел смысл.
В этом блоке:
<v-text-field v-model="searchData.keyword" v-validate="'required'" :error-messages="errors.collect('keyword')" label="Keyword" data-vv-name="keyword" required ></v-text-field>
Мы используем vee-validate
, чтобы проверить, заполнено ли обязательное поле ключевого слова для поиска. В противном случае отобразится сообщение об ошибке и выполнение запроса будет остановлено.
Мы также вложили наш компонент SearchResults
в компонент Search
страницы, включив:
components: { SearchResults }
Между тегом script
и <SearchResults />
в шаблоне.
Наконец, мы добавляем нашу верхнюю панель и меню, помещая в App.vue
следующее:
<template> <v-app> <v-navigation-drawer v-model="drawer" app> <v-list nav dense> <v-list-item-group v-model="group" active-class="deep-purple--text text--accent-4"> <v-list-item> <v-list-item-title>New Yourk Times Vuetify App</v-list-item-title> </v-list-item> <v-list-item> <v-list-item-title> <router-link to="/">Home</router-link> </v-list-item-title> </v-list-item> <v-list-item> <v-list-item-title> <router-link to="/search">Search</router-link> </v-list-item-title> </v-list-item> </v-list-item-group> </v-list> </v-navigation-drawer> <v-app-bar app> <v-toolbar-title class="headline text-uppercase"> <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon> <span>New York Times Vuetify App</span> </v-toolbar-title> <v-spacer></v-spacer> </v-app-bar> <v-content> <v-container fluid> <router-view /> </v-container> </v-content> </v-app> </template> <script> export default { name: "app", data: () => { return { showNavigation: false, drawer: false, group: null }; } }; </script> <style> .center { text-align: center; } form { width: 95vw; margin: 0 auto; } .md-toolbar.md-theme-default { background: #009688 !important; height: 60px; } .md-title, .md-toolbar.md-theme-default .md-icon { color: #fff !important; } </style>
Если вам нужна верхняя панель с левым ящиком навигации, вы должны точно следовать структуре кода, приведенной выше.
После того, как весь код написан, мы имеем следующее: