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

文摘   2024-11-27 11:21   美国  

我喜欢编写 Javascript  不使用构建系统[1] ,昨天我第 N 次遇到需要弄清楚如何在代码中导入 Javascript 库的问题,而且花了很长时间才搞清楚,因为库的安装说明假设你正在使用构建系统。

幸运的是,到目前为止,我已经基本学会了如何应对这种情况,要么成功使用该库,要么决定太难就换一个库,所以这里是我希望多年前就有的导入 Javascript 库的指南。

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

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

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

Javascript 文件的三种类型

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

  1. 定义全局变量的"经典"类型文件。这是可以直接 <script src> 并且能正常工作的文件类型。如果能获得这种类型就太好了,但并非总是可用
  2. ES 模块(可能依赖或不依赖其他文件)
  3. "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.jspackage.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),步骤是:

  1. 在 HTML 中设置导入映射
  2. 在 JS 代码中添加导入语句,如 import { configureOAuth } from '@atcute/oauth-browser-client';
  3. 像这样在 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,你需要做更多侦探工作来弄清楚你正在处理什么。

  1. "经典"JS文件

    • 如何使用:<script lay-src="whatever.js"></script>
    • 识别方式:
      • 网站在设置说明中有一个友好的横幅说"使用CDN!"之类的
      • .umd.js 扩展名
      • 尝试将其放入 <script src=... 标签中看是否有效
  2. 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"(尽管不太清楚具体指的是哪个文件)
  3. CommonJS模块

    • 使用方式:

      • 使用  https://esm.sh[15]  将其转换为ES模块,如 https://esm.sh/@atproto/oauth-client-browser@0.3.0
      • 以某种方式构建(??)
    • 识别方式:

      • 查找代码中的 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上告诉我!

参考链接

  1. 不使用构建系统: https://jvns.ca/blog/2023/02/16/writing-javascript-without-a-build-system/
  2. https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js
  3. Chart.js: https://www.chartjs.org/
  4. 这个文档: https://nodejs.org/api/packages.html#modules-packages
  5. @atcute/oauth-browser-client: https://github.com/mary-ext/atcute/tree/trunk/packages/oauth/browser-client
  6. esbuild 的元文件: https://esbuild.github.io/api/#metafile
  7. github.com/jvns/bsky-oauth-example: https://github.com/jvns/bsky-oauth-example
  8. download-esm: https://simonwillison.net/2023/May/2/download-esm/
  9. esbuild: https://esbuild.github.io/
  10. 关于使用esbuild的一些笔记: https://jvns.ca/blog/2021/11/15/esbuild-vue/
  11. CanIUse: https://caniuse.com/import-maps
  12. esm.sh: https://esm.sh/
  13. skypack.dev: https://www.skypack.dev/
  14. github issue: https://github.com/evanw/esbuild/issues/442
  15. https://esm.sh: https://esm.sh/#docs
  16. jspm: https://jspm.org/getting-started
  17. https://esm.sh/: https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/esm.sh
  18. Marco Rogers: https://polotek.net/

幻想发生器
图解技术本质
 最新文章