揭秘 JavaScript 的导入机制
如今,为了成功地使用 JavaScript 或 TypeScript,越来越需要了解正在编写的代码和在浏览器中运行的代码之间发生的一些非常复杂的转换。
当我们谈论便利时,一个有用的经验法则是问:“它是否能直接在浏览器中运行?
下面是你今天可能在代码库中看到的一些导入示例:
import icon from './icon.svg';
import data from './data.json';
import styles from './styles.css';
import foo from '~/foo.js';
import foo from 'bar:foo';
你知道这些是做什么的吗?
从某种意义上说,就它们在原生 Web 平台上运行的能力而言,它们都是“非标准”的(如果在浏览器中运行,这些导入甚至报错)。另一方面,就它们在许多代码库中的流行程度而言,它们也是相当“标准”的。
SVG
import icon from './icon.svg';
- 它是否提供给您一个 SVG 文件的原始内容,以便将其插入到 HTML 中?例如输入
<div>{icon}</div>
, 返回<div><svg ...></svg></div>
。 - 它是否为您提供了在图像中使用的
src
引用?例如输入<img src={icon} />
返回<img src="/path/from/bunlder/icon.svg" />
。(然后打包工具会以某种方式获取 icon.svg 文件,并将其输出为静态文件到指定的路径中,例如在您的 ./build 文件夹中)? - 还是做别的事情?
答案是:谁知道呢。如果你看到该代码,你必须检查你的构建工具(或插件),以了解该代码在运行时将做什么,因为它在浏览器中无法工作。
JSON
import data from './data.json';
如果将此代码放入浏览器环境中,答案是 它会引发错误。
但是,如果您在代码库中看到它,那么了解幕后发生的转换可能非常直观:某些东西正在从磁盘读取文件,期望该文件的内容是JSON,并将其转换为JS对象以用于代码。
这是在Node.js中编写类似内容的快捷方式:
import fs from 'fs';
const data = JSON.parse(
fs.readFileSync("./data.json").toString()
);
您必须了解导入 JSON 文件不是标准的。需要某种打包工具/编译器/工具才能实现这一目标。
JSON 的导入属性正在标准化,并且已经可用于某些环境:
import data from './data.json' with { type: 'json' }
CSS
import styles from './styles.css';
// or sometimes just `import './styles.css'`
- 像 SVG 一样,它是否为您提供了打包器在编译时输出的静态 CSS 文件的路径,然后在链接标签中使用它,例如
<link rel='stylesheet' href={styles}>
? - 它是否读取 CSS 文件的内容并在运行时的某个时刻作为样式标签注入到 DOM 中,例如
<style>{styles}</style>
? - 或者它是否返回一个对象,可在jsx等使用其类名,例如
<div class={styles.foo}>...</div>
?
而且,在任何这些情况下,该 CSS 文件的内容发生了哪些转换?是否经过预处理/后处理?类名是否被随机化以进行自动范围界定?此外,如果样式被注入到 DOM 中,那么样式是如何被移除的呢?它会在某种程度上自动地针对每个页面进行移除吗?或者一旦注入到 DOM 中,这些样式就永远存在于其中吗?
或者,是否发生了我上面没有描述的其他事情?
答案是:谁知道呢。这取决于您使用的框架/打包工具/工具。它在浏览器中不起作用,因此您必须查看文档以获取工具,以了解它如何处理这些类型的导入。
CSS 的导入属性正在标准化,为您提供一个可构造的样式表:
import sheet from './styles.css' with { type: 'css' }
Module Identifiers
这是干嘛的?
import foo from '~/foo.js';
也许你已经这样看过了:
import foo from '@/foo.js';
答案是:谁知道呢。同样,这取决于您的工具,因为这不是标准化的:
模块标识符的含义和结构取决于模块loader或模块打包工具,它们不是 ECMAScript 规范的一部分。
最常见的是,这些标识符允许您为项目指定根目录,这样您不必多次键入 ../
来导航对其他文件的引用(或在移动文件时更改导入),而是可以指定项目的根目录,使导入更像绝对路径。
但是再次强调,这不是标准化的,也不能在浏览器中工作。如果您在代码库中看到它,那么就任何事都有可能发生。您需要深入了解您的特定工具才能知道它的功能。
Prefixes
import foo from 'bar:foo';
模块中的冒号,有什么特别之处吗?
真的没有标准化的含义。然而,Node 采用了这个语法来解决与名称空间核心模块相关的问题:
前缀为主的核心模块在核心模块和用户空间之间提供了明确的界限,减少了添加新核心模块所涉及的许多冲突。
所以你很可能已经看到了这样的代码:
import fs from 'node:fs';
再说一次,真的没有标准,但如果Node.js正在这样做,那么它很快就会成为一种常见的做法。
Bare Module Specifiers, Extension-less Imports, Wildcards, and More
裸模块导入:
import foo from 'package';
除非您具有关联的 import map 设置,否则平台本身不支持这些设置。
因此,如果您看到裸模块导入,可能需要设置一个导入映射,以便可以直接将其放入浏览器中,或者您可能需要设置一个打包工具,这是您需要自己弄清楚的。
无扩展导入:
import Component from './Component';
这些是非标准导入,在浏览器中不起作用。您必须深入研究您的工具以了解那里发生了什么(谁知道它是 JS 还是 TS 文件)。
通配符模块声明:
import foo from "./foo.txt!text";
同样,这是一个非标准导入,其功能因代码库而异。查看您的工具设置以了解更多信息。
结论
我相信还有更多的导入语法我没有涵盖。即使在我介绍的那些语法中,也肯定有更多的功能我没有讲到。
重点是:在许多情况下,仅仅通过代码无法判断实际发生了什么。您必须将视野放到您的工具(每个代码库都不同)的层面上,才能了解底层正在发生哪些转换和构建时间优化。一些导入最终成为运行时导入,一些被编译消除,一些被打包,而一些则...谁知道呢。
这就像古老的巴别塔,语言已经分裂成许多种,并且根据使用的工具,“import”有着不同的意义。这是多么具有讽刺意味的一个名字。
虽然对于许多人来说,Deno 的模块加载方法可能有些冗长,但我认为它遵循了浏览器的语义,这点很好。如果在 Deno 中可以正常运行,那么在浏览器中也能原生运行(除了那些令人头疼的 TS 导入)。Deno 的 Web 兼容性更高,这点让人感觉很棒。在服务器或客户端运行时,通用的期望和更少的黑科技能够让工作代码更加一致。
我感觉越来越难以理解从您编写的代码到在浏览器中运行的代码之间的复杂区别。但我仍然认为,对于许多项目而言,将代码编写为在浏览器中运行是非常有价值和强大的。这样可以减缓熵增,获得简单但快速的反馈循环。
转载自:https://juejin.cn/post/7246641306894090298