深入Vite:再谈ESM的高阶特性

news2024/12/24 9:30:39

谈到前端模块化的发展历史,就一定不会漏掉ESM,除此之外,还有大家熟知的CommonJS、AMD、CMD以及ES6等。目前, ESM 已经逐步得到各大浏览器厂商以及 Node.js 的原生支持,正在成为主流前端模块化方案。

而 Vite 本身就是借助浏览器原生的 ESM 解析能力(type=“module”)实现了开发阶段的 no-bundle,即不用打包也可以构建 Web 应用。不过我们对于原生 ESM 的理解仅仅停留在 type="module"这个特性上面未免有些狭隘了,一方面浏览器和 Node.js 各自提供了不同的 ESM 使用特性,如 import maps、package.json 的 imports和exports 属性等等,另一方面前端社区开始逐渐向 ESM 过渡,有的包甚至仅留下 ESM 产物,Pure ESM 的概念随之席卷前端圈,而与此同时,基于 ESM 的 CDN 基础设施也如雨后春笋般不断涌现,诸如esm.sh、skypack、jspm等等。

因此你可以看到,ESM 已经不仅仅局限于一个模块规范的概念,它代表了前端社区生态的走向以及各项前端基础设施的未来,不管是浏览器、Node.js 还是 npm 上第三方包生态的发展,无一不在印证这一点。

接下来,我们一起来看一下浏览器和 Node.js 中基于 ESM 实现的一些高级特性,然后分析一下Pure ESM 模式,以及这种模式下存在哪些痛点,以及如何解决这些痛点。

一、高阶特性

1.1 import map

在浏览器中我们可以使用包含type="module"属性的script标签来加载 ES 模块,而模块路径主要包含三种:

  • 绝对路径,如 https://cdn.skypack.dev/react
  • 相对路径,如./module-a
  • bare import即直接写一个第三方包名,如react、lodash

对于前两种模块路径浏览器是原生支持的,而对于 bare import,在 Node.js 能直接执行,因为 Node.js 的路径解析算法会从项目的 node_modules 找到第三方包的模块路径,但是放在浏览器中无法直接执行。而这种写法在日常开发的过程又极为常见,除了将 bare import 手动替换为一个绝对路径,还有其它的解决方案吗?

答案是有的。现代浏览器内置的 import map 就是为了解决上述的问题,我们可以用一个简单的例子来使用这个特性:

<!DOCTYPE html>
<html lang="en">


<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>


<body>
  <div id="root"></div>
  <script type="importmap">
  {
    "imports": {
      "react": "https://cdn.skypack.dev/react"
    }
  }
  </script>


  <script type="module">
    import React from 'react';
    console.log(React)
  </script>
</body>


</html>

在浏览器中执行这个 HTML,如果正常执行,那么你可以看到浏览器已经从网络中获取了 React 的内容,如下图所示:

image.png

在支持 import map的浏览器中,在遇到type="importmap"的 script 标签时,浏览器会记录下第三方包的路径映射表,在遇到bare import时会根据这张表拉取远程的依赖代码。如上述的例子中,我们使用 skypack这个第三方的 ESM CDN 服务,通过https://cdn.skypack.dev/react这个地址我们可以拿到 React 的 ESM 格式产物。

import map特性虽然简洁方便,但浏览器的兼容性却是个大问题,在 CanIUse 上的兼容性数据如下:

image.png

它只能兼容市面上 68% 左右的浏览器份额,而反观type="module"的兼容性(兼容 95% 以上的浏览器),import map的兼容性实属不太乐观。但幸运的是,社区已经有了对应的 Polyfill 解决方案——es-module-shims,完整地实现了包含 import map在内的各大 ESM 特性,还包括:

  • dynamic import。即动态导入,部分老版本的 Firefox 和 Edge 不支持。
  • import.meta和import.meta.url。当前模块的元信息,类似 Node.js 中的 __dirname、__filename。
  • modulepreload。以前我们会在 link 标签中加上 rel=“preload” 来进行资源预加载,即在浏览器解析 HTML 之前就开始加载资源,现在对于 ESM 也有对应的modulepreload来支持这个行为。
  • JSON Modules和 CSS Modules,即通过如下方式来引入json或者css。
