Full Stack Travel Story App Using MERN Stack | MongoDB, Express, React, Node.js | MERN Project

概述

前言

原作者用的是commonJS语法, 代码结构也很混乱特别是后端部分,这里按照我在别处学到的内容做了一些修改,确保一个文件中不会有太多代码。

作者没有开源源代码,这里的代码可能存在一些小问题。

简介

Travel-Story

YouTube视频链接

In this video, we will build a Full Stack Travel Story App using the MERN stack (MongoDB, Express, React, Node.js). This app allows users to sign up, log in, and create personal travel stories with features like image uploads, and adding travel date. We also implement search functionality to find stories, filter by date range, and pin favorite stories to the top. Users can also edit or delete their stories..

The backend features secure JWT authentication, MongoDB for storing user data and travel stories, and APIs for adding, editing, deleting stories, and uploading images. This tutorial covers the entire development process, from setting up the frontend and backend to integrating APIs and building the UI components.

使用教程

git clone 整个项目

添加.env配置信息

cd backend 先执行npm install再执行npm start

cd frontend 先执行npm install再执行npm run dev

默认打开http://localhost:5173

TimeStamps

Frontend React Project Setup

00:00 - Demo of Travel Story App

04:50 - Frontend React app setup

执行如下指令:

1
2
3
4
5
mkdir TravelStory
cd TravelStory
mkdir frontend
mkdir backend
cd frontend

执行npm create vite@latest .创建项目,选择React和JavaScript。

然后执行如下指令:

1
2
npm install
npm run dev

输出结果如下所示:

1
2
3
4
5

added 264 packages in 3m

102 packages are looking for funding
  run `npm fund` for details

在src目录下创建components、pages和utils目录,在pages目录下创建Home、Login和SignUp页面

安装VSCode插件ES7+ React/Redux/React-Native snippets

新建.jsx文件使用rafce快速生成相应内容

删除src目录下的App.css文件

修改index.css文件和App.jsx文件

添加google font Poppins

https://fonts.google.com/specimen/Poppins

在index.css文件开头加入如下内容:

1
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

08:41 - Tailwind CSS setup

vite 版本

Install Tailwind CSS with Vite

Install tailwindcss and its peer dependencies, then generate your tailwind.config.js and postcss.config.js files.

1
2
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

即在frontend目录下执行npm install -D tailwindcss postcss autoprefixer

再执行npx tailwindcss init -p

输出结果如下所示:

1
2
3
4
5
6
7
8

added 85 packages in 7s

125 packages are looking for funding
  run `npm fund` for details

Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

Configure your template paths

Add the paths to all of your template files in your tailwind.config.js file.

tailwind.config.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Add the Tailwind directives to your CSS

Add the @tailwind directives for each of Tailwind’s layers to your ./src/index.css file.

1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;

此时index.css文件内容如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  html {
    font-family: "Poppins", "sans-serif";
  }

  body {
    background-color: #fdfeff;
    overflow-x: hidden;
  }
}

此时tailwind.config.js内容如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    fontFamily: {
      display: ["Poppins", "sans-serif"],
    },
    extend: {
      // Colors used in the project
      colors: {
        primary: "#05B6D3",
        secondary: "#EF863E",
      }
    },
  },
  plugins: [],
}

11:33 - react-router-dom installation & setup

执行npm i react-route-dom

Backend

14:32 - Backend Node.js Express project setup

backend目录下执行npm init -y

输出信息如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Wrote to /home/xxxxxxxx/TravelStory/backend/package.json:

{
  "name": "travelstory",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

backend目录下执行npm i express mongoose jsonwebtoken dotenv cors nodemon bcrypt

输出结果如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated npmlog@5.0.1: This package is no longer supported.
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated are-we-there-yet@2.0.0: This package is no longer supported.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated gauge@3.0.2: This package is no longer supported.

added 183 packages in 9s

22 packages are looking for funding
  run `npm fund` for details

backend目录下创建index.js

修改package.json文件,增加如下内容:"start": "nodemon index.js"

创建models文件夹和.env文件

此时index.js文件内容如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const bcrypt = require("bcrypt");
const express = require("express");
const cors = require("cors");

const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());
app.use(cors({ origin: "*" }));

