的结构加载,第二种是ES5语法(使用

在生产中使用JavaScript模块:当前状态。 第一部分

两年前,我写了一篇关于现在被称为模块/无模块模式的技术的文章。 它的应用程序允许您使用ES2015 +的功能编写JavaScript代码,然后使用捆绑程序和编译器创建代码库的两个版本。 其中一种包含现代语法(使用<script type="module">的结构加载,第二种是ES5语法(使用<script nomodule>加载)。module / nomodule模式允许发送至支持模块的浏览器,与不支持此功能的浏览器相比,代码要少得多,现在大多数Web框架和命令行工具都支持此模式。



以前,即使考虑将现代JavaScript代码发送到生产环境的能力,并且即使大多数浏览器都支持模块,我还是建议以捆绑形式收集代码。

怎么了 主要是因为我感到将模块加载到浏览器中的速度很慢。 尽管最近的协议(例如HTTP / 2)在理论上支持多个文件的有效加载,但当时的所有性能研究都得出结论,使用捆绑器比使用模块更有效。

但是必须承认,这些研究是不完整的。 使用其中研究的模块的测试用例包括在生产中部署的未优化和未最小化的源代码文件。 没有将优化包与具有优化经典脚本的模块进行比较。

但是,老实说,当时没有最佳的方式来部署模块。 但是现在,由于对捆绑程序技术进行了一些现代改进,因此可以同时使用静态和动态导入命令以ES2015模块的形式部署生产代码,并且与使用可用选项相比,其性能更高。不使用模块。

应当指出,在发布原始资料的站点 (我们今天发布的译文的第一部分)上,模块已在生产中使用了几个月。

对模块的误解


我与之交谈的许多人完全拒绝使用模块,甚至没有将其视为大规模生产应用程序的选择之一。 其中许多人引用了我已经提到的研究。 即,该部分的内容指出不应在生产环境中使用这些模块,除非存在以下问题:“小型Web应用程序包含少于100个模块,这些模块在相对较小的”依赖树中有所不同(即-深度不超过5级的人。”

如果您曾经浏览过任何项目的node_modules目录,那么您可能知道即使一个小型应用程序也可以轻松拥有100多个依赖模块。 我想向您介绍一些最受欢迎的npm软件包中有多少个模块。
包装方式
模块数
日期-fns
729
Lodash-es
643
rxjs
226

这就是有关模块的主要误解所在。 程序员认为,在生产中使用模块时,他们只有两种选择。 第一种是以现有形式(包括node_modules目录)部署所有源代码。 第二个是根本不使用模块。

但是,如果仔细查看上述研究的建议,您会发现没有什么可说加载模块比加载常规脚本要慢。 并没有说完全不应该使用模块。 它只是在谈论这样一个事实:如果有人在生产中部署了数百个未感染的模块文件,Chrome将无法像单个缩小的捆绑包一样快地加载它们。 因此,该研究建议继续使用捆绑器,编译器和压缩器。

但是你知道吗? 事实是您可以使用所有这些功能并在生产中使用模块。

实际上,模块是我们应该努力将代码转换为的格式,因为浏览器已经知道如何加载模块(而不能执行此操作的浏览器可以使用nomodule机制加载回退)。 如果查看最流行的捆绑程序生成的代码,则会发现许多模板片段,其目的仅在于动态加载其他代码和管理依赖关系。 但是,如果仅使用模块和表达式importexport那么所有这些都将不是必需的。

幸运的是,至少一个流行的现代捆绑器( Rollup )支持以输出数据形式的模块。 这意味着您可以使用捆绑器处理代码并在生产环境中部署模块(无需使用模板片段来加载代码)。 并且,由于Rollup很好地实现了摇树算法(我在捆绑器中看到的最好的),因此使用Rollup以模块形式构建程序可以使您获得的代码小于应用时获得的相同代码的大小。今天可用的其他机制。

应该注意的是,他们计划在下一个版本的Parcel中添加对模块的支持。 Webpack尚不支持将模块作为输出格式,但这里 -集中讨论此问题的讨论。

关于模块的另一个误解是,有些人认为只有在100%的项目依赖项使用模块时才能使用模块。 不幸的是(我感到非常遗憾),大多数npm软件包仍在准备使用CommonJS格式发布(某些模块,即使是使用ES2015功能编写的模块,在发布到npm之前也已转换为CommonJS格式)!

在这里,我想再次指出,Rollup有一个插件( rollup-plugin-commonjs ),该插件可接收使用CommonJS编写的输入源代码并将其转换为ES2015代码。 当然,如果从一开始就使用依赖格式, 那么最好使用ES2015模块格式。 但是,如果某些依赖性不是这样,则不会阻止您使用生产中的模块来部署项目。

在本文的以下部分中,我将向您展示如何将使用模块的捆绑包中的项目收集起来(包括使用动态导入和代码分离),并讨论为什么这些解决方案通常比经典脚本更高效,并展示它们如何工作使用不支持模块的浏览器。

最佳代码构建策略


构建生产代码始终是一种尝试平衡各种解决方案的利弊的尝试。 一方面,开发人员希望他的代码尽快加载和执行。 另一方面,他不想下载项目用户不会使用的代码。

另外,开发人员需要确信自己的代码最适合缓存。 代码捆绑的最大问题是,代码中的任何更改,甚至一行更改都将导致整个捆绑包的缓存无效。 如果您部署由数千个小模块组成的应用程序(确切地以源代码中存在的形式呈现),则可以安全地对代码进行较小的更改,同时知道大多数应用程序代码都将被缓存。 但是,正如我已经说过的那样,这种开发方法可能意味着与您使用更传统的方法相比,首次访问资源时加载代码所花费的时间可能更长。

结果,我们面临着一项艰巨的任务,那就是找到正确的方法将这些捆分成几部分。 我们需要在加载材料的速度和它们的长期缓存之间取得适当的平衡。

默认情况下,大多数捆绑程序都使用基于动态导入命令的代码拆分技术。 但是我要说的是,仅以动态导入为重点来划分代码不会将其分成足够小的片段。 对于具有许多回头用户的站点(即在缓存很重要的情况下)尤其如此。

我认为应该将代码分成尽可能小的片段。 值得减少片段的大小,直到片段的数量增长到足以影响项目的下载速度为止。 尽管我绝对建议大家对情况进行分析,但如果您相信我提到的研究中所做的近似计算,则当加载少于100个模块时,加载速度不会明显下降。 单独进行的有关HTTP / 2性能的研究并未显示下载少于50个文件时项目速度明显下降。 但是,他们在那里仅测试了文件数量分别为1、6、50和1000的选项。因此,大约100个文件是您可以轻松导航到的值,而不必担心下载速度降低。

那么,最好的方法是主动而不是过于主动地将代码分成几部分? 除了基于动态导入命令拆分代码外,我建议您仔细研究将代码拆分为npm软件包。 通过这种方法,从node_modules文件夹导入到项目中的node_modules将根据包名称分成完成的代码的单独片段。

包装分离


我在上面说过,捆绑器的某些现代功能使组织高性能方案以部署基于模块的项目成为可能。 我所说的是由两个新的汇总功能代表的。 第一个是通过动态import()命令( 在v1.0.0中添加import() 自动进行代码分离 。 第二个选项是程序基于manualChunks选项(在v1.11.0中添加)执行的手动代码分离

由于这两个功能,现在可以非常轻松地配置构建过程,在该过程中,可以在程序包级别拆分代码。

这是一个使用manualChunks选项的示例配置,因此,从node_modules导入的每个模块node_modules属于一个单独的代码段,该代码段的名称与软件包名称相对应(从技术上讲,是node_modules文件夹中软件包目录的名称):

 export default {  input: {    main: 'src/main.mjs',  },  output: {    dir: 'build',    format: 'esm',    entryFileNames: '[name].[hash].mjs',  },  manualChunks(id) {    if (id.includes('node_modules')) {      //   ,    `node_modules`.      //   - ,       .      const dirs = id.split(path.sep);      return dirs[dirs.lastIndexOf('node_modules') + 1];    }  }, } 

manualChunk选项接受一个函数,该函数以单个参数形式接受模块文件的路径。 此函数可以返回字符串名称。 它返回的内容将指向当前模块应添加到的程序集片段。 如果函数不返回任何内容,则模块将被添加到默认片段中。

考虑一个从lodash-es包中导入cloneDeep()cloneDeep()find() lodash-eslodash-es程序。 如果在构建此应用程序时应用上述配置,则每个模块(以及这些模块导入的每个lodash模块)都将放置在单个输出文件中,其名称类似于npm.lodash-es.XXXX.mjs (此处XXXX是唯一的) lodash-es片段中的模块文件哈希值)。

在文件末尾,您将看到类似于以下内容的导出表达式。 请注意,此表达式仅包含添加到片段的模块的导出命令,而不是所有lodash模块的导出命令。

 export {cloneDeep, debounce, find}; 

然后,如果其他任何片段中的代码使用了这些lodash模块(也许仅是lodash debounce()方法),则在这些片段的上部,将有一个类似于以下内容的import表达式:

 import {debounce} from './npm.lodash.XXXX.mjs'; 

希望此示例阐明了汇总中手动代码分离如何工作的问题。 另外,我认为使用importexport表达式进行代码分离的结果比片段代码更易于阅读和理解,而片段代码的形成使用了仅在特定捆绑程序中使用的非标准机制。

例如,很难弄清楚下一个文件中发生了什么。 这是我使用webpack拆分代码的旧项目之一的输出。 在支持模块的浏览器中,几乎不需要此代码中的所有内容。

 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([["import1"],{ /***/ "tLzr": /*!*********************************!*\  !*** ./app/scripts/import-1.js ***!  \*********************************/ /*! exports provided: import1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "import1", function() { return import1; }); /* harmony import */ var _dep_1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./dep-1 */ "6xPP"); const import1 = "imported: " + _dep_1__WEBPACK_IMPORTED_MODULE_0__["dep1"]; /***/ }) }]); 

如果有数百个npm依赖项怎么办?


就像我说的那样,我相信在代码级分离非常积极但不太过于积极的情况下,通常在程序包级别进行代码级分离可以使开发人员处于良好的位置。

当然,如果您的应用程序从成百上千个不同的npm软件包中导入模块,则浏览器仍无法有效地加载所有模块。

但是,如果您确实有很多npm依赖项,则暂时不应该完全放弃此策略。 请记住,您可能不会在每个页面上都下载所有npm依赖项。 因此,找出实际加载的依赖项很重要。

尽管如此,我确信有些真实的应用程序具有如此多的npm依赖关系,以致这些依赖关系无法简单地表示为单独的片段。 如果您的项目只是这样-我建议您寻找一种对程序包进行分组的方法,因为这些程序包的片段缓存react-dom也将被执行,因此其中高概率代码可以同时更改的代码(例如reactreact-dom )同时。 稍后,我将显示一个示例,其中所有React依赖项都分组在同一片段中

待续...

亲爱的读者们! 您如何处理项目中的代码分离问题?

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


All Articles