<script type="module">
// 获取 json 对象
import json from 'https://site.com/data.json' assert { type: 'json' };
// 获取 CSS Modules 对象
import sheet from 'https://site.com/sheet.css' assert { type: 'css' };
</script>

值得一提的是,es-module-shims 基于 wasm 实现,性能并不差,相比浏览器原生的行为没有明显的性能下降。大家可以去这个地址查看具体的 benchmark 结果。

由此可见,import map虽然并没有得到广泛浏览器的原生支持,但是我们仍然可以通过 Polyfill 的方式在支持 type=“module” 的浏览器中使用 import map。

1.2 Nodejs 包导入导出策略

在 Node.js 中(>=12.20 版本)有一般如下几种方式可以使用原生 ES Module:

  • 文件以 .mjs 结尾;
  • package.json 中声明type: “module”。

那么,Node.js在处理 ES Module 导入导出的时候,如果是处理 npm 包级别的情况,其中的细节可能比你想象中更加复杂。

首先来看看如何导出一个包,你有两种方式可以选择: main和 exports属性。这两个属性均来自于package.json,并且根据 Node 官方的 resolve 算法(查看详情),exports 的优先级比 main 更高,也就是说如果你同时设置了这两个属性,那么 exports会优先生效。并且,main 的使用比较简单,设置包的入口文件路径即可。

"main": "./dist/index.js"

需要重点梳理的是exports属性,它包含了多种导出形式: 默认导出、子路径导出和条件导出,这些导出形式如以下的代码所示。

// package.json
{
  "name": "package-a",
  "type": "module",
  "exports": {
    // 默认导出,使用方式: import a from 'package-a'
    ".": "./dist/index.js",
    // 子路径导出,使用方式: import d from 'package-a/dist'
    "./dist": "./dist/index.js",
    "./dist/*": "./dist/*", // 这里可以使用 `*` 导出目录下所有的文件
    // 条件导出,区分 ESM 和 CommonJS 引入的情况
    "./main": {
      "import": "./main.js",
      "require": "./main.cjs"
    },
  }
}

其中,条件导出可以包括如下常见的属性:

  • node: 在 Node.js 环境下适用,可以定义为嵌套条件导出,如
{
  "exports": {
    {
      ".": {
       "node": {
         "import": "./main.js",
         "require": "./main.cjs"
        }     
      }
    }
  },
}
  • import: 用于 import 方式导入的情况,如import(“package-a”);
  • require: 用于 require 方式导入的情况,如require(“package-a”);
  • default,兜底方案,如果前面的条件都没命中,则使用 default 导出的路径。

当然,条件导出还包含 types、browser、develoment、production 等属性。在介绍完"导出"之后,我们再来看看"导入",也就是 package.json 中的 imports字段,一般是这样声明的:

{
  "imports": {
    // key 一般以 # 开头
    // 也可以直接赋值为一个字符串: "#dep": "lodash-es"
    "#dep": {
      "node": "lodash-es",
      "default": "./dep-polyfill.js"
    },
  },
  "dependencies": {
    "lodash-es": "^4.17.21"
  }
}

然后,可以在自己的包中使用 import 语句进行导入:

// index.js
import { cloneDeep } from "#dep";


const obj = { a: 1 };


// { a: 1 }
console.log(cloneDeep(obj));

Node.js 在执行的时候会将#dep定位到lodash-es这个第三方包,当然,你也可以将其定位到某个内部文件。这样相当于实现了路径别名的功能,不过与构建工具中的 alias 功能不同的是,“imports” 中声明的别名必须全量匹配,否则 Node.js 会直接抛错。

二、Pure ESM

什么是 Pure ESM ? Pure ESM 最初是在 Github 上的一个帖子中被提出来的,其中有两层含义,一个是让 npm 包都提供 ESM 格式的产物,另一个是仅留下 ESM 产物,抛弃 CommonJS 等其它格式产物。