//Test api
app.post("/hello", async (req, res) => {
    return res.status(200).json({ message: "hello" });
})

app.listen(8000);
module.exports = app;

执行npm start启动

可以通过http://localhost:8000/hello查看输出信息

18:51 - MongoDB Atlas configuration

https://cloud.mongodb.com/

新建项目Travel-Story

Deploy your cluster

Use a template below or set up advanced configuration options. You can also edit these configuration options once the cluster is created.

Add a connection IP address

Create a database user

保存密码

Connecting with MongoDB Driver

创建config.json装填MONGO_DB_URI

23:05 - Creating User schema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import mongoose from "mongoose"

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now(),
  },
})

const User = mongoose.model("User", userSchema)

export default User

24:26 - Create Account API

执行node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

输出结果如下所示:

1
39cd7527a217f289590110725fa0885fd067cc835eb97bebdbeaa3373658d68ab9164ebf20285bdbc83e0a354e9a2932a0b46c00d02ed5d2a4dd3bed30c320df

Error connecting to MongoDB Could not connect to any servers in your MongoDB Atlas cluster. One common reason is that you’re trying to access the database from an IP that isn’t whitelisted. Make sure your current IP address is on your Atlas cluster’s IP whitelist: https://www.mongodb.com/docs/atlas/security-whitelist/

Mongodb开启所有IP

network access 添加 0.0.0.0

31:10 - Login API

35:02 - Get User API and JWT token authentication

本人在此安装了cookie-parser,相应指令为npm i cookie-parser

40:04 - Add Travel Story API

要在Postman中生成JWT令牌,您可以按照以下步骤操作:

  1. 打开Postman应用程序并创建一个新的请求。
  2. 在请求中选择“Authorization”选项卡。
  3. 在“Type”下拉菜单中选择“Bearer Token”。
  4. 在“Token”输入框中输入您的JWT令牌。
  5. 单击“Send”按钮,您的请求将包含JWT令牌。

请注意,您需要事先获得JWT令牌才能在Postman中使用它。您可以从您的身份验证服务器或第三方服务中获取JWT令牌。

generateToken.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import jwt from "jsonwebtoken";

const generateTokenAndSetCookie = (userId, res) => {
	const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
		expiresIn: "15d",
	});

	res.cookie("jwt", token, {
		maxAge: 15 * 24 * 60 * 60 * 1000, // MS
		httpOnly: true, // prevent XSS attacks cross-site scripting attacks
		sameSite: "strict", // CSRF attacks cross-site request forgery attacks
		secure: process.env.NODE_ENV !== "development",
	});
};

export default generateTokenAndSetCookie;

在上面的代码中返回或输出token均可,视情况而定。

49:05 - Get All Travel Stories API

51:07 - Image Upload API using Multer

backend目录下执行npm i multer

1
2
3
4
5
6
7
cd backend
xxxxxxxx@DESKTOP-393QBO3:~/TravelStory/backend$ npm i multer

added 16 packages in 3s

23 packages are looking for funding
  run `npm fund` for details

Multer是一个用来处理文件上传的Node.js中间件。它允许你轻松地上传文件到服务器。Multer非常灵活,支持处理单个文件上传、多个文件上传,以及各种自定义的上传设置。在Express框架中,Multer可以轻松地与路由处理函数一起使用,使得处理文件上传变得非常简单。它可以处理文件的解析、文件大小的限制、文件类型的限制等等。Multer在Node.js开发中用途广泛,特别是在需要实现文件上传功能的Web应用程序中。

1
2
3
import upload from "../utils/multer.js";
import fs from "fs";
import path from "path";
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const imageUpload = async (req, res) => {

	try {
		if (!req.file) {
			return res.status(400).json({ error: true, message: "No image uploaded" })
		}

		const imageUrl = `http://localhost:5000/uploads/${req.file.filename}`;
		res.status(201).json({ imageUrl });
	} catch (error) {
		res.status(500).json({ error: true, message: error.message })
	}
}
1
router.post("/image-upload", upload.single("image"), imageUpload)

