如何从头开始构建 Node.Js 项目?
在本文中,我们将讨论如何正确构建 Node.js 应用程序,以及它的重要性。此外,我们还将探讨哪些设计决策能够帮助我们打造成功的数字产品。也许您正在从头构建一个新的 Node.js 应用程序,也许您想重构现有的应用程序,又或者您想探索Node.js 应用程序架构并学习最佳实践和模式。无论出于何种原因,本文都将为您提供帮助。
为什么你应该阅读这篇文章?
嗯,互联网上确实有很多博客文章探讨这个主题。虽然有一些关于 Node.js 项目架构的优秀文章,但却没有一篇能够深入讲解。此外,许多博客文章只详细阐述了某些主题(例如分层架构),却没有告诉你应用程序中所有组件是如何协同工作的。这就是我选择撰写这篇文章的原因。我尝试研究并将所有信息压缩成一篇易于理解的文章,这样你就不用费力了。
我们将简要介绍如何正确构建 Node.js 应用程序,并在构建实际虚拟应用程序时讨论所有设计决策背后的原因。
我们将讨论
- 文件夹结构
- 配置环境变量
- MVC 模式(模型、视图、控制器)
- 分层架构
- 封装配置
我们将从简单的概念入手,并在此基础上逐步完善。读完本文后,你将能够编写出令你引以为豪的代码。
兴奋吗?🤩 让我们开始吧!
文件夹结构
在构建大型项目时,组织至关重要。我们定义文件夹结构是为了方便日后查找代码片段。作为开发人员,我们经常与他人合作。清晰的代码结构使我们能够轻松地在项目上进行协作。
以下是我日常工作中使用的示例文件夹结构,效果非常好。我们已经用这个结构成功交付了几个项目。这个结构是我们经过多次尝试和失败后得出的。欢迎您使用此结构或进行修改。
好了,让我们构建第一个 Hello World API 端点。在构建示例应用程序时,我们将用代码逻辑填充这些文件夹。
首先,让我们看一下我们的server.js
文件
const http = require('http');
const app = require('./app');
const port = process.env.PORT || 3000;
const server = http.createServer(app);
server.listen(port);
注意,我们需要用到这个app.js
文件。我们将把所有应用逻辑都写在里面app.js
。它将成为应用的主要入口点。我们来快速看一下代码。
const express = require('express');
const app = express();
// routes
app.use((req, res, next) => {
res.status(200).json({
message: 'Hello world!!!'
});
});
module.exports = app;
目前,我们只在 中添加了一条路由app.js
。分离这两个文件的主要原因是为了封装逻辑。让我们看一下npm
我用来运行此应用程序的脚本。
"scripts": {
"dev": "nodemon ./src/server.js"
},
请确保您能够通过执行以下操作来运行该应用程序npm run dev
。
让我们添加资源路线
我猜你肯定迫不及待地想创建更多路线。现在就开始吧。我们将在api/routes
文件夹中创建以下文件。
api/routes/authors.js
api/routes/books.js
让我们从这些路由返回一些虚拟 JSON 数据。
/**
* GET request to /books
*/
router.get('/', (req, res, next) => {
res.status(200).json({
message: 'All Books were fetched'
});
});
/**
* GET request to /books/:id
*/
router.get('/:id', (req, res, next) => {
res.status(200).json({
message: 'Book with id was fetch'
});
});
您现在也可以对作者路由执行类似的操作。稍后我们将讨论关注点分离,以及如何使用模型视图控制器模式构建我们的应用程序。在此之前,让我们先讨论另一个重要主题:设置环境变量。
配置我们的环境变量
作为程序员,我们常常低估组织和配置环境变量的重要性。我们的应用程序能够在各种环境中运行,这一点至关重要。这些环境可能是同事的电脑、服务器、Docker 容器或其他云服务提供商的服务器。因此,在构建 Node.js 应用程序时,设置环境变量至关重要。
我使用dotenv
库来管理此应用程序中的环境变量。首先,我使用 安装了该库npm i install dotenv --save
。然后在根目录中创建了一个.envfile
。我们将所有环境变量都添加到此.env
文件中。以下是我的示例.env
设置。
PORT=3000
API_URL=https://api.some/endpoint
API_KEY=kkaskdwoopapsdowo
MONGO_URL=
一个很好的做法是从.env
文件中收集变量,并将它们映射到命名良好的变量中,然后通过模块导出它们。让我们创建一个文件config/index.js
。
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
endpoint: process.env.API_URL,
masterKey: process.env.API_KEY,
port: process.env.PORT
};
这样做的主要原因是为了在一个地方管理我们的环境变量。出于某些原因,我们可能会使用多个.env
文件。例如,我们可能决定使用单独的文件.env
来部署 Docker。我们可能还有其他配置变量。我们希望有效地管理这些变量,因此我们遵循此约定。
好的,现在让我们看看如何将这些变量导入server.js
const http = require('http');
const app = require('./app');
const { port } = require('./config');
const server = http.createServer(app);
server.listen(port);
我们已经设置好了环境变量。现在让我们深入研究一下模型-视图-控制器 (MVC) 模式。
模型-视图-控制器模式
现代 Web 应用程序庞大而复杂。为了降低复杂性,我们使用职责分离原则 (SRP)。使用 SRP 可确保松耦合、可维护性和可测试性。MVC 模式体现了这种职责分离的理念。让我们来看看 MVC 的不同部分。
模型:
模型组件负责应用程序的数据域。模型对象负责从数据库存储、检索和更新数据。
看法:
它是我们应用程序的用户界面。在大多数现代 Web 应用程序中,视图层通常被另一个单页应用程序(例如 React.js 或 Angular 应用程序)取代。
控制器:
它们负责处理用户交互。它们与模型交互以检索信息并最终响应用户请求。在较小的应用程序中,控制器可以容纳业务逻辑。然而,对于较大的应用程序来说,这并不是一个好的做法;我们将在本文后面探讨分层架构,以进一步阐述其原因。
现在,让我们看看如何将此模式添加到我们的应用程序中。我将使用它mongodb
作为此演示的数据库。我创建了一个新的控制器和一个模型来实现此模式。首先,让我们看一下作者模型。
const mongoose = require('mongoose');
const authorSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: { type: String, required: true },
books: { type: Object, required: false }
});
module.exports = mongoose.model('Author', authorSchema);
我们也在模型中定义与数据库相关的模式。控制器目前将处理所有获取数据和业务逻辑。所以,我们先来看看控制器。
module.exports = {
createAuthor: async (name) => {
const author = new Author({
_id: new mongoose.Types.ObjectId(),
name: name
});
try {
const newAuthorEntry = await author.save()
return newAuthorEntry;
} catch (error) {
throw error
}
},
getAuthor: async (id) => {
// ..
},
getAllAuthors: async() => {
// ...
}
}
现在我们可以按照如下方式精简我们的路由器:
/**
* POST create /author
*/
router.post("/", async (req, res, next) => {
const author = await authorController.createAuthor(req.body.name)
res.status(201).json({
message: "Created successfully",
author
})
});
使用这种模式可以分离我们的关注点,并保持代码简洁、有序且易于测试。我们的组件现在遵循单一职责原则。例如,我们的路由只负责返回响应;控制器处理大部分业务逻辑,而模型则负责数据层。
注意:要获取到目前为止的代码,请检查以下 github repo:
假设我们的业务需求发生了变化。现在,当我们添加一位新作者时,我们必须检查他是否有畅销书,以及该作者是自费出版的还是属于某个出版社。所以,如果我们现在开始在控制器中实现这些逻辑,看起来会相当混乱。
例如,查看下面的代码:
createAuthor: async (name) => {
const author = new Author({
_id: new mongoose.Types.ObjectId(),
name: name
});
try {
// cehck if author is best-seller
const isBestSeller = await axios.get('some_third_part_url');
// if best seller do we have that book in our store
if(isBestSeller) {
// Run Additional Database query to figure our
//...
//if not send library admin and email
//...
// other logic and such
}
const newAuthorEntry = await author.save()
return newAuthorEntry;
} catch (error) {
throw error
}
},
现在,该控制器负责执行多个操作,这使得测试变得更加困难、混乱,并且破坏了单一责任原则。
我们如何解决这个问题?使用分层架构!
Node.js 的分层架构
我们希望应用关注点分离原则,将业务逻辑从控制器中分离出来。我们将创建一些小型服务函数,供控制器调用。这些服务只负责完成一项任务,这样就能将业务逻辑封装起来。这样,如果将来需求发生变化,我们只需修改某些服务函数,从而避免多米诺骨牌效应。采用分层架构,我们可以构建敏捷的应用程序,并在必要时轻松引入更改。这种架构也称为三层架构。
以下是我们将要做的事情的直观分解:
好了,让我们分解一下之前的控制器,以便使用这个架构。首先,我们需要创建服务来处理特定的事件。
createAuthor: async (name) => {
const author = new Author({
_id: new mongoose.Types.ObjectId(),
name: name
});
try {
await AuthorService.checkauthorSalesStatus();
await BookService.checkAvailableBooksByAuthor(name);
const newAuthorEntry = await author.save();
return newAuthorEntry;
} catch (error) {
throw error
}
},
请注意,服务功能旨在完成一项特定任务。这样,我们的服务就被封装起来,易于测试,并且对未来的更改开放,而不会产生任何重大副作用。
封装配置
我们在 Node.js 应用程序中编写了大量的配置代码。这些代码通常在应用程序启动时运行。将它们封装在一个函数中是一个好习惯。这样可以让我们更好地跟踪这些文件,并在必要时进行调试。
app.js
让我们用一个例子来详细说明一下。下面是我们的
const express = require('express');
const app = express();
const mongoose = require('mongoose');
const { mongoUrl } = require('./config');
const bodyParser = require('body-parser');
//routes
const authorsRoutes = require('./api/routes/authors');
const booksRoutes = require('./api/routes/books');
mongoose.connect(mongoUrl, { useNewUrlParser: true });
mongoose.Promise = global.Promise;
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
);
if (req.method === "OPTIONS") {
res.header("Access-Control-Allow-Methods", "PUT, POST, PATCH, DELETE, GET");
return res.status(200).json({});
}
next();
});
app.use('/authors', authorsRoutes);
app.use('/books', booksRoutes);
module.exports = app;
我们有几项只是配置代码。例如,数据库连接、body 解析器和 cors 设置都是服务器配置代码。我们可以将它们移到config
文件夹中各自独立的函数中。
const mongoose = require('mongoose');
const { mongoUrl } = require('./index');
module.exports = {
initializeDB: async () => {
mongoose.connect(mongoUrl, { useNewUrlParser: true });
mongoose.Promise = global.Promise;
},
cors: async (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
);
if (req.method === "OPTIONS") {
res.header("Access-Control-Allow-Methods", "PUT, POST, PATCH, DELETE, GET");
return res.status(200).json({});
}
next();
}
}
现在我们可以在我们的app.js
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const config = require('./config/init')
//routes
const authorsRoutes = require('./api/routes/authors');
const booksRoutes = require('./api/routes/books');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(config.cors);
app.use('/authors', authorsRoutes);
app.use('/books', booksRoutes);
module.exports = app;
就这样。我们app.js
现在看起来干净多了。
最后,以下是 Node.js 项目架构需要牢记的要点:
-
应用合理的文件夹结构:它使我们能够轻松定位文件和代码,并促进团队更好地协作;
-
配置环境变量:合理配置和管理环境变量,避免部署;
-
MVC模式(模型、视图、控制器):应用MVC模式实现代码解耦、可测试、可维护;
-
分层架构:应用分层架构来分离你的关注点。广泛使用服务来封装你的业务逻辑;
-
封装配置:将配置代码与应用程序逻辑分开。
我们简要回顾了 Node.js 项目架构的核心概念。希望本文对您有所帮助,并为您提供一些关于如何构建您自己的项目的见解。我很乐意听听您对这篇博文的看法。请在评论区分享您的想法,如果您喜欢这篇文章,请点赞并分享。下次再见!
文章来源:https://dev.to/shadid12/how-to-architect-a-node-js-project-from-ground-up-1n22