当这个概念被提出来之后社区当中出现了很多不同的声音,有人赞成,也有人不满。但不管怎么样,社区中的很多 npm 包已经出现了 ESM First 的趋势,可以预见的是越来越多的包会提供 ESM 的版本,来拥抱社区 ESM 大一统的趋势,同时也有一部分的 npm 包做得更加激进,直接采取Pure ESM模式,如大名鼎鼎的chalk和imagemin,最新版本中只提供 ESM 产物,而不再提供 CommonJS 产物。

不过,对于没有上层封装需求的大型框架,如 Nuxt、Umi,在保证能上 Pure ESM的情况下,直接上不会有什么问题;但如果是一个底层基础库,最好提供好 ESM 和 CommonJS 两种格式的产物。

接下来,我们看一下如何使用 ESM ,我们可以直接导入 CommonJS 模块,如:

// react 仅有 CommonJS 产物
import React from 'react';
console.log(React)

Node.js 执行以上的原生 ESM 代码并没有问题,但反过来,如果你想在 CommonJS 中 require 一个 ES 模块,就行不通了。
 
image.png

其根本原因在于 require 是同步加载的,而 ES 模块本身具有异步加载的特性,因此两者天然互斥,即我们无法 require 一个 ES 模块。

那是不是在 CommonJS 中无法引入 ES 模块了呢? 也不尽然,我们可以通过 dynamic import来引入:

image.png

不知道你注意到没有,为了引入一个 ES 模块,我们必须要将原来同步的执行环境改为异步的,这就带来如下的几个问题:

  • 如果执行环境不支持异步,CommonJS 将无法导入 ES 模块;
  • jest 中不支持导入 ES 模块,测试会比较困难;
  • 在 tsc 中,对于 await import()语法会强制编译成 require的语法(详情),只能靠eval(‘await import()’)绕过去。

总而言之,CommonJS 中导入 ES 模块比较困难。因此,如果一个基础底层库使用 Pure ESM,那么你依赖这个库时产物最好为 ESM 格式。也就是说,Pure ESM是具有传染性的,底层的库出现了 Pure ESM 产物,那么上层的使用方也最好是 Pure ESM,否则会有上述的种种限制。

但从另一个角度来看,对于大型框架(如 Nuxt)而言,基本没有二次封装的需求,框架本身如果能够使用 Pure ESM ,那么也能带动社区更多的包(比如框架插件)走向 Pure ESM,同时也没有上游调用方的限制,反而对社区 ESM 规范的推动是一件好事情。

不过,npm 上大部分的包还是属于基础库的范畴,那对于大部分包,我们采用导出 ESM/CommonJS 两种产物的方案,会不会对项目的语法产生限制呢?

我们知道,在 ESM 中无法使用 CommonJS 中的 __dirname、__filename、require.resolve 等全局变量和方法,同样的,在 CommonJS 中我们也没办法使用 ESM 专有的 import.meta对象,那么如果要提供两种产物格式,这些模块规范相关的语法怎么处理呢?

在传统的编译构建工具中,我们很难逃开这个问题,但新一代的基础库打包器tsup给了我们解决方案。

三、新一代的基础库打包器

tsup 是一个基于 Esbuild 的基础库打包器,主打无配置(no config)打包。借助它我们可以轻易地打出 ESM 和 CommonJS 双格式的产物,并且可以任意使用与模块格式强相关的一些全局变量或者 API,比如某个库的源码如下:

export interface Options {
  data: string;
}


export function init(options: Options) {
  console.log(options);
  console.log(import.meta.url);
}

由于代码中使用了 import.meta 对象,这是仅在 ESM 下存在的变量,而经过 tsup 打包后的 CommonJS 版本却被转换成了下面这样:

var getImportMetaUrl = () =>
  typeof document === "undefined"
    ? new URL("file:" + __filename).href
    : (document.currentScript && document.currentScript.src) ||
      new URL("main.js", document.baseURI).href;
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();


// src/index.ts
function init(options) {
  console.log(options);
  console.log(importMetaUrl);
}

可以看到,ESM 中的 API 被转换为 CommonJS 对应的格式,反之也是同理。最后,我们可以借助之前提到的条件导出,将 ESM、CommonJS 的产物分别进行导出,如下所示。

{
  "scripts": {
    "watch": "npm run build -- --watch src",
    "build": "tsup ./src/index.ts --format cjs,esm --dts --clean"
  },
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      // 导出类型
      "types": "./dist/index.d.ts"
    }
  }
}

