利用 MERN 堆栈的绝对威力构建全栈公路旅行地图应用程序🔥

2025-05-24

利用 MERN 堆栈的绝对威力构建全栈公路旅行地图应用程序🔥

本文将重点介绍 MERN 堆栈应用程序构建过程中最关键的任务和概念,以便您更好地理解并从零开始构建 MERN 堆栈应用程序。它面向那些认真学习 MERN 堆栈并希望专注于基本知识的读者。我们将构建一个全栈公路旅行地图应用程序,用户可以在其中定位和绘制位置,并查看其他用户定位的地点,所有这些都将使用 MERN 堆栈并充分利用 Mapbox API 的强大功能。本篇博文将讲解 MERN 堆栈技术的基础知识以及高级概念和操作。

以下是我们应用程序最终版本的快速预览:

演示

演示

演示

演示

您可以在单独的文章中详细了解 MERN 堆栈。

https://aviyel.com/post/1278

设置文件夹结构

在项目目录中创建两个名为客户端和服务器的文件夹,然后在 Visual Studio Code 或您选择的任何其他代码编辑器中打开它们。

制作目录

文件夹结构

现在,我们将创建一个 MongoDB 数据库、一个 Node 和 Express 服务器、一个用于表示我们项目案例研究应用程序的数据库模式,以及使用 npm 和相应的包创建、读取、更新和删除数据库中数据和信息的 API 路由。因此,打开命令提示符,导航到服务器的目录,然后运行下面的代码。

npm init -yes
Enter fullscreen mode Exit fullscreen mode

配置 package.json 文件

在终端中执行以下命令来安装依赖项。

npm install cors dotenv express express-rate-limit mongoose nodemon body-parser helmet morgan rate-limit-mongo
Enter fullscreen mode Exit fullscreen mode

依赖项

  • Dotenv:Dotenv 是一个零依赖模块,它将环境变量从 .env 文件加载到 process.env 中

  • cors:此模块允许放宽对 API 的安全性

  • express:快速、不固执己见、极简的节点 Web 框架。

  • express-rate-limit:Express 的基础 IP 速率限制中间件。用于限制对公共 API 和/或端点(例如密码重置)的重复请求。

  • mongoose:它是 MongoDB 和 Node.js 的对象数据建模库

  • nodemon:此模块通过在检测到目录中的文件更改时自动重新启动应用程序来帮助开发基于 node.js 的应用程序。

  • body-parser:Node.js 主体解析中间件。

  • helmet:Helmet.js 通过保护 Express 应用程序返回的 HTTP 标头来填补 Node.js 和 Express.js 之间的空白。

  • morgan:node.js 的 HTTP 请求记录器中间件

  • rate-limit-mongo :用于 express-rate-limit 中间件的 MongoDB 存储。

依赖项安装

安装依赖项后,“package.json”文件应如下所示。

包 JSON

另外,请记住更新脚本。

脚本

现在转到您的服务器目录,在那里创建一个 src 文件夹和一个 index.js 文件。

设置index.js

  • 导入 express 模块。

  • 导入并配置dotenv模块

  • 导入头盔模块。

  • 导入 morgan 模块。

  • 导入 CORS 模块

  • 使用 express() 初始化我们的应用程序。

//src/index.js
const express = require('express');
// NOTE morgan is a logger
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const mongoose = require('mongoose');

require('dotenv').config();

// app config
const app = express();
Enter fullscreen mode Exit fullscreen mode

现在,我们可以使用该应用实例上的所有其他方法了。让我们从最基础的设置开始。别忘了设置端口和 cors。

const express = require('express');
// NOTE morgan is a logger
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const mongoose = require('mongoose');

require('dotenv').config();

const app = express();

const port = process.env.PORT || 4000;

app.use(morgan('common'));
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN,
}));

app.use(express.json());

app.get('/', (req, res) => {
  res.json({
    message: 'Hello There',
  });
});
Enter fullscreen mode Exit fullscreen mode

现在是时候将我们的服务器应用程序连接到真正的数据库了。这里我们将使用 MongoDB 数据库,特别是 MongoDB 云 Atlas 版本,这意味着我们的数据库将托管在他们的云上。

