如何从头开始构建 Node.Js 项目?

2025-06-07

如何从头开始构建 Node.Js 项目?

最初发布

在本文中,我们将讨论如何正确构建 Node.js 应用程序,以及它的重要性。此外,我们还将探讨哪些设计决策能够帮助我们打造成功的数字产品。也许您正在从头构建一个新的 Node.js 应用程序,也许您想重构现有的应用程序,又或者您想探索Node.js 应用程序架构并学习最佳实践和模式。无论出于何种原因,本文都将为您提供帮助。

为什么你应该阅读这篇文章?

嗯,互联网上确实有很多博客文章探讨这个主题。虽然有一些关于 Node.js 项目架构的优秀文章,但却没有一篇能够深入讲解。此外,许多博客文章只详细阐述了某些主题(例如分层架构),却没有告诉你应用程序中所有组件是如何协同工作的。这就是我选择撰写这篇文章的原因。我尝试研究并将所有信息压缩成一篇易于理解的文章,这样你就不用费力了。

我们将简要介绍如何正确构建 Node.js 应用程序,并在构建实际虚拟应用程序时讨论所有设计决策背后的原因。

我们将讨论

  1. 文件夹结构
  2. 配置环境变量
  3. MVC 模式(模型、视图、控制器)
  4. 分层架构
  5. 封装配置

我们将从简单的概念入手,并在此基础上逐步完善。读完本文后,你将能够编写出令你引以为豪的代码。

兴奋吗?🤩 让我们开始吧!

文件夹结构

在构建大型项目时,组织至关重要。我们定义文件夹结构是为了方便日后查找代码片段。作为开发人员,我们经常与他人合作。清晰的代码结构使我们能够轻松地在项目上进行协作。

以下是我日常工作中使用的示例文件夹结构,效果非常好。我们已经用这个结构成功交付了几个项目。这个结构是我们经过多次尝试和失败后得出的。欢迎您使用此结构或进行修改。

文件夹结构

好了,让我们构建第一个 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);
Enter fullscreen mode Exit fullscreen mode

注意,我们需要用到这个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;
Enter fullscreen mode Exit fullscreen mode

目前,我们只在 中添加了一条路由app.js。分离这两个文件的主要原因是为了封装逻辑。让我们看一下npm我用来运行此应用程序的脚本。

"scripts": {
    "dev": "nodemon ./src/server.js"
},
Enter fullscreen mode Exit fullscreen mode

请确保您能够通过执行以下操作来运行该应用程序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'
    });
});
Enter fullscreen mode Exit fullscreen mode

您现在也可以对作者路由执行类似的操作。稍后我们将讨论关注点分离,以及如何使用模型视图控制器模式构建我们的应用程序。在此之前,让我们先讨论另一个重要主题:设置环境变量。

配置我们的环境变量

作为程序员,我们常常低估组织和配置环境变量的重要性。我们的应用程序能够在各种环境中运行,这一点至关重要。这些环境可能是同事的电脑、服务器、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=
Enter fullscreen mode Exit fullscreen mode

一个很好的做法是从.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
};
Enter fullscreen mode Exit fullscreen mode

这样做的主要原因是为了在一个地方管理我们的环境变量。出于某些原因,我们可能会使用多个.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);
Enter fullscreen mode Exit fullscreen mode

我们已经设置好了环境变量。现在让我们深入研究一下模型-视图-控制器 (MVC) 模式。

模型-视图-控制器模式

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);
Enter fullscreen mode Exit fullscreen mode

我们也在模型中定义与数据库相关的模式。控制器目前将处理所有获取数据和业务逻辑。所以,我们先来看看控制器。

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() => {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以按照如下方式精简我们的路由器:

/**
 * 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
    })
});
Enter fullscreen mode Exit fullscreen mode

使用这种模式可以分离我们的关注点,并保持代码简洁、有序且易于测试。我们的组件现在遵循单一职责原则。例如,我们的路由只负责返回响应;控制器处理大部分业务逻辑,而模型则负责数据层。

注意:要获取到目前为止的代码,请检查以下 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
        }
},
Enter fullscreen mode Exit fullscreen mode

现在,该控制器负责执行多个操作,这使得测试变得更加困难、混乱,并且破坏了单一责任原则

我们如何解决这个问题?使用分层架构

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
        }
},
Enter fullscreen mode Exit fullscreen mode

请注意,服务功能旨在完成一项特定任务。这样,我们的服务就被封装起来,易于测试,并且对未来的更改开放,而不会产生任何重大副作用。

封装配置

我们在 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;
Enter fullscreen mode Exit fullscreen mode

我们有几项只是配置代码。例如,数据库连接、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();
    }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以在我们的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;
Enter fullscreen mode Exit fullscreen mode

就这样。我们app.js现在看起来干净多了。

最后,以下是 Node.js 项目架构需要牢记的要点:

  1. 应用合理的文件夹结构:它使我们能够轻松定位文件和代码,并促进团队更好地协作;

  2. 配置环境变量:合理配置和管理环境变量,避免部署;

  3. MVC模式(模型、视图、控制器):应用MVC模式实现代码解耦、可测试、可维护;

  4. 分层架构:应用分层架构来分离你的关注点。广泛使用服务来封装你的业务逻辑;

  5. 封装配置:将配置代码与应用程序逻辑分开。

我们简要回顾了 Node.js 项目架构的核心概念。希望本文对您有所帮助,并为您提供一些关于如何构建您自己的项目的见解。我很乐意听听您对这篇博文的看法。请在评论区分享您的想法,如果您喜欢这篇文章,请点赞并分享。下次再见!

文章来源:https://dev.to/shadid12/how-to-architect-a-node-js-project-from-ground-up-1n22
PREV
旋转轮盘——一个可定制的轮盘,大小不到 30kb,无需 JavaScript 后备。
NEXT
我如何对 RxJs 进行逆向工程并学习反应式编程?