Простое руководство с примерами
Vue.js - популярный интерфейсный фреймворк для создания одностраничных приложений. Он обеспечивает структуру и абстракцию. Приложения структурированы путем разделения частей приложений на компоненты. Связывание данных также является важным преимуществом использования Vue.js. У каждого компонента своя логика, шаблон для отображения вещей и стиль. Он также обеспечивает дополнительную маршрутизацию и хранилище потоков. Bootstrap - популярный фреймворк, который предоставляет набор стилизованных виджетов, которые хорошо выглядят, сокращая время на разработку. Vue.js делает создание интерфейсных приложений очень простым и приятным.
Bootstrap создан Twitter для стилизации собственных элементов пользовательского интерфейса. Он предоставляет виджеты в HTML, CSS и простом JavaScript. Разработчики создали надстройку Bootstrap для Vue.js, которая обеспечивает все те же преимущества Bootstrap в сочетании с преимуществами Vue.js, такими как привязка данных. Версия Bootstrap для Vue.js, называемая Vue-Bootstrap, поставляется в виде набора компонентов.
Чтобы использовать BootstrapVue, мы импортируем виджеты в наше приложение Vue и ссылаемся на компоненты BootstrapVue в наших шаблонах. Полный список компонентов доступен, а также есть несколько директив для модификации существующих элементов.
В этой истории мы собираемся создать приложение, использующее Meal API.
Создание приложения
Создать Vue.js с помощью Bootstrap очень просто. Для начала нам понадобится Vue CLI. Устанавливаем, запустив npm install -g @vue/cli
. Нам нужно инициализировать проект. Запустите vue create meal-app
. После выполнения команды вы можете перейти в папку meal-app
и начать писать приложение.
Теперь мы можем приступить к созданию приложения. Запускаем vue serve
, чтобы запустить сервер разработки. Он будет обновляться каждый раз, когда мы меняем код в папке нашего проекта.
Нам нужно установить несколько зависимостей, таких как BootstrapVue и HTTP-клиент, чтобы мы могли отправлять и получать данные с приложением. Для этого запускаем:
npm i bootstrap bootstrap-vue superagent vee-validate vue-router
Нам нужны эти библиотеки, потому что мы используем Bootstrap для создания нашего приложения. bootstrap bootstrap-vue
восполняет эту потребность. superagent
- это выбранный нами HTTP-клиент. vee-validate
- это библиотека для проверки форм Vue.js. vue-router
используется для маршрутизации URL-адресов, которые пользователь вводит в наши компоненты, чтобы наши компоненты были видны.
Теперь у нас есть все необходимое для написания кода. Начнем с App.vue
, где добавим:
<template> <div id="app"> <nav-bar></nav-bar> <div id='router-view'> <router-view/> </div> </div> </template> <script> export default { name: "app" }; </script> <style> #app { font-family: "Avenir", Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } #router-view { padding: 20px 0px; margin: 0 auto; } </style>
Это основная составляющая нашей страницы. Он отображает материал, к которому маршрутизатор направляет в <router-view/>
, а также нашу панель навигации, которую мы создадим.
Далее в main.js
мы помещаем:
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' router.beforeEach((to, from, next) => { document.title = to.meta.title; next() }) let APIURL = 'http://mealapi.jauyeung.net/index.php/'; Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, template: '<App/>', components: { App } }) export { APIURL };
Мы помещаем заголовок нашей страницы в этот блок:
router.beforeEach((to, from, next) => { document.title = to.meta.title; next() })
Нам нужно вызвать next()
, чтобы загрузился следующий маршрут.
Маршрутизатор Vue отслеживает изменения в URL-адресе и соответственно устанавливает заголовок.
Блок ниже - это запись для нашего приложения.
new Vue({ el: '#app', router, template: '<App/>', components: { App } })
Мы помещаем наше приложение в элемент с ID app
. Теперь добавляем некоторые компоненты в папку components
. Если его там нет, создайте папку.
Затем в папке создаем следующие файлы:
Filter.vue FilterRow.vue Latest.vue MealRow.vue NavBar.vue Random.vue Search.vue
В роутере мы создаем index.js
и вводим:
import Vue from 'vue' import Router from 'vue-router' import Latest from '@/components/Latest' import Random from '@/components/Random' import Search from '@/components/Search' import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' import VeeValidate from 'vee-validate'; import MealRow from '../components/MealRow' import NavBar from '../components/NavBar' import Filter from '../components/Filter' import FilterRow from '../components/FilterRow' Vue.component('meal-row', MealRow); Vue.component('nav-bar', NavBar); Vue.component('filter-row', FilterRow); Vue.use(Router); Vue.use(BootstrapVue); Vue.use(VeeValidate); export default new Router({ routes: [ { path: '/', name: 'latest', component: Latest, meta: { title: 'Home' } }, { path: '/random', name: 'random', component: Random, meta: { title: 'Random' } }, { path: '/search', name: 'search', component: Search, meta: { title: 'Search' } }, { path: '/search/:keyword', name: 'search-keyword', component: Search, meta: { title: 'Search' } }, { path: '/search/:type/:keyword', name: 'filter-keyword', component: Filter, meta: { title: 'Filter' } } ] })
Это определяет все наши маршруты и надстройки Vue, которые мы используем с Vue.use
. В следующем блоке мы регистрируем компоненты, которые мы вкладываем в наши шаблоны:
Vue.component('meal-row', MealRow); Vue.component('nav-bar', NavBar); Vue.component('filter-row', FilterRow);
У нас будет страница поиска, страница для отображения случайных рецептов и страница для отображения последнего рецепта из Meal API.
В Filter.vue
мы вводим:
<template> <div class="container"> <div class='row'> <div class="col-12"> <h1> <span v-if="$route.params.type == 'category'">Category - </span> <span v-if="$route.params.type == 'area'">Area - </span> {{$route.params.keyword}} </h1> <div v-for="meal in meals" v-bind:key='meal.idMeal'> <filter-row :meal='meal'></filter-row> </div> </div> </div> </div> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; import MealRow from "./MealRow"; export default { name: "search", data() { return { keyword: "", meals: [] }; }, methods: { searchByParam(type, keyword) { request.get(`${APIURL}filter/${type}/${keyword}`).end((err, res) => { this.meals = res.body.meals; if (!res.body.meals){ return; } this.meals = this.meals.map(m => { m.ingredients = []; m.measures = []; for (let key in m) { if (key.includes("strIngredient")) { let index = +key.replace("strIngredient", ""); m.ingredients[index] = m[key]; } if (key.includes("strMeasure")) { let index = +key.replace("strMeasure", ""); m.measures[index] = m[key]; } } m.ingredients = m.ingredients.filter(i => { return i; }); m.measures = m.measures.filter(m => { return m; }); return m; }); }); } }, beforeMount() { if (this.$route.params.keyword) { this.searchByParam(this.$route.params.type, this.$route.params.keyword); } }, beforeRouteUpdate(to, from, next) { this.searchByParam(to.params.type, to.params.keyword); next(); } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>
Отсюда мы получаем результаты поиска из API. Мы инициируем запрос к Meals API и возвращаем результаты. Результаты возвращаются в шаблоны с помощью функции привязки данных Vue и отображаются путем перебора результатов с помощью директивы v-for
. Директивы - это части кода Vue.js, которые изменяют существующий элемент.
Например, когда вы переходите к /#/search/chicken
, ключевое слово chicken
принимается this.$route.params.keyword
переменной и запускается функция searchByParam
для выполнения поиска.
FilterRow.vue
- дочерний компонент Filter.vue
. К нему добавляем следующее:
<template> <div class="row"> <div class="col"> <h1>{{meal.strMeal}}</h1> <img :src="meal.strMealThumb" @click="showModal" class="meal-thumb"> <b-modal ref="mealModal" hide-footer :title="meal.strMeal"> <div class="row"> <div class="col"> <h1>{{meal.strMeal}}</h1> <p> <b>Area:</b> {{meal.strArea}}</p> <p> <b>Category:</b> {{meal.strCategory}}</p> <img :src="meal.strMealThumb" class="meal-photo"> <table class="table"> <thead> <tr> <th>Ingredient</th> <th>Measure</th> </tr> </thead> <tbody> <tr v-for="(ingredient, index) in meal.ingredients" v-bind:key="index"> <td>{{meal.ingredients[index]}}</td> <td>{{meal.measures[index]}}</td> </tr> </tbody> </table> </div> </div> <b-btn class="mt-3" variant="outline-danger" block @click="hideModal">Close</b-btn> </b-modal> </div> </div> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; export default { name: "meal-row", props: ["meal"], data() { return {}; }, methods: { showModal() { this.$refs.mealModal.show(); this.getMeal(this.meal.idMeal); }, hideModal() { this.$refs.mealModal.hide(); }, getMeal(id) { request.get(`${APIURL}lookup/${id}`).end((err, res) => { this.meal = res.body.meals[0]; this.meal.ingredients = []; this.meal.measures = []; for (let key in this.meal) { if (key.includes("strIngredient")) { let index = +key.replace("strIngredient", ""); this.meal.ingredients[index] = this.meal[key]; } if (key.includes("strMeasure")) { let index = +key.replace("strMeasure", ""); this.meal.measures[index] = this.meal[key]; } } this.meal.ingredients = this.meal.ingredients.filter(i => { return i; }); this.meal.measures = this.meal.measures.filter(m => { return m; }); }); } }, computed: { computedClass: function() { if (this.meal != null) { return this.meal; } return {}; } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .meal-photo { width: 100%; } .meal-thumb { cursor: pointer; width: 100%; } </style>
Свойство meal
- это то место, где мы передаем объект для отображения FilterRow
компонента. b-modal
- это модальное окно BootstrapVue, которое мы используем для отображения всплывающего окна с данными рецепта. Мы можем скрыть модальное окно, вызвав this.$refs.mealModal.hide()
. request.get
из библиотеки superagent
. Он возвращает обещание, где мы можем связать then
функцию после нее, чтобы запустить дополнительный код после выполнения обещания.
В Latest.vue
мы помещаем:
<template> <div class="container"> <div class="row"> <div class="col"> <h1>Today's Meal: {{meal.strMeal}}</h1> <p>Area: {{meal.strArea}}</p> <p>Category: {{meal.strCategory}}</p> <img :src="meal.strMealThumb" class="meal-photo"> <table class="table"> <thead> <tr> <th>Ingredient</th> <th>Measure</th> </tr> </thead> <tbody> <tr v-for="(ingredient, index) in meal.ingredients" v-bind:key="index"> <td>{{meal.ingredients[index]}}</td> <td>{{meal.measures[index]}}</td> </tr> </tbody> </table> </div> </div> </div> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; export default { name: "latest", data() { return { meal: {} }; }, methods: {}, beforeMount: function() { request.get(`${APIURL}latest`).end((err, res) => { this.meal = res.body.meals[0]; this.meal.ingredients = []; this.meal.measures = []; for (let key in this.meal) { if (key.includes("strIngredient")) { let index = +key.replace("strIngredient", ""); this.meal.ingredients[index] = this.meal[key]; } if (key.includes("strMeasure")) { let index = +key.replace("strMeasure", ""); this.meal.measures[index] = this.meal[key]; } } this.meal.ingredients = this.meal.ingredients.filter(i => { return i; }); this.meal.measures = this.meal.measures.filter(m => { return m; }); }); } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .meal-photo { width: 100%; } </style>
Эта страница получает последнюю еду и отображает ее пользователю на домашней странице нашего приложения, поскольку у нас есть следующие строки inindex.js
.
{ path: '/', name: 'latest', component: Latest, meta: { title: 'Home' } },
Поле title
- это заголовок страницы. Он будет в поле to.meta.title
объекта to
:
router.beforeEach((to, from, next) => { document.title = to.meta.title; next() })
В MealRow.vue
мы помещаем:
<template> <div class="row"> <div class="col"> <h1>{{meal.strMeal}}</h1> <p> <b>Area:</b> {{meal.strArea}}</p> <p> <b>Category:</b> {{meal.strCategory}}</p> <table class="table"> <thead> <tr> <th>Ingredient</th> <th>Measure</th> </tr> </thead> <tbody> <tr v-for="(ingredient, index) in meal.ingredients" v-bind:key="index"> <td>{{meal.ingredients[index]}}</td> <td>{{meal.measures[index]}}</td> </tr> </tbody> </table> <img :src="meal.strMealThumb"> </div> </div> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; export default { name: "meal-row", props: ["meal"], data() { return {}; }, methods: {}, computed: { computedClass: function() { if (this.meal != null) { return this.meal; } return {}; } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> img { max-width: 100%; } </style>
Он отображает ингредиенты для рецептов.
NavBar.vue
содержит панель навигации нашего приложения. Ставим:
<template> <b-navbar toggleable="md" type="dark" variant="info"> <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> <b-navbar-brand> <img :src="require('../assets/logo-small.png')" id='logo'> Meal App </b-navbar-brand> <b-collapse is-nav id="nav_collapse"> <b-navbar-nav> <b-nav-item :to="{ name: 'latest'}" :active="$route.name == 'latest'">Home</b-nav-item> <b-nav-item :to="{ name: 'random'}" :active="$route.name.includes('random')">Random</b-nav-item> <b-nav-item :to="{ name: 'search'}" :active="$route.name.includes('search')">Search</b-nav-item> <b-nav-item-dropdown text="Categories" right> <b-dropdown-item v-for="(cat, index) in categories" :active="$route.path.includes(`search/category/${cat.strCategory}`)" :key='index' :to="`/search/category/${cat.strCategory}`"> {{cat.strCategory}} </b-dropdown-item> </b-nav-item-dropdown> <b-nav-item-dropdown text="Areas" right> <b-dropdown-item v-for="(area, index) in areas" :active="$route.path.includes(`search/area/${area.strArea}`)" :key='index' :to="`/search/area/${area.strArea}`"> {{area.strArea}} </b-dropdown-item> </b-nav-item-dropdown> </b-navbar-nav> <!-- Right aligned nav items --> <b-navbar-nav class="ml-auto"> <b-nav-form @submit="search"> <b-form-input size="sm" class="mr-sm-2" type="text" placeholder="Search" name='keyword' v-model="keyword" v-validate="'required'" /> <b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button> </b-nav-form> </b-navbar-nav> </b-collapse> </b-navbar> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; export default { name: "nav-bar", data() { return { keyword: "", categories: [], areas: [], ingredients: [] }; }, methods: { search(evt) { evt.preventDefault(); if (this.errors.any()){ return; } this.$router.push({ name: 'search-keyword', params: { keyword: this.keyword } }); } }, beforeMount() { request.get(`${APIURL}categories`).end((err, res) => { this.categories = res.body.meals; }); request.get(`${APIURL}area`).end((err, res) => { this.areas = res.body.meals; }); request.get(`${APIURL}ingredients`).end((err, res) => { this.ingredients = res.body.meals; }); } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #logo { width: 100px; margin-top: -3px; } </style>
Мы отображаем раскрывающийся список «Категории» после получения категорий из API MealDB и делаем то же самое с меню «Области».
Обратите внимание, что у нас есть:
<b-navbar toggleable="md" type="dark" variant="info"> <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> <b-navbar-brand> <img :src="require('../assets/logo-small.png')" id='logo'> Meal App </b-navbar-brand> <b-collapse is-nav id="nav_collapse"> <b-navbar-nav> <b-nav-item :to="{ name: 'latest'}" :active="$route.name == 'latest'">Home</b-nav-item> <b-nav-item :to="{ name: 'random'}" :active="$route.name.includes('random')">Random</b-nav-item> <b-nav-item :to="{ name: 'search'}" :active="$route.name.includes('search')">Search</b-nav-item> <b-nav-item-dropdown text="Categories" right> <b-dropdown-item v-for="(cat, index) in categories" :active="$route.path.includes(`search/category/${cat.strCategory}`)" :key='index' :to="`/search/category/${cat.strCategory}`"> {{cat.strCategory}} </b-dropdown-item> </b-nav-item-dropdown> <b-nav-item-dropdown text="Areas" right> <b-dropdown-item v-for="(area, index) in areas" :active="$route.path.includes(`search/area/${area.strArea}`)" :key='index' :to="`/search/area/${area.strArea}`"> {{area.strArea}} </b-dropdown-item> </b-nav-item-dropdown> </b-navbar-nav> <!-- Right aligned nav items --> <b-navbar-nav class="ml-auto"> <b-nav-form @submit="search"> <b-form-input size="sm" class="mr-sm-2" type="text" placeholder="Search" name='keyword' v-model="keyword" v-validate="'required'" /> <b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button> </b-nav-form> </b-navbar-nav> </b-collapse> </b-navbar>
Это определяет нашу левую и правую навигацию. BootstrapVue упрощает определение панели навигации. В левой части есть меню, а в правой части - поле поиска, которое будет перенаправлять на компонент Filter.vue
, когда мы отправим ключевое слово для поиска.
Обратите внимание, что мы используем form-input
для входных данных. Таким образом, мы получаем выгоду от двусторонней привязки данных, которую предоставляет Vue.js. v-model
- это место, где мы выполняем привязку данных. v-validate
предоставляется пакет vee-validate
Vue. Это означает, что поле формы является обязательным. Он также добавляет объект this.errors
к нашему компоненту с блоком ниже:
if (this.errors.any()){ return; }
Мы также добавляем evt.preventDefault();
, чтобы предотвратить стандартное поведение браузера при отправке, которое заключается в отправке запроса на некоторый сервер и обновлении страницы. Вместо этого это позволяет остальной части функции обработчика отправки продолжать работу и позволяет нам сделать запрос к MealDB API, чтобы получить нужные данные.
Это предотвратит отправку формы с недопустимыми данными.
Следующий блок выполняет перенаправление с ключевым словом в качестве параметра запроса.
this.$router.push({ name: 'search-keyword', params: { keyword: this.keyword } });
В Random.vue
мы вводим:
<template> <div class="container"> <h1>Random Recipes</h1> <div class='row'> <div class="col-12"> <div v-for="meal in meals" :key='meal.idMeal'> <meal-row :meal='meal'></meal-row> </div> </div> </div> </div> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; export default { name: "random-recipe", data() { return { meals: [] }; }, methods: { search() { request.get(`${APIURL}random`).end((err, res) => { this.meals = res.body.meals; this.meals = this.meals.map(m => { m.ingredients = []; m.measures = []; for (let key in m) { if (key.includes("strIngredient")) { let index = +key.replace("strIngredient", ""); m.ingredients[index] = m[key]; } if (key.includes("strMeasure")) { let index = +key.replace("strMeasure", ""); m.measures[index] = m[key]; } } m.ingredients = m.ingredients.filter(i => { return i; }); m.measures = m.measures.filter(m => { return m; }); return m; }); }); } }, beforeMount() { this.search(); } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> </style>
Это генерирует случайный рецепт из MealDB API, вызывая конечную точку random
и отображая все, что от нее возвращается.
В Search.vue
мы вводим:
<template> <div class="container"> <div class='row'> <div class="col-12"> <h1>Search</h1> </div> </div> <div class='row'> <div class="col-12"> <b-form @submit="search" @reset="keyword = ''"> <b-form-group label="Keyword"> <b-form-input type="text" v-model="keyword" required name="keyword" v-validate="'required'" placeholder="Search keyword"> </b-form-input> <span v-show="errors.has('keyword')">{{ errors.first('keyword') }}</span> </b-form-group> <b-button type="submit" variant="primary">Search</b-button> </b-form> </div> </div> <div class='row'> <div class="col-12" v-if="meals && meals.length > 0"> <h1>Results</h1> <div v-for="meal in meals" v-bind:key='meal.idMeal'> <meal-row :meal='meal'></meal-row> </div> </div> <div class="col-12" v-else> <h2>No Results Found</h2> </div> </div> </div> </template> <script> const request = require("superagent"); import { APIURL } from "../main"; import MealRow from "./MealRow"; export default { name: "search", data() { return { keyword: "", meals: [] }; }, methods: { search(evt) { evt.preventDefault(); if (this.errors.any()) { return; } request.get(`${APIURL}search/${this.keyword}`).end((err, res) => { this.meals = res.body.meals; if (!res.body.meals){ return; } this.meals = this.meals.map(m => { m.ingredients = []; m.measures = []; for (let key in m) { if (key.includes("strIngredient")) { let index = +key.replace("strIngredient", ""); m.ingredients[index] = m[key]; } if (key.includes("strMeasure")) { let index = +key.replace("strMeasure", ""); m.measures[index] = m[key]; } } m.ingredients = m.ingredients.filter(i => { return i; }); m.measures = m.measures.filter(m => { return m; }); return m; }); }); }, searchParam(keyword) { this.keyword = keyword; request .get(`${APIURL}search/${keyword}`) .end((err, res) => { this.meals = res.body.meals; this.meals = this.meals.map(m => { m.ingredients = []; m.measures = []; for (let key in m) { if (key.includes("strIngredient")) { let index = +key.replace("strIngredient", ""); m.ingredients[index] = m[key]; } if (key.includes("strMeasure")) { let index = +key.replace("strMeasure", ""); m.measures[index] = m[key]; } } m.ingredients = m.ingredients.filter(i => { return i; }); m.measures = m.measures.filter(m => { return m; }); return m; }); }); } }, beforeMount() { this.$route.params.keyword && this.searchParam(this.$route.params.keyword); }, beforeRouteUpdate(to, from, next) { this.searchParam(to.params.keyword); next(); } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>
Это страница поиска, где мы можем искать по ключевым словам. Он работает, отправляя ключевое слово, введенное в форму, и отображая данные.
В папке assets
мы можем добавить логотип или удалить изображение с панели инструментов.
Результат
После добавления всего этого кода у нас есть:
Подпишитесь на мою рассылку сейчас по адресу http://jauyeung.net/subscribe/. Следуйте за мной в Твиттере по адресу https://twitter.com/AuMayeung