依赖注入,JavaScript和ES6模块

JavaScript中依赖项注入的另一种实现是使用ES6模块,它能够在浏览器和nodejs中使用相同的代码,而无需使用编译器。


图片


切入点是我对DI的看法,DI在现代Web应用程序中的地位,DI容器的基本实现,该容器可以在正面和背面创建对象,并解释了Michael Jackson的用途。


我强烈要求那些在文章中觉得琐碎的人不要强奸自己,也不要读到最后,这样一来,当他们感到失望时,就不要放下“减号”。 我并不反对“缺点”,而只有反对者附有评论时,出版物中的确切内容才引起负面反应。 这是一篇技术文章,因此请尽量屈服于演示文稿的样式,并精确批评上述内容的技术内容。 谢谢啦


应用程序中的对象


我确实很尊重函数式编程,但是我的大部分职业都是致力于创建由对象组成的应用程序。 JavaScript令我印象深刻的是,其中的功能也是对象。 在创建应用程序时,我想到的是对象,这是我的专业变形。


根据生命周期,应用程序中的对象可以分为以下几类:


  • 永久的 -在申请的某个阶段出现,仅在申请完成时销毁;
  • 临时的 -在需要执行某些操作时出现,并在此操作完成时销毁;

在这方面,在编程中有如下设计模式:



从我的角度来看,该应用程序由永久存在的独来独往的人组成,他们要么自己执行所需的操作,要么生成临时对象以执行这些操作。


对象容器


依赖注入是一种可以轻松在应用程序中创建对象的方法。 也就是说,在应用程序中有一个特殊的对象,它“知道”如何创建所有其他对象。 这样的对象称为对象容器(有时称为对象管理器)。


对象容器不是神圣的对象 ,因为 它的任务只是创建应用程序的重要对象,并为其提供对其他对象的访问。 由容器生成并位于其中的绝大多数应用程序对象对容器本身一无所知。 可以将它们放置在任何其他环境中,并提供必要的依赖关系,并且它们在此处也将发挥出色的作用(测试人员知道我的意思)。


实施地点


总的来说,有两种将依赖项注入到对象中的方法:


  • 通过构造函数;
  • 通过财产(或其访问者);

我基本上使用了第一种方法,因此我将继续通过构造函数进行依赖注入的观点进行描述。


假设我们有一个包含三个对象的应用程序:


图片


在PHP中(这种语言具有悠久的DI传统,我目前有很多工作经验,稍后我将继续使用JS),这种情况可能会反映出类似情况:


class Config { public function __construct() { } } class Service { private $config; public function __construct(Config $config) { $this->config = $config; } } class Application { private $config; private $service; public function __construct(Config $config, Service $service) { $this->config = $config; $this->service = $service; } } 

此信息应该足够,以便DI容器(例如, aguel / container )(如果进行了适当配置)也可以根据创建Application对象的请求来创建其ServiceConfig依赖关系,并将它们的参数传递给Application对象的构造函数。


依赖标识符


对象容器如何理解Application对象的构造函数需要两个ConfigService对象? 通过反射API( JavaPHP )分析对象或直接分析对象代码(代码注释)。 也就是说,在一般情况下,我们可以确定对象构造函数希望在输入中看到的变量的名称,如果语言是可键入的,我们还可以获取这些变量的类型。


因此,作为对象的标识符,容器可以使用构造函数的输入参数的名称或输入参数的类型进行操作。


创建对象


该对象可以由程序员显式创建,并放在相应标识符下的Container中(例如,“ configuration”)


 /** @var \League\Container\Container $container */ $container->add("configuration", $config); 

可以由Container根据某些特定规则创建。 这些规则大体归结为将对象的标识符与其代码匹配。 可以显式设置规则(以代码,XML,JSON等形式映射)


 [ ["object_id_1", "/path/to/source1.php"], ["object_id_2", "/path/to/source2.php"], ... ] 

或采用某种算法的形式:


 public function getSource($id) {. return "/path/to/source/${id}.php"; } 

