前端的认识(宇宙厂:2026年重新认识前端 JavaScript 模块化?)

前端的认识(宇宙厂:2026年重新认识前端 JavaScript 模块化?)
宇宙厂:2026年重新认识前端 JavaScript 模块化?

大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

1. 聊聊 export 和 import 语句

JavaScript 提供了一种将程序拆分为独立模块的方法,这些模块可以在需要时导入,从而更高效、更快速地加载代码。例如,开发者可以只加载应用所需的几个特定功能的模块,而非加载所有可用功能的整个 JavaScript 库。

每个模块都是 JavaScript 代码,存储在一个单独的文件(.js 扩展名)中,模块可以使用 export 语句导出函数、变量、常量和类等,而导出的功能可以通过 import 语句导入到另一个模块中。

// 导出模块 greeting.jsexport const someConst = "Some value";export function welcome(user) {  console.log(`Welcome back, ${user}!`);}

下面是对上面模块的导入:

// 导入模块 main.jsimport { welcome } from "./greeting.js";welcome("Casey");

接着在 HTML 文档中导入模块示例:

<!-- 在 index.html 中导入模块 --><script type="module" src="modules/main.js"></script>

开发者还可以在模块文件末尾使用单个导出语句,其中包括要导出的功能的逗号分隔列表,并用花括号括起来:

// 模块末尾统一导入模块 greeting.jsconst someConst = "Some value";function welcome(user) {  console.log(`Welcome back, ${user}!`);}export { welcome, someConst };

export 的函数、变量、常量和类必须是顶级作用域,开发者不能在块语句(例如:函数或 if 语句)中使用导出。导入也是如此,除非使用动态导入 (Dynamic Import),否则无法通过条件导入或函数导入。

