Як створити генератор символів RPG із повним стеком за допомогою MongoDB, Express, Vue та Node (стек MEVN)

Я розробник настільних ігор і із задоволенням створюю програми, які мають потенціал для надання певних послуг, пов’язаних з іграми. У цій статті ми розглянемо кроки створення генератора рольових ігрових персонажів за допомогою MongoDB, Express, Vue та Node (також відомого як стек "MEVN").

Передумови: у цьому посібнику передбачається, що у вас встановлені та налаштовані Node / NPM та MongoDB з готовим редактором коду та CLI (або IDE).

Якщо ви бажаєте дотримуватися візуального підручника, ви можете переглянути супровідне відео до цієї статті нижче:

Я також повинен згадати, що цей підручник не був би можливим без статті Беннета Дунгана про побудову REST API, підручника Аніти Шарми з повного стеку веб-додатків MEVN та статті Метта Марібойока на цю ж тему.  

Я використовував кожну з цих статей на додачу до офіційної документації (для Vue, Express та багато іншого), навчаючись створювати власні програми MEVN (докладніше про мою подорож із веб-API ви можете прочитати тут).

Ви можете отримати доступ до цілого сховища цього підручника на GitHub.

Передній кінець

Наш додаток дозволить нам створювати нових персонажів ігрових ігрових персонажів і переглядати їх взагалі, з наступним стеком:

  • Клієнт Vue
  • Вузол / Експрес-сервер
  • База даних MongoDB

Клієнт Vue надсилатиме запити HTTP на сервер Node / Express (або "API"), який, у свою чергу, зв'язуватиметься з нашою базою даних MongoDB для надсилання даних назад у стек.

Ми почнемо з відкриття командного рядка, створення нового каталогу для нашого проекту та переходу до цього каталогу:

mkdir mevn-character-generator cd mevn-character-generator

Потім ми встановимо Vue CLI глобально, щоб допомогти нам побудувати базовий додаток:

npm install -g @vue/cli

Далі ми будемо використовувати інтерфейс інтерфейсу Vue для створення нового додатка під назвою "Клієнт" у нашому каталозі mevn-character-generator:

vue create client

Ви можете просто натиснути "Enter" у підказці, щоб продовжити.

Ми можемо запустити наш додаток, спершу перейшовши до папки / client:

cd client npm run serve

Коли сценарій завершиться, ми можемо відкрити сторінку браузера та перейти до URL-адреси, вказаної нашим терміналом (зазвичай // localhost: 8080 або 8081). Ми повинні побачити щось подібне:

Приємно! Інтерфейс клієнта Vue створив для нас базовий додаток і відображає його прямо в браузері. Він також автоматично перезавантажує сторінку при зміні файлу та видає помилки, якщо щось у коді виглядає не так.

Давайте відкриємо каталог проекту в нашому редакторі коду, щоб поглянути на структуру файлів, яка повинна виглядати так:

Якщо у вас OCD, як і у мене, ви можете видалити файл "favicon.ico" та папку "/ assets", оскільки вони нам не потрібні для цього проекту.

Поринувши в /src/main.js, ми бачимо:

import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app') 

Цей файл є основною точкою входу для нашого клієнта. Він повідомляє браузеру, щоб підключити наш файл App.vue до div з ідентифікатором "#app" у /public/index.html.

Давайте подивимось на /src/App.vue (я пропустив деякий код для читабельності):

 import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } }   #app { ... } 

App.vue - типовий компонент Vue з тегами, та та.

Розділ між тегами - це HTML, який ми бачимо на екрані. Усередині ми бачимо посилання на зображення, яке ми видалили, і компонент, який отримує повідомлення "Ласкаво просимо до вашого додатка Vue.js".

Розділ імпортує інші використовувані компоненти та експортує всі дані, які ми хочемо включити до нашого додатку. Зверніть увагу, що в App.vue ми імпортуємо HelloWorld.vue з іншого каталогу та експортуємо його як компонент, щоб наш main.js мав доступ до нього.

