MAM:前端组装无痛苦

您好,我叫Dmitry Karlovsky,我……很崇拜MAM。 M AM决定了Gnostic M模,从而节省了例程的大部分费用。


典型的不可知模块


与传统模块不同, 不可知模块不是源文件,而是一个目录,该目录中可以包含多种语言的源代码: JS / TS上的程序逻辑, TS / JS上的程序逻辑测试, view.tree上的组件组成,样式在CSSlocale=*.json中的locale=*.json ,图片等,等等。 如果需要,可以很轻松地获得其他任何语言的支持。 例如,手写笔用于编写样式,HTML则用于描述模板。


通过分析源自动跟踪模块之间的依赖关系。 如果模块已打开,则将其整体打开-每个模块的源代码都将转置并放入相应的捆绑包中:脚本-单独,样式-单独,测试-​​单独。 对于不同的平台-捆绑软件:对于节点-自己的浏览器-他们自己的。


完全自动化,缺乏配置和样板,最小的软件包大小,自动抽取依赖项,在一个代码库中开发数百个异化库和应用程序,而无痛苦。 哇,真上瘾! 将监护人从怀孕,紧张的孩子中移出,欢迎您来到潜艇!


经营理念


MAM是一项大胆的实验,旨在从根本上改变代码的组织方式和使用它的过程。 基本原则如下:


约定而不是配置。 合理,简单和通用的协议使您可以自动化整个例程,同时保持不同项目之间的便利性和统一性。


基础结构是独立的,代码是独立的。 当您需要开发数十个甚至数百个库和应用程序时,这种情况并不少见。 不要为每个部署基础设施的组装,开发,部署等。 一次询问就足够了,然后再申请诸如py的铆钉应用。


不要为不使用的东西付费。 您使用某种模块-包含在其所有依赖项中的模块。 不使用-无法打开。 模块越小,捆绑包中的粒度越大,不必要的代码也越少。


最小冗余代码。 将代码分解为模块应该就像将所有代码写入单个文件一样简单。 否则,开发人员将懒于将大型模块分解为小型模块。


没有版本冲突。 只有一个版本-当前版本。 如果可以将它们用于更新旧版本,则无需花费资源来支持旧版本。


手指放在脉搏上。 关于不兼容的最快反馈不会使代码变坏。


最简单的方法是最可靠的。 如果正确的道路需要额外的努力,请确保没有人会去找他们。


进口/出口


我们打开第一个使用现代模块系统的项目: 一个模块的长度不足300行,其中有30行是进口的。


但是这些仍然是花朵: 要实现9行功能,需要8个输入。


我最喜欢的: 没有一行有用的代码。 从模块堆将值转移20行到一列,以便以后可以从一个模块而不是从二十个模块导入。


所有这些都是一个样板,这导致以下事实:开发人员过于懒惰,无法将小段代码分配到单独的模块中,而不是大块模块。 即使它们不是很懒惰,事实证明要么是用于导入小模块的大量代码,要么是将大量模块导入自身并批量导出的特殊模块。


所有这些都会导致代码的粒度较低,并使未使用的代码束的大小膨胀,这很幸运,可以接近所使用的代码。 对于JS,他们正试图通过增加组装流水线,添加所谓的“摇树”来解决此问题,这种方法可以减少导入时的多余部分。 这会减慢装配速度,但并非一切都被削减了。


想法: 如果我们不导入而只是使用并使用,那么收藏家本人会弄清楚需要导入什么呢?


现代IDE可以自动为您使用的实体生成导入。 如果IDE可以执行此操作,那么是什么阻止收集器执行此操作? 在文件的命名和位置上有一个简单的约定就足够了,这对于用户来说是方便的,并且对于计算机来说是可以理解的。 PHP长期以来就有这样的标准约定: PSR-4 。 MAM对.ts和 .jam.js文件也引入了相同的含义:以$开头的名称是某些全局实体的全限定名,其代码是通过从FQN获取的路径中加载的,方法是用斜杠替换定界符。 两个模块的简单示例:


我的/ alert / alert.ts


 const $my_alert = alert // FQN    

我的/app/app.ts


 $my_alert( 'Hello!' ) // ,   /my/alert/ 