设置 MongoDB Atlas 云集群

MongoDB 是一个面向文档的开源跨平台数据库。MongoDB 是一个 NoSQL 数据库,它将数据存储在类似 JSON 的文档中,并具有可选的模式。2018 年 10 月 16 日之前的所有版本均遵循 AGPL 许可证。2018 年 10 月 16 日之后发布的所有版本(包括之前版本的错误修复)均遵循 SSPL v1 许可证。您还可以从以下文章中了解有关 MongoDB 设置和配置的更多信息。

https://aviyel.com/post/1323

要设置并启动您的 MongoDB 集群,请按照下面提到的完全相同的步骤进行操作。

MongoDB 官方网站*
MongoDB Atlas 已启动

注册 MongoDB
MongoDB Atlas 注册

登录 MongoDB
MongoDb Atlas 登录

创建项目
创建项目

添加成员
添加成员

建立数据库
建立数据库

创建集群
创建集群

选择云服务提供商
选择服务提供商

配置安全性
安全配置

数据库部署到云端
数据库部署到云端

导航到网络访问选项卡并选择“添加 IP 地址”。
添加 IP 地址

现在,选择连接方法。
连接方法

连接到集群
连接到集群

在 index.js 中创建一个名为 DATABASE_CONNECTION 的新变量。创建一个字符串,并将复制的 mongo DB 连接 URL 粘贴到其中。现在,在其中输入您的用户名和密码,删除所有括号并输入您自己的凭据。稍后我们将创建环境变量来保护凭据,但现在让我们以这种方式添加它。我们需要的第二个参数是 PORT,因此现在只需输入 4000。最后,我们将使用 mongoose 连接到数据库,因此输入 mongoose.connect(),这是一个带有两个参数的函数。DATABASE_CONNECTION 将是第一个参数,而具有两个选项的对象将是第二个参数。第一个是 useNewUrlParser(我们将启用它),第二个是 useUnifiedTopology(我们也将启用它)。这些对象是可选的,但我们会在控制台上看到一些错误或警告。让我们在 then() 函数中使用 .then() 和 .catch() 来连接它。这只会调用应用程序并调用 listen,传入两个参数:PORT 和回调函数,该函数将在应用程序成功连接到数据库时执行。最后,如果连接数据库失败,我们将在控制台中记录错误消息。你的 index.js 文件现在应该如下所示。

//src/index.js
const express = require('express');
// NOTE morgan is a logger
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const mongoose = require('mongoose');

require('dotenv').config();

const app = express();

const DATABASE_CONNECTION = process.env.DATABASE_URL;

mongoose.connect(DATABASE_CONNECTION, {
  useNewUrlParser: true,
  newUnifiedTopology: true,
});

app.use(morgan('common'));
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN,
}));

app.use(express.json());

app.get('/', (req, res) => {
  res.json({
    message: 'Hello There',
  });
});

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Currently Listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

将 mongodb+srv 插入到 .env 文件中。

PORT=4000
DATABASE_URL=mongodb+srv://pramit:<password>@cluster0.8tw83.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
CORS_ORIGIN=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

现在,我们已经成功将服务器连接到数据库,在开始构建后端应用程序的路由和数据库模式之前,我们先来创建中间件。为此,我们需要创建一个名为 middlewares.js 的新文件,并在该文件中创建两个名为 notFound 和 errorHandler 的函数。

并导出这些函数。因此,让我们创建 notFound 中间件。通常,此中间件应该是注册的最后一个中间件,因此它接收 req、res 和 next。基本上,如果请求在这里发出,则意味着我们没有找到用户正在搜索的路由。因此,我们将创建一个变量并向其发送一条消息,然后将其传递给下一个中间件,即 errorHander 中间件。在此之前,别忘了传递 404 的响应状态。现在,让我们创建 errorHandler 中间件,它有四个参数而不是三个,因此我们将有 (error,req, res, next)。我们要做的第一件事是设置状态码并检查它是否为 200,或者使用已指定的状态码。然后,我们只需设置状态码,然后我们将使用一些 JSON 进行响应以显示错误消息。

