详讲js中的模块和导入导出

目录

模块

对于浏览器:

模块核心功能

始终使用 “use strict”

模块级作用域

模块代码仅在第一次导入时被解析

import.meta

在一个模块中,“this” 是 undefined

浏览器特定功能

模块脚本是延迟的,常规脚本是立即进行的

Async 适用于内联脚本(inline script)

外部脚本

不允许裸模块(“bare” module)

兼容性,“nomodule”

构建工具

导入导出

导出 export

导入 import *

Export default

重新导出

重新导出默认导出

动态导入

import() 表达式


模块

模块:一个模块(module)就是一个文件。一个脚本就是一个模块

模块可以相互加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数:

  • export 关键字标记了可以从当前模块外部访问的变量和函数。
  • import 关键字允许从其他模块导入功能。
对于浏览器:

由于模块支持特殊的关键字和功能,因此我们必须通过使用 <script type="module"> 特性(attribute)来告诉浏览器,此脚本应该被当作模块(module)来对待。

<!doctype html>
<script type="module">
  import {sayHi} from './say.js';
  document.body.innerHTML = sayHi('John');
</script>
  • 浏览器会自动获取并解析(evaluate)导入的模块(如果需要,还可以分析该模块的导入),然后运行该脚本。
  • 模块只通过 HTTP(s) 工作,而非本地
    • 如果你尝试通过 file:// 协议在本地打开一个网页,你会发现 import/export 指令不起作用。你可以使用本地 Web 服务器,例如 static-server,或者使用编辑器的“实时服务器”功能,例如 VS Code 的 Live Server Extension 来测试模块。

模块核心功能

始终使用 “use strict”
  • 模块始终在严格模式下运行。例如,对一个未声明的变量赋值将产生错误(译注:在浏览器控制台可以看到 error 信息)
<script type="module">
  a = 5; // error
</script>

模块级作用域

每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。

  • 同一页面上的两个脚本,都是 type="module"。它们看不到彼此的顶级变量:
<script type="module">
  // 变量仅在这个 module script 内可见
  let user = "John";
</script>

<script type="module">
  console.log(user); // Error: user is not defined
</script>

注意:在浏览器中,我们可以通过将变量显式地分配给 window 的一个属性,使其成为窗口级别的全局变量。这样所有脚本都会看到它,无论脚本是否带有 type="module",请避免这样做。

模块代码仅在第一次导入时被解析

如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次,即在第一次被导入时。然后将其导出(export)的内容提供给进一步的导入(importer)

  • 顶层模块代码应该用于初始化,创建模块特定的内部数据结构。如果我们需要多次调用某些东西 —— 我们应该将其以函数的形式导出
    • 这种行为实际上非常方便,因为它允许我们“配置”模块。
// 📁 admin.js
export let admin = {
  name: "John"
};
// 📁 1.js
import { admin } from './admin.js';
admin.name = "Pete";
// 📁 2.js
import { admin } from './admin.js';
alert(admin.name); // Pete

// 1.js 和 2.js 引用的是同一个 admin 对象
// 在 1.js 中对对象做的更改,在 2.js 中也是可见的
import.meta

import.meta 对象包含关于当前模块的信息。它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL。

<script type="module">
  console.log(import.meta.url); // 脚本的 URL
  // 对于内联脚本来说,则是当前 HTML 页面的 URL
</script>

在一个模块中,“this” 是 undefined

在一个模块中,顶级 this 是 undefined。

<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>

浏览器特定功能

模块脚本是延迟的,常规脚本是立即进行的
  • 模块脚本 总是 被延迟的,与 defer 特性(在 脚本:async,defer 一章中描述的)对外部脚本和内联脚本(inline script)的影响相同。

也就是说:

  • 下载外部模块脚本 <script type="module" src="..."> 不会阻塞 HTML 的处理,它们会与其他资源并行加载。
  • 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比 HTML 加载速度更快),然后才会运行。
  • 保持脚本的相对顺序:在文档中排在前面的脚本先执行。

它的一个副作用是,模块脚本总是会“看到”已完全加载的 HTML 页面,包括在它们下方的 HTML 元素。

