从零开始构建一个 MERN 堆栈的简单博客网站🔥
几年前,Web 应用程序开发远不及如今。如今,选择如此之多,以至于新手常常不知所措,不知道哪种最适合自己。这不仅体现在整体堆栈上,也体现在开发工具上;选择如此之多。本博客教程强调 MERN 堆栈是开发完整 Web 应用程序的理想选择,并以非常详细的方式引导读者了解整个开发过程。
那么,MERN 堆栈到底是什么?
MERN 技术栈是一套用于创建现代单页应用 (SPA) 的流行技术。MERN 代表 MongoDB、Express、React 和 Node.js:
-
Node.js是一个流行的服务器端框架,它允许我们在 Web 服务器上运行 JavaScript 代码。
-
Express是一个 Node.js Web 应用程序框架,它使 Node 应用程序开发更简单、更快捷。
-
MongoDB是一个 NoSQL 数据库,以集合和文档的形式持久存储数据。
-
React是一个用于创建用户界面的 JavaScript 前端库。
在本博客教程中,我们将创建一个利用 MERN 堆栈执行 CRUD 操作的全栈博客应用程序。本博客教程将帮助您了解 MERN 堆栈的基本操作。
这是我们应用程序的最终预览。
设置后端
在项目目录中创建两个文件夹,分别为客户端和服务器,然后在 Visual Studio Code 或您选择的任何代码编辑器中打开它。
现在,我们首先使用 npm 配置后端并安装必要的软件包,然后配置 MongoDB 数据库,使用 Node 和 Express 配置服务器,设计数据库模式来定义我们的博客,并配置 API 路由来创建、读取、更新和删除数据库中的博客数据和信息。所以,现在导航到服务器的目录并从命令提示符运行下面的代码。
npm init -y
更新 package.json
要安装依赖项,请在终端中执行以下命令。
npm install cors express dotenv mongoose nodemon body-parser
安装依赖项后,“package.json”文件应如下所示。
另外,请记住更新脚本和类型。
现在,导航到您的服务器目录并在其中创建一个 server.js 文件。
配置 server.js
-
导入 express 模块。
-
导入 bodyParser 模块
-
导入 mongoose 模块
-
导入 CORS 模块
-
使用 express() 启动我们的应用程序。
//server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
const app = express();
现在我们可以在该应用实例上使用所有不同的方法了。首先,我们来做一些常规设置。我们将使用 app.use,并简单地传递 bodyParser 并将其大小限制在 20 到 50mb 之间,因为我们要发送一些可能非常大的图片。我们还将扩展为 true,并对 bodyParser 的 URL 进行相同的操作,并传递相同的参数。现在,我们还将使用 CORS 并将其作为函数调用。
//server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
const app = express();
app.use(bodyParser.json({ limit: '50mb', extended: true }))
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }))
app.use(cors());
现在是时候将我们的服务器应用程序链接到真实的数据库了,因此我们将使用 MongoDB 数据库,特别是 MongoDB 云 Atlas 版本,这意味着我们将把我们的数据库托管到他们的云上。
设置MongoDB云集群
MongoDB 是一个面向文档的开源跨平台数据库。MongoDB 是一个 NoSQL 数据库,它将数据存储在类似 JSON 的文档中,并支持可选的模式。MongoDB 是由 MongoDB Inc. 根据服务器端公共许可证创建和分发的数据库。
创建一个集群并等待集群构建完成后再继续(通常需要 5-10 分钟左右)
在数据库中创建一个用户。您需要 MongoDB URI 的用户名和密码,最后创建一个数据库用户。
现在,在 server.js 中创建一个新变量并将其命名为 DB_CONNECTION。在其中创建一个字符串,并粘贴复制的 mongo DB 连接 URL。现在,在其中输入您的用户名和密码,确保删除所有括号并输入您自己的凭据。我们稍后会通过创建环境变量来保护凭据,但现在我们先这样添加。第二个我们需要的是 PORT,因此只需输入端口号,目前为 6000。最后,我们将使用 mongoose 连接到数据库,因此输入 mongoose.connect() 函数,该函数包含两个不同的参数。第一个参数是 DB CONNECTION,第二个参数是一个包含两个不同选项的对象。第一个参数是 useNewUrlParser,我们将其设置为 true,第二个参数是 useUnifiedTopology,我们也将其设置为 true。这些对象不是必需的,但我们会在控制台上看到一些错误或警告。接下来,让我们链接 a.then() 和 .catch(),因为这将返回一个承诺,所以在 .then() 内部将调用应用程序并调用 listen,它有两个参数,第一个是 PORT,第二个是回调函数,如果我们的应用程序连接成功,则将执行该函数,最后,如果与数据库的连接不成功,我们将简单地在控制台记录我们的错误消息。
//server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(bodyParser.json({ limit: "50mb", extended: true }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors());
const DB_CONNECTION = process.env.DATABASE_URL;
const PORT = process.env.PORT || 6000;
mongoose
.connect(DB_CONNECTION, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() =>
app.listen(PORT, () =>
console.log(`Server is running @ : http://localhost:${PORT}`)
)
)
.catch((error) => console.error(error));
将 mongodb+srv 插入到 .env 文件中。
PORT=4000
DATABASE_URL=mongodb+srv://admin:<password>@cluster0.ddtsa.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
就是这样;我们已成功将服务器链接到数据库。
现在我们已经成功连接到数据库,接下来开始为后端应用程序创建路由。为此,我们需要在服务器内部创建一个名为 routes 的新文件夹。在 routes 文件夹中,我们将创建一个名为 blogPosts.routes.js 的 js 文件。
您的文件夹结构应该是这样的。
我们将在 blogPosts.routes.js 中添加所有路由,因此首先必须从“express”导入 express,并配置路由器。现在我们可以开始添加路由了。
// routes/blogPosts.routes.js
import express from "express";
const router = express.Router();
router.get("/", (req, res) => {
res.send("Awesome MERN BLOG");
});
export default router;
让我们开始在你的 server.js 文件中导入 blogPost 路由。现在我们可以使用 Express 中间件将这个 blogPost 连接到我们的应用程序。
// server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
import dotenv from "dotenv";
import blogPosts from "./routes/blogPosts.js";
dotenv.config();
const app = express();
app.use(bodyParser.json({ limit: "50mb", extended: true }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors());
// remember to add this after cors
app.use("/api/blogs", blogPosts);
const DB_CONNECTION = process.env.DATABASE_URL;
const PORT = process.env.PORT || 6000;
mongoose
.connect(DB_CONNECTION, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() =>
app.listen(PORT, () =>
console.log(`Server is running at: http://localhost:${PORT}`)
)
)
.catch((error) => console.log(error));
在继续之前,让我们为后端应用程序创建一个文件夹结构,使其更具可扩展性。因此,我们在 controllers 文件夹中创建一个名为 controllers 的新文件夹,同时我们还将创建一个名为 blogPosts.controller.js 的文件。controllers 只是一个包含路由特定逻辑的文件。
因此,你的 blogPosts.routes.js 和 blogPosts.controller.js 应该类似于以下内容。
//routes/blogPosts.routes.js
import express from 'express';
import { getAllBlogPosts } from '../controllers/blogPosts.controller.js';
const router = express.Router();
router.get('/', getAllBlogPosts);
export default router;
blogPosts.controller.js
//controllers/blogPosts.controller.js
import express from "express";
import mongoose from "mongoose";
const router = express.Router();
export const getAllBlogPosts = (req, res) => {
res.send("Awesome MERN BLOG");
};
export default router;
让我们为我们的博客文章创建一个新模型,因此在里面创建一个名为 models 的文件夹和一个名为 blogs.js 的文件。
文件夹结构应该类似于以下内容
// models/blogs.js
import mongoose from "mongoose";
const blogSchema = mongoose.Schema({
title: String,
description: String,
tags: [String],
fileUpload: String,
upvote: {
type: Number,
default: 0,
},
creator: String,
createdAt: {
type: Date,
default: new Date(),
},
});
var BlogPost = mongoose.model("BlogArticle", blogSchema);
export default BlogPost;
现在我们的模型已经完成,让我们开始添加更多的路线。
// routes/blogPosts.routes.js
import express from "express";
import {
getAllBlogPosts,
addBlogPost,
getSinglePost,
updateSingleBlogPost,
removeSingleBlogPost,
likeBlogPost,
} from "../controllers/blogPosts.controller.js";
const router = express.Router();
router.get("/", getAllBlogPosts);
router.post("/", addBlogPost);
router.get("/:id", getSinglePost);
router.patch("/:id", updateSingleBlogPost);
router.delete("/:id", removeSingleBlogPost);
router.patch("/:id/likeedBlogPost", likeBlogPost);
export default router;
现在,在控制器的文件夹中,将以下代码添加到您的 blogPosts.controllers.js 文件中。
getAllBlogPosts 方法获取所有博客信息。
export const getAllBlogPosts = async (req, res) => {
try {
const blogPosts = await BlogPost.find();
res.status(200).json(blogPosts);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
addBlogPost 方法仅添加/插入一个博客
export const addBlogPost = async (req, res) => {
const { title, description, fileUpload, creator, tags } = req.body;
const createNewPost = new BlogPost({
title,
description,
fileUpload,
creator,
tags,
});
try {
await createNewPost.save();
res.status(201).json(createNewPost);
} catch (error) {
res.status(409).json({ message: error.message });
}
};
getSinglePost 方法获取单个博客文章
export const getSinglePost = async (req, res) => {
const { id } = req.params;
try {
const singlepost = await BlogPost.findById(id);
res.status(200).json(singlepost);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
updateSingleBlogPost 方法更新单个博客文章
export const updateSingleBlogPost = async (req, res) => {
const { id } = req.params;
const { title, description, creator, fileUpload, tags } = req.body;
if (!mongoose.Types.ObjectId.isValid(id))
return res.status(404).send(`post ${id} not found`);
const updatedBlogPost = {
creator,
title,
description,
tags,
fileUpload,
_id: id,
};
await BlogPost.findByIdAndUpdate(id, updatedBlogPost, { new: true });
res.json(updatedBlogPost);
};
removeSingleBlogPost 方法删除单个博客文章
export const removeSingleBlogPost = (req, res) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id))
return res.status(404).send(`post ${id} not found`);
await BlogPost.findByIdAndRemove(id);
res.json({ message: "Successfully deleted" });
};
likeBlogPost 方法为帖子点赞
export const likeBlogPost = async (req, res) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id))
return res.status(404).send(`No post with id: ${id}`);
const post = await BlogPost.findById(id);
const updatedBlogPost = await BlogPost.findByIdAndUpdate(
id,
{ upvote: post.upvote + 1 },
{ new: true }
);
res.json(updatedBlogPost);
};
你的 blogPosts.controller.js 应该类似于以下内容
// blogPosts.controller.js
import express from "express";
import mongoose from "mongoose";
import BlogPost from "../models/blogs.js";
const router = express.Router();
export const getAllBlogPosts = async (req, res) => {
try {
const blogPosts = await BlogPost.find();
res.status(200).json(blogPosts);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
export const addBlogPost = async (req, res) => {
const { title, description, fileUpload, creator, tags } = req.body;
const createNewPost = new BlogPost({
title,
description,
fileUpload,
creator,
tags,
});
try {
await createNewPost.save();
res.status(201).json(createNewPost);
} catch (error) {
res.status(409).json({ message: error.message });
}
};
export const getSinglePost = async (req, res) => {
const { id } = req.params;
try {
const singlepost = await BlogPost.findById(id);
res.status(200).json(singlepost);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
export const updateSingleBlogPost = async (req, res) => {
const { id } = req.params;
const { title, description, creator, fileUpload, tags } = req.body;
if (!mongoose.Types.ObjectId.isValid(id))
return res.status(404).send(`post ${id} not found`);
const updatedBlogPost = {
creator,
title,
description,
tags,
fileUpload,
_id: id,
};
await BlogPost.findByIdAndUpdate(id, updatedBlogPost, { new: true });
res.json(updatedBlogPost);
};
export const likeBlogPost = async (req, res) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id))
return res.status(404).send(`No post with id: ${id}`);
const post = await BlogPost.findById(id);
const updatedBlogPost = await BlogPost.findByIdAndUpdate(
id,
{ upvote: post.upvote + 1 },
{ new: true }
);
res.json(updatedBlogPost);
};
export const removeSingleBlogPost = async (req, res) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id))
return res.status(404).send(`post ${id} not found`);
await BlogPost.findByIdAndRemove(id);
res.json({ message: "Successfully deleted" });
};
export default router;
重新启动服务器后,您应该会看到类似这样的内容:
配置我们的前端
我们先用 create-react-app 设置前端。之后会从头开始创建 UI 及其功能。现在就开始我们的应用程序吧。
安装 React 应用程序
让我们从前端部分开始,使用 React 进行开发。如果您的系统上尚未安装 Node.js,那么首先应该安装它。请访问 Node.js 官方网站并安装正确且合适的版本。我们需要 Node.js 才能使用 Node 包管理器(也称为 NPM)。
现在,在你选择的代码编辑器中打开“client”文件夹。在本教程中,我将使用 VScode。下一步,打开集成终端并输入 npx create-react-app 。此命令将在当前目录中创建应用程序,该应用程序将被命名为“client”。
安装通常只需几分钟。通常,我们会使用 npm 将软件包下载到项目中,但在本例中,我们使用软件包运行器 npx,它会为我们下载并配置所有内容,以便我们能够使用一个出色的模板开始工作。现在是时候启动我们的开发服务器了,只需输入 npm start,浏览器就会自动打开 react-app。
React 样板清理
在开始构建项目之前,我们必须先清理一下 create-react-app 提供的一些文件。清理完成后,你的 src 文件应该如下所示。
安装一些软件包
我们需要为这个项目安装一些第三方软件包。因此,请将以下命令复制并粘贴到您的终端中
npm install @material-ui/core axios moment react-file-base64 redux react-redux redux-thunk
安装所有这些包后,你的 packge.json 文件应该如下所示:
安装了项目的所有依赖项后,让我们向其中添加两个组件并将它们命名为 Blogs、BlogPosts 和 BlogPostsForm。
现在我们已经完成了所有设置,让我们转到 App.js 文件并开始编写一些代码。在此之前,让我们在 src 文件夹中创建一个 Assets 文件夹,并添加您选择的徽标图像。之后,再创建一个名为 style 的文件夹,并在其中创建一个名为 app.styles.js 的文件,并将以下代码粘贴到其中。
// src/styles/app.styles.js
import { makeStyles } from "@material-ui/core/styles";
export default makeStyles(() => ({
navigationBar: {
borderRadius: 10,
margin: "6px 0px",
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
title: {
color: "#8661d1",
fontFamily: "Poppins",
fontStyle: "bold",
},
image: {
marginRight: "25px",
},
}));
最后,转到 App.js 并从核心材料 UI 库中导入所有必要的组件文件、样式和组件,然后按如下所示实现它。
//App.js
import React, { useState, useEffect } from "react";
import "./App.css";
import { Container, AppBar, Typography, Grow, Grid } from "@material-ui/core";
import blogLogo from "./Assets/blogLogo.gif";
import BlogPosts from "./components/BlogPosts";
import BlogPostsForm from "./components/BlogPostsForm";
import useStyles from "./styles/app.styles.js";
function App() {
const appStyles = useStyles();
return (
<div className="App">
<Container maxWidth="xl">
<AppBar
className={appStyles.navigationBar}
position="static"
color="inherit"
>
<img
className={appStyles.image}
src={blogLogo}
alt="icon"
height="100"
/>
<Typography className={appStyles.title} variant="h4" align="center">
Mern awesome blog
</Typography>
</AppBar>
<Grow in>
<Container>
<Grid
container
justify="space-between"
alignItems="stretch"
spacing={2}
>
<Grid item xs={12} sm={7}>
<BlogPostsForm />
</Grid>
<Grid item xs={12} sm={4}>
<BlogPosts />
</Grid>
</Grid>
</Container>
</Grow>
</Container>
</div>
);
}
export default App;
现在让我们最终将前端与后端连接起来。为此,让我们创建一个名为 api 的文件夹,并在其中创建一个名为 api.js 的文件。因此,让我们导入 axios 来进行 api 调用,然后指定后端服务器 url 并编写一个使用 axios 简单获取帖子的函数。
import axios from "axios";
const url = "http://localhost:4000/api/blogs";
export const fetchAllBlogPosts = () => axios.get(url);
现在,让我们专注于为我们的 React 应用添加 Redux 功能,因为我们所有的后端操作都将通过 Redux 完成,所以我们需要调度这些操作。为此,我们需要创建一些文件和文件夹来构建应用程序,以便应用程序具有可扩展性。因此,在 src 文件夹中,创建一个名为 actions 的文件夹和一个名为 reducers 的文件夹,并在这两个文件夹中创建一个名为 blogPosts.js 的文件。
您的文件夹结构应该类似于以下内容。
在继续之前,我们先修复一下 index.js 文件,这样就可以在里面使用 redux 了。在这个文件中,我们导入 provider,它会跟踪 store(全局状态),并允许我们从应用程序的任何位置访问 store,这样我们就不必在父组件甚至子组件中访问了,这样我们就可以轻松地从任何地方访问该状态。之后,我们从 redux 包中导入 createStore、applyMiddleware 和 compose。最后,我们从 redux-thunk 包中导入 thunk,并相应地设置我们的 index.js 文件。
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { reducers } from "./reducers/blogPosts.js";
import App from "./App";
import "./index.css";
const store = createStore(reducers, compose(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
如果您现在运行您的应用程序,您可能会遇到未找到模块的错误。
让我们进入 Reducer 文件夹并修复该错误。因此,我们在其中创建一个 index.js 文件,并从 redux 包中导入 combineReducers,导出并调用该函数,并在其中放入一个对象。现在,我们可以实现此应用案例中的所有单个 Reducer,该案例仅包含 blogPosts。
// reducers/index.js
import { combineReducers } from "redux";
import blogPosts from "./blogPosts";
export const reducers = combineReducers({ blogPosts });
如果一切顺利,你的应用程序现在应该可以顺利运行了。现在我们需要在主 App.js 文件中调度我们的 action。
// App.js
import React, { useEffect } from "react";
import "./App.css";
import { Container, AppBar, Typography, Grow, Grid } from "@material-ui/core";
import blogLogo from "./Assets/blogLogo.gif";
import Blogs from "./components/Blogs";
import BlogPostsForm from "./components/BlogPostsForm";
import useStyles from "./styles/app.styles.js";
import { useDispatch } from "react-redux";
import { fetchAllBlogPosts } from "./actions/blogPosts";
function App() {
const dispatch = useDispatch();
const appStyles = useStyles();
useEffect(() => {
dispatch(fetchAllBlogPosts());
}, [dispatch]);
return (
<div className="App">
<Container maxWidth="xl">
<AppBar
className={appStyles.navigationBar}
position="static"
color="inherit"
>
<img
className={appStyles.image}
src={blogLogo}
alt="icon"
height="100"
/>
<Typography className={appStyles.title} variant="h2" align="center">
Mern awesome blog
</Typography>
</AppBar>
<Grow in>
<Grid
container
justifyContent="space-between"
alignItems="stretch"
spacing={2}
>
<Grid item xs={12} sm={3}>
<BlogPostsForm />
</Grid>
<Grid item xs={12} sm={9}>
<Blogs />
</Grid>
</Grid>
</Grow>
</Container>
</div>
);
}
export default App;
现在,让我们转到我们的操作并导入我们的 api,然后创建一些操作创建器,它们只是返回操作的函数,所以让我们实际实现 redux 来从我们的后端传递或分派数据函数。
// actions/blogPosts.js
import * as api from "../api/api.js";
export const fetchAllBlogPosts = () => async (dispatch) => {
try {
const { data } = await api.fetchAllBlogPosts();
dispatch({ type: GET_ALL_BLOGS, payload: data });
} catch (error) {
console.log(error.message);
}
};
最后,让我们回到我们的减速器并处理获取和提取所有博客文章的逻辑。
// reducers/blogPosts.js
export default (posts = [], action) => {
switch (action.type) {
case "GET_ALL_BLOGS":
return action.payload;
default:
return posts;
}
};
现在,让我们从子组件中检索这些数据,所以让我们转到 Blogs 组件并从全局 redux 存储中获取数据,我们可以借助 useSelector 来做到这一点
//components/Blogs
import React from "react";
import { Grid, CircularProgress } from "@material-ui/core";
import { useSelector } from "react-redux";
import BlogPosts from "../BlogPosts";
import useStyles from "./styles";
const Blogs = () => {
const posts = useSelector((state) => state.blogPosts);
const classes = useStyles();
console.log("this is post", posts);
return (
<>
<BlogPosts />
</>
);
};
export default Blogs;
当您运行应用程序时,您可能会看到一个空数组和网络错误;要解决这个问题,只需在 package.json 文件中包含一个代理即可。
因此,如果您仍然看到该空数组,则表示数据已成功获取,现在是时候实现表单了,以便我们可以向数据库发出发布请求并向其中实际添加新帖子。
因此,让我们转到 BlogPostsForm 组件并创建一个表单。第一步,让我们从 Material UI 核心库中导入所有我们将在表单中实现的组件。
// BlogPostsForm.js
import React, { useState, useEffect } from "react";
import { Paper, TextField, Typography, Button } from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import FileBase from "react-file-base64";
import useStyles from "./styles";
import { addBlogPosts, editBlogPosts } from "../../actions/blogPosts";
const BlogPostsForm = ({ blogPostId, setBlogPostId }) => {
const [blogInfo, setBlogInfo] = useState({
creator: "",
title: "",
description: "",
tags: "",
fileUpload: "",
});
const post = useSelector((state) =>
blogPostId
? state.posts.find((message) => message._id === blogPostId)
: null
);
const dispatch = useDispatch();
const blogPostsStyles = useStyles();
useEffect(() => {
if (post) setBlogInfo(post);
}, [post]);
const handleSubmit = async (e) => {
e.preventDefault();
if (blogPostId === 0) {
dispatch(addBlogPosts(blogInfo));
} else {
dispatch(editBlogPosts(blogInfo));
}
};
return (
<Paper className={blogPostsStyles.paper}>
<form
autoComplete="on"
noValidate
className={`${blogPostsStyles.root} ${blogPostsStyles.form}`}
onSubmit={handleSubmit}
>
<Typography variant="h5">
{blogPostId ? `Update "${post.title}"` : "✨ Create a blog ✨"}
</Typography>
<div className={blogPostsStyles.chooseFile}>
<Typography> 🖼️ Upload Blog Image</Typography>
<FileBase
type="file"
multiple={false}
onDone={({ base64 }) =>
setBlogInfo({ ...blogInfo, fileUpload: base64 })
}
/>
</div>
<TextField
name="title"
variant="outlined"
label="🔥 Blog Title"
fullWidth
value={blogInfo.title}
onChange={(e) => setBlogInfo({ ...blogInfo, title: e.target.value })}
/>
<TextField
name="description"
variant="outlined"
label="📙 Blog Description"
fullWidth
multiline
rows={7}
value={blogInfo.description}
onChange={(e) =>
setBlogInfo({ ...blogInfo, description: e.target.value })
}
/>
<TextField
name="creator"
variant="outlined"
label="✍️ Author name"
fullWidth
value={blogInfo.creator}
onChange={(e) =>
setBlogInfo({ ...blogInfo, creator: e.target.value })
}
/>
<Typography>Tags (5 max seperated by comma)</Typography>
<TextField
name="tags"
variant="outlined"
label="🏷️ Tags"
fullWidth
value={blogInfo.tags}
onChange={(e) =>
setBlogInfo({ ...blogInfo, tags: e.target.value.split(",") })
}
/>
<Button
className={blogPostsStyles.publishButton}
variant="contained"
color="secondary"
size="large"
type="submit"
>
Publish 📝
</Button>
</form>
</Paper>
);
};
export default BlogPostsForm;
另外,不要忘记修改 style.js 中的 blogPostForm 样式
// components/BlogPostsForm/styles.js
import { makeStyles } from "@material-ui/core/styles";
export default makeStyles((theme) => ({
root: {
"& .MuiTextField-root": {
margin: theme.spacing(1),
},
},
paper: {
padding: theme.spacing(5),
},
chooseFile: {
width: "95%",
margin: "10px 0",
},
publishButton: {
marginBottom: 10,
},
form: {
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
},
}));
因此,在继续之前,让我们先修复我们的 API
// api/api.js
import axios from "axios";
const url = "http://localhost:4000/api/blogs";
export const fetchBlogPosts = () => axios.get(url);
export const addNewBlogPost = (newBlog) => axios.post(url, newBlog);
export const editSingleBlogPost = (id, editedBlogPost) =>
axios.patch(`${url}/${id}`, editedBlogPost);
成功添加和导出 addNewBlogPost 和 editSingleBlogPost 函数后,让我们通过分别创建一些名为 addBlogPosts 和 editBlogPosts 的操作来实际实现它们。
您的 blogPosts.js 操作应该看起来像这样。
// actions/blogPosts.js
import * as api from "../api/api.js";
export const fetchAllBlogPosts = () => async (dispatch) => {
try {
const { data } = await api.fetchBlogPosts();
dispatch({ type: "GET_ALL_BLOG_POST", payload: data });
} catch (error) {
console.log(error.message);
}
};
export const addBlogPosts = (post) => async (dispatch) => {
try {
const { data } = await api.addNewBlogPost(post);
dispatch({ type: "ADD_NEW_BLOG_POST", payload: data });
} catch (error) {
console.log(error.message);
}
};
export const editBlogPosts = (id, post) => async (dispatch) => {
try {
const { data } = await api.editSingleBlogPost(id, post);
dispatch({ type: "EDIT_SINGLE_BLOG_POST", payload: data });
} catch (error) {
console.log(error.message);
}
};
之后,让我们更新 Reducers 部分。
export default (posts = [], action) => {
switch (action.type) {
case "GET_ALL_BLOG_POST":
return action.payload;
case "ADD_NEW_BLOG_POST":
return [...posts, action.payload];
case "EDIT_SINGLE_BLOG_POST":
return posts.map((post) =>
post._id === action.payload._id ? action.payload : post
);
default:
return posts;
}
};
最后,让我们更新我们的 App.js 以包含 blogPostId 状态,我们将其作为 prop 传递给我们的 BlogPostsForm 和 Blogs 组件。
//App.js
import React, { useState, useEffect } from "react";
import "./App.css";
import { Container, AppBar, Typography, Grow, Grid } from "@material-ui/core";
import blogLogo from "./Assets/blogLogo.gif";
import Blogs from "./components/Blogs";
import BlogPostsForm from "./components/BlogPostsForm";
import useStyles from "./styles/app.styles.js";
import { useDispatch } from "react-redux";
import { fetchAllBlogPosts } from "./actions/blogPosts";
function App() {
const [blogPostId, setBlogPostId] = useState(0);
const dispatch = useDispatch();
const appStyles = useStyles();
useEffect(() => {
dispatch(fetchAllBlogPosts());
}, [blogPostId, dispatch]);
return (
<div className="App">
<Container maxWidth="xl">
<AppBar
className={appStyles.navigationBar}
position="static"
color="inherit"
>
<img
className={appStyles.image}
src={blogLogo}
alt="icon"
height="100"
/>
<Typography className={appStyles.title} variant="h2" align="center">
Mern awesome blog
</Typography>
</AppBar>
<Grow in>
<Grid
container
justifyContent="space-between"
alignItems="stretch"
spacing={2}
>
<Grid item xs={12} sm={3}>
<BlogPostsForm
blogPostId={blogPostId}
setBlogPostId={setBlogPostId}
/>
</Grid>
<Grid item xs={12} sm={9}>
<Blogs setBlogPostId={setBlogPostId} />
</Grid>
</Grid>
</Grow>
</Container>
</div>
);
}
export default App;
修复 App.js 之后,我们将转到 Blogs 组件,并在其中使用传递的 props,并将其钻取到 BlogPosts 组件中
// components/Blogs.js
import React from "react";
import { Grid, CircularProgress } from "@material-ui/core";
import { useSelector } from "react-redux";
import BlogPosts from "../BlogPosts";
import useStyles from "./styles";
const Blogs = ({ setBlogPostId }) => {
const posts = useSelector((state) => state.posts);
const classes = useStyles();
console.log("this is post", posts);
return !posts.length ? (
<CircularProgress />
) : (
<Grid
className={classes.container}
container
alignItems="stretch"
spacing={4}
>
{posts.map((post) => (
<Grid key={post._id} item xs={12} sm={12}>
<BlogPosts post={post} setBlogPostId={setBlogPostId} />
</Grid>
))}
</Grid>
);
};
export default Blogs;
现在我们几乎完成了所有工作,是时候开始着手处理单独的博客文章了。首先,前往 BlogPosts 组件并安装 Material UI 图标,然后从 Material UI 核心库中导入几个组件,最后将以下代码复制并粘贴到其中。
// components/BlogPosts.js
import React from "react";
import {
Typography,
CardMedia,
Button,
Card,
CardActions,
CardContent,
} from "@material-ui/core/";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit";
import moment from "moment";
import { useDispatch } from "react-redux";
import blogImageLogo from "../../Assets/blogLogo.gif";
import { upvoteBlogPosts, removeBlogPosts } from "../../actions/blogPosts";
import useStyles from "./styles";
const BlogPosts = ({ post, setCurrentId }) => {
const dispatch = useDispatch();
const blogPostStyles = useStyles();
return (
<>
<Card className={blogPostStyles.blogContainer}>
<CardMedia
className={blogPostStyles.imageContainer}
image={post.fileUpload || blogImageLogo}
title={post.title}
/>{" "}
<div className={blogPostStyles.nameOverlay}>
<Typography variant="h6"> {post.creator} </Typography>{" "}
<Typography variant="body2">
{" "}
{moment(post.createdAt).fromNow()}{" "}
</Typography>{" "}
</div>{" "}
<div className={blogPostStyles.editOverlay}>
<Button
style={{
color: "white",
}}
size="small"
onClick={() => setCurrentId(post._id)}
>
<EditIcon fontSize="default" />
</Button>{" "}
</div>{" "}
<div className={blogPostStyles.tagSection}>
<Typography variant="body2" color="textSecondary" component="h2">
{" "}
{post.tags.map((tag) => `#${tag} `)}{" "}
</Typography>{" "}
</div>{" "}
<Typography
className={blogPostStyles.titleSection}
gutterBottom
variant="h5"
component="h2"
>
{post.title}{" "}
</Typography>{" "}
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">
{" "}
{post.description}{" "}
</Typography>{" "}
</CardContent>{" "}
<CardActions className={blogPostStyles.cardActions}>
<Button
size="small"
color="primary"
onClick={() => dispatch(upvoteBlogPosts(post._id))}
>
<ArrowUpwardIcon fontSize="small" /> {post.likeCount}{" "}
</Button>{" "}
<Button
size="small"
color="primary"
onClick={() => dispatch(removeBlogPosts(post._id))}
>
<DeleteIcon fontSize="big" />
</Button>{" "}
</CardActions>{" "}
</Card>{" "}
</>
);
};
export default BlogPosts;
最后,让我们创建一个操作来实际点赞和移除博客文章。首先,在 API 内部创建一个函数,分别命名为 upvoteSingleBlogPost 和 removeBlogPost,然后将其导出。
// api/api.js
import axios from "axios";
const url = "http://localhost:4000/api/blogs";
export const fetchBlogPosts = () => axios.get(url);
export const addNewBlogPost = (newBlog) => axios.post(url, newBlog);
export const editSingleBlogPost = (id, editedBlogPost) =>
axios.patch(`${url}/${id}`, editedBlogPost);
export const upvoteSingleBlogPost = (id) =>
axios.patch(`${url}/${id}/likedBlogPost`);
export const removeBlogPost = (id) => axios.delete(`${url}/${id}`);
最后,让我们修复减速器,这样我们的应用程序就完成了。
// reducers/blogPosts.js
export default (posts = [], action) => {
switch (action.type) {
case "GET_ALL_BLOG_POST":
return action.payload;
case "ADD_NEW_BLOG_POST":
return [...posts, action.payload];
case "EDIT_SINGLE_BLOG_POST":
return posts.map((post) =>
post._id === action.payload._id ? action.payload : post
);
case "UPVOTE_SINGLE_BLOG_POST":
return posts.map((post) =>
post._id === action.payload._id ? action.payload : post
);
case "DELETE_SINGLE_BLOG_POST":
return posts.filter((post) => post._id !== action.payload);
default:
return posts;
}
};
我们已经讨论了大量材料,为您提供从头开始构建成熟的 MERN 堆栈应用程序所需的知识。
完整的源代码可以在这里找到。
https://github.com/aviyeldevrel/devrel-tutorial-projects/tree/main/MERN-awesome-blog
结论
在本博客教程的第一部分,我们使用 Node.js、Express 和 MongoDB 构建了后端服务器。我们使用 Mongoose 库将 Node.js/Express 服务器连接到 MongoDB。然后,在本教程的第二部分,我们创建了 React 前端应用程序,并使用 Redux 为我们的 MERN Stack 博客应用程序全局管理状态。干杯!祝您编程愉快!
主要文章可在此处查看 => https://aviyel.com/post/1304
编码愉快!!
如果您是项目维护者、贡献者或只是开源爱好者,请关注@aviyelHQ或在 Aviyel 上注册以获得早期访问权限。
加入 Aviyel 的 Discord => Aviyel 的世界
推特 =>[ https://twitter.com/AviyelHq ]
文章来源:https://dev.to/aviyel/building-a-mern-stack-simple-blog-site-from-absolute-scratch-5pm