Теги призначені для вашого власного блискучого та яскравого CSS, який ми не використовуватимемо для цього підручника (womp womp).

Давайте підемо за потоком до /src/components/HelloWorld.vue:

{{ msg }}

... export default { name: 'HelloWorld', props: { msg: String } } ...

HelloWorld.vue має структуру компонентів, подібну до App.vue. Він очікує отримати реквізит "msg" як рядок від батьківського компонента, який його викликає (що в даному випадку App.vue). Потім HelloWorld.vue подає повідомлення безпосередньо в шаблон HTML між фігурними дужками як {{msg}}.

Важливо також зазначити, що теги тут (які ми досі не використовуємо) мають масштаб, тобто, якщо ви хочете застосувати CSS до цього компонента самостійно, ви можете це зробити.

Давайте видалимо весь HTML у HelloWorld.vue і змінимо ім'я файлу на "CharacterViewer.vue". Оновіть код до:

Character Viewer

export default { name: 'CharacterViewer' }

Це набагато простіше, але вимагає від нас змінити всі посилання на "HelloWorld" в App.vue:

 import CharacterViewer from './components/CharacterViewer.vue' export default { name: 'App', components: { CharacterViewer } } 

The Vue CLI, which may have been throwing you errors while deleting and re-arranging stuff, should reload. If you check out your browser again, you'll see:

Pretty exciting. Let's add a "Character Creator" component by duplicating CharacterViewer.vue and calling it "CharacterCreator.vue", replacing the code:

Character Creator

export default { name: 'CharacterCreator' }

Then reference our new component in App.vue:

 import CharacterViewer from './components/CharacterViewer.vue' import CharacterCreator from './components/CharacterCreator.vue' export default { name: 'App', components: { CharacterViewer, CharacterCreator } } 

Cool. Now the website should show us:

That's great, but let's say that we want to dynamically view each of the components independently of one another. We could use radial menus as selectors that will guide the logic of our app, but I'm partial to using buttons when creating a user interface.

Let's add a couple:

 View all characters Create a character import CharacterViewer from './components/CharacterViewer.vue' import CharacterCreator from './components/CharacterCreator.vue' export default { name: 'App', components: { CharacterViewer, CharacterCreator }, data: function () { return { toggle: "character-viewer" } } } 

To understand the above code, let's work our way from the bottom of the script upwards.

We've added a "data" function to the export section of our app, which returns an object that can store data for us. This data can in turn help us manage the state of the app. In this code, we've created a "toggle" that's set to "character-viewer."

In the HTML template above the script, we've created two buttons: one to "View all characters" and the other to "Create a character." The attribute "v-on:click" within the tags tells Vue that when clicked, Vue should change the value of "toggle" to "character-viewer" or "character-creator," depending on which button is being clicked.

Just below the buttons, the "v-show" directives instruct Vue to only show the "CharacterViewer" component if "toggle" is equal to "character-viewer", or the "CharacterCreator" component if it's equal to "character-creator."

Congrats, our app now renders content dynamically based on user input!

Now, we can move to creating the basic structure for viewing and creating roleplaying game characters.  In CharacterCreator.vue, update the code:

Character Creator

Character Name:

Character Profession: Mage Thief Warrior

{{name}}

{{profession}}

export default { name: 'CharacterCreator', data: function () { return { name: "", profession: "" } } }

We've just created a text input where players can input a character name, and a simple dropdown list from which they can choose a profession.  

The "v-model" attribute binds each of those inputs to the "name" and "profession" values in our data object within the script.  

We've also temporarily added a {{name}} and {{profession}} into the HTML template so that we can make sure that everything's working properly. Upon saving, the Vue CLI should automatically re-render the app to look like this when clicking on "Create a character":

It's certainly not pretty, but it works! I'll leave the design up to your mad CSS skills.

The Back End

Let's move to the back end. Open a new command line and navigate to the root directory (mevn-character-generator). Create a new directory for our server and navigate into it:

mkdir server cd server