一行中的整个模块-哪个更简单? 结果很快就到了:创建和使用模块的简单性可以最小化它们的大小。 结果,最大化了粒度。 就像樱桃一样-尽量减少捆束的大小,而不会摇晃树。


验证模块的JSON / mol /数据系列就是一个很好的例子。 如果在代码中的某处使用$mol_data_integer函数,则$mol_data_integer依赖的模块/mol/data/integer/mol/data/number将包含在捆绑包中。 但是,例如,收集器/mol/data/email甚至都不会从磁盘读取,因为没人依赖它。


乱七八糟


自从我们开始踢Angular以来,我们不会停止。 您认为应该在哪里寻找applyStyles函数applyStyles ? 您不会猜到/packages/core/src/render3/styling_next/bindings.ts 。 在任何地方放置任何东西的能力导致以下事实:在每个项目中,我们观察到一个独特的文件定位系统,通常不符合任何逻辑。 而且,如果IDE经常被“跳转到定义”保存,则在github上查看代码或查看pull请求都将失去这个机会。


想法: 如果实体名称严格对应于其位置怎么办?


要将代码放置在文件/angular/packages/core/src/render3/stylingNext/bindings.ts ,在MAM体系结构中,您必须将实体命名为$angular_packages_core_src_render3_stylingNext_applyStyles ,但当然没有人会采取行动,因为名称太多了。 但是我希望代码中的名称简短明了,因此开发人员将尝试从名称中排除所有不必要的内容,仅保留重要的内容: $angular_render3_applyStyles 。 它将相应地位于/angular/render3/applyStyles/applyStyles.ts


请注意,MAM如何利用开发人员的弱点来获得预期的结果:每个实体都会获得一个简短的全局唯一名称,该名称可以在任何上下文中使用。 例如,在提交消息中,这些名称使您可以快速而准确地了解他们在说什么:


 73ebc45e517ffcc3dcce53f5b39b6d06fc95cae1 $mol_vector: range expanding support 3a843b2cb77be19688324eeb72bd090d350a6cc3 $mol_data: allowed transformations 24576f087133a18e0c9f31e0d61052265fd8a31a $mol_data_record: support recursion 

或者,假设您想在Internet上找到$ mol_fiber模块的所有提及 -由于有了FQN,它比以往任何时候都更容易。


循环依赖


让我们在一个文件中编写7行简单代码:


 export class Foo { get bar() { return new Bar(); } } export class Bar extends Foo {} console.log(new Foo().bar); 

尽管存在周期性依赖性,但它仍然可以正常工作。 我们将其分为3个文件:


我的/ foo.js


 import { Bar } from './bar.js'; export class Foo { get bar() { return new Bar(); } } 

我的/ bar.js


 import { Foo } from './foo.js'; export class Bar extends Foo {} 

我的/ app.js


 import { Foo } from './foo.js'; console.log(new Foo().bar); 

糟糕, ReferenceError: Cannot access 'Foo' before initialization 。 什么样的废话? 为了解决这个问题,我们的app.js需要知道foo.js依赖于bar.js 因此,我们首先需要导入bar.js ,后者将导入foo.js 之后,我们就可以正确导入foo.js


我的/ app.js


 import './bar.js'; import { Foo } from './foo.js'; console.log(new Foo().bar); 

那些浏览器,NodeJS,Webpack,Parcel-它们都在循环依赖的基础上运行异常。 而且,他们只是禁止使用它们-可以立即使代码复杂化,从而避免循环。 但是它们可以正常工作,然后变得有害,并给出一个无法理解的错误。


想法: 如果在组装过程中我们只是按照正确的顺序粘贴文件,就像所有代码最初都写在一个文件中一样?


让我们使用MAM的原理拆分代码:


我的/ foo / foo.ts


 class $my_foo { get bar() { return new $my_bar(); } } 

我的/ bar / bar.ts


 class $my_bar extends $my_foo {} 

我的/app/app.ts


 console.log(new $my_foo().bar); 

最初全部相同的7行代码。 他们只是在没有其他萨满教的情况下工作。 问题在于,收集器了解到, my/barmy/foo的依赖性比my/foomy/bar的依赖性更严格。 这意味着您应该按以下顺序将这些模块包括在包中: my/foomy/barmy/app


