节点模块化架构

Node Modular Architecture

我正在构建一个现在相当大的 nodejs 应用程序。为了避免单体式节点应用程序,我沿用了更加模块化系统的架构路线,将多个组件分解为单独的 npm 模块。这些是使用 npm 发布并安装在依赖模块中的。我有大约 6 个不同的模块(我想分成更多),现在管理这些包变得困难了。

问题是:

  1. 存在嵌套依赖,所以如果我更改模块 A,模块 B 依赖于模块 A,模块 C 依赖于模块 B,那么当我更新模块 A 时,我需要发布它的新版本,这意味着我需要在模块 B 中更新它,这意味着我还需要发布它,然后最后我需要在模块 A 中安装那个新版本……你可以看到这可能是一个痛苦的地方。而且package.json所有版本的更新都是手动的,容易出错,等待每次发布也很耗时。
  2. 模块可以共享 npm 依赖项,因此有时会在包更新时发生冲突。模块越多,冲突的几率越高。

好处 是我们有一个非常模块化的系统,其中的库可以很容易地重用,并且由于没有任何循环依赖性而强制执行了清晰的模块层次结构。

可能的解决方案是:

  1. Monolith - 将依赖项作为单个存储库中的单个应用程序进行管理,每个模块都成为一个服务。这意味着只需要一次更新,所有模块 api 都将同步。但是,在代码中引用库可能会有点麻烦(因为我相信它们必须相对于本地文件进行引用),我不确定如何强制执行模块之间的结构层次结构和代码重用对存储库外的模块更难。

  2. 微服务 - 让每个模块成为一个微服务。这保留了模块化系统的所有优点,但我担心它会增加构建的复杂性,并且管理所有服务本身将成为一项全职工作。

  3. 继续 - 想出一种方法来保持当前架构,但消除推送更新等的麻烦。也许脚本更新版本和收缩包装以确保正确的依赖关系。我认为这既困难又可能导致它成为一个不同种类的整体系统。

选项 1 对我来说似乎是最易于管理的,但如果没有必要,我不想失去模块化结构。

这是一个相当宽泛的问题,但任何 suggestions/advice/comments 都会很有帮助。

谢谢

你考虑过不同的模块化结构吗?使用微服务还是整体应用的决定会影响组件之间的通信方式、系统的可扩展性和可部署性,但您仍然需要遵循包设计的最佳实践。否则你在更新低级别包时也会有同样的连锁反应。

您当前的包 C 依赖包 B 依赖包 A 的结构导致包管理困难,因为您需要对较低级别的包进行太多修改。

这种类型的问题是up-front包装设计过多的标志,而包装设计确实应该随手做。

你目前结构的优点是它没有任何acylic 依赖项。如果你更新模块 B,你确切地知道模块 C 受到影响, 模块 A 没有任何变化。它应该保持这种状态。

管理依赖关系

依赖结构

有与您遇到的问题直接相关的包设计原则:

The Stable Dependencies Principle
靠稳定的方向

鉴于原始结构 C -> B -> A,您的大部分更改应该发生在 C 并且 A 应该没有任何理由更改。

The Stable-Abstractions Principle
包应该是抽象的,因为它是稳定的

和前面的原理有关。摘要 类 省略了具体的实现,使用 Javascript.

有很多方法可以做到这一点

如果你很好地遵循这些原则,你可能会发现这不是问题 拥有三个以上的包,因为较低级别的包不会有太大变化。


包裹中应该包含什么?

按功能打包

最近流行的 MVC 框架有一个结构,将控制器、模型和视图分离到不同的文件夹中。这种结构的伸缩性不是很好,在项目扩展一段时间后,很难直观地看到项目做了什么,不同部分是如何相互连接的,而且查看与一个项目相关的所有文件也不是很方便特定功能。这种称为逐层打包的方法扩展性不是很好。

组织包裹的更好方法是 Package by Feature。现在分层也不是坏事,按功能打包的时候,应该还是这个分层结构。

我建议采用解决方案 2。

  • 将所有代码分解成小模块。
  • 与事件发射器实现松耦合。
  • 将每个模块存储为自己的 npm 包没有任何附加价值,除非它们在您的应用程序之外独立使用。

你描述的这两个问题只是因为每个模块都独立存储为一个npm包造成的。

好处

  • 问题 1 已解决,因为您不再需要在 package.json 中管理 npm 包。
  • 问题 2 已解决,因为您只有一个 package.json 管理所有依赖关系
  • 由于使用了单独的 node.js 模块,您仍然拥有一个干净的模块化结构。

示例实现