Now initialize the directory:

npm init

You can just keep hitting "enter" at the prompts if you don't care to change any of the specifics.

Then install our dependencies and save them to the project:

npm install --save express dotenv nodemon mongoose cors

Let's take a second to look at each of these items in turn. Express is going to serve as the main back end web framework, while dotenv allows us to declare certain environment variables that will help us with pathing and configuration. Nodemon automatically watches our server for changes and restarts it for us, and Mongoose serves as an ODM to map our data onto MongoDB. Finally, CORS allows us to make cross-origin requests between our client and server, a topic I've written about here.

That's a lot of dependencies! Back in our code editor, we need to create a few files and directories to scaffold a server with which to work. In our new /server directory, create four files called "server.js", ".env", "characters.js", and "character.js":

Replace the "test" script in our package.json with the "dev" one below:

{ "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "dependencies": { "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", "mongoose": "^5.9.3", "nodemon": "^2.0.2" }, "devDependencies": {}, "scripts": { "dev": "nodemon server.js" }, "author": "", "license": "ISC" } 

Now, when we type "npm run dev" in the command line, it'll run Nodemon with server.js as the entry point for the back end of our app.

We'll create our server by adding the following code to server.js:

require('dotenv').config(); const express = require('express'); const server = express(); const cors = require('cors'); server.use(express.json()); server.use(cors()); server.get("/", (req, res) => { res.send("Hello World!"); }) server.listen(3000, () => console.log("Server started!"));

We're doing a lot here up front, but we'll thank ourselves later. First, we're importing any environmental variables that we'll need for running our development server, as well as Express and CORS. We're creating a server that runs on Express and is able to parse JSON and use CORS.  

Then, we're telling the server that when a user navigates to the root directory ("/") in a browser, they should be sent the message "Hello World!"  

Finally, we tell the server to listen on port 3000, and log to the console that the "Server started!"

Type the following in a separate command line from the one running our Vue app, making sure you're in the /server directory:

npm run dev

Open a browser to //localhost:3000. You should see:

Neat!

Now that the server's up, we need to get our database working. Open a third command line and type in the following:

mongod

This should get our database running, but will depend on how you installed and configured MongoDB before tackling this tutorial. In some cases, you'll need to work with the path of your database and of MongoDB itself to get it all square.

Once the "mongod" command is working, add the following line to your .env file:

DATABASE_URL = mongodb://localhost/characters

We'll use the above in a second as we hook up our database. Add the following code to your server.js file, just under the line about requiring CORS:

const mongoose = require('mongoose'); mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true }); const db = mongoose.connection; db.on('error', (error) => console.error(error)); db.once('open', () => console.log('Connected to database!'));

Here, we're importing Mongoose into our server, and connecting it to the DATABASE_URL that we declared in the .env file.  

This connection is assigned to the "db" variable for easy reference, and if there's an error, we've asked the server to log it to the console. Otherwise, if everything's working correctly, the console should log that we're "Connected to database!"

Save all of your files, allowing Nodemon to restart the server with the CLI messages that the "Server started!" and that you're "Connected to database!"

Now that everything's wired up on the back end, we'll need to add a Mongoose "schema," which is a model of what our data should look like. Add the below to character.js:

const mongoose = require('mongoose'); const characterSchema = new mongoose.Schema({ name: { type: String, required: true }, profession: { type: String, required: true } }); module.exports = mongoose.model('Character', characterSchema);

After importing Mongoose, we've added a new schema that maps the character name and profession that we've created in our front end client to the requisite fields in the back end database. Both are of type "String," and are required when posting to the database.

We need to tell the server how to access the database and what to do once it's there, but it'll get messy if we try to add all of that code to server.js. Let's delete the code block that begins with "server.get..." and replace it with:

const router = require('./characters'); server.use('/characters', router);

This snippet just says to the server, "when someone goes to the /characters HTTP endpoint, do whatever's in the characters.js file."

Your entire server.js file should now look like the following:

require('dotenv').config(); const express = require('express'); const server = express(); const cors = require('cors'); const mongoose = require('mongoose'); mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true }); const db = mongoose.connection; db.on('error', (error) => console.error(error)); db.once('open', () => console.log('Connected to database!')); server.use(express.json()); server.use(cors()); const router = require('./characters'); server.use('/characters', router); server.listen(3000, () => console.log("Server started!"));

Note: it's a best practice to keep your models and routes in "/models" and "/routes" folders, respectively, but we've simplified the paths for this tutorial.

Let's get that characters.js file working. Start by entering the following:

const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.send("Hello World!") }); module.exports = router;

If we navigate to //localhost:3000/characters, we get the "Hello World!" message that we saw previously. Not too shabby – we've successfully migrated our code to a separate file to keep things a bit more tidy.

Adding a bit more to characters.js will help us fill out the remainder of our back end API, but let's pause for a moment to consider what we're attempting to do.

In this project, we want to be able to make GET and POST requests from the client to the server, which will in turn "Read" from and "Create" items in the database (representing the "R" and "C" in "CRUD"). We'll start with the GET method as we already have a structure for it:

const express = require('express'); const router = express.Router(); const Character = require('./Character'); router.get('/', async (req, res) => { try { const characters = await Character.find(); res.json(characters); } catch (err) { res.status(500).json({ message: err.message }); } }); module.exports = router;

We're creating an asynchronous function that, when it receives a request, attempts to find all Characters in our database that fit our Mongoose schema. It then sends them all back up the stack as a JSON response. If something goes awry, it instead sends a 500 error.

Reloading the page that we have open on //localhost:3000/characters will return an exciting "[]", but that's great! It just means that the GET request is returning an empty array because the database is empty. Good job!

Wiring the Front End and Back End

Let's return to our client! In a command line at the mevn-character-generator/client directory, install Axios:

npm install --save axios

Axios allows us to make HTTP requests from within our client. If you're interested, you can read more about how it works with Vue here.

Back in our /client/src/components/CharacterViewer.vue file, we need to make GET requests to the server so that we can pull characters from the database, and we'll do so using Axios:

Character Viewer

{{characters}}

import axios from 'axios' export default { name: 'CharacterViewer', data: function () { return { characters: null } }, methods: { getCharacters: function () { axios .get('//localhost:3000/characters') .then(response => (this.characters = response.data)) } }, mounted: function () { this.getCharacters(); } }

In the script section, we've created a data variable called "characters", which starts out as "null."  

In our "methods" object, which is where Vue stores functions that you can use throughout your component, we've created a "getCharacters()" function. "getCharacters()" will call Axios to GET the //localhost:3000/characters endpoint and store the data of its response in the "characters" variable.  

When the component is mounted for the first time, it will run "getCharacters()" to GET all characters from the database and display them within the HTML in the template section above.

We still won't see anything exciting on our client page (still rendering at //localhost:8080 or 8081) because we haven't added any characters to the database yet.

Pro tip! If you're nervous about this step and not sure if things are working correctly, you can use a third party app like Postman to make HTTP requests to an API without having to first wire up your client.

Let's jump back to our /server/characters.js router and add logic for a POST request:

const express = require('express'); const router = express.Router(); const Character = require('./Character'); router.get('/', async (req, res) => { try { const characters = await Character.find(); res.json(characters); } catch (err) { res.status(500).json({ message: err.message }); } }); router.post('/', async (req, res) => { const character = new Character({ name: req.body.name, profession: req.body.profession }); try { const newCharacter = await character.save(); res.status(201).json(newCharacter); } catch (err) { res.status(400).json({ message: err.message }); } }); module.exports = router;

Below the GET request, we've added an asynchronous POST function that creates a "character," which is a new copy of the Character.js Mongoose schema. The request that comes to the server should include a "name" and "profession" in the body, which should be saved into the database as a "newCharacter" and returned as the JSON response with a 201 success.

If there's an error, the server should send it up the chain with a status of 400.