tsup 在解决了双格式产物问题的同时,本身利用 Esbuild 进行打包,性能非常强悍,也能生成类型文件,同时也弥补了 Esbuild 没有类型系统的缺点,还是非常推荐大家使用的。

当然,回到 Pure ESM 本身,我觉得这是一个未来可以预见的趋势,但对于基础库来说,现在并不适合切到Pure ESM,如今作为过渡时期,还是发 ESM/CommonJS 双格式的包较为靠谱,而tsup这种工具能降低基础库构建上的成本。当所有的库都有 ESM 产物的时候,我们再来落地 Pure ESM 就轻而易举了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/702169.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python除零错误:原因,解决办法和实践建议

Python 除零错误&#xff1a;原因&#xff0c;解决办法和实践建议 介绍 Python 作为一门广泛使用的高级编程语言&#xff0c;它的强大之处就体现在它的简洁性、可读性和易用性上。但是在实践中&#xff0c;有时候我们会遇到一些让我们不得不头痛的问题&#xff0c;其中之一就…

(0017) H5-vue创建项目vue init webpack

1、初始化项目 1、vue 命令讲解 vue list &#xff1a;查看可以基于那些模板创建vue应用vue init <template-name> <project-name>init&#xff1a;表示要用vue-cli来初始化项目 <template-name>&#xff1a;表示模板名称&#xff0c;vue-cli官方提供的5种…

【LeetCode】218. 天际线问题

218. 天际线问题&#xff08;困难&#xff09; 思路 题意转化 完整思路分析 multiset的使用 multiset 是关联容器的一种&#xff0c;是排序好的集合&#xff08;元素默认升序&#xff09;&#xff0c;并且允许有相同的元素。 不能直接修改 multiset 容器中元素的值。因…

柯美658 558 458 308 554 364等报错故障代码C2152,C2153,C2154,C2155,C2156 C2253.C2254维修

代码基本都是转印带故障代码,转印带有两个传感器是检测转印带工作和没有工作时加压有没有归位的,传感器基本不会坏,更多的情况是因为转印带上的废粉落在了传感器上导致传感器故障,清洁即可。

剑指offer--JZ24 反转链表

反转链表需要三个指针&#xff0c;一个保留前一个结点的指针&#xff0c;一个后移指针&#xff0c;一个当前指针。C语言版本代码如下&#xff1a; #include <stdio.h> #include <stdlib.h>// 单链表节点结构定义 struct ListNode {int val;struct ListNode* next;…

用acsii , unicode,utf-8讨论为什么采用中文编程不行

一、背景介绍 很多刚接触计算机的同学&#xff0c;可能会发出一个疑问&#xff0c;为什么不能直接使用中文编程&#xff1f; 要了解这个问题&#xff0c;还得从计算机的起源说起&#xff01; 在计算机软件里面&#xff0c;一切的信息都可以用 1 和 0 来表示&#xff08;严格…

mongodb节点一直处于recovering状态问题修复

mongoDB版本&#xff1a;5.0.4 该节点mongod服务日志一直在刷如下日志 {"t":{"$date":"2023-06-19T15:24:50.15608:00"},"s":"I", "c":"REPL", "id":5579708, "ctx":"…

云计算的发展趋势及其对企业的影响

第一章&#xff1a;引言 近年来&#xff0c;云计算在IT行业迅猛发展&#xff0c;成为企业提升业务效率和创新能力的重要工具。通过云计算&#xff0c;企业能够将数据和应用程序存储在云端的服务器上&#xff0c;实现灵活的资源调配和高效的数据管理。本文将探讨云计算的发展趋…

Win11无法连接Win7的打印机解决方法

win11无法连接win7的打印机怎么解决&#xff1f;在日常的办公中&#xff0c;局域网可以实现文件管理&#xff0c;打印机共享文件打印等功能&#xff0c;但是如果两个机器系统各不同的话&#xff0c;可能有的就会提示无法连接&#xff0c;下面就把基本简单的共享设置方法分享给大…

uview的折叠面板和u-tabs的扩展