PostMan无法上传图片,解决办法详见链接。

https://stackoverflow.com/questions/60036239/upload-file-failed-postman

This was what was in my Postman Settings. But in my Desktop, the folder was names as Postman Agent, so the path was ~/Postman Agent/files. Therefore Postman couldn’t read from my system, rename Postman Agent in your local machine to Postman.

This is more like a bug with Postman (already open issue, still not corrected).

enter image description here

下面这句代码报错,报错信息为

1
2
3
4
5
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
                                             ^

ReferenceError: __dirname is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/home/xxxxxxxx/TravelStory/backend/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

有问题的代码如下:

app.use("/uploads", express.static(path.join(__dirname, "uploads")));

这该怎么改正?

要解决这个问题,可以将该文件的扩展名从.js改为.cjs,或者将package.json中的"type": "module"改为"type": "commonjs"。这样就可以在CommonJS模块的范围内使用__dirname了。修改后的代码示例如下:

1
app.use("/uploads", express.static(path.join(__dirname, "uploads")));

要在ES6模块的范围内使用__dirname,可以通过使用import.meta.url来动态获取当前模块的路径。修改后的代码如下所示:

1
2
3
4
5
6
7
8
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import express from 'express';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

app.use("/uploads", express.static(path.join(__dirname, "uploads")));

这样就可以在ES6模块中使用__dirname来获取当前模块的路径了。

58:21 - Delete Image API

01:04:15 - Edit Travel Story API

01:09:53 - Delete Travel Story API

01:13:58 - Update isFavourite API

01:18:19 - Search Stories API

01:21:58 - Filter Stories by Date Range

Frontend

01:26:12 - Login screen UI

添加images,修改tailwind.config.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    fontFamily: {
      display: ["Poppins", "sans-serif"],
    },
    extend: {
      // Colors used in the project
      colors: {
        primary: "#05B6D3",
        secondary: "#EF863E",
      },
      backgroundImage: {
        'login-bg-img': "url('./src/assets/images/bg-image.png')",
        'signup-bg-img': "url('./src/assets/images/signup-bg-image.jpg')",
      }
    },
  },
  plugins: [],
}

在index.css中添加如下内容

1
2
3
4
5
6
7
8
9
@layer components {
  .input-box {
    @apply w-full text-sm bg-cyan-600/5 rounded px-5 py-3 mb-4 outline-none;
  }

  .btn-primary {
    @apply w-full text-sm font-medium text-white bg-cyan-500 shadow-lg shadow-cyan-200/50 p-[10px] rounded-full my-1 hover:bg-cyan-100 hover:text-primary;
  }
}

创建passwordInput.jsx文件

安装npm i react-icons

新增utils/helper.js

01:42:28 - Login API integration

在frontend目录下安装axios npm i axios

utils目录下创建constants.jsaxiosInstance.js

01:54:14 - Sign-up screen UI

01:57:29 - Create Account API integration

02:01:57 - Home page UI

02:04:55 - Get User Info API integration

在Cards目录下添加ProfileInfo.jsx

02:08:00 - Navbar Profile Card

02:14:20 - Get All Stories API integration

创建emptycard、StoryCard

02:17:07 - Travel Story Card component

按照moment 执行npm i moment

02:26:30 - Function to update isFavourite

安装react-toastify执行npm i react-modal

安装react-modal 执行npm i react-modal

02:34:14 - Add/Edit Travel Story

index.css中添加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  html {
    font-family: "Poppins", "sans-serif";
  }

  body {
    background-color: #fdfeff;
    overflow-x: hidden;
  }
}

/* Customize scrollbar styles*/
.scrollbar::-webkit-scrollbar {
  width: 3px;
  height: 3px;
}

.scrollbar::-webkit-scrollbar-thumb {
  background-color: #057c8e;
  border-radius: 3px;
}

