在没有构建系统的情况下导入前端Javascript库

216 阅读10分钟

我喜欢在没有构建系统的情况下编写 Javascript,昨天我第一百万次遇到了一个问题,我需要弄清楚如何在不使用构建系统的情况下在代码中导入 Javascript 库,并且花了很长时间才弄清楚如何导入它,因为库的设置说明假设您正在使用构建系统。

幸运的是,在这一点上,我已经学会了如何驾驭这种情况,要么成功使用该库,要么决定它太难并切换到不同的库,所以这里是我希望几年前必须导入 Javascript 库的指南。

我将只讨论在前端使用 Javacript 库,并且只讨论如何在无构建系统设置中使用它们。

在这篇文章中,我将谈论:

  1. 库可能提供的三种主要 Javascript 文件类型(ES 模块、“经典”全局变量类型和 Common JS)
  2. 如何弄清楚 Javascript 库在其构建中包含哪些类型的文件
  3. 在代码中导入每种类型文件的方法

三种 Javascript 文件

库可以提供 3 种基本类型的 Javascript 文件:

  1. 定义全局变量的“经典”类型的文件。这是一种你可以只<script src>就可以工作的文件。如果你能得到它,但不总是可用的,那就太好了
  2. 一个 ES 模块(可能依赖也可能不依赖于其他文件,我们将讨论这个问题)
  3. 一个“Common JS”模块。这是为 Node 准备的,如果不使用构建系统,您根本无法在浏览器中使用它。

我不确定“经典”类型是否有更好的名字,但我就称之为“经典”。还有一种类型叫做“AMD”,但我不确定它在 2024 年有多相关。

现在我们知道了 3 种类型的文件,让我们来谈谈如何弄清楚库实际提供了哪些文件!

在哪里可以找到文件:NPM 构建

每个 Javascript 库都有一个构建,它会上传到 NPM。你可能会想(就像我最初做的那样)-Julia!关键是我们没有使用 Node 来构建我们的库!我们为什么要谈论 NPM?

但是如果你使用的是来自 CDN 的链接,比如cdnjs.cloudflare.com/ajax/libs/C…,你仍然在使用 NPM 构建!CDN 上的所有文件最初都来自 NPM。

正因为如此,我有时喜欢npm install库,即使我根本不打算使用节点来构建我的库——我会创建一个新的临时文件夹,在那里npm install,然后在完成后删除它。我喜欢能够在我的文件系统上的 NPM 构建中查看文件,因为这样我就可以 100%确定我看到了库在其构建中提供的所有内容,并且 CDN 没有对我隐瞒什么。

因此,让我们npm install一些库,并尝试找出它们在构建中提供的 Javascript 文件类型!

示例库 1:chart. js

首先让我们看看Chart. js,一个绘图库。

$ cd /tmp/whatever
$ npm install chart.js
$ cd node_modules/chart.js/dist
$ ls *.*js
chart.cjs  chart.js  chart.umd.js  helpers.cjs  helpers.js

这个库似乎有 3 个基本选项:

选项 1: chart.cjs.cjs后缀告诉我这是一个Common JS 文件,用于 Node。这意味着如果没有某种构建步骤,就不可能直接在浏览器中使用它。

选项 2:chart.js.js后缀本身并不能告诉我们它是什么样的文件,但是如果我打开它,我会看到import '@kurkle/color';这是一个 ES 模块的直接标志-import ...语法是 ES 模块语法。

选项 3:chart.umd.js。“UMD”代表“通用模块定义”,我认为这意味着您可以将此文件与基本的<script src>、Common JS 或我不理解的第三种称为 AMD 的东西一起使用。

如何使用 UMD 文件

当我使用 Chart. js 时,我选择了选项 3。我只需要将其添加到我的代码中:

<script src='./chart.umd.js'> </script>

然后我可以将库与全局Chart环境变量一起使用。再简单不过了。我只是将chart.umd.js复制到我的 Git 存储库中,这样我就不必担心使用 NPM 或 CDN 会出现故障或其他任何事情。

构建文件并不总是在dist目录中