//middlewares.js
const notFound = (req, res, next) => {
  const error = new Error(`Not Found - ${req.originalUrl}`);
  res.status(404);
  next(error);
};

const errorHandler = (error, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode);
  res.json({
    message: error.message,
    stack: process.env.NODE_ENV === "production" ? "nope" : error.stack,
  });
};

module.exports = {
  notFound,
  errorHandler,
};
Enter fullscreen mode Exit fullscreen mode

所以,修改完middlewares.js文件后,在index.js文件中根据需要导入并使用中间件。

//src/index.js
const express = require("express");
// NOTE morgan is a logger
const morgan = require("morgan");
const helmet = require("helmet");
const cors = require("cors");
const mongoose = require("mongoose");

require("dotenv").config();

const middlewares = require("./middlewares");
const app = express();

const DATABASE_CONNECTION = process.env.DATABASE_URL;

mongoose.connect(DATABASE_CONNECTION, {
  useNewUrlParser: true,
  newUnifiedTopology: true,
});

app.use(morgan("common"));
app.use(helmet());
app.use(
  cors({
    origin: process.env.CORS_ORIGIN,
  })
);

app.use(express.json());

app.get("/", (req, res) => {
  res.json({
    message: "Hello There",
  });
});

app.use(middlewares.notFound);
app.use(middlewares.errorHandler);

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Currently Listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

让我们创建一个 LogEntry 模型。创建一个名为 models 的文件夹,并在其中创建一个名为 LogEntry.model.js 的文件,并在该文件中定义标题、描述、评论、图片、评分、经纬度,构建您的数据库模式,如下所示。

//models/LogEntry.model.js
const mongoose = require("mongoose");
const { Schema } = mongoose;

const requiredNumber = {
  type: Number,
  required: true,
};

const logEntrySchema = new Schema(
  {
    title: {
      type: String,
      required: true,
    },
    description: String,
    comments: String,
    image: String,
    rating: {
      type: Number,
      min: 0,
      max: 10,
      default: 0,
    },
    latitude: {
      ...requiredNumber,
      min: -90,
      max: 90,
    },
    longitude: {
      ...requiredNumber,
      min: -180,
      max: 180,
    },
    visitDate: {
      required: true,
      type: Date,
    },
  },
  {
    timestamps: true,
  }
);

const LogEntry = mongoose.model("collections", logEntrySchema);

module.exports = LogEntry;
Enter fullscreen mode Exit fullscreen mode

您的文件和文件夹的结构现在应该看起来像这样。

文件夹结构

现在我们已经成功创建了数据库模式,接下来我们开始为后端应用程序创建路由。为此,我们需要在 src 目录中创建一个新文件夹,并将其命名为 routes。在 routes 文件夹中,我们将创建一个名为 logs.routes.js 的 js 文件。首先,我们必须从“express”导入 express,同时配置路由器并导入我们刚刚创建的数据库模式。现在,我们可以开始向其中添加路由了。

文件夹结构

const { Router } = require("express");

const LogEntry = require("../models/LogEntry.model.js");

const { API_KEY } = process.env;

const router = Router();
Enter fullscreen mode Exit fullscreen mode

获取所有固定位置信息。