几个月前,我使用这些原则重构了一个整体式 node.js 应用程序,它确实简化了维护,并且没有增加构建过程的开销。

模式如下:

主模块是app.js

var sys = require('sys')
    , events = require('events')
    , UserModel = require('./model/user') // this is a microservice taking care of managing user data
    , Logic = require('./controller/logic')   // this is another microservice doing some work

var App = function (server, app) {

    this.controller = (
        logic: new Logic(this) // "this" is the app instance, it's passed to all microservices, which will benefit from the access to event emitter...
    }
    this.model = {
        new UserModel(this)
    }

    // ...
}

sys.inherits(App, events.EventEmitter)

module.exports = App

微服务如下所示:

/**
* Logic functions
* 
* this controller does ...
*
* @constructor
*
*/
function Logic(app) {

    /****************************************
    *
    * Private methods
    *
    ****************************************/

    /**
    * this is a private function in the controller
    * 
    * @private
    */
    function computeSomething () {

        var res = 3

        app.emit('computed', res) // emit event, that can be liseted to by some other modules

        return res
    }


    /****************************************
    *
    * Public methods
    *
    ****************************************/    

    /**
    * 
    * This function can be called directly by the other modules using "app.controller.logic.work()"
    * 
    */
    this.work = function () {

        return 'result is ' + computeSomething()
    }


    /****************************************
    * 
    * Event listeners
    * 
    ****************************************/

    /**
    * listener: event from the app - loose-coupling magic happens thanks to this. Recommended over public functions.
    *
    * @private
    */
    app.on('data-ready', this.work)

}

module.exports = Logic

我正在研究一个理论模型来解决这个问题,只是做了一些研究,一些实验和一些常识。

模块化原则列表

  1. 代码按功能实际组织在文件夹中。
  2. 代码只做一件事,而且做得很好。
  3. 每个功能都可以随时添加或删除,因为它与其他功能没有依赖关系。
  4. 与其他模块的所有通信都不是直接进行的。而是使用沙盒或中间件。
  5. 如果几个模块需要一个共同的功能,他们从上层的半不可变()结构中获取它。

优缺点 这种方法寻找一个特定的目标:松耦合。背后的想法是每个模块都可以自己实现,可以单独开发和测试,很多人可以同时贡献特性。 看看 WordPress,或节点生态系统。以这个例子为例,把它移到你的项目中。

一个例子

CSS 对我来说是一个清楚的例子,说明了这种模块化方法是如何工作的。如果您的站点有许多页面和其中的许多部分,并且您的客户希望每个部分都有不同的外观和感觉,那么您最终可能会在一个缩小版中包含数百个 CSS 定义大 CSS 文件。

CSS 的治理可能需要变量、预处理器、PostCSS、Javascript 处理....但真正的问题是您永远不会使用每页上有多个 CSS 定义。

如果所有页面都可以拆分为模块化类型,并且每种类型都有自己的规则,那么您最终可能会得到更多的代码,但一次只应用一个较小的文件。可能您不需要缩小任何文件,因为所有文件都有所需的内容。

文件夹结构提案 我的想法是所有代码都应该以相同的方式组织。 主要文件夹:

  • 核心
  • 扩展程序
  • 引擎

在每个结构中都是这样的: 核心
----- 扩展管理器
-------------- css
---------- img
---------- 媒体
-------------- js
---------- 查看
---------- 分贝
---------- 数据
---------- 辅助
----------- config.json
-------------- README.txt
-------------- settings.txt
----- 沙盒
-------------- css
---------- img
---------- 媒体
-------------- js
---------- 查看
---------- 分贝
---------- 数据
---------- 辅助 ----------- config.json
-------------- README.txt
-------------- settings.txt
----- 全球
-------------- css
---------- img
---------- 媒体
-------------- js
---------- 查看
---------- 分贝
---------- 数据
---------- 辅助
----------- config.json
-------------- README.txt
-------------- settings.txt
扩展
----- 约会
-------------- css
---------- img
---------- 媒体
-------------- js
---------- 查看
---------- 分贝
---------- 数据
---------- 辅助
----------- config.json
-------------- README.txt
-------------- settings.txt
----- 日历
-------------- css
---------- img
---------- 媒体
-------------- js
---------- 查看
---------- 分贝
---------- 数据
---------- 辅助
----------- config.json
-------------- README.txt
-------------- settings.txt
----- 促销活动
-------------- css
---------- img
---------- 媒体
-------------- js
---------- 查看
---------- 分贝
---------- 数据
---------- 辅助
----------- config.json
-------------- README.txt
-------------- settings.txt

希望这些想法对您有所帮助,欢迎任何意见。