第一个&#xff1a;首先要安装uview UI框架 &#xff08;已发布如何安装&#xff09; 第二个&#xff1a;使用uview 中的折叠面板&#xff08;Collapse 折叠面板 | uView 2.0 - 全面兼容nvue的uni-app生态框架 - uni-app UI框架&#xff09; 第三点&#xff1a;明白一个插槽使用…

linux - spin lock实现分析

linux - spin lock实现分析 spinlock1 spinlock的数据结构2 spinlock的接口2.1 spin_lock_init2.2 spin_lock2.3 spin_unlock2.4 spin_lock_irq2.5 spin_unlock_irq2.6 spin_lock_irqsave2.7 spin_unlock_irqrestore2.8 spin_lock_bh2.9 spin_unlock_bh spinlock 1 spinlock的数…

第四十三章 开发Productions - ObjectScript Productions - 使用记录映射器 - 编辑记录映射字段和组合

文章目录 第四十三章 开发Productions - ObjectScript Productions - 使用记录映射器 - 编辑记录映射字段和组合编辑记录映射字段和组合NameDatatypeAnnotationWidth (fixed-width record maps only)RequiredRepeating (delimited record maps only)IgnoreTrailing Data (fixed…

RabbitMQ快速上手(延迟队列)

安装 官网 参考文章&#xff1a; ​ https://blog.csdn.net/miaoye520/article/details/123207661 ​ https://blog.csdn.net/lvoelife/article/details/126658695 安装Erlang&#xff0c;并添加环境变量ERLANG_HOME&#xff0c;命令行运行erl 安装rabbitmq&#xff0c;rab…

Pastebin设计之旅:从零设计网络文本存储系统

项目简介&#xff1a;Pastebin是一个在线的文本存储平台&#xff0c;让用户可以存储和分享代码片段或者其他类型的文本。它支持多种编程和标记语言的语法高亮&#xff0c;用户可以选择让他们的"paste"公开或私有。无需注册就可以使用&#xff0c;但注册用户可以更方便…

森海塞尔重磅推出TC Bars智能音视频一体机, 为中小型协作空间缔造理想解决方案

森海塞尔重磅推出TC Bars智能音视频一体机&#xff0c; 为中小型协作空间缔造理想解决方案 全球音频行业先驱森海塞尔重磅推出首款内置摄像头的可扩展一体化会议设备 德国韦德马克&#xff0c;2023年6月13日——森海塞尔作为先进音频技术的首选&#xff0c;致力于使协作与学习…

力扣 617. 合并二叉树

题目来源&#xff1a; C题解1&#xff1a;使用队列实现层序遍历。基于root1&#xff0c;遇到可覆盖部分&#xff0c;直接将该节点指向对应节点&#xff0c;遇到重复部分&#xff0c;则修改root1该节点相应的值。 /*** Definition for a binary tree node.* struct TreeNode {*…

超市零售数据可视化分析(Plotly 指南)

CSDN 上不能插入 HTML&#xff0c;可以在 GitHub Page 上查看&#xff1a; https://paradiseeee.github.io/2022/07/30/超市零售数据可视化分析/ 项目首次发布于 Kesci 上 – 超市零售数据分析。感兴趣的可以直接上去 Fork 之后自己做。由于上面只能用 Jupyter Notebook&#x…

多旋翼无人机试验系统设计与实现

摘 要 世界的航空业的大门被20世纪莱特兄弟制造的“飞行者一号”开启&#xff0c;直至今日处于飞速发展的阶段。随着时代的进步&#xff0c;各种微电子、微传感、通信技术的飞速发展&#xff0c;让无人机在时代内成为一种新型的空中力量。除了军用方面的多种用途&#xff0c;无…

《Linux操作系统编程》第九章 数据查找和筛选工具 : 了解流编辑器sed和报表生成器awk的简单使用

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

uni-app滚动分页 兼容(App 小程序 H5)

因为手机端本身屏幕空间不大 所以大家一般都会选择用滚动分页 首先 我在根目录下创建了一个 api目录 下面创建了一个bookApi.js 其中写了一个请求函数 getBookList 根据当前页 page 和 每页展示多少条 pageSize 获取数据 那么 我的组件代码是这样的 <template><scro…