<script type="module">
  alert(typeof button); // object:脚本可以“看见”下面的 button
  // 因为模块是被延迟的(deferred,所以模块脚本会在整个页面加载完成后才运行
</script>
//相较于下面这个常规脚本:
<script>
  alert(typeof button); // button 为 undefined,脚本看不到下面的元素
  // 常规脚本会立即运行,常规脚本的运行是在在处理页面的其余部分之前进行的
</script>
<button id="button">Button</button>
  • 上面的第二个脚本实际上要先于前一个脚本运行!所以我们会先看到 undefined,然后才是 object
  • 为了避免用户疑惑,可以放置加载指示器或者其他方式来解决这个问题
Async 适用于内联脚本(inline script)

对于非模块脚本,async 特性(attribute)仅适用于外部脚本。异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档。

对于模块脚本,它也适用内联脚本。

  • 下面的内联脚本具有 async 特性,因此它不会等待任何东西。
<!-- 所有依赖都获取完成(analytics.js)然后脚本开始运行 -->
<!-- 不会等待 HTML 文档或者其他 <script> 标签 -->
<script async type="module">
  import {counter} from './analytics.js';
  counter.count();
</script>
外部脚本

具有 type="module" 的外部脚本(external script)在两个方面有所不同:

  • 具有相同 src 的外部脚本仅运行一次:
  • 从另一个源(例如另一个网站)获取的外部脚本需要 CORS header,换句话说,如果一个模块脚本是从另一个源获取的,则远程服务器必须提供表示允许获取的 header Access-Control-Allow-Origin。默认这样做可以确保更好的安全性。
<!-- another-site.com 必须提供 Access-Control-Allow-Origin -->
<!-- 否则,脚本将无法执行 -->
<script type="module" src="http://another-site.com/their.js"></script>
不允许裸模块(“bare” module)

在浏览器中,import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为“裸(bare)”模块。在 import 中不允许这种模块。

import {sayHi} from 'sayHi'; // Error,“裸”模块
// 模块必须有一个路径,例如 './sayHi.js' 或者其他任何路径

某些环境,像 Node.js 或者打包工具(bundle tool)允许没有任何路径的裸模块,因为它们有自己的查找模块的方法和钩子(hook)来对它们进行微调。但是浏览器尚不支持裸模块

兼容性,“nomodule”

旧时的浏览器不理解 type="module"。未知类型的脚本会被忽略。对此,我们可以使用 nomodule 特性来提供一个后备:

<script type="module">
  alert("Runs in modern browsers");
</script>

<script nomodule>
  alert("Modern browsers know both type=module and nomodule, so skip this")
  alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>

构建工具

在实际开发中,浏览器模块很少被以“原始”形式进行使用。通常,我们会使用一些特殊工具,例如 Webpack,将它们打包在一起,然后部署到生产环境的服务器。

使用打包工具的一个好处是 —— 它们可以更好地控制模块的解析方式,允许我们使用裸模块和更多的功能,例如 CSS/HTML 模块等。

构建工具做以下这些事儿:

  1. 从一个打算放在 HTML 中的 <script type="module"> “主”模块开始。
  2. 分析它的依赖:它的导入,以及它的导入的导入等。
  3. 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的 import 调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。
  4. 在处理过程中,可能会应用其他转换和优化:
  • 删除无法访问的代码。
  • 删除未使用的导出(“tree-shaking”)。
  • 删除特定于开发的像 console 和 debugger 这样的语句。
  • 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
  • 压缩生成的文件(删除空格,用短的名字替换变量等)。

如果我们使用打包工具,那么脚本会被打包进一个单一文件(或者几个文件),在这些脚本中的 import/export 语句会被替换成特殊的打包函数(bundler function)。因此,最终打包好的脚本中不包含任何 import/export,它也不需要 type="module",我们可以将其放入常规的 <script>:

<!-- 假设我们从诸如 Webpack 这类的打包工具中获得了 "bundle.js" 脚本 -->
<script src="bundle.js"></script>

导入导出

导出 export

  • 导出 class/function 后没有分号
    • 在类或者函数前的 export 不会让它们变成 函数表达式。尽管被导出了,但它仍然是一个函数声明
