JavaScript 模块:优点、缺点和不足之处🧐
如果你曾经遇到过一段原生 JavaScript 代码,想要将其重构为模块,或者已经有一个CommonJS模块想要转换成其他类型ES6 Modules,那么你可能遇到过一些棘手的情况。我最近就遇到了一个这样的问题,并发现了一些在使用模块时需要注意的差异和要点。和往常一样,我觉得分享这些内容或许能帮到其他人,所以就有了这篇文章😊。
CommonJS 和require
这是在 ES6 被大神们带到人间之前,许多开源项目中最常见的代码类型😁。
用法
const _ = require('underscore'); // from an npm package
const reverseString = require('./reverseString.js'); // from internal module
定义
reverseString.js你需要编写类似这样的代码才能使其正常工作:
const reverseString = (sentence) => sentence.split("").reverse().join("");
module.exports = reverseString;
你唯一需要注意的是,你赋值给它的值与module.exports你使用时得到的值相同require。如果你想使用你刚刚导出的函数:
const reverseString = require('./reverseString.js');
console.log(reverseString("madam")); // madam, gotcha 😂
多导出
在实际应用中,我们可能需要从模块中导出多个函数。这很简单,只需将它们全部封装在一个对象中即可。假设你有一个名为 `<filename>` 的文件stringHelpers.js:
const reverseString = (sentence) => {...};
const toUpperCase = (sentence) => {...};
const convertToCamelCase = (sentence) => {...};
module.exports = {
reverseString: reverseString,
toUpperCase, // you can omit the assignment if the name is equal
toLowerCase: convertToLowerCase,
};
如您所见,该值module.exports将是一个对象,这意味着在使用它时,您必须使用该对象的属性:
const stringHelpers = require('./stringHelpers.js');
console.log(stringHelpers.reverseString('racecar')); // racecar 🤣
我们还可以用另一种方式重写我们的模块:
module.exports = {};
module.exports.reverseString = () => {...};
module.exports.toUpperCase = () => {...};
module.exports.toLowerCase = () => {...};
创建模块的这两种方法完全相同,您可以根据自己的喜好选择遵循哪种约定。
ES6 模块
ES6 Modules 的CommonJS创建是为了提供一种同时满足异步模块定义 (Async Module Definition) 用户需求的格式AMD。与CommonJSAsync Modules 相比,ES6 Modules最简单的形式是始终导出一个对象。
const reverseString = (sentence) => {...};
export default reverseString;
默认导出
模块的一大优势在于隐藏内部实现细节,只暴露所需部分。在本例中,我只导出一个函数,并且将其导出为 `<module>` default。当使用 `<module>` 导出时default,你可以使用其原始名称甚至别名导入它。此外,你还可以省略花括号。
import reverseString from './reverseString.js';
import defaultExport from './reverseString.js';
console.log(reverseString('madam')); //madam
console.log(defaultExport('madam')); //madam
如果您查看从文件中导出的对象,您会看到以下对象:
{
default: (sentence) => {...}
}
您也可以直接导出该函数:
export const reverseString = (sentence) => {...};
这将导致:
{
reverseString: (sentence) => {...}
}
在这种情况下,您需要使用其名称才能导入它,此外,您还必须使用花括号:
import { reverseString } from './reverseString.js';
console.log(reverseString('madam')); //madam
混合出口
除了命名导出之外,您还可以设置默认导出:
export const reverseString = (sentence) => {...};
export const toUpperCase = (sentence) => {...};
const convertToCamelCase = (sentence) => {...};
export default convertToCamelCase;
这将使您获得:
{
reverseString: (sentence) => {...},
toUpperCase: (sentence) => {...},
default: (sentence) => {...}
}
导入时,您可以使用它们的名称,或者将所有内容导入到一个对象中:
import convertToCamelCase, { reverseString, toUpperCase } from './stringHelpers.js';
// or
import * as stringHelpers from './stringHelpers.js';
公平地说,导出后也可以更改已命名导出的名称:
import { reverseString as madeUpName } from './stringHelpers.js';
导入整个模块
有时,你需要执行一段代码,但不需要访问模块的任何内部值。在这种情况下,你可以导入整个模块,使其全局代码得以执行:
// other code or possible exports
window.addEventListener("load", function() {
console.log("Window is loaded");
});
然后导入整个模块:
import './loadEventListener.js';
为什么要使用模块😍?
使用 ES6 模块(甚至 CommonJS 格式)有很多好处。我将在这里介绍其中的一些:
- 便于代码共享(包括内部共享和项目间共享)。
- 独立可测试性。
- 能够隐藏实现细节。
- 单一职责原则,代码可以拆分成具有特定用途的小块。
- 简化依赖检测/注入。
- 为一段代码定义清晰的接口。
- 可以与依赖注入系统配合使用,加载一段代码。
- 可以帮助代码树摇动消除未使用的代码。
有什么猫腻吗🤔?
使用 ES6 模块时,您应该注意以下几点:
- 它们默认以严格模式运行(您无需
use strict再进行其他设置)。 - 最高级别值为
this。undefined - 顶级变量是模块局部变量。
- ES6 模块是异步加载和执行的。这意味着浏览器会
HTML先完成解析和加载,然后再执行模块代码。加载可以并行进行,也可以预先使用 `.` 完成link rel=preload。
不给糖就捣蛋?
这大概是我最喜欢的部分了。你可以动态加载模块并执行它。这是通过使用import关键字作为函数而不是普通命令来实现的。
import('/modules/my-module.js')
.then((module) => {
// Do something with the module.
});
或者更好的是:
const module = await import('/modules/my-module.js');
哇,那太好了,但是为什么是🧐呢?
假设你的应用程序在移动端和桌面端的用户体验或行为有所不同。这个问题不能仅仅通过响应式设计来解决,因此你需要构建一个页面渲染器,根据访问者的平台加载和渲染每个页面。
从技术角度来说,这只是一种策略模式,页面渲染器在运行时决定加载哪个模块。使用动态导入可以轻松解决这个问题。动态导入还有许多其他应用场景可以受益。
然而,能力越大,责任越大。使用这项强大的功能时务必谨慎,因为它也有其自身的缺点。至少,你会失去自动打包延迟加载的数据块、类型推断等功能。
我该如何使用它们?
本文已展示了许多如何在其他文件或模块中使用模块的示例。但是,有时您需要在浏览器中使用它们(例如通过 `<script>` 标签HTML)。Chrome、Safari、Firefox 和 Edge 都支持 ES6 模块,但您需要将 `<script>` 标签的类型从 `<script>` 更改为 `<module>`:
// html.js
export function tag (tag, text) {
const el = document.createElement(tag)
el.textContent = text
return el
}
<script type="module">
import { tag } from './html.js'
const h1 = tag('h1', ' Hello Modules!')
document.body.appendChild(h1)
</script>
或者,也可以直接将模块文件导入到另一个文件中,并使用外部引用:
// app.js
import { tag } from './html.js'
const h1 = tag('h1', ' Hello Modules!')
document.body.appendChild(h1)
<script type="module" src="app.js"></script>
提示:目前仍有一些老旧浏览器(例如IE11)不支持此功能,因此请确保您有备用方案。这可以通过属性来实现。nomodule
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
概括
我们对比了 ES6 模块前后的代码,并了解了它们在语法上的一些差异。我们看到了 JavaScript 模块系统的强大功能,以及在大型代码库中使用模块的优势。最后,我们回顾了动态导入,它潜力巨大,但使用时应谨慎。
希望您喜欢这篇文章,下次再见👋🏼。
文章来源:https://dev.to/yashints/javascript-modules-the-good-the-bad-and-the-ugly-36nb