收藏家如何理解这一点? 现在,启发式方法很简单-通过检测依赖关系的行中的缩进数量。 请注意,在我们的示例中,较强的依存关系具有零缩进,而较弱的依赖关系具有双重缩进。


不同的语言


碰巧的是,对于不同的事物,我们针对这些不同的事物使用了不同的语言。 最常见的是:JS,TS,CSS,HTML,SVG,SCSS,Less,Stylus。 每个模块都有其自己的模块系统,该模块系统不会以任何方式与其他语言进行交互。 不用说,大约100500种更特定的语言。 因此,要连接组件,您必须分别连接其脚本,单独的样式,分别注册模板,分别配置其所需的静态文件的部署等,等等。


感谢装入程序,Webpack试图解决此问题。 但是他有一个切入点是已经可以连接其他语言的文件的脚本。 如果我们不需要脚本? 例如,我们有一个具有漂亮样式的板块模块,我们希望它们在浅色主题中具有相同的颜色,而在黑暗中具有其他颜色:


 .dark-theme table { background: black; } .light-theme table { background: white; } 

而且,如果我们依赖于该主题,则应加载一个脚本,该脚本将根据一天中的时间安装所需的主题。 也就是说,CSS实际上取决于JS。


想法: 如果模块化系统不依赖语言怎么办?


由于在MAM中,模块化系统与语言是分离的,因此依赖项可以是跨语言的。 CSS可能取决于JS,后者可能取决于TS,TS可能取决于另一个JS。 这是由于以下事实而实现的:在模块上检测到源依赖关系,并且模块完全连接并且可以包含任何语言的源代码。 对于主题示例,它看起来像这样:


/my/table/table.css


 /* ,   /my/theme */ [my_theme="dark"] table { background: black; } [my_theme="light"] table { background: white; } 

/my/theme/theme.js


 document.documentElement.setAttribute( 'my_theme' , ( new Date().getHours() + 15 ) % 24 < 12 ? 'light' : 'dark' , ) 

顺便说一下,使用这种技术,您可以实现Modernizr ,但是不需要300个不需要的检查,因为捆绑包中仅包含CSS真正依赖的那些检查。


许多图书馆


通常,构建捆绑软件的入口点是某种文件。 对于Webpack,这是JS。 如果您开发了许多可移植的库和应用程序,那么您将需要很多捆绑软件。 对于每个捆绑包,您需要创建一个单独的入口点。 对于Parcel,入口点是HTML,无论如何都必须为应用程序创建HTML。 但是对于图书馆来说,这不太合适。


想法: 如果没有任何准备就可以将任何模块组装成一个独立的捆绑包,该怎么办?


让我们汇总一下$ mol_build MAM项目构建器的最新版本:


 mam mol/build 

现在运行该收集器,然后让他再次组装自己,以确保他仍然能够组装自己:


 node mol/build/-/node.js mol/build 

虽然不行,但让我们请他与程序集一起运行测试:


 node mol/build/-/node.test.js mol/build 

如果一切顺利,请在NPM中发布结果:


 npm publish mol/build/- 

如您所见,组装模块时,将使用名称创建一个子目录-所有组装工件都放置在该目录中。 让我们浏览一下您可以在其中找到的文件:


  • web.dep.json有关依赖图的所有信息
  • web.js浏览器脚本包
  • web.js.map他的sorsmaps
  • web.esm.js它采用es模块的形式
  • web.esm.js.map以及它的sorsmaps
  • web.test.js测试包
  • web.test.js.map以及sorsmap测试
  • web.d.ts捆绑包含脚本捆绑包中所有内容的类型
  • web.css样式捆绑
  • web.css.map并对其进行排序
  • web.test.html在浏览器中运行性能测试的入口点
  • web.view.tree捆绑包view.tree中包含的所有组件的声明
  • web.locale=*.json具有本地化文本的捆绑包,每个捆绑包都有自己的捆绑包
  • package.json允许您立即在NPM中发布组装的模块
  • node.dep.json有关依赖图的所有信息
  • node.js节点脚本捆绑
  • node.js.map-它的sorsmaps
  • node.esm.js它采用es模块的形式
  • node.esm.js.map以及它的sorsmaps
  • node.test.js相同的捆绑包,也可以进行测试
  • node.test.js.map以及它的sorsmaps
  • node.d.ts包含脚本包中所有内容类型的包
  • node.view.tree捆绑包view.tree中包含的所有组件的声明
  • node.locale=*.json具有本地化文本的捆绑包,每个捆绑包都有自己的捆绑包