许多库会将其构建放在dist目录中,但并非总是如此!构建文件的位置在库的package.json中指定。

例如,这是 Chart. js 的package.json的摘录。

"jsdelivr": "./dist/chart.umd.js",
  "unpkg": "./dist/chart.umd.js",
  "main": "./dist/chart.cjs",
  "module": "./dist/chart.js",

我认为这是说如果你想使用 ES 模块(module),你应该使用dist/chart.js,但是 jsDelivr 和 unpkg CDN 应该使用./dist/chart.umd.js。我想main是用于 Node 的。

chart.jspackage.json还说"type": "module"根据这个留档,它告诉节点默认将文件视为 ES 模块。我认为它没有具体告诉我们哪些文件是 ES 模块,哪些不是,但它确实告诉我们有些东西是 ES 模块。

示例库 2:@atcute/oauth-browser-client

@atcute/oauth-browser-client是一个用于在浏览器中使用 OAuth 登录 Bluesky 的库。

让我们看看它在构建中提供了哪些类型的 Javascript 文件!

$ npm install @atcute/oauth-browser-client
$ cd node_modules/@atcute/oauth-browser-client/dist
$ ls *js
constants.js  dpop.js  environment.js  errors.js  index.js  resolvers.js

似乎这里唯一合理的根文件是index.js,它看起来像这样:

export { configureOAuth } from './environment.js';
export * from './errors.js';
export * from './resolvers.js';

这种export语法意味着它是一个ES 模块。这意味着我们可以在浏览器中使用它,而无需构建步骤!让我们看看如何做到这一点。

如何将 ES 模块与importmaps一起使用

使用 ES 模块并不像添加<script src="whatever.js">那样简单。相反,如果 ES 模块有依赖项(如@atcute/oauth-browser-client),步骤是:

  1. 用超文本标记语言设置导入映射
  2. 将导入语句,如import { configureOAuth } from '@atcute/oauth-browser-client';放在您的 JS 代码中
  3. 在超文本标记语言中包含 JS 代码,如下所示:<script type="module" src="YOURSCRIPT.js"></script>

我们需要一个导入映射,而不是像import { BrowserOAuthClient } from "./oauth-client-browser.js"这样做的原因是模块内部有更多的导入语句,比如import {something} from @atcute/client,我们需要告诉浏览器从哪里获取@atcute/client及其所有其他依赖项的代码。

这是我在@atcute/oauth-browser-client中使用的importmaps:

<script type="importmap">
{
  "imports": {
    "nanoid": "./node_modules/nanoid/bin/dist/index.js",
    "nanoid/non-secure": "./node_modules/nanoid/non-secure/index.js",
    "nanoid/url-alphabet": "./node_modules/nanoid/url-alphabet/dist/index.js",
    "@atcute/oauth-browser-client": "./node_modules/@atcute/oauth-browser-client/dist/index.js",
    "@atcute/client": "./node_modules/@atcute/client/dist/index.js",
    "@atcute/client/utils/did": "./node_modules/@atcute/client/dist/utils/did.js"
  }
}
</script>

让这些导入映射工作是相当繁琐的,我觉得一定有一个工具来自动生成它们,但我还没有找到一个。绝对可以编写一个脚本,使用esbuild 的元文件自动生成导入映射,但我还没有这样做,也许有更好的方法。

我昨天决定设置importmap来让github.com/jvns/bsky-o…工作,所以在那个存储库中有一些示例代码。

还有人给我指了指 Simon Willison 的download-esm,它将下载一个 ES 模块,并将导入重写为直接指向 JS 文件,这样你就不需要importmaps了。我还没有尝试过,但这似乎是个好主意。

importmaps的问题:文件太多

不过,在浏览器中使用importmaps确实遇到了一些问题——它需要下载几十个 Javascript 文件来加载我的网站,而我正在开发的网络服务器由于某种原因跟不上。我一直看到文件无法随机加载,然后不得不重新加载页面,希望这次能成功。

当我将我的站点部署到生产环境时,这不再是一个问题,所以我想这是我的本地开发环境的问题。