//声明导出
export const  a = 1

//声明导出分开
const  a = 1
const  b = 1
export {a ,b}

//导入另外命名为
export {a as a1, b as b1};
import {b1} from 'module';
b1()

导入 import *

import {xxx, xxx} from 'module';
xxx();
// 导入一个对象
import * as utils from 'module';
utils.xxx();

//import “as” 另命名为
import {xxx as bas} from 'module';
bas()

明确列入的好处:

  • 现代的构建工具(webpack 和其他工具)将模块打包到一起并对其进行优化,以加快加载速度并删除未使用的代码。
  • 明确列出要导入的内容会使得名称较短:xxx() 而不是 utils.xxx()。
  • 导入的显式列表可以更好地概述代码结构:使用的内容和位置。它使得代码支持重构,并且重构起来更容易

Export default

在实际中,主要有两种模块。

  • 包含库或函数包的模块
  • 声明单个实体的模块,例如模块 user.js 仅导出 class User

开发者倾向于使用第二种方式,以便每个“东西”都存在于它自己的模块中,模块提供了一个特殊的默认导出 export default 语法,以使“一个模块只做一件事”的方式看起来更好。

  • import 命名的导出时需要花括号,而 import 默认的导出时不需要花括号
//导出
export default class User { // 只需要添加 "default" 即可
  constructor(name) {
    this.name = name;
  }
}
//引入
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可
new User('John');

// contants.js 导出单个值,而不使用变量
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
//可以自定义命名
import common from  './contants'
import utils from  './contants'
console.log('common:', common)

// 没有函数名 同变量
export default function(user) { 
  alert(`Hello, ${user}!`);
}

//导出
const detail = ()=>{
    return <div>detail</div>
}
export default detail
import detail from './detail.js'; // 不需要花括号 {User},只需要写成 User 即可
  • 每个文件应该只有一个 export default:

重新导出

“重新导出(Re-export)”语法 export ... from ... 允许导入内容,并立即将其导出(可能是用的是其他的名字),比如:希望想要使用我们的包的人,应该只从“主文件” auth/index.js 导入

  • “主文件”,auth/index.js 导出了我们希望在包中提供的所有功能。
  • 其他使用我们包的开发者不应该干预其内部结构,不应该搜索我们包的文件夹中的文件。我们只在 auth/index.js 中导出必要的部分,并保持其他内容“不可见”
// 📁 auth/index.js
// 导入 login/logout 然后立即导出它们
import {login, logout} from './helpers.js';
export {login, logout};
// 将默认导出导入为 User,然后导出它
import User from './user.js';
export {User};
...

// 📁 auth/index.js
// 重新导出 login/logout
export {login, logout} from './helpers.js';

// 将默认导出重新导出为 User
export {default as User} from './user.js';
...
import {login} from "auth/index.js"

区别:export ... from 与 import/export 相比的显着区别是重新导出的模块在当前文件中不可用

重新导出默认导出

重新导出时,默认导出需要单独处理。

  • export User from './user.js' 无效。这会导致一个语法错误。
    • 要重新导出默认导出,我们必须明确写出 export {default as User},就像上面的例子中那样。
  • export * from './user.js' 重新导出只导出了命名的导出,但是忽略了默认的导出。
export * from './user.js'; // 重新导出命名的导出
export {default} from './user.js'; // 重新导出默认的导出

动态导入

静态导入的限制:

  • 不能动态生成inport的任何参数
  • 模块路径必须是原始类型字符串,不能是函数调用
  • 无法根据条件或者在运行时导入

import() 表达式

import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式

  • 动态导入在常规脚本中工作时,它们不需要 script type="module".
  • 不能将 import 复制到一个变量中,或者对其使用 call/apply。因为它不是一个函数
    • 尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())。
let modulePath = prompt("Which module to load?");
import(modulePath)
  .then(obj => <module object>)
  .catch(err => <loading error, e.g. if no such module>)


//在异步函数中,我们可以使用 
let module = await import(modulePath)
//contants
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

export  const  a = 1;

// index
const  {a} = await import('./contants')
const  {a,...rest} = await import('./contants')

const common = await import('./contants')
console.log('common:', common)