Перевод статьи Aneeta Sharma: Build full stack web apps with MEVN Stack [Part 1/2]. Опубликовано с разрешения автора.
Мы в нашей CloudFactory всегда стремимся идти в ногу со временем. И даже несмотря на тот факт, что наша фирма является в первую очередь Ruby-компанией, мы любим инвестировать в изучение новых технологий.
У нас стояла задача выбрать фронтенд-фреймворк для будущего full-stack проекта. Выбор стоял между MEAN и MERN, и поскольку фреймворк Vue.js - это что-то новое и интересное, то мы захотели его попробовать и дали зеленый свет MEVN.
Акроним MEVN означает - MongoDB + Express.js + VueJS + Node.js. Цель этой статьи - показать, как можно создать базовое MEVN-приложение на стеке технологий MongoDB/Express/VueJS/Node.js.
- Full-stack приложение MEVN
- CRUD-операции при помощи Express.js
- Подключение к MongoDB (мы будем использовать Mongoose)
- MongoDB v3.0.5
- Express.js v4.15.4
- Vue.js v2.4.2
- Node.js v8.5.0
В статье будет описан процесс создания каркаса приложения на стеке MEVN. Работа с базой данных MongoDB будет описана во второй части данного руководства. Исходный код создаваемого приложения расположен в репозитории - MEVN-boilerplate.
Примечание переводчика: я взял на себя смелость пошагово воссоздать описываемое в данной статье приложение. В результате у меня получился работоспособный аналог, исходный код которого расположен в репозитории - MEVN-Application. В процессе создания текущего проекта мною были внесены незначительные изменения, которые не повлияли на общий функционал приложения.
Итак, приступим!
Для начала создадим директорию с будущим приложением:
$ mkdir mevn-application
$ cd mevn-application
В директорию с проектом необходимо создать поддиректорию для фронтенд-части приложения. Для этого воспользуемся консольной утилитой vue-cli
фреймворка Vue.js. Эта утилита устанавливается глобально в систему, через менеджер пакетов npm
:
$ npm i -g vue-cli
Сгенерируем директорию client
при помощи команды:
$ vue init webpack client
При запуске данная команда задаст серию вопросов о будущем приложении - имя проекта, имя автора проекта, описание проекта, использование eslint
, использование тестов и так далее. При желании можно выбрать значения по умолчанию путем нажатия клавиши Enter.
Примерный вид выполнения команды vue-cli
представлен на изображении ниже:
Примечание переводчика: в моем приложении mevn-application не установлена поддержка unit и e2e тестирования, так как это значительно утяжеляет размер проекта; к тому же возможности тестирования не будут использоваться в данном руководстве.
Теперь необходимо перейти в готовую директорию client
и запустить внутри нее команду npm install
для установки всех зависимостей проекта, перечисленных в файле client/package.json
.
Примечание переводчика: в проекте mevn-application мною использовался менеджер пакетов yarn
; это является личным предпочтением и не более того.
$ cd client
$ yarn install
$ yarn run dev
Последняя команда yarn run dev
запускает локальный сервер по адресу http://localhost:8080/#/
и автоматически запускает браузер по умолчанию по этому же адресу. В результате в окне браузера будет отображена стартовая страница проекта на Vue.js:
Базовая заготовка фронтенд-части будущего приложения готова. Теперь нужно вернуться немного назад и создать backend-часть приложения на основе Express.js.
$ cd ..
$ mkdir server
$ cd server
Выполним инициализацию серверной части проекта путем запуска команды yarn init
. Будет также задана серия вопросов о проекте и в результате сформируется файл package.json
.
Также давайте создадим директорию src
внутри которой будут находиться все файлы будущего сервера. В частности, создадим в папке src
файл index.js
, который будет главным файлом сервера:
$ mkdir src
$ touch src/index.js
Добавим в проект поддержку автоматической перезагрузки сервера при каждом изменении файлов проекта. Для этого воспользуемся популярным пакетом nodemon
и добавим его как зависимость для разработки:
$ yarn add nodemon --dev
В файле server/package.json
добавим секцию scripts
и пропишем туда команду для nodemon
, которая заставит пакет отслеживать изменения всех файлов с расширением js
внутри директории src
:
...
"scripts": {
"start": "nodemon --ext js --watch src"
}
...
В результате файл package.json
будет выглядеть таким образом:
{
"name": "server",
"version": "1.0.0",
"description": "server-part",
"main": "src/index.js",
"author": "Moe Green",
"license": "MIT",
"private": true,
"scripts": {
"start": "nodemon --ext js --watch src"
},
"devDependencies": {
"nodemon": "^1.14.10"
}
}
Теперь проверим работу серверной части приложения - для этого добавим в файле index.js
строку console.log('Hello World')
и запустим файл server/src/index.js
командой:
$ yarn start
Примечание переводчика: автор статьи использует команду npm start
, что ничего не меняет.
Если все было сделано правильно, то пакет nodemon
успешно запустится и в консоли будет выведено сообщение из файла:
Теперь настало время приступить к настройке сервера. Для этого установим пакет express.js
как зависимость проекта:
$ yarn add express
Затем установим дополнительные пакеты для разработки сервера. Пакет morgan
для ведения логов, пакет body-parser
для парсинга приходящих со стороны клиента данных, пакет cors
для задействования CORS:
$ yarn add morgan body-parser cors
В результате файл package.json
примет такой вид:
{
"name": "server",
"version": "1.0.0",
"description": "server-part",
"main": "src/index.js",
"author": "Moe Green",
"license": "MIT",
"private": true,
"scripts": {
"start": "nodemon --ext js --watch src"
},
"dependencies": {
"body-parser": "^1.18.2",
"cors": "^2.8.4",
"express": "^4.16.2",
"morgan": "^1.9.0"
},
"devDependencies": {
"nodemon": "^1.14.10"
}
}
В файле src/index.js
выполним подключение установленных пакетов, а также запустим их на выполнение как middleware:
const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const morgan = require('morgan')
const app = express()
app.use(morgan('combined'))
app.use(bodyParser.json())
app.use(cors())
В конце файла src/index.js
запустим сервер express
на локальном порту 8081
. Для этого предварительно создадим в директории src
поддиректорию config
с файлом конфигурации config.js
внутри. Этот файл будет содержать все необходимые переменные и константы для сервера; в частности - номер локального порта:
$ mkdir config
$ touch config/config.js
Содержание файла config/config.js
:
module.exports = {
port: 8081
}
Подключим конфигурационный файл config/config.js
в главный файл сервера src/index.js
:
...
const config = require('./config/config')
...
... и наконец включим сервер express
:
...
app.listen(process.env.PORT || config.port,
() => console.log(`Server start on port ${config.port} ...`))
...
В итоге на данном этапе разработки сервер express
будет выглядеть таким образом:
const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const morgan = require('morgan')
const config = require('./config/config')
const app = express()
app.use(morgan('combined'))
app.use(bodyParser.json())
app.use(cors())
app.listen(process.env.PORT || config.port,
() => console.log(`Server start on port ${config.port} ...`)
Серверная часть приложения запущена на порту 8081 (localhost:8081
). Фронтенд-часть приложения запущена на порту 8080 (localhost:8080
).
Теперь начинается самое интересное - давайте создадим для сервера первый маршрут (endpoint) и пусть это будет путь до страницы со всеми записями (posts). Для начала мы просто протестируем, правильно ли сервер отвечает на наши запросы:
app.get('/posts', (req, res) => {
res.send(
[{
title: "Hello World!",
description: "Hi there! How are you?"
}]
)
})
Если теперь мы "постучимся" на сервер по адресу http://localhost:8081/posts
из Postman или из браузера, то в ответ должны получить ответ:
Отлично - сервер express
работает и правильно отвечает на наши запросы!
Настало время "связать" обе части нашего приложения. Сделать так, чтобы фронтенд-часть (client) смогла посылать запросы на backend-часть (server) и получать от сервера ответы.
Для этого воспользуемся очень популярным пакетом axios
. Перейдем в клиентскую часть приложения (client) и выполним там установку этого пакета как зависимость проекта:
$ cd client
$ yarn add axios
В директории client/src
создадим поддиректорию services
с файлом api.js
внутри. В этом файле мы подключим библиотеку axios
, а затем экспортируем ее из этого файла как функцию с предустановленной настройкой - базовым адресом запроса по умолчанию. Теперь каждый раз, как axios
будет "стучаться" на сервер, он будет "идти" по этому адресу:
import axios from 'axios'
export default () => {
return axios.create({
baseURL: 'http://localhost:8081'
})
}
В директории services
создадим еще один файл PostsService.js
, в котором подключим файл api.js
как модуль. В итоге внутри модуля PostsService.js
мы можем пользоваться готовым заранее настроенным axios
. Экспортируем модуль PostsService.js
как объект, у которого будет целый ряд методов - каждый метод для определенного случая.
Одним из таких случаев на данный момент у нас является факт получения данных с сервера. Для этого создадим метод fetchPosts
:
import api from '@/services/api'
export default {
fetchPosts () {
return api().get('posts')
}
}
На первый взгляд функция fetchPosts
может показаться непонятной и обескураживающей. Но на самом деле здесь все просто. api()
- это вызов на исполнение возвращаемой модулем api
функции. Эта запись равносильна записи axios.get('posts')
. Передача аргумента posts
также может вызвать вопрос, но дело в том, что axios умеет "склеивать" адреса, поэтому в итоге получим такой адрес - http://localhost:8081/posts
.
В файле маршрутов src/routes/index.js
добавим маршрут для страницы (компонента) PostsPage
, на которой будут отображаться все записи (posts), полученные с сервера:
import Start from '@/components/pages/StartPage'
import Posts from '@/components/pages/PostsPage'
const routes = [
{
path: '/',
name: 'Start',
component: Start
},
{
path: '/posts',
name: 'Posts',
component: Posts
}
]
export default routes
... и подключим созданный маршрут в индексный файл src/router/index.js
маршрутизатора:
import Vue from 'vue'
import Router from 'vue-router'
import routes from '@/routes'
Vue.use(Router)
export default new Router({
mode: 'history',
routes
})
Нам осталось создать сам компонент PostsPage
по пути components/pages/PostsPage.vue
. Но предварительно мы установим еще один пакет - bootstrap
для быстрой и правильной стилизации vue-компонентов.
$ yarn add bootstrap
Подключать данный плагин будем через систему плагинов. Создадим поддиректорию src/plugins
и в ней файл bootstrap.js
, отвечающий за настройку самого Bootstrap. В нашем случае ничего настраивать не будем, а просто импортируем установленный bootstrap
:
import 'bootstrap/dist/css/bootstrap.css'
... и затем подключим его в src/main.js
:
import Vue from 'vue'
import App from './App'
import router from './router'
import '@/plugins/bootstrap'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
Примечание переводчика: данная система подключения плагинов была подмечена мною здесь - vue-2-boilerplate.
Осталось создать сам компонент PostsPage.vue
(переводчик данной статьи большой поклонник шаблонизатора Pug, поэтому при создании Vue-компонентов везде и всюду будет такая "экзотическая" разметка):
<template lang="pug">
.container
.row
.col-xs-12
h1
| Posts
h3
| This file will list all the posts
section.panel.panel-success( v-if="posts.length" )
.panel-heading
| list of posts
table.table.table-striped
tr
th Title
th Description
th Action
tr( v-for="(post, index) in posts", :key="post.title" )
td {{ post.title }}
td {{ post.description }}
section.panel.panel-danger( !v-if="posts.length" )
p
| There are no posts ... Lets add one now!
div
router-link( :to="{ name: 'NewPost' }" )
| add new post
</template>
<script>
import PostsService from '@/services/PostsService'
export default {
name: 'PostsPage',
data () {
return {
posts: []
}
},
methods: {
async getPosts () {
const response = await PostsService.fetchPosts()
this.posts = response.data.posts
}
},
mounted () {
this.getPosts()
}
}
</script>
Что здесь происходит? Во-первых, в шаблоне template
компонента определяем две секции - одну section.panel.panel-success
для отображения записей (posts), если они есть ("пришли" с сервера); другую - section.panel.panel-danger
, если записей нет.
В скриптах script
компонента мы подключаем модуль PostsService
для того, чтобы воспользоваться методом fetchPosts
:
import PostsService from '@/services/PostsService'
Затем в компоненте создаем метод getPosts
, который через async/await вызовет на исполнение метод fetchPosts
объекта PostsService
. Результат будет записан в массив this.posts
. И наконец мы повесим метод getPosts
на хук mounted
, чтобы он вызывался на исполнение каждый раз, как компонент PostsPage.vue
будет смонтирован в DOM.
Ну и в секции section.panel.panel-success
данные из массива this.posts
будут "разбрасываться" по таблице через директиву v-for
.
Напоследок стоит обратить внимание на использование директивы v-if
. В официальной документации демонстрируется сочетание двух директив - v-if
+ v-else
. Однако, в моем коде прописано другое сочетание - v-if="posts.length"
+ !v-if="posts.length"
. Сделано это для большей практичности данного примера, ибо - A v-else element must immediately follow a v-if or a v-else-if element - otherwise it will not be recognized..
Теперь если запустить клиентскую часть yarn run dev
и перейти в браузере по адресу http://localhost:8080/posts
, то мы должны получить такой результат:
Все хорошо — на странице записей нет, потому что мы их не получили с сервера, так и должно быть. Сервер не вернул записи (posts), так как он не подключен к базе данных MongoDB и не может получить их оттуда.
Подключением к базе данных MongoDB и операциями CRUD мы займемся в следующей части данного руководства.
Building a Simple CRUD Application with Express and MongoDB Getting Started with Node.js, Express, MongoDB Full Stack Apps with Vue.js and Express.JS