.scrollbar::-webkit-scrollbar-track {
  background-color: rgb(172, 201, 229);
}

@layer components {
  .input-box {
    @apply w-full text-sm bg-cyan-600/5 rounded px-5 py-3 mb-4 outline-none;
  }

  .btn-primary {
    @apply w-full text-sm font-medium text-white bg-cyan-500 shadow-lg shadow-cyan-200/50 p-[10px] rounded-full my-1 hover:bg-cyan-100 hover:text-primary;
  }

  .login-ui-box {
    @apply w-80 h-[450px] rounded-full bg-primary absolute rotate-45
  }

  .model-box {
    @apply w-[80vw] md:w-[40%] h-[80vh] bg-white rounded-lg mx-auto mt-14 p-5 overflow-y-scroll scrollbar z-50;
  }

  .icon-btn {
    @apply text-[22px] text-slate-300 cursor-pointer hover:text-red-500;
  }

  .btn-small {
    @apply flex items-center gap-1 text-xs font-medium bg-cyan-50 text-primary shadow-cyan-100/0 border border-cyan-100 hover:bg-primary hover:text-white rounded px-3 py-[3px];
  }
  .input-label {
    @apply text-xs text-slate-400;
  }
  .btn-delete {
    @apply bg-rose-50 text-rose-500 shadow-cyan-100/0 border border-rose-100 hover:border-rose-500 hover:text-white;
  }
}

新增Input目录下DateSelector

02:47:12 - Date Selector component

安装react-day-picker 执行npm i react-day-picker

main.jsx中添加import "react-day-picker/style.css";

index.css中添加

1
2
3
4
5
6
.rdp-root {
    --rdp-accent-color: #01b0cb;
    --rdp-accent-background-color: #dffbff;
    --rdp-day_button-border-radius: 8px;
    --rdp-selected-font: bold medium var(--rdp-font-family);
  }

02:58:34 - Custom Image Picker component

增加ImageSelector组件

添加TagInput组件

03:22:10 - Function to add new Travel Story

03:24:34 - Utility function to upload image

utils下面添加uploadImage.js

03:31:43 - View Travel Story popup modal

在home目录下添加ViewTravelStory.jsx

03:45:11 - Function to update story

03:51:20 - Function to delete Travel Story image

03:58:37 - Function to delete story

04:09:54 - Search Bar component

04:14:05 - Search Stories API integration

04:17:59 - Date Range Picker component

04:21:31 - Filter Travel Stories by date range

04:25:35 - Filter Info Title component

Cards下新建FilterInfoTitle.jsx

附录

没用上的utilities.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const jwt = require('jsonwebtoken')

function authenticateToken(req, res, next) {
    const authHeader = req.headers["authorization"];
    const token = authHeader && authHeader.split[" "][1];

    // No token, unauthorized
    if (!token) return res.sendState(401);
    
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        // Token invalid, forbidden
        if(err) return res.sendStatus(401);
        req.user = user;
        next();
    });
}

module.exports = {
    authenticateToken,
};

自用需要配置.env

1
2
3
4
5
6
7
PORT=xxxx

JWT_SECRET=xxxxxxxxxxx

MONGO_DB_URI=xxxxxxx

NODE_ENV=xxxxxx

Wiki: bcrypt vs brypt.js

Short summary: bcrypt is a native (C++) module, thus much faster than bcryptjs which is a pure js module.

bcrypt sometimes requires additional steps to build correctly, especially if you are using architectures other than x86_64 or a glibc based distro. You will need additional dependencies to compile from source.

bcryptjs is plain js, hence works everywhere, even browsers. bcrypt runs only on NodeJS, Node-WebKit or Electron.

Free SVG converter

https://www.freeconvert.com/jpg-to-svg/download

https://picsvg.com/

Picsvg

Need to convert a picture to SVG format ?

Picsvg is a free online converter that can convert an image to a SVG file.You can upload an image file (jpg,gif,png) up to 4 Mb, then you can select effects to enhance the SVG image result.