Crazily enough, this code is all we need to wrap up the back end of our app. If we head to our /client/src/components/CharacterCreator.vue file, we can tie everything together:

Character Creator

Character Name:

Character Profession: Mage Thief Warrior

Create Character import axios from 'axios'; export default { name: 'CharacterCreator', data: function () { return { name: null, profession: null } }, methods: { postCharacter: function () { axios .post('//localhost:3000/characters', { name: this.name, profession: this.profession }); } } }

We've added a "postCharacter()" function to the CharacterCreator.vue component, which will send a POST request to //localhost:3000/characters endpoint with a "name" and "profession" in the body.

The "name" and "profession" are drawn from the variables within our data object, which are themselves bound to the inputs that we created earlier by the "v-model" attribute.

We've added a "Create Character" button that calls the "postCharacter()" function when clicked. When we make a POST request using the character creator, we can now do this:

And our GET request looks like:

IT'S WORKING. But we need to clean up our GET request so that it's more readable, especially when new users are added. Here's what we'll add to the portion of CharacterViewer.vue:

Character Viewer

{{character.name}} is a {{character.profession}}!

Refresh Characters

Here, we're using "v-for" to ask Vue to iterate over each of the characters in the response data (currently stored in the "characters" variable) and display their names and professions.

The Vue CLI will get irritated if you don't provide a unique key for each of the iterated items, so we use "v-bind" to create a key based on the item's index.

We've also added a "Refresh Characters" button that will call the "getCharacters()" function so we can see new characters as they're added without having to refresh the page.

The Character Viewer looks a lot cleaner:

And with that, our app is fully functional!  Great job!

...

...

But what if we want to eliminate that "Refresh Characters" button and just have all characters load whenever we navigate to the Character Viewer section of the app?

First, we'll need to make these changes to App.vue:

 View all characters Create a character import CharacterViewer from './components/CharacterViewer.vue' import CharacterCreator from './components/CharacterCreator.vue' import axios from "axios" export default { name: 'App', components: { CharacterViewer, CharacterCreator }, data: function () { return { toggle: "character-viewer", characters: null } }, methods: { getCharacters: function () { axios .get('//localhost:3000/characters') .then(response => (this.characters = response.data)) } }, mounted: function () { this.getCharacters(); } } 

We've migrated the "getCharacters()" functionality to App.vue and are now calling it when the app is mounted, as well as whenever we click on the "View all characters" button. We're also passing the "characters" variable - which is storing our response data from the server API - as props to the "CharacterViewer" component in the section.

All that's left is to clean up CharacterViewer.vue and indicate that it should expect an Array called "characters" as props:

Character Viewer

{{character.name}} is a {{character.profession}}!

export default { name: 'CharacterViewer', props: { characters: Array } }

WE'VE DONE IT.  

We've created a fully functional roleplaying game character generator. Its Vue client responds dynamically to user input, and can make GET and POST requests to a Node/Express server API, which in turn reads from and writes to a MongoDB database.

Well done. You can use this project as a template for your own MEVN full stack apps, or work with the HTML and CSS to make it more feature-rich and user friendly.

A fun next step would be to research RESTful APIs in more depth and add PATCH and DELETE requests so that you can update or delete characters as necessary. A helpful starting point would be the Express documentation, or Bennett Dungan's article on building a REST API.

You can also learn how to deploy this kind of app to Heroku here.

Happy coding!

If you enjoyed this article, please consider checking out my games and books, subscribing to my YouTube channel, or joining the Entromancy Discord.

M. S. Farzan, Ph.D. has written and worked for high-profile video game companies and editorial websites such as Electronic Arts, Perfect World Entertainment, Modus Games, and MMORPG.com, and has served as the Community Manager for games like Dungeons & Dragons Neverwinter and Mass Effect: Andromeda. He is the Creative Director and Lead Game Designer of Entromancy: A Cyberpunk Fantasy RPG and author of The Nightpath Trilogy. Find M. S. Farzan on Twitter @sominator.