导入前端JavaScript库而不使用构建系统

文摘   2024-11-25 09:08   北京  

导入前端JavaScript库而不使用构建系统

我喜欢在不使用构建系统的情况下编写JavaScript,昨天我又一次遇到了一个问题,需要弄清楚如何在不使用构建系统的情况下在我的代码中导入一个JavaScript库,因为该库的安装说明假设你正在使用构建系统。

幸运的是,到这个时候我已经基本上学会了如何应对这种情况,要么成功使用该库,要么决定它太难了,转而使用另一个库。所以这里有一个我多年前就希望拥有的导入JavaScript库的指南。

我只打算讨论在前端使用JavaScript库,以及如何在不使用构建系统的设置中使用它们。

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

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

三种类型的JavaScript文件

一个库可以提供的JavaScript文件有三种基本类型:

  1. 定义全局变量的“经典”类型文件。这种类型的文件你可以直接使用<script src>,它就会正常工作。如果你能得到它当然很好,但并不总是可用。
  2. 一个ES模块(可能依赖于其他文件,我们稍后会讨论)
  3. 一个“CommonJS”模块。这是给Node用的,如果不使用构建系统,你根本无法在浏览器中使用它。

我不确定是否有更好的名字来称呼“经典”类型,但我只是称它为“经典”。还有一种叫做“AMD”的类型,但我不确定它在2024年的相关性如何。

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

在哪里找到文件:NPM构建

每个JavaScript库都有一个构建,它上传到NPM。你可能会想(就像我最初想的那样)——朱莉娅!我们不使用Node来构建我们的库的重点是什么!我们为什么要谈论NPM?

但如果你使用像https://cdnjs.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js这样的CDN链接,你仍然在使用NPM构建!所有CDN上的文件最初都来自NPM。

因此,有时我喜欢即使不打算使用Node来构建我的库,也会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后缀告诉我这是一个CommonJS文件,用于在Node中使用。这意味着如果不进行某种构建步骤,你根本无法在浏览器中直接使用它。

选项2:chart.js.js后缀本身并不能告诉我们这是什么类型的文件,但如果我打开它,我看到import '@kurkle/color';这是一个立即的迹象,表明这是一个ES模块——import ...语法是ES模块语法。

选项3:chart.umd.js。“UMD”代表“通用模块定义”,我认为这意味着你可以使用这个文件,无论是使用基本的<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",根据这篇文档,这告诉Node默认将文件视为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模块。这意味着我们可以在不使用构建步骤的情况下在浏览器中使用它!让我们看看如何做到这一点。

如何使用带有importmaps的ES模块

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

  1. 在你的HTML中设置一个importmap
  2. 在你的JS代码中放入import语句,如import { configureOAuth } from '@atcute/oauth-browser-client';
  3. 像这样在你的HTML中包含你的JS代码:<script type="module" lay-src="YOURSCRIPT.js"></script>

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

这是我为@atcute/oauth-browser-client使用的importmap的样子:

<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>

让这些importmaps工作相当繁琐,我觉得肯定有一个工具可以自动生成它们,但我还没有找到一个。完全可以编写一个脚本来自动生成importmaps,使用esbuild的metafile,但我还没有这样做,也许有更好的方法。

我昨天决定设置importmaps,以使github.com/jvns/bsky-oauth-example工作,所以那个仓库里有一些示例代码。

还有人向我推荐了Simon Willison的download-esm,它将下载一个ES模块并重写import语句,直接指向JS文件,这样你就不需要importmaps了。我还没有尝试过,但它看起来像个好主意。

importmaps的问题:文件太多

我在使用importmaps时确实遇到了一些问题——它需要下载几十个JavaScript文件来加载我的网站,出于某种原因,我的开发环境中的web服务器跟不上。我一直看到文件随机失败加载,然后不得不重新加载页面,希望这次它们能成功。

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

另外,关于ES模块的一点稍微烦人的事情是,你需要运行一个web服务器来使用它们,我相信这是有一个很好的理由的,但是当你可以不启动web服务器就打开你的index.html文件时,它更容易。

因为“文件太多”的事情,我认为实际上以这种方式使用带有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这样的构建系统。我在《关于使用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.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使用CommonJS模块

最初我以为不学习构建系统就不可能使用CommonJS模块,但后来有人告诉我esm.sh!这是一个CDN,可以将任何东西转换成ES模块。skypack.dev也做类似的事情,我不确定它们之间的区别是什么,但有人提到如果一个不起作用,有时他们会尝试另一个。

对于@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。

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

三种类型的文件总结

这里有一个你可能遇到的三种JS文件的总结,使用它们的选项,以及如何识别它们。

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

  1. “经典”JS文件
    • 网站在其安装说明中有一个大的友好横幅,上面写着“使用这个和CDN!”或类似的东西
    • 一个.umd.js扩展名
    • 尝试把它放在<script src=...标签中,看看它是否工作
    • 如何使用它:<script lay-src="whatever.js"></script>
    • 识别它的方法:
  2. ES模块
    • 查看importexport语句。(不是module.exports = ...,那是CommonJS)
    • 一个.mjs扩展名
    • 也许package.json中的"type": "module"(尽管我不清楚这到底指的是哪个文件)
    • 如果没有依赖项,直接在代码中import {whatever} from "./my-module.js"
    • 如果有依赖项,创建一个importmap并import {whatever} from "my-module"
    • 使用esbuild或任何ES模块打包器
    • 或者使用download-esm来消除对importmap的需求
    • 使用它的方法:
    • 识别它的方法:
  3. CommonJS模块
    • 查看代码中的require()module.exports = ...
    • 一个.cjs扩展名
    • 也许package.json中的"type": "commonjs"(尽管我不清楚这到底指的是哪个文件)
    • 使用https://esm.sh将其转换成ES模块,像https://esm.sh/@atproto/oauth-client-browser@0.3.0
    • 以某种方式使用构建
    • 使用它的方法:
    • 识别它的方法:

拥有标准化的ES模块真的很不错

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

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

很多时候,当我谈论这些事情时,我会得到像“我讨厌JavaScript!!!它是最糟糕的!!!”这样的回应。但我的经验是,有很多很棒的JavaScript工具(我昨天刚刚了解到https://esm.sh,看起来很棒!我爱esbuild!),如果我花时间学习它们的工作原理,我可以利用其中的一些工具,让我的生活变得更轻松。

所以这篇文章的目标绝对不是抱怨JavaScript,而是理解这个领域,以便我可以以一种对我来说感觉良好的方式使用这些工具。

我仍然有的问题

以下是我仍然有的一些问题,如果我找到了答案,我会把它们添加到文章中。

  • 有没有一个工具可以为我在本地设置的ES模块自动生成importmaps?(显然有:jspm)
  • 我如何在我的电脑上将CommonJS模块转换成ES模块,就像https://esm.sh那样?(显然esbuild可以做到这一点,尽管命名导出不起作用)

所有的工具

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

  • Simon Willison的download-esm,它将下载一个ES模块并将import语句转换为指向JS文件,所以你不需要importmap
  • https://esm.sh和skypack.dev
  • esbuild
  • JSPM可以生成importmaps

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

就这样!

感谢Marco Rogers,他教了我这篇文章中的很多东西。我可能在这篇文章中犯了一些错误,如果有人知道,请告诉我——在Bluesky或Mastodon上告诉我!


编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章