[Web翻译]前端的嵌套依赖

893 阅读8分钟

原文地址:dev.to/open-wc/nes…

原文作者:dev.to/dakmor

发布时间:2019年7月2日 ・8分钟阅读

所以你有了这个很棒的想法,现在你想真正做到这一点。我很确定你不想从头开始,所以让我们使用现有的开源包。

如果你想一起玩,所有的代码都在github上。

在我们的例子中,我们想使用lit-元素和lit-html。

mkdir nested-dependecies-in-frontend
cd nested-dependecies-in-frontend
npm install lit-element lit-html@1.0.0 --save-exact

注意:我们这里是故意使用pinned版本。

然后我们只需在main.js中加载这两个包。

import { LitElement } from "lit-element";
import { html } from "lit-html";

console.log(LitElement);
console.log(html);

为了了解我们的应用程序有多大,我们想创建一个滚动捆绑包。首先,安装Rollup。

npm install -D rollup

然后创建一个rollup.config.js。

export default {
  input: "main.js",
  output: {
    file: "bundle.js",
    format: "iife"
  },
};

接下来,在我们package.json的脚本块中添加 "build": "rollup -c rollup.config.js && du -h bundle.js" 到我们的package.json的scripts块中,这样我们就可以很容易地构建文件并输出它的文件大小。

让我们通过npm run build来运行它:)

(!) Unresolved dependencies
https://rollupjs.org/guide/en#warning-treating-module-as-external-dependency
lit-element (imported by main.js)
lit-html (imported by main.js)

哦,不灵了! 😭

好吧,我以前听过这个问题...... 我们需要添加一些插件,以便Rollup能够理解节点解析的方式(即裸模块指定器,如import { html } from 'lit-html')。

npm i -D rollup-plugin-node-resolve
+ import resolve from "rollup-plugin-node-resolve";
+
   export default {
    input: "main.js",
    output: {
      file: "bundle.js",
      format: "iife"
    },
+  plugins: [resolve()]
  };
$ npm run build
# ...
created bundle.js in 414ms
96K     bundle.js

所以,这似乎很好用。💪

如果有人喜欢 yarn 会发生什么?

先安装yarn,然后再编译,结果应该是一样的吧?

$ yarn install
$ yarn build
# ...
created bundle.js in 583ms
124K    bundle.js

哇!这真是出乎意料--124K的yarn构建和96K的npm构建?

看来yarn构建的文件中包含了一些额外的文件......也许是一个包被重复了?

$ yarn list --pattern lit-*
├─ lit-element@2.2.0
│ └─ lit-html@1.1.0
└- lit-html@1.0.0

是的,lit-html1.0.01.1.0版本都已安装。

原因很可能是我们用上面的npm install --save-exact lit-html@1.0.0 命令安装lit-html时,把它钉在了根依赖的1.0.0版本上。

虽然npm似乎能很好地重构它,但我觉得使用npm并不安全,因为如果依赖树变大,npm也喜欢安装嵌套的依赖。

$ npm ls lit-element lit-html
├─┬ lit-element@2.2.0
│ └── lit-html@1.0.0  deduped
└── lit-html@1.0.0

特别是当你使用一些测试版(比如0.x.x)的依赖关系时,就会变得非常棘手。在这种情况下,SemVer 说每一个 0.x.0 版本都意味着一个突破性的改变。这意味着0.8.0将被视为与0.9.0不兼容。因此,即使您使用的API在两个版本中都能正常工作,您也会得到嵌套的依赖关系,这可能会无声无息地破坏您的应用程序😱。

节点解析如何工作

在nodejs中,当你使用裸指定符导入一个文件时,例如:import { LitElement } from "lit-element"; Node的模块解析函数得到字符串lit-element,并开始搜索module.paths中列出的所有目录,寻找导入模块,你可以像检查节点repl中的任何其他值一样检查。

$ node
module.paths
[
  '/some/path/nested-dependencies-in-frontend/node_modules',
  '/some/path/node_modules',
  '/some/node_modules',
  '/node_modules',
]
# unimportant folders are hidden here

基本上,node会查看每一个node_modules文件夹,从模块的父目录开始,沿着文件树向上移动,直到找到一个与模块指定符相匹配的目录名(在我们的例子中是lit-element)。解析算法总是从当前模块的父目录开始,所以它总是相对于你从哪里导入文件。如果我们从lit-element的目录中检查module.paths,我们会看到一个不同的列表。

$ cd node_modules/lit-element
$ node
module.paths
[
  '/some/path/nested-dependencies-in-frontend/node_modules/lit-element/node_modules',
  '/some/path/nested-dependencies-in-frontend/node_modules',
  '/some/path/node_modules',
  '/some/node_modules',
  '/node_modules',
]

现在我们可以理解什么是节点的嵌套依赖了。每个模块都可以有自己的node_modules目录,无休止的,该模块的文件中引用的import总是会先在离自己最近的node_modules目录中查找......

Node上嵌套依赖的优点前端嵌套依赖的缺点
每个包都可以有自己的版本,每个依赖的版本同一个代码两次发货意味着更长的下载和处理时间。
包不受应用程序中其他包的依赖性影响如果相同的代码从两个不同的地方导入两次,可能会出现一些问题(例如,通过WeakMaps或singleletons进行性能优化)。
访问许多额外的文件不需要支付 "高额费用"。检查一个文件是否存在是一个额外的请求。
在服务器上,你通常不会太在意有多少额外的代码(文件大小)。总的来说,简而言之,你的网站会越来越慢

问题

简而言之,喜欢嵌套的自动模块解析对前端来说可能是危险的。

  • 我们关心的是加载和解析性能
  • 我们关心的是文件的大小
  • 有些包必须是单子(即在模块图中是唯一的),才能在我们的应用程序中正常工作。
    • 例子包括lit-htmlgraphql
  • 我们应该完全控制客户端浏览器上的内容。

