我喜欢编写 Javascript 不使用构建系统[1] ,昨天我第 N 次遇到需要弄清楚如何在代码中导入 Javascript 库的问题,而且花了很长时间才搞清楚,因为库的安装说明假设你正在使用构建系统。
幸运的是,到目前为止,我已经基本学会了如何应对这种情况,要么成功使用该库,要么决定太难就换一个库,所以这里是我希望多年前就有的导入 Javascript 库的指南。
我只会讨论在前端使用 Javascript 库,并且只讨论在无构建系统的设置中如何使用它们。
在这篇文章中,我将讨论:
- 库可能提供的三种主要 Javascript 文件类型(ES 模块、"经典"全局变量类型和 CommonJS)
- 如何确定库的构建中包含哪些类型的文件
- 在代码中导入每种类型文件的方法
Javascript 文件的三种类型
库可以提供的 3 种基本 Javascript 文件类型:
- 定义全局变量的"经典"类型文件。这是可以直接
<script src>
并且能正常工作的文件类型。如果能获得这种类型就太好了,但并非总是可用 - ES 模块(可能依赖或不依赖其他文件)
- "CommonJS"模块。这是针对 Node 的,没有构建系统的情况下在浏览器中完全无法使用。
我不确定是否有更好的名称来描述"经典"类型,但我就这样称呼它。另外还有一种叫"AMD"的类型,但我不确定在 2024 年它有多相关。
既然我们知道了这 3 种文件类型,让我们来讨论如何确定库实际提供哪种类型的文件!
查找文件:NPM 构建
每个 Javascript 库都有一个上传到 NPM 的构建。你可能会想(就像我最初那样)- Julia!重点是我们不使用 Node 来构建库!我们为什么要讨论 NPM?
但是,如果你使用 CDN 链接,如 https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js[2] ,你仍然在使用 NPM 构建!CDN 上的所有文件最初都来自 NPM。
因此,即使我不打算使用 Node 构建库,我有时也喜欢 npm install
库 - 我会创建一个新的临时文件夹,在那里 npm install
,然后删除它。我喜欢能够在文件系统上查看 NPM 构建中的文件,因为这样我可以 100% 确定我看到了库在其构建中提供的所有内容,而 CDN 没有对我隐藏什么。
所以让我们 npm install
几个库,并尝试弄清楚它们的构建中提供哪些类型的 Javascript 文件!
示例库 1:chart.js
首先让我们看看 Chart.js[3] ,一个绘图库。
$ 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
后缀告诉我这是一个 CommonJS 文件,用于在 Node 中使用。这意味着在没有构建步骤的情况下,在浏览器中直接使用是不可能的。
选项 2:chart.js
。单独的 .js
后缀并不能告诉我这是什么类型的文件,但如果我打开它,会看到 import '@kurkle/color';
,这立即表明这是一个 ES 模块 - import ...
语法是 ES 模块语法。
选项 3:chart.umd.js
。"UMD"代表"Universal Module Definition",我认为这意味着你可以使用这个文件,无论是基本的 <script src>
、CommonJS 还是我不太理解的第三种叫 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"
,根据 这个文档[4] ,这告诉 Node 默认将文件视为 ES 模块。我认为它并不具体告诉我们哪些文件是 ES 模块,哪些不是,但它确实告诉我们其中有 某些 是 ES 模块。
示例库 2:@atcute/oauth-browser-client
@atcute/oauth-browser-client
[5] 是一个用于在浏览器中使用 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 模块。这意味着我们可以在浏览器中不需要构建步骤就能使用它!让我们看看如何做到这一点。
如何使用 importmaps 的 ES 模块
使用 ES 模块并不像简单地添加 <script lay-src="whatever.js">
那么容易。相反,如果 ES 模块有依赖项(比如 @atcute/oauth-browser-client
),步骤是:
- 在 HTML 中设置导入映射
- 在 JS 代码中添加导入语句,如
import { configureOAuth } from '@atcute/oauth-browser-client';
- 像这样在 HTML 中包含你的 JS 代码:
<script type="module" lay-src="YOURSCRIPT.js"></script>
我们需要导入映射而不是直接执行类似 import { BrowserOAuthClient } from "./oauth-client-browser.js"
的操作,是因为模块内部有更多的导入语句,如 import {something} from @atcute/client
,我们需要告诉浏览器在哪里获取 @atcute/client
及其所有其他依赖项的代码。
下面是我为 @atcute/oauth-browser-client
使用的导入映射:
<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 的元文件[6] 编写一个脚本自动生成导入映射是可能的,但我还没有这样做,也许还有更好的方法。
我昨天设置了导入映射以使 github.com/jvns/bsky-oauth-example[7] 能够工作,所以那个仓库中有一些示例代码。
另外,有人向我推荐了 Simon Willison 的 download-esm[8] ,它会下载一个 ES 模块并重写导入以直接指向 JS 文件,这样就不需要导入映射了。我还没有尝试,但这看起来是个很棒的想法。
导入映射的问题:文件太多
不过,我确实遇到了在浏览器中使用导入映射的一些问题 - 它需要下载几十个 Javascript 文件来加载我的网站,出于某些原因,我的开发 Web 服务器无法跟上。我不断看到文件加载失败,然后不得不重新加载页面并希望它们能成功。
当我将网站部署到生产环境时,这不再是个问题,所以我猜这是我本地开发环境的问题。
另外,关于 ES 模块的一个稍微烦人的事情是,你需要运行一个 Web 服务器才能使用它们,我相信这是出于某个好的理由,但当你可以直接打开 index.html
文件而不需要启动 Web 服务器时会更容易。
由于"文件太多"的问题,我认为以这种方式使用带有importmaps的ES模块实际上并不太吸引我,但知道这是可能的还是很好的。
如何在不使用importmaps的情况下使用ES模块
如果ES模块没有依赖项,那就更容易了 - 你根本不需要importmaps!你可以:
- 在HTML中放置
<script type="module" lay-src="YOURCODE.js"></script>
。type="module"
很重要。 - 在
YOURCODE.js
中放置import {whatever} from "https://example.com/whatever.js"
替代方案:使用esbuild
如果你不想使用importmaps,你也可以使用像 esbuild[9] 这样的构建系统。我在 关于使用esbuild的一些笔记[10] 中讨论过如何做到这一点,但这篇博文是关于完全避免构建系统的方法,所以我不打算在这里讨论那个选项。不过我仍然很喜欢esbuild,并且认为在这种情况下它是一个不错的选择。
importmaps的浏览器支持情况
CanIUse[11] 显示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.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()
语法是CommonJS语法,这意味着我们根本无法在浏览器中使用这个文件,我们需要使用某种构建步骤,而ESBuild也无法工作。
另外,在这个库的 package.json
中写着 "type": "commonjs"
,这是另一种表明它是CommonJS的方式。
如何使用 esm.sh[12] 处理CommonJS模块
最初我认为在不学习构建系统的情况下使用CommonJS模块是不可能的,但后来有人在Bluesky上告诉我关于 esm.sh[11] !这是一个CDN,可以将任何内容转换为ES模块。 skypack.dev[13] 做类似的事情,我不确定它们有什么区别,但有人提到如果一个不起作用,他们有时会尝试另一个。
对于 @atproto/oauth-client-browser
,使用看起来很简单,我只需要在HTML中放置:
<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也可以将CommonJS模块转换为ES模块
我还了解到,你也可以使用 esbuild
将CommonJS模块转换为ES模块,尽管有一些限制 - import { BrowserOAuthClient } from
语法不起作用。这里有一个关于此的 github issue[14] 。
我认为 esbuild
方法可能比 esm.sh
方法更吸引我,因为它是我已经在我的计算机上的工具,所以我更信任它。不过我还没有太多地尝试过这个。
三种文件类型的总结
这里是你可能遇到的三种JS文件类型的总结,以及使用它们的选项和识别方法。
不幸的是,.js
或 .min.js
文件扩展名可能是这3个选项中的任何一个,所以如果文件是 something.js
,你需要做更多侦探工作来弄清楚你正在处理什么。
"经典"JS文件
- 如何使用::
<script lay-src="whatever.js"></script>
- 识别方式:
- 网站在设置说明中有一个友好的横幅说"使用CDN!"之类的
.umd.js
扩展名- 尝试将其放入
<script src=...
标签中看是否有效
- 如何使用::
ES模块
使用方式:
- 如果没有依赖项,直接在代码中
import {whatever} from "./my-module.js"
- 如果有依赖项,创建导入映射并
import {whatever} from "my-module"
- 或使用 download-esm[7] 以消除对导入映射的需求
- 使用 esbuild[8] 或任何ES模块打包器
- 如果没有依赖项,直接在代码中
识别方式:
- 查找
import
或export
语句(不是module.exports = ...
,那是CommonJS) .mjs
扩展名- 可能是
package.json
中的"type": "module"
(尽管不太清楚具体指的是哪个文件)
- 查找
CommonJS模块
使用方式:
- 使用 https://esm.sh[15] 将其转换为ES模块,如
https://esm.sh/@atproto/oauth-client-browser@0.3.0
- 以某种方式构建(??)
- 使用 https://esm.sh[15] 将其转换为ES模块,如
识别方式:
- 查找代码中的
require()
或module.exports = ...
.cjs
扩展名- 可能是
package.json
中的"type": "commonjs"
(尽管不太清楚具体指的是哪个文件)
- 查找代码中的
拥有标准化的ES模块真的很棒
从我的角度来看,CommonJS模块和ES模块的主要区别是ES模块是一个真正的标准。这让我在使用它们时感到更有信心,因为浏览器致力于网络标准的向后兼容性 - 如果我今天使用ES模块编写一些代码,我可以确定它在15年后仍将以相同的方式工作。
这也让我对使用像 esbuild
这样的工具感到更好,因为即使esbuild项目消亡,由于它实现了一个标准,似乎未来很可能会有另一个类似的工具可以替代它。
很多时候当我谈论这些东西时,我会得到这样的回复:"我讨厌JavaScript!!!它是最糟糕的!!!"。但我的经验是,JavaScript有很多很棒的工具(我昨天刚刚了解到 https://esm.sh[11] ,看起来很棒!我喜欢esbuild!),如果我花时间了解事物的工作原理,我就可以利用这些工具,让生活变得更轻松。
所以这篇文章的目标绝对不是抱怨JavaScript,而是为了理解生态系统,以一种对我来说感觉良好的方式使用工具。
我仍然有的问题
以下是我仍然有的问题,如果我学到答案,我会将其添加到文章中。
- 是否有一个工具可以为我本地设置的ES模块自动生成导入映射?(显然是有的: jspm[16] )
- 如何在我的电脑上将CommonJS模块转换为ES模块,就像 https://esm.sh[11] 那样?(显然esbuild可以做到这一点,尽管 命名导出不起作用[13] )
- 当人们通常将CommonJS模块构建为常规JS代码时,代码在做什么?显然有像webpack、rollup、esbuild等工具,但这些工具是否都实现了自己的JS解析器/静态分析?有多少JS解析器?
- 是否有办法将ES模块打包成单个文件(如
atcute-client.js
),但在浏览器中我仍然可以从该文件导入多个不同的路径(如@atcute/client/lexicons
和@atcute/client
)?
所有工具
以下是我们在这篇文章中讨论的每个工具的列表:
- Simon Willison的 download-esm[7] ,它可以下载ES模块并将导入转换为指向JS文件,这样你就不需要importmap
- https://esm.sh/[17] 和 skypack.dev[12]
- esbuild[8]
- JSPM[15] 可以生成importmaps
写这篇文章让我思考,尽管我通常不想在每次更新项目时都运行构建,但我可能愿意有一个构建步骤(使用download-esm
或类似工具),我只在设置项目时运行一次,之后除非更新依赖版本,否则不再运行。
就是这样!
感谢 Marco Rogers[18] ,他教我了这篇文章中的很多内容。我可能在这篇文章中犯了一些错误,如果你知道它们是什么,请在Bluesky或Mastodon上告诉我!
参考链接
- 不使用构建系统: https://jvns.ca/blog/2023/02/16/writing-javascript-without-a-build-system/
- https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js
- Chart.js: https://www.chartjs.org/
- 这个文档: https://nodejs.org/api/packages.html#modules-packages
@atcute/oauth-browser-client
: https://github.com/mary-ext/atcute/tree/trunk/packages/oauth/browser-client- esbuild 的元文件: https://esbuild.github.io/api/#metafile
- github.com/jvns/bsky-oauth-example: https://github.com/jvns/bsky-oauth-example
- download-esm: https://simonwillison.net/2023/May/2/download-esm/
- esbuild: https://esbuild.github.io/
- 关于使用esbuild的一些笔记: https://jvns.ca/blog/2021/11/15/esbuild-vue/
- CanIUse: https://caniuse.com/import-maps
- esm.sh: https://esm.sh/
- skypack.dev: https://www.skypack.dev/
- github issue: https://github.com/evanw/esbuild/issues/442
- https://esm.sh: https://esm.sh/#docs
- jspm: https://jspm.org/getting-started
- https://esm.sh/: https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/esm.sh
- Marco Rogers: https://polotek.net/