router.get("/", async (req, res, next) => {
  try {
    const entries = await LogEntry.find();
    res.json(entries);
  } catch (error) {
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

插入/添加具有授权访问的固定位置

router.post("/", async (req, res, next) => {
  try {
    if (req.get("X-API-KEY") !== API_KEY) {
      res.status(401);
      throw new Error("Unauthorized Access");
    }
    const logEntry = new LogEntry(req.body);
    const createdEntry = await logEntry.save();
    res.json(createdEntry);
  } catch (error) {
    if (error.name === "ValidationError") {
      res.status(422);
    }
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

导出路由器

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

你的 logs.routes.js 应该类似于以下内容

//src/routes/logs.routes.js
const { Router } = require("express");

const LogEntry = require("../models/LogEntry.model.js");

const { API_KEY } = process.env;

const router = Router();

router.get("/", async (req, res, next) => {
  try {
    const entries = await LogEntry.find();
    res.json(entries);
  } catch (error) {
    next(error);
  }
});

router.post("/", async (req, res, next) => {
  try {
    if (req.get("X-API-KEY") !== API_KEY) {
      res.status(401);
      throw new Error("Unauthorized Access");
    }
    const logEntry = new LogEntry(req.body);
    const createdEntry = await logEntry.save();
    res.json(createdEntry);
  } catch (error) {
    if (error.name === "ValidationError") {
      res.status(422);
    }
    next(error);
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

现在,更新你的 .env 文件

NODE_ENV=production
PORT=4000
DATABASE_URL=mongodb+srv://pramit:<password>@cluster0.8tw83.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
CORS_ORIGIN=http://localhost:3000
API_KEY=roadtripmapper
Enter fullscreen mode Exit fullscreen mode

首先,将日志路由导入到你的 index.js 文件中。现在,我们可以使用 Express 中间件将地图固定日志信息连接到我们的应用程序。最终,你的根 index.js 文件应该如下所示。

//src/index.js
const express = require("express");
// NOTE morgan is a logger
const morgan = require("morgan");
const helmet = require("helmet");
const cors = require("cors");
const mongoose = require("mongoose");

require("dotenv").config();

const middlewares = require("./middlewares");
const logs = require("./routes/logs.routes.js");
const app = express();

const DATABASE_CONNECTION = process.env.DATABASE_URL;

mongoose.connect(DATABASE_CONNECTION, {
  useNewUrlParser: true,
  newUnifiedTopology: true,
});

app.use(morgan("common"));
app.use(helmet());
app.use(
  cors({
    origin: process.env.CORS_ORIGIN,
  })
);

app.use(express.json());

app.get("/", (req, res) => {
  res.json({
    message: "Hello There",
  });
});

app.use("/api/logs", logs);

app.use(middlewares.notFound);
app.use(middlewares.errorHandler);

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Currently Listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

重新启动服务器后,您应该会看到如下内容:

服务器正在运行


使用 React 设置前端

下一步,让我们从前端开始,用 React 构建它。如果您的机器上尚未安装 Node.js,首先需要安装它。请访问 Node.js 官方网站并下载最新版本。您需要 Node.js 才能使用 Node 包管理器(通常称为 NPM)。现在,在您常用的代码编辑器中导航到客户端文件夹。我选择的工具是 Visual Studio Code。然后,在集成终端中输入 npx create-react-app。此命令将在当前目录中创建一个名为 client 的客户端应用程序。

创建 React 应用

有一篇单独的文章,你可以从中了解有关

清理样板反应项目。

https://aviyel.com/post/1190

现在您已经安装并清理了 react-boilerplate,是时候在其中安装一些软件包了。因此,请将以下命令复制并粘贴到您的终端中。

npm i react-hook-form react-map-gl react-rating-stars-component react-responsive-animate-navbar
Enter fullscreen mode Exit fullscreen mode

依赖项

  • react-hook-form:React Hooks 的高性能、灵活且可扩展的表单库。

  • react-map-gl:react-map-gl 是一套 React 组件,旨在为 Mapbox GL JS 兼容库提供 React API

  • react-rating-stars-component:适用于您的 React 项目的简单星级评定组件。

  • react-responsive-animate-navbar:简单、灵活且完全可定制的响应式导航栏组件。

依赖项安装

安装所有这些软件包后,客户端的 packge.json 文件应如下所示:

json 包

安装完项目的所有依赖项后,我们在 components 文件夹内构建两个单独的文件夹 /components,并将其命名为 RoadTripNav 和 TripEntryForm 。

添加所有组件后,您的文件和文件夹结构应该看起来像这样。

文件夹结构

现在您已设置好项目的所有组件,是时候开始编写代码了。首先,从“react-responsive-animate-navbar”导入 ReactNavbar,并自定义导航栏的颜色,将徽标添加到公共文件夹并直接导入,别忘了添加一些社交链接。以下是代码示例。

RoadTripNavComponent

// components/RoadTripNav
import React from "react";
import * as ReactNavbar from "react-responsive-animate-navbar";
// import roadTripSvg from "../../assets/roadtrip.svg";

const RoadTripNav = () => {
  return (
    <ReactNavbar.ReactNavbar
      color="rgb(25, 25, 25)"
      logo="./logo.svg"
      menu={[]}
      social={[
        {
          name: "Twitter",
          url: "https://twitter.com/pramit_armpit",
          icon: ["fab", "twitter"],
        },
      ]}
    />
  );
};

export default RoadTripNav;
Enter fullscreen mode Exit fullscreen mode

在继续之前,我们先来设置一下 Mapbox。首先,前往 Mapbox 网站并登录,或者如果您还没有账户,请注册。接下来,在 Mapbox Studio 中创建您自己的自定义地图样式并发布。最后,返回仪表盘并复制 MapBox 提供的默认公共 API 密钥。

Mapbox

登录或创建您的 MapBox 帐户

Mapbox 登录

点击设计自定义地图样式

Mapbox工作室

在 Mapbox 工作室中自定义您自己的地图风格

MapBox 工作室编辑器

复制默认公共令牌
访问令牌

成功获取公共令牌后,请前往 env 文件(如果没有,请创建一个),然后创建一个名为 REACT_APP_MAPBOX_TOKEN 的变量,并将该令牌粘贴到该变量中。您的 env 文件应该如下所示。

REACT_APP_MAPBOX_TOKEN= ************************************ // add token
Enter fullscreen mode Exit fullscreen mode

在继续下一步之前,让我们在根源目录中创建一个 api 和 style 文件夹。在 api 文件夹中创建一个 API.js 文件,在 style 文件夹中创建一个 index.css 文件,用于添加应用程序的所有样式。您的文件夹结构应该如下所示。

文件夹结构

现在,转到新创建的 API 文件,并构建两个函数,分别名为“listLogEntries”和“createLogEntries”,分别用于从后端收集所有日志条目和创建或发送 post 请求/将条目发布到后端,以及导出这些函数。另外,别忘了包含服务器运行的 URL。

API

//api/API.js
const API_URL = "http://localhost:4000";
// const API_URL = window.location.hostname === "localhost" ? "http://localhost:4000" : "https://road-trip-map-mern.herokuapp.com" ;

export async function listLogEntries() {
  const response = await fetch(`${API_URL}/api/logs`);
  // const json = await response.json();
  return response.json();
}

export async function createLogEntries(entry) {
  const api_key = entry.api_key;
  delete entry.api_key;
  const response = await fetch(`${API_URL}/api/logs`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "X-API-KEY": api_key,
    },
    body: JSON.stringify(entry),
  });
  // const json = await response.json();
  // return response.json();
  let json;
  if (response.headers.get("content-type").includes("text/html")) {
    const message = await response.text();
    json = {
      message,
    };
  } else {
    json = await response.json();
  }
  if (response.ok) {
    return json;
  }
  const error = new Error(json.message);
  error.response = json;
  throw error;
}
Enter fullscreen mode Exit fullscreen mode

让我们创建一个用于提交固定地图位置的表单。为此,请从我们之前创建的组件文件夹中打开 TripEntryForm 组件,从 react-hook-form 导入 useForm 钩子,从 api 导入 createLogentries,然后从 React 库导入 useState 钩子,因为这个钩子使我们能够将状态集成到我们的函数式组件中。与类组件中的状态不同,useState() 不适用于对象值。如有必要,我们可以直接使用原语,并为多个变量创建多个 React 钩子。现在,创建两个状态:加载和错误,然后从“react-hook-form”库中的 useForm() 钩子解构 register 和 handleSubmit。完成后,是时候制作表单了,但首先让我们创建一个函数来处理提交请求。为此,创建一个异步 onSubmit 函数,并在其中创建一个 try-catch 代码块。在 try 块内将加载设置为 true,配置经度和纬度,控制台记录数据,并调用 onClose 函数,最后在 catch 块内,将错误消息传递给错误状态,将加载设置为 false 并简单地控制台记录错误消息,然后在 return 语句内简单地创建一个表单,如下面的代码所示。

行程登记表

// components/TripEntryForm.js
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { createLogEntries } from "../../api/API";
import "./TripEntryForm.css";

const TripEntryForm = ({ location, onClose }) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const { register, handleSubmit } = useForm();

  const onSubmit = async (data) => {
    try {
      setLoading(true);
      data.latitude = location.latitude;
      data.longitude = location.longitude;
      const created = await createLogEntries(data);
      console.log(created);
      onClose();
    } catch (error) {
      setError(error.message);
      console.error(error);
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="trip-form">
      {error ? <h3 className="error-message">{error}</h3> : null}
      <label htmlFor="api_key">Enter Password</label>
      <input
        type="password"
        name="api_key"
        placeholder="For demo, password => {roadtripmap} "
        required
        ref={register}
      />

      <label htmlFor="title">Title</label>
      <input name="title" placeholder="Title" required ref={register} />

      <label htmlFor="comments">Comments</label>
      <textarea
        name="comments"
        placeholder="Comments"
        rows={3}
        ref={register}
      ></textarea>

      <label htmlFor="description">Description</label>
      <textarea
        name="description"
        placeholder="Describe your journey"
        rows={4}
        ref={register}
      ></textarea>

      <label htmlFor="image">Image</label>
      <input name="image" placeholder="Image URL" ref={register} />

      <label htmlFor="rating">Rating (1 - 10)</label>
      <input name="rating" type="number" min="0" max="10" ref={register} />

      <label htmlFor="visitDate">Visit Date</label>
      <input name="visitDate" type="date" required ref={register} />

      <button disabled={loading}>
        <span>{loading ? "Submitting..." : "Submit your Trip"}</span>
      </button>
    </form>
  );
};

export default TripEntryForm;
Enter fullscreen mode Exit fullscreen mode

另外,不要忘记在该组件文件夹中添加 TripEntryForm 样式,并将其命名为 TripEntryForm.css,并粘贴如下所示的确切 CSS 代码

TripEntry 表单样式

//TripEntryForm.css
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&family=Poppins:ital,wght@0,200;0,400;1,200;1,300&family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");

.trip-form label {
  margin: 0.5rem 0;
  display: block;
  width: 100%;
  color: rgb(255, 255, 255);
  font-family: "Fredoka One", cursive;
}
.trip-form input {
  margin: 0.5rem 0;
  background-color: #2c2e41;
  border-radius: 5px;
  border: 0;
  box-sizing: border-box;
  color: rgb(255, 255, 255);
  font-size: 12px;
  height: 100%;
  outline: 0;
  padding: 10px 5px 10px 5px;
  width: 100%;
  font-family: "Fredoka One", cursive;
}

.trip-form textarea {
  margin: 0.5rem 0;
  background-color: #2c2e41;
  border-radius: 5px;
  border: 0;
  box-sizing: border-box;
  color: rgb(255, 255, 255);
  font-size: 12px;
  height: 100%;
  outline: 0;
  padding: 10px 5px 10px 5px;
  width: 100%;
  font-family: "Fredoka One", cursive;
}

.error-message {
  color: red;
}

.trip-form button {
  background-color: #fb5666;
  border-radius: 12px;
  border: 0;
  box-sizing: border-box;
  color: #eee;
  cursor: pointer;
  font-size: 18px;
  height: 50px;
  margin-top: 38px;
  outline: 0;
  text-align: center;
  width: 100%;
}

button span {
  position: relative;
  z-index: 2;
}

button:after {
  position: absolute;
  content: "";
  top: 0;
  left: 0;
  width: 0;
  height: 100%;
  transition: all 2.35s;
}

button:hover {
  color: #fff;
}

button:hover:after {
  width: 100%;
}

.small_description {
  font-size: 60px;
}
Enter fullscreen mode Exit fullscreen mode

现在转到此 repo 并下载那里可用的所有 SVG 文件。

https://github.com/pramit-marattha/road-trip-mapper-mern-app/tree/main/client/src/assets

下载完所有 svg 文件后,转到主应用程序组件,开始从我们之前安装的库中导入所有关键要求,例如来自“react-map-gl”库的 ReactMapGl、marker 和 popup,从 assets 文件夹导入所有组件和 svg,最后创建四个状态 logEntries,其初始值为空数组,showPopup 的初始值为空对象,addEntryLocation 的默认值为 null,对于 viewport,指定初始值与下面提到的代码完全相同,或者您可以添加任何您想要的值。创建一个名为 getEntries 的异步函数,该函数异步调用先前在 api 文件中建立的 listLogEntries 函数,其主要任务是检索用户输入的所有条目并将它们提供给 logEntries 状态,然后通过使用此 Hook 在 useEffect() 钩子中调用该函数,您告诉 React 您的组件需要在渲染后执行某些操作。

React 会记住你传递的函数(我们将其称为“effect”),并在执行 DOM 更新后调用它。为此,我们设置了文档标题,但我们也可以执行数据获取或调用其他一些命令式 API。将 useEffect() 放置在组件内部,让我们可以直接从 effect 中访问 c​​ount 状态变量(或任何 props)。我们不需要特殊的 API 来读取它——它已经在函数作用域中了。Hooks 拥抱 JavaScript 闭包,避免引入 JavaScript 已经提供解决方案的 React 特定 API。useEffect() hook 有点类似于我们所知的类组件的生命周期方法。它在组件的每次渲染(包括初始渲染)后运行。因此,它可以被认为是 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。如果我们想要控制 effect 何时运行的行为(仅在初始渲染时,或仅在特定状态变量发生变化时),我们可以将依赖项传递给 effect 来实现。此钩子还提供了一个清理选项,允许在组件被销毁之前清理资源。效果的基本语法:useEffect(didUpdate)。

创建一个名为 showMarkerPopup 的函数,并为其提供事件参数。在该函数中,析构“event.lngltd”中的经纬度,并将其传递给 addEntryLocation 状态。最后,按照如下所示的代码,在 return 语句中使用所有导入的组件。

应用程序组件

//src/app.js
import * as React from "react";
import { useState, useEffect } from "react";
import ReactMapGL, { Marker, Popup } from "react-map-gl";
import { listLogEntries } from "./api/API";
import MapPinLogo from "./assets/mapperPin.svg";
import MarkerPopup from "./assets/MarkerPopup.svg";
import TripEntryForm from "./components/TripEntryForm";
import ReactStars from "react-rating-stars-component";
import RoadTripNav from "./components/RoadTripNav/RoadTripNav";

const App = () => {
  const [logEntries, setLogEntries] = useState([]);
  const [showPopup, setShowPopup] = useState({});
  const [addEntryLocation, setAddEntryLocation] = useState(null);
  const [viewport, setViewport] = useState({
    width: "100vw",
    height: "100vh",
    latitude: 27.7577,
    longitude: 85.3231324,
    zoom: 7,
  });

  const getEntries = async () => {
    const logEntries = await listLogEntries();
    setLogEntries(logEntries);
    console.log(logEntries);
  };

  useEffect(() => {
    getEntries();
  }, []);

  const showMarkerPopup = (event) => {
    console.log(event.lngLat);
    const [longitude, latitude] = event.lngLat;
    setAddEntryLocation({
      longitude,
      latitude,
    });
  };

  return (
    <>
      <RoadTripNav />
      <ReactMapGL
        {...viewport}
        mapStyle="mapbox://styles/pramitmarattha/ckiovge5k3e7x17tcmydc42s3" 
        mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
        onViewportChange={(nextViewport) => setViewport(nextViewport)}
        onDblClick={showMarkerPopup}
      >
        {logEntries.map((entry) => (
          <React.Fragment key={entry._id}>
            <Marker latitude={entry.latitude} longitude={entry.longitude}>
              <div
                onClick={() =>
                  setShowPopup({
                    // ...showPopup,
                    [entry._id]: true,
                  })
                }
              >
                <img
                  className="map-pin"
                  style={{
                    width: `${5 * viewport.zoom}px`,
                    height: `${5 * viewport.zoom}px`,
                  }}
                  src={MapPinLogo}
                  alt="Map Pin Logo"
                />
              </div>
            </Marker>
            {showPopup[entry._id] ? (
              <Popup
                latitude={entry.latitude}
                longitude={entry.longitude}
                closeButton={true}
                closeOnClick={false}
                dynamicPosition={true}
                onClose={() => setShowPopup({})}
                anchor="top"
              >
                <div className="popup">
                  <ReactStars
                    count={10}
                    value={entry.rating}
                    size={29}
                    activeColor="#ffd700"
                  />
                  <div className="popup_image">
                    {entry.image && <img src={entry.image} alt={entry.title} />}
                  </div>
                  <h3>{entry.title}</h3>
                  <p>{entry.comments}</p>
                  <small>
                    Visited :{" "}
                    {new Date(entry.visitDate).toLocaleDateString("en-US", {
                      weekday: "long",
                      year: "numeric",
                      month: "long",
                      day: "numeric",
                    })}
                  </small>
                  <p>Ratings: {entry.rating}</p>
                  <div className="small_description">{entry.description}</div>
                </div>
              </Popup>
            ) : null}
          </React.Fragment>
        ))}
        {addEntryLocation ? (
          <>
            <Marker
              latitude={addEntryLocation.latitude}
              longitude={addEntryLocation.longitude}
            >
              <div>
                <img
                  className="map-pin"
                  style={{
                    width: `${8 * viewport.zoom}px`,
                    height: `${8 * viewport.zoom}px`,
                  }}
                  src={MarkerPopup}
                  alt="Map Pin Logo"
                />
              </div>
              {/* <div style={{color:"white"}}>{entry.title}</div> */}
            </Marker>

            <Popup
              latitude={addEntryLocation.latitude}
              longitude={addEntryLocation.longitude}
              closeButton={true}
              closeOnClick={false}
              dynamicPosition={true}
              onClose={() => setAddEntryLocation(null)}
              anchor="top"
            >
              <div className="popup">
                <TripEntryForm
                  onClose={() => {
                    setAddEntryLocation(null);
                    getEntries();
                  }}
                  location={addEntryLocation}
                />
              </div>
            </Popup>
          </>
        ) : null}
      </ReactMapGL>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

最后一步是将所有样式添加到我们的项目中,这可以通过转到我们之前建立的样式文件夹并将下面提到的代码复制并粘贴到 index.css 文件中来完成。

索引 CSS

/* styles/index.css */
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&family=Poppins:ital,wght@0,200;0,400;1,200;1,300&family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");

body {
  margin: 0;
  font-family: "Fredoka One", cursive;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

.map-pin {
  position: absolute;
  transform: translate(-50%, -100%);
  z-index: -1;
}

.popup {
  width: 20vw;
  height: auto;
  padding: 1rem;
  background-color: #8661d1;
  border-radius: 5px;
  z-index: 999;
}

.popup img {
  width: 40%;
  height: auto;
  border-radius: 5%;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  padding-top: 1rem;
}

.popup_image {
  display: flex;
  justify-content: center;
  align-items: center;
}

.small_description {
  font-size: 1.5rem;
  color: #fff;
  border-radius: 5px;
  z-index: 999;
}

button {
  border: none;
  color: #fa5252;
  padding-right: 1rem;
  border-radius: 50%;
  font-size: 4rem;
  margin-top: 0.2rem;
  height: auto;
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

最后,启动客户端和服务器。

反应运行

应用程序启动并运行

演示

该应用程序的完整源代码可在此处获取。

https://github.com/aviyeldevrel/devrel-tutorial-projects/tree/main/MERN-roadtrip-mapper

主要文章可在此处查看 => https://aviyel.com/post/1430

编码愉快!!

如果您是项目维护者、贡献者或只是开源爱好者,请关注@aviyelHQ或在 Aviyel 上注册以获得早期访问权限。

加入 Aviyel 的 Discord => Aviyel 的世界

推特 =>[ https://twitter.com/AviyelHq ]

文章来源:https://dev.to/aviyel/building-a-fullstack-road-trip-mapper-app-using-the-absolute-power-of-mern-stack-117g
PREV
从零开始构建一个 MERN 堆栈的简单博客网站🔥
NEXT
使用 React 和 Firebase 构建全栈笔记本应用程序 📓🔥