只需将静态信息与路径一起复制即可。 以显示自己的源代码应用程序为例。 它的来源在这里:


  • /mol/app/quine/quine.view.tree
  • /mol/app/quine/quine.view.ts
  • /mol/app/quine/index.html
  • /mol/app/quine/quine.locale=ru.json

不幸的是,在一般情况下,收集器无法知道我们在运行时将需要这些文件。 但是我们可以在附近放置一个特殊文件来告诉他:


/mol/app/quine/quine.meta.tree


 deploy \/mol/app/quine/quine.view.tree deploy \/mol/app/quine/quine.view.ts deploy \/mol/app/quine/index.html deploy \/mol/app/quine/quine.locale=ru.json 

作为Assembly /mol/app/quine ,将通过以下方式复制它们:


  • /mol/app/quine/-/mol/app/quine/quine.view.tree
  • /mol/app/quine/-/mol/app/quine/quine.view.ts
  • /mol/app/quine/-/mol/app/quine/index.html
  • /mol/app/quine/-/mol/app/quine/quine.locale=ru.json

现在,目录/mol/app/quine/-可以放在任何静态主机上,并且该应用程序将完全起作用。


目标平台


JS可以在客户端和服务器上执行。 而且,当您可以编写一个代码并且可以在任何地方使用时,它有多酷。 但是,有时同一件事在客户端和服务器上的实现根本不同。 我想例如将一种实现用于一个节点,将另一种实现用于浏览器。


想法: 如果文件的目的反映在文件名中怎么办?


MAM在文件名中使用标签系统。 例如, $mol_state_arg模块提供对用户定义的应用程序参数的访问。 在浏览器中,这些参数是通过地址栏设置的。 并在节点中,通过命令行参数。 $mol_sate_arg通过使用单个接口实现两个选项并将它们放置在文件中,从而从这些细微差别中抽象出了其余的应用程序:


  • / mol /状态/ arg / arg。 web .ts-浏览器的实现
  • / mol /状态/ arg / arg。 node .ts- 节点的实现

无论目标平台是什么,都包括未标记这些标签的源。


在测试中观察到类似的情况-它们希望存储在其余源的旁边,但是它们不希望包含在发送给最终用户的捆绑软件中。 因此,测试还标有单独的标签:


  • / mol /状态/ arg / arg。 test .ts-模块测试,它们将属于测试包

标签可以是参数化的。 例如,对于每个模块,可以使用多种语言编写的文本,它们应包含在相应的语言包中。 文本文件是一个常规的JSON字典,其名称以语言环境命名:


  • / mol / app /生活/生活。 locale = ru .json-俄语文本
  • / mol / app /生活/生活。 locale = jp .json-日语文本

最后,如果我们想将文件放在附近,但希望收集器忽略它们而不自动将它们包含在捆绑软件中,该怎么办? 只需在其名称的开头添加任何非字母数字字符即可。 例如:


  • / hyoo /玩具/ git-以句点开头,因此收集器将忽略此目录

版本控制


Google首次发布了AngularJS,并在NPM中将其发布为angular 。 然后,他创建了一个名称相似的全新框架Angular,并以相同的名称发布了该框架,但已经是版本2。现在,这两个烟花正在独立开发。 主要版本之间仅发生一次API中断更改。 而另一个- 在未成年人之间 。 并且由于不可能将具有相同依赖关系的两个版本放在同一级别,因此当库的两个版本同时在应用程序中共存一段时间时,就没有任何平稳过渡的话题。