在PHP中,用于将类名与具有其源代码的文件匹配的规则是标准化的( PSR-4 );在Java中,匹配是在JVM( 类加载器 )配置级别完成的。 如果容器在创建对象时提供了对源的自动搜索,则类名足以作为此类容器中对象的标识符。


命名空间


通常在项目中,除了自己的代码外,还使用第三方模块。 随着依赖性管理器(maven,composer,npm)的出现,模块的使用已大大简化,项目中模块的数量也大大增加。 命名空间允许同一个名称的代码元素存在于来自不同模块(类,函数,常量)的单个项目中。


最初使用(Java)内置名称空间的语言有:


 package vendor.project.module.folder; 

在开发语言(PHP)时,在其中添加了名称空间的语言有以下几种:


 namespace Vendor\Project\Module\Folder; 

好的名称空间实现可让您明确地处理代码的任何元素:


 \Doctrine\Common\Annotations\Annotation\Attribute::$name 

命名空间解决了在项目中组织许多软件元素的问题,文件结构解决了在磁盘上组织文件的问题。 因此,它们之间不仅有很多共同点,有时还有很多共同点-例如,在Java中,必须使用此类的代码将名称空间中的公共类唯一地附加到文件。


因此,使用项目名称空间中的对象类的标识符作为Container中的对象标识符是一个好主意,并且可以用作创建规则的基础,以在创建所需对象时自动检测源代码。


 $container->add(\Vendor\Project\Module\ObjectType::class, $obj); 

代码启动


在PHP composer模块名称空间在composer.json模块描述符中映射到模块内部的文件系统:


 "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } } 

如果JS中有名称空间,则JS社区可以在package.json进行类似的映射。


JS依赖项标识符


上面,我指出容器可以使用构造函数的输入参数的名称或输入参数的类型作为标识符。 问题是:


  1. JS是一种具有动态类型的语言,在声明函数时不提供指定类型的语言。
  2. JS使用的缩小器可以重命名输入参数。

awilix DI容器的开发人员建议将对象用作构造函数的唯一输入参数,并将该对象的属性用作依赖项:


 class UserController { constructor(opts) { this.userService = opts.userService } } 

JS中对象属性标识符可以由字母数字字符“ _”和“ $”组成,并且不能以数字开头。


由于我们需要将依赖项标识符映射到文件系统中其源的路径以进行自动加载,因此最好放弃使用“ $”并使用PHP的经验。 在namespace运算符出现在某些框架中(例如,在Zend 1中)之前,将以下名称用于类:


 class Zend_Config_Writer_Json {...} 

因此,我们可以将我们三个对象( ApplicationConfigService )的Application反映在JS上,如下所示:


 class Vendor_Project_Config { constructor() { } } class Vendor_Project_Service { constructor({Vendor_Project_Config}) { this.config = Vendor_Project_Config; } } class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

如果我们发布每个类的代码:


 export default class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

在我们项目模块中的文件中:


  • ./src/
    • ./Application.js
    • ./Config.js
    • ./Service.js

然后,我们可以在Container的配置中将模块的根目录与模块的根“名称空间”相连:


 const ns = "Vendor_Project"; const path = path.join(module_root, "src"); container.addSourceMapping(ns, path); 

然后从此信息开始,根据依赖项标识符( ${module_root}/src/Config.js )构造到相应源( ${module_root}/src/Config.js )的${module_root}/src/Config.js


ES6模块


ES6 提供了用于加载ES6模块常规设计:


 import { something } from 'path/to/source/with/something'; 

由于我们需要将一个对象(类)附加到一个文件,因此在源代码中默认情况下导出此类是有意义的:


 export default class Vendor_Project_Path_To_Source_With_Something {...} 

原则上,不可能为类写这么长的名称,只是Something也可以工作,但是在Zend 1中,他们编写了并且没有中断,并且项目名称在项目中的唯一性对IDE的功能(自动完成和上下文提示)都产生了积极的影响,因此在调试时:


图片


在这种情况下,导入类并创建对象如下所示:


 import Something from 'path/to/source/with/something'; const something = new Something(); 