Promise.all(  Array.from({ length: 10 }).map(    (_, index) => import(`/modules/module-${index}.js`)    // 注意:动态导入使用了表达式  )).then((modules) => modules.forEach((module) => module.load()));

导入声明会被提升到模块作用域的顶部,开发者也可以直接从 HTML 文件中的脚本导入模块:

<!-- index.html --><script type="module">  import {welcome} from './modules/greeting.js';  welcome("Casey");  // 打印: "Welcome back, Casey!"</script>

需要注意的是, import 语句中 from 之后以 ./ 开头的字符串。此字符串是模块文件的相对路径,必须以 ./、../ 或 / 开头。

  • / :相对于站点根目录的路径
  • ./: 当前位置
  • ../: 从当前位置向上一级,连续使用多个 ../ 可以向上移动多个层级

同时,该字符串也可以是绝对 URL,但如果站点或站点的目录结构将来可能会发生变化,则不建议这样做。

import React from "https://esm.sh/react";import React from "https://esm.sh/react@18";import React from "https://esm.sh/react@next";import { renderToString } from "https://esm.sh/react-dom/server";

from 之后字符串的求值取决于具体环境。例如,某些环境要求模块文件扩展名为 .js,而其他环境则更倾向于使用 .mjs 扩展名。Node.js 允许使用裸模块名称,即完全没有路径或扩展名导入模块,而现代浏览器则依赖导入映射。

2. 通过导入映射 (Import Map) 实现裸模块导入

导入映射是一个 JSON 对象,允许开发者控制浏览器在导入 JavaScript 模块时如何解析模块描述符,其提供了导入语句或 import() 运算符中用作模块描述符的文本与解析描述符时将替换文本的相应值之间的映射。

导入映射用于解析静态和动态导入中的模块描述符,因此必须在任何使用映射中声明的描述符导入模块的 <script> 元素。但需要注意,导入映射仅适用于导入语句或 import() 运算符中加载到文档中的模块的描述符,不适用于 <script> 的 src 属性中指定的路径,也不适用于加载到 Worker 或 Worklet 中的模块。

<script type="importmap">  {    "imports": {      "greeting": "./modules/greeting.js"    }  }</script><script type="module">  import {welcome} from "greeting";  welcome("Casey");  // 打印: "Welcome back, Casey!"</script>

导入映射允许开发者使用裸模块名称,即重新映射模块路径,允许开发者模拟从包中导入模块并在应用程序中使用相同的模块描述符引用多个模块版本。

3. 模块与标准脚本的区别

加载模块文件或直接包含 import 语句的 HTML 中 <script> 元素需要 type="module" 属性,直接嵌入在 <script> 元素中的模块脚本相当于顶级模块,尽管 “嵌入模块” 通常不导出功能,即只从单独的模块文件中导入功能。

<script type="module" src="modules/main.js"></script>

<script> 元素在用于加载模块时默认具有 defer 属性(无需显式设置),其相当于告诉浏览器脚本将在文档解析之后但在 DOMContentLoaded 之前执行,即使模块直接嵌入在 <script> 元素中,其也会自动以延迟模式加载和执行。开发者可以通过向 <script> 元素添加 async 属性来覆盖此默认行为。

对于模块脚本,如果存在 async 属性,则脚本及其所有依赖项将在解析时并行获取,并在可用时立即执行。同时,即使没有使用 "use strict"; 语句,模块也会自动且始终以严格模式执行。

前端的认识(宇宙厂:2026年重新认识前端 JavaScript 模块化?)

3.1 模块脚本具有顶级作用域

模块脚本拥有顶级作用域,即模块中声明的变量在其他脚本中不可用,除非手动导入。

<script type="module">  let myVariable = "Hello";  // 声明模块变量</script><script>  console.log(myVariable);  // 打印: ReferenceError: myVariable is not defined</script>

同时,导入的功能只能在导入的脚本中访问,即在全局作用域内不可用。

<script type="module">  import {someConst} from './modules/greeting.js';  console.log(someConst);  // 打印: "Some value"</script><script>  console.log(someConst);  // 打印: ReferenceError: someConst is not defined</script>

但是,全局定义的变量在所有模块内都可用:

<script>  const user = "高级前端进阶"  // 注意:定义变量时没用 type="module"</script><script type="module">  import {welcome} from './modules/greeting.js';  welcome(user);  // 打印: "Welcome back, 高级前端进阶!"</script>

在模块的全局执行上下文中使用的关键字 this 指向 undefined,这与在非模块脚本的全局作用域中使用的 this 不同,后者指向全局对象。

而且需要注意,已导入的变量无法重新赋值,其行为与 const 变量类似,即只能由导出模块重新赋值。但与 const 一样,开发者仍可修改对象的属性。

3.2 模块只执行一次

当在多个 <script> 元素中导入同一个模块时,模块只会在首次导入时执行一次。

<script type="module" src="modules/main.js"></script><!-- 打印: "Welcome back, Casey!" --><script type="module" src="modules/main.js"></script><!-- 不打印任何内容  -->

当然,开发者可以在多个模块中导入相同的函数:

<script type="module">  import {welcome} from './modules/greeting.js';  welcome("Casey");  // 打印: "Welcome back, Casey!"</script><script type="module">  import {welcome} from './modules/greeting.js';  welcome("Max");  // 打印: "Welcome back, Max!"</script>

也可以在多个模块中导入同一个对象:

// 模块 myObj.jsexport const someObj = { someProp: "Some value" };

下面在多个模块中导入同一个对象,一个模块对导出值的修改会影响另一个模块:

<script type="module">  import {someObj} from './modules/myObj.js';  someObj.someProp = "Altered value"  // 导出是对象时,另一个模块修改了导入值</script><script type="module">  import {someObj} from './modules/myObj.js';  console.log(someObj.someProp);  // 打印: "Altered value"</script>

参考资料

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

https://library.fridoverweij.com/docs/jstutorial/modules.html

https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script

https://medium.com/@miracerdin/javascript-modules-and-bundlers-f5e2a503dd9

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

相关阅读