节点式的模块解析,是为服务器端环境设计的,当在浏览器中采用时,这些问题会变成严重的问题。 我认为,即使节点解析在技术上是可行的,但作为前端开发者,为一个复杂的数据网格加载一次以上的代码绝不应该是我们的目标。

解决办法

值得庆幸的是,我们现在已经有了可以解决这些问题的办法,而且还有一些建议可以在未来完全消除这种变通的需要。

今天的工作

下面是一些在你的前端代码中使用裸模块指定器的提示。

  • 确保你的依赖树中的模块都使用相似的版本范围的公共依赖关系
  • 尽可能避免钉入特定的包版本(就像我们在上面的npm i -S lit-html@1.0.0)。
  • 如果你使用的是npm
    • 安装完包后运行npm dedupe来删除嵌套的重复包。
    • 你可以尝试删除你的package-lock.json,然后重新安装。有时会有神奇的帮助🧙♂️。
  • 如果你在使用yarn

展望未来

如果我们能准确地告诉JavaScript环境(即浏览器)在哪个path上找到某个字符串指定的文件,我们就不需要节点式的解析或编程时的重复数据删除例程。

我们会写这样的东西,并将其传递给浏览器,以指定哪些路径映射到哪些包。

{
  "lit-html": "./node_modules/lit-html.js",
  "lit-element": "./node_modules/lit-element.js"
}

使用这个导入地图来解决包的路径,意味着lit-htmllit-element永远只有一个版本,因为全局环境已经知道在哪里可以找到它们。

幸运的是✨,这已经是一个提议的规范,叫做导入地图。既然是为浏览器准备的,就根本不需要做任何转换! 你只需要提供地图,在开发时就不需要任何构建步骤?

听起来很疯狂😜? 让我们来试一试吧! 🤗

注意:请注意,这是一个实验性的API提案,它还没有被最终确定或被实现者接受。

目前它只在Chrome 75+中工作,在一个标志后面。 所以在URL栏输入chrome://flags/,然后搜索Built-in module infra and import maps并启用它。 这里有一个直接链接:chrome://flags/#enable-built-in-module-infra

在浏览器中使用导入地图

为了使用导入地图,让我们创建一个index.html文件。

<html lang="en-GB">
<head>
  <script type="importmap">
    {
      "imports": {
        "lit-html": "./node_modules/lit-html/lit-html.js",
        "lit-html/": "./node_modules/lit-html/",
        "lit-element": "./node_modules/lit-element/lit-element.js",
        "lit-element/": "./node_modules/lit-element/"
      }
    }
  </script>
  <title>My app</title>
</head>

<body>
  <crowd-chant>
    <span slot="what">Bare Imports!</span>
    <span slot="when">Now!</span>
  </crowd-chant>

  <script type="module" src="./main.js"></script>
</body>

</html>

并调整main.js

import { html, LitElement } from "lit-element";

class CrowdChant extends LitElement {
  render() {
    return html`
      <h2>What do we want?</h2>
      <slot name="what"></slot>
      <h2>When do we want them?</h2>
      <time><slot name="when">Now!</slot></time>
    `;
  }
}

customElements.define("crowd-chant", CrowdChant);

保存文件,然后通过在同一目录下运行npx http-server -o在本地提供服务。

这将打开 http://localhost:8080/ ,你将看到你的自定义元素呈现在屏幕上。🎉

这个🔮是什么黑魔法?在没有任何捆绑程序、工具或构建步骤的情况下,我们用我们所熟知和喜爱的那种裸露的指定器写了一个组件化的应用。

让我们来分析一下。

import { html } from 'lit-html';
// will actually import "./node_modules/lit-html/lit-html.js"
// because of
// "lit-html": "./node_modules/lit-html/lit-html.js",

import { repeat } from 'lit-html/directives/repeat.js'
// will actually import "./node_modules/lit-html/directives/repeat.js"
// beacause of
// "lit-html/": "./node_modules/lit-html/",

所以这意味着

  1. 你可以直接导入软件包,因为软件包的名称是映射到一个特定的文件的
  2. 你可以导入子目录和文件,因为packageName+'/'会被映射到它的目录中。
  3. 从子目录导入文件时,不能省略.js。

这一切对我的生产制造意味着什么?

重要的是要再次指出,这仍然是实验性的技术。在任何情况下,你可能还是希望使用像Rollup这样的工具为生产网站进行优化构建。我们正在一起探索这些新的API将为我们的网站和应用程序做什么。底层的import-maps提案仍然不稳定,但这不应该阻止我们进行实验并从中提取效用。毕竟,我们中的大多数人都能自如地使用babel来实现装饰器等实验性语法,尽管在写这篇文章的时候,该提案至少有四种味道。

如果你今天想尝试导入地图,即使在不支持的浏览器中,你也需要一个构建步骤或者像systemjs这样的运行时解决方案。对于构建步骤选项,你将用一些尊重你的导入地图而不是使用节点解析的东西来替换 rollup-plugin-node-resolve

如果你能把rollup指向你的index.html,然后让它找出你的入口点是什么,是否有一个导入地图,那不是很好吗?

这就是为什么我们在open-wc发布了对导入地图的实验性支持,我们的rollup-plugin-index-html

你可以在dev.to上阅读所有关于它的内容。请关注本空间的公告😉。

请在Twitter上关注我们,或者在我的个人Twitter上关注我。 请务必在open-wc.org查看我们的其他工具和推荐。

感谢BennyLars的反馈,并帮助我把我的涂鸦变成一个可跟踪的故事。