还有一个关于 ES 模块的有点恼人的事情是,你需要运行一个网络服务器才能使用它们,我相信这是有充分理由的,但是当你可以在不启动网络服务器的情况下打开你的index.html文件时,它会更容易。

因为“文件太多”的事情,我认为以这种方式实际使用带有importmaps的 ES 模块对我来说并不那么有吸引力,但很高兴知道这是可能的。

如何在没有importmaps的情况下使用 ES 模块

如果 ES 模块没有依赖项,那么就更容易了——您不需要导入映射!您可以:

  • <script type="module" src="YOURCODE.js"></script>放在超文本标记语言中。type="module"很重要。
  • 在 YOURCODE. jsimport {whatever} from "https://example.com/whatever.js"YOURCODE.js

替代方案:使用 esbuild

如果你不想使用importmaps,你也可以使用像esbuild这样的构建系统。我在一些关于使用 esbuild 的注意事项中谈到了如何做到这一点,但是这篇博客文章是关于完全避免构建系统的方法,所以我不会在这里谈论这个选项。不过,我仍然喜欢 esbuild,我认为在这种情况下这是一个不错的选择。

浏览器对importmaps的支持是什么?

CanIuse说importmaps在“2023 年基线:主要浏览器中的新版本”中,所以我的感觉是,在 2024 年,这可能还是有点太新了?我想我会使用importmaps来编写一些有趣的实验代码,我只想让我自己和 12 个人使用,但是如果我想让我的代码更广泛地使用,我会使用esbuild

示例库 3:@atproto/oauth-client-browser

让我们看最后一个示例库!这是一个不同于@atcute/oauth-browser-client的 Bluesky 身份验证库。

$ npm install @atproto/oauth-client-browser
$ cd node_modules/@atproto/oauth-client-browser/dist
$ ls *js
browser-oauth-client.js  browser-oauth-database.js  browser-runtime-implementation.js  errors.js  index.js  indexed-db-store.js  util.js

同样,这里似乎唯一真正的候选文件是index.js。但这与前面的示例库不同!让我们看看index.js

在 index.index.js中有一堆这样的东西:

__exportStar(require('@atproto/oauth-client'), exports);
__exportStar(require('./browser-oauth-client.js'), exports);
__exportStar(require('./errors.js'), exports);
var util_js_1 = require('./util.js');

这个require()语法是公共 JS 语法,这意味着我们根本不能在浏览器中使用这个文件,我们需要使用某种构建步骤,ESBuild 也不会起作用。

同样在这个库的package.json中,它说"type": "commonjs",这是告诉它是公共 JS 的另一种方式。

如何使用一个公共 JS 模块esm.sh

起初我认为不学习构建系统就不可能使用 Common JS 模块,但是后来有个 Bluesky 告诉我esm.sh!这是一个 CDN,可以把任何东西翻译成 ES 模块。skypack.dev做了类似的事情,我不知道有什么区别,但是一个人提到,如果一个不起作用,有时他们会尝试另一个。

对于@atproto/oauth-client-browser使用它似乎很简单,我只需要把这个在我的超文本标记语言:

<script type='module' src='script.js'>
  {' '}
</script>

然后把这个放在script.js

import { BrowserOAuthClient } from 'https://esm.sh/@atproto/oauth-client-browser@0.3.0';

这似乎只是工作,这很酷!当然,这仍然是一种使用构建系统的方式——只是 esm.sh 代替我运行构建。我对这种方法的主要担忧是:

  • 我真的不相信 CDN 会永远工作-通常我喜欢将依赖项复制到我的存储库中,以便它们在未来不会因某种原因消失。
  • 我听说过一些 CDN 存在安全漏洞的问题,这让我很害怕。我也不知道
  • 我真的不明白 esm.sh 在做什么

ESBuild 还可以将 Common JS 模块转换为 ES 模块

我还了解到,你也可以使用esbuild来转换一个 Common JS 模块到一个 ES 模块,虽然有一些限制-import { BrowserOAuthClient } from语法不工作。这里有一个github 问题关于它