正面和背面导入


导入在浏览器和Node.js中均可使用,但有细微差别。 例如,浏览器无法理解nodejs模块的导入:


 import path from "path"; 

我们在浏览器中收到一个错误:


 Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../". 

也就是说,如果我们希望我们的代码在浏览器 nodejs中都可以工作,则不能使用浏览器 nodejs无法理解的构造。 我特别关注这一点,因为这样的结论太自然了,无法考虑。 如何呼吸。


DI在现代Web应用程序中的地位


由于我的个人经验,与本出版物中的所有其他内容一样,这纯粹是我的个人观点。


在Web应用程序中,JS实际上几乎占据了浏览器的前端。 在服务器端,Java,PHP,.Net,Ruby,python大量地被挖掘...但是随着nodejs的出现,JavaScript也渗透到服务器中。 并且其他语言(包括DI)中使用的技术开始渗透到服务器端JS。


JavaScript的开发归因于浏览器中代码的异步行为。 异步不是JS的特殊功能,而是天生的。 现在,服务器和前端上都存在JS并没有使任何人感到惊讶,而是鼓励在Web应用程序的两端使用相同的方法。 和相同的代码。 当然,正面和背面在本质上以及在要解决的任务上都太不同了,以至于在各处都使用相同的代码。 但是我们可以假设在一个或多或少复杂的应用程序中将存在浏览器,服务器和通用代码。


DI已在前面的RequireJS中使用


 define( ["./config", "./service"], function App(Config, Service) {} ); 

的确,在这里,依赖项的标识符以指向源的链接的形式显式地立即写入(您可以在引导加载程序配置中配置标识符的映射)。


在现代Web应用程序中,DI不仅存在于服务器端,而且还存在于浏览器中。


迈克尔·杰克逊与它有什么关系?


在nodejs中启用 ES模块支持(标志--experimental-modules )时,引擎*.mjs的文件的内容标识为EcmaScript-modules(不同于*.cjs Common-modules )。


这种方法有时称为“ Michael Jackson解决方案 ”,脚本称为Michael Jackson脚本( *.mjs )。


我同意解决KDPV的这种阴谋诡计,但是... 伙计们的傻瓜迈克尔杰克逊(Michael Jackson)...


另一个DI实现


好吧,正如您所期望的 自行车 DI模块- @ teqfw / di


这不是现成的解决方案,而是基本的实现。 所有依赖项都应为ES模块,并为浏览器和nodejs使用通用功能。


为了解决依赖关系,该模块使用awilix方法:


 constructor(spec) { /** @type {Vendor_Module_Config} */ const _config = spec.Vendor_Module_Config; /** @type {Vendor_Module_Service} */ const _service = spec.Vendor_Module_Service; } 

要运行后面的示例:


 import Container from "./src/Container.mjs"; const container = new Container(); container.addSourceMapping("Vendor_Module", "../example"); container.get("Vendor_Module_App") .then((app) => { app.run(); }); 

在服务器上:


 $ node --experimental-modules main.mjs 

要运行前面的示例( example.html ):


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>DI in Browser</title> <script type="module" src="./main.mjs"></script> </head> <body> <p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p> <p>Open browser console to see output.</p> </body> </html> 

您需要将模块放在服务器上,然后在浏览器中打开example.html页面(或使用IDE的功能)。 如果直接打开example.html ,那么Chrom中的错误是:


 Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https. 

如果一切顺利,那么在控制台(浏览器或Node.js)中将显示以下内容:


 Create object with ID 'Vendor_Module_App'. Create object with ID 'Vendor_Module_Config'. There is no dependency with id 'Vendor_Module_Config' yet. 'Vendor_Module_Config' instance is created. Create object with ID 'Vendor_Module_Service'. There is no dependency with id 'Vendor_Module_Service' yet. 'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]). 'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]). Application 'Vendor_Module_Config' is running. 

总结


AMD,CommonJS,UMD?


ESM

Source: https://habr.com/ru/post/zh-CN464347/


All Articles