我喜欢在没有构建系统的情况下编写 Javascript,昨天我第一百万次遇到了一个问题,我需要弄清楚如何在不使用构建系统的情况下在代码中导入 Javascript 库,并且花了很长时间才弄清楚如何导入它,因为库的设置说明假设您正在使用构建系统。
幸运的是,在这一点上,我已经学会了如何驾驭这种情况,要么成功使用该库,要么决定它太难并切换到不同的库,所以这里是我希望几年前必须导入 Javascript 库的指南。
我将只讨论在前端使用 Javacript 库,并且只讨论如何在无构建系统设置中使用它们。
在这篇文章中,我将谈论:
- 库可能提供的三种主要 Javascript 文件类型(ES 模块、“经典”全局变量类型和 Common JS)
- 如何弄清楚 Javascript 库在其构建中包含哪些类型的文件
- 在代码中导入每种类型文件的方法
三种 Javascript 文件
库可以提供 3 种基本类型的 Javascript 文件:
- 定义全局变量的“经典”类型的文件。这是一种你可以只
<script src>
就可以工作的文件。如果你能得到它,但不总是可用的,那就太好了 - 一个 ES 模块(可能依赖也可能不依赖于其他文件,我们将讨论这个问题)
- 一个“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.js
的package.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
),步骤是:
- 用超文本标记语言设置导入映射
- 将导入语句,如
import { configureOAuth } from '@atcute/oauth-browser-client';
放在您的 JS 代码中 - 在超文本标记语言中包含 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. js
import {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
,你需要做更多的侦探工作来弄清楚你在处理什么。
- “经典”JS 文件
- 如何使用它::
<script src="whatever.js"></script>
- 识别它的方法:
- 该网站在其设置说明中有一个很大的友好横幅,上面写着“将其与 CDN 一起使用!”什么的
- 一个
.umd.js
扩展 - 试着把它放在
<script src=...
标签中,看看它是否有效
- 如何使用它::
- ES 模块
- 使用方法:
- 如果没有依赖项,只需直接在代码中
import {whatever} from "./my-module.js"
即可 - 如果有依赖项,请创建一个导入映射并
import {whatever} from "my-module"
- 或使用download-esm删除对导入映射的需要
- 使用esbuild或任何 ES 模块捆绑器
- 如果没有依赖项,只需直接在代码中
- 识别它的方法:
- 寻找一个
import
或export
语句。(module.exports = ...
,这是公共 JS) - 一个
.mjs
扩展名 - 也许
package.json
中的"type": "module"
(尽管我不清楚这到底指的是哪个文件)
- 寻找一个
- 使用方法:
- 通用 JS 模块
- 使用方法:
- 使用esm.sh将其转换为 ES 模块,如
https://esm.sh/@atproto/oauth-client-browser@0.3.0
- 以某种方式使用构建(??)
- 使用esm.sh将其转换为 ES 模块,如
- 识别它的方法:
- 在代码中查找
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
)?
所有的工具
以下是我们在这篇文章中讨论的所有工具的列表:
- Simon Willison 的download-esm将下载 ES 模块并将导入转换为指向 JS 文件,因此您不需要导入映射
- esm.sh和skypack.dev
- esbuild
- JSPM可以生成importmaps
写这篇文章让我想到,即使我通常不想有一个构建,我运行我每次更新项目,我可能愿意有一个构建步骤(使用download-esm
或东西),我运行只有一次时,设置项目,永远不会再次运行,除非我更新我的依赖版本。