看来Angular团队已经踩到了所有可能的耙子。 还有一件事:框架代码分为几个大模块。 最初,他们独立地对它们进行了版本控制,但是很快,即使他们自己也开始对模块的哪些版本相互兼容感到困惑,更不用说普通开发者了。 因此, Angular切换到了端到端版本控制 ,在该版本中,即使不更改代码,模块的主版本也可以更改。 无论是维护者本身还是整个生态系统,对多个模块的多个版本的支持都是一个大问题。 毕竟,所有社区成员的大量资源都花在了确保与已经过时的模块的兼容性上。


语义版本控制的美好主意分解为苛刻的现实-您永远都不知道在更改次要版本或补丁版本时是否会破坏某些内容。 因此,在许多项目中,依赖项的特定版本是固定的。 但是,这样的修复程序不会影响传递依赖关系,当从头开始安装时,传递依赖关系可能会吸引到最新版本,但如果已经安装,则可能会保持不变。 这种混乱导致一个事实,即您永远不能依赖固定版本,而需要定期检查与(至少是传递性)依赖项的当前版本的兼容性。


但是锁定文件呢? 如果您正在开发通过依赖关系安装的库,则锁定文件将无济于事,因为包管理器将忽略该文件。 对于最终应用程序,锁定文件将为您提供所谓的“程序集的可复制性”。 但说实话。 您需要从同一来源构建最终应用程序多少次? 恰好一次。 接收输出(与任何NPM无关)是装配工件:可执行二进制文件,docker容器或仅是归档文件,其中包含运行它所需的所有代码。 希望您不要在产品上npm install吗?


有些人发现锁定文件的使用在于CI服务器完全编译了开发人员已提交的内容。 但是等等,开发人员自己可以简单地将其组装在本地计算机上。 , , , . Continuous Integration , , , , - . CI , .


, , . , Angular@4 ( 3). , , " " " ". Angular@4 , Angular@5. Angular@6, . Angular TypeScript . . , 2 , … , business value , , , , .


, , , , 2 . : , — , — . 3 React, 5 jQuery, 7 lodash.


: — ?


. - . , . , . , . , . , . , , . : issue, , workaround, pull request, , . , , . . .


, . , , . . . : , , -. - — . , , - . , , , NPM . , . .


, ? — . mobx , mobx2 API . — , : , . mobx mobx2 , API. API, .


. — . , :


 var pages_count = $mol_atom2_sync( ()=> $lib_pdfjs.getDocument( uri ).promise ).document().numPages 

mol_atom2_sync lib_pdfjs , :


 npm install mol_atom2_sync@2.1 lib_pdfjs@5.6 

, , — , . ? — , *.meta.tree , :


/.meta.tree


 pack node git \https://github.com/nin-jin/pms-node.git pack mol git \https://github.com/eigenmethod/mol.git pack lib git \https://github.com/eigenmethod/mam-lib.git 

. .


NPM


MAM — NPM . , — . , , NPM .


NPM , $node. , - -:


/my/app/app.ts


 $node.portastic.find({ min : 8080 , max : 8100 , retrieve : 1 }).then( ( ports : number[] ) => { $node.express().listen( ports[0] ) }) 

, . - lib NPM . , NPM- pdfjs-dist :


/lib/pdfjs/pdfjs.ts


 namespace $ { export let $lib_pdfjs : typeof import( 'pdfjs-dist' ) = require( 'pdfjs-dist/build/pdf.min.js' ) $lib_pdfjs.disableRange = true $lib_pdfjs.GlobalWorkerOptions.workerSrc = '-/node_modules/pdfjs-dist/build/pdf.worker.min.js' } 

/lib/pdfjs/pdfjs.meta.tree


 deploy \/node_modules/pdfjs-dist/build/pdf.worker.min.js 

, .



. create-react-app angular-cli , . , , eject . . , , .


: ?


MAM . .


MAM MAM , :


 git clone https://github.com/eigenmethod/mam.git ./mam && cd mam npm install npm start 

8080 . , — MAM.


( — acme ) ( — hello home ):


/acme/acme.meta.tree


 pack hello git \https://github.com/acme/hello.git pack home git \https://github.com/acme/home.git 

npm start :


 npm start acme/hello acme/home 

. — . , , . — : https://t.me/mam_mol

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


All Articles