我认为esbuild方法可能比esm.sh方法更吸引我,因为它是我电脑上已经有的工具,所以我更信任它。不过我还没有尝试过这么多。

三类文件汇总

以下是您可能遇到的三种类型的 JS 文件的摘要、如何使用它们的选项以及如何识别它们。

无济于事的是,.js.min.js文件扩展名可能是这三个选项中的任何一个,所以如果文件是something.js,你需要做更多的侦探工作来弄清楚你在处理什么。

  1. “经典”JS 文件
    • 如何使用它:<script src="whatever.js"></script>
    • 识别它的方法:
      • 该网站在其设置说明中有一个很大的友好横幅,上面写着“将其与 CDN 一起使用!”什么的
      • 一个.umd.js扩展
      • 试着把它放在<script src=...标签中,看看它是否有效
  2. ES 模块
    • 使用方法:
      • 如果没有依赖项,只需直接在代码中import {whatever} from "./my-module.js"即可
      • 如果有依赖项,请创建一个导入映射并import {whatever} from "my-module"
      • 使用esbuild或任何 ES 模块捆绑器
    • 识别它的方法:
      • 寻找一个importexport语句。(module.exports = ...,这是公共 JS)
      • 一个.mjs扩展名
      • 也许package.json中的"type": "module"(尽管我不清楚这到底指的是哪个文件)
  3. 通用 JS 模块
    • 使用方法:
      • 使用esm.sh将其转换为 ES 模块,如https://esm.sh/@atproto/oauth-client-browser@0.3.0
      • 以某种方式使用构建(??)
    • 识别它的方法:
      • 在代码中查找require()module.exports = ...
      • 一个.cjs扩展名
      • 也许package.json中的"type": "commonjs"(尽管我不清楚这到底指的是哪个文件)

将 ES 模块标准化真的很好

从我的角度来看,Common JS 模块和 ES 模块之间的主要区别在于 ES 模块实际上是一个标准。这让我对使用它们更有信心,因为浏览器承诺永远向后兼容 Web 标准——如果我今天用 ES 模块编写一些代码,我可以肯定它在 15 年后仍然会以同样的方式工作。

这也让我对使用像esbuild这样的工具感觉更好,因为即使 esbuild 项目失败,因为它正在实现一个标准,我觉得将来可能会有另一个类似的工具可以替换它。

JS 社区已经构建了很多非常酷的工具

很多时候,当我谈论这些东西时,我会得到这样的回应:“我讨厌 javascript!!!这是最糟糕的!!!”。但我的经验是,有很多很棒的 Javascript 工具(我昨天才知道esm.sh,这似乎很棒!我喜欢 esbuild!),如果我花时间学习事物是如何工作的,我可以利用其中的一些工具,让我的生活变得轻松得多。

所以这篇文章的目标绝对不是抱怨 Javascript,而是了解情况,这样我就可以以一种让我感觉良好的方式使用工具。

我仍然有问题

这里还有一些问题,如果我知道答案,我会把答案添加到帖子中。

  • 有没有一个工具可以自动为我在本地设置的 ES 模块生成importmaps?(显然是的:jspm
  • 如何在我的计算机上转换一个 Common JS 模块到 ES 模块,esm.sh这样做?(显然 esbuild 可以这样做,尽管命名导出不起作用
  • 当人们通常将 Common JS 模块构建到常规 JS 代码中时,是什么代码在做这件事?显然有像 webpack、rollup、esbuild 等工具,但是这些工具都实现了自己的 JS 解析器/静态分析吗?有多少 JS 解析器?
  • 有没有什么办法捆绑一个 ES 模块到一个单一的文件(如atcute-client.js),但这样在浏览器中,我仍然可以导入多个不同的路径从该文件(如@atcute/client/lexicons@atcute/client)?

所有的工具

以下是我们在这篇文章中讨论的所有工具的列表:

写这篇文章让我想到,即使我通常不想有一个构建,我运行我每次更新项目,我可能愿意有一个构建步骤(使用download-esm或东西),我运行只有一次时,设置项目,永远不会再次运行,除非我更新我的依赖版本。