- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章JavaScript引入模块的历史简介由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块(module)”。一个模块可以包含用于特定目的的类或函数库.
很长一段时间,JavaScript 都没有语言级(language-level)的模块语法。这不是一个问题,因为最初的脚本又小又简单,所以没必要将其模块化.
但是最终脚本变得越来越复杂,因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块.
列举一些(出于历史原因):
现在,它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们.
语言级的模块系统在 2015 年的时候出现在了标准(ES6)中,此后逐渐发展,现在已经得到了所有主流浏览器和 Node.js 的支持。因此,我们将从现在开始学习现代 JavaScript 模块(module).
1、什么是模块?
一个模块(module)就是一个文件。一个脚本就是一个模块。就这么简单.
模块可以相互加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数:
export 关键字标记了可以从当前模块外部访问的变量和函数.
import 关键字允许从其他模块导入功能.
例如,我们有一个 sayHi.js 文件导出了一个函数:
// sayHi.js 。
export function sayHi(user) { 。
alert(`Hello, ${user}!`); 。
} 。
……然后另一个文件可能导入并使用了这个函数:
// main.js 。
import {sayHi} from './sayHi.js'; 。
。
alert(sayHi); // function... 。
sayHi('John'); // Hello, John! 。
import 指令通过相对于当前文件的路径 ./sayHi.js 加载模块,并将导入的函数 sayHi 分配(assign)给相应的变量.
让我们在浏览器中运行一下这个示例.
由于模块支持特殊的关键字和功能,因此我们必须通过使用<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 来测试模块.
2、模块核心功能 。
与“常规”脚本相比,模块有什么不同呢?
下面是一些核心的功能,对浏览器和服务端的 JavaScript 来说都有效.
3、始终使用 “use strict” 。
模块始终默认使用 use strict,例如,对一个未声明的变量赋值将产生错误(译注:在浏览器控制台可以看到 error 信息).
<script type="module"> 。
a = 5; // error 。
</script> 。
4、模块级作用域 。
每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的.
在下面这个例子中,我们导入了两个脚本,hello.js 尝试使用在 user.js 中声明的变量 user,失败了:
<!doctype html> 。
<script type="module" src="user.js"></script> 。
<script type="module" src="hello.js"></script> 。
模块期望 export 它们想要被外部访问的内容,并 import 它们所需要的内容.
所以,我们应该将 user.js 导入到 hello.js 中,并从中获取所需的功能,而不要依赖于全局变量.
这是正确的变体:
import {user} from './user.js'; 。
。
document.body.innerHTML = user; // John 。
在浏览器中,每个 <script type="module"> 也存在独立的顶级作用域(译注:在浏览器控制台可以看到 error 信息).
<script type="module"> 。
// 变量仅在这个 module script 内可见 。
let user = "John"; 。
</script> 。
。
<script type="module"> 。
alert(user); // Error: user is not defined 。
</script> 。
如果我们真的需要创建一个 window-level 的全局变量,我们可以将其明确地赋值给 window,并以 window.user 来访问它。但是这需要你有足够充分的理由,否则就不要这样做.
5、模块代码仅在第一次导入时被解析 。
如果同一个模块被导入到多个其他位置,那么它的代码仅会在第一次导入时执行,然后将导出(export)的内容提供给所有的导入(importer).
这有很重要的影响。让我们通过示例来看一下:
首先,如果执行一个模块中的代码会带来副作用(side-effect),例如显示一条消息,那么多次导入它只会触发一次显示 —— 即第一次:
// alert.js 。
alert("Module is evaluated!"); 。
// 在不同的文件中导入相同的模块 。
。
// 1.js 。
import `./alert.js`; // Module is evaluated! 。
。
// 2.js 。
import `./alert.js`; // (什么都不显示) 。
在实际开发中,顶级模块代码主要用于初始化,内部数据结构的创建,并且如果我们希望某些东西可以重用 — 请导出它.
下面是一个高级点的例子.
我们假设一个模块导出了一个对象:
// admin.js 。
export let admin = { 。
name: "John" 。
}; 。
如果这个模块被导入到多个文件中,模块仅在第一次被导入时被解析,并创建 admin 对象,然后将其传入到所有的导入.
所有的导入都只获得了一个唯一的 admin 对象:
// 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 导入的是同一个对象 。
// 在 1.js 中对对象做的更改,在 2.js 中也是可见的 。
所以,让我们重申一下 —— 模块只被执行一次。生成导出,然后它被分享给所有对其的导入,所以如果某个地方修改了 admin 对象,其他的模块也能看到这个修改.
这种行为让我们可以在首次导入时 设置 模块。我们只需要设置其属性一次,然后在进一步的导入中就都可以直接使用了.
例如,下面的 admin.js 模块可能提供了特定的功能,但是希望凭证(credential)从外部进入 admin 对象:
// admin.js 。
export let admin = { }; 。
。
export function sayHi() { 。
alert(`Ready to serve, ${admin.name}!`); 。
} 。
在 init.js 中 —— 我们 APP 的第一个脚本,设置了 admin.name。现在每个位置都能看到它,包括在 admin.js 内部的调用.
// init.js 。
import {admin} from './admin.js'; 。
admin.name = "Pete"; 。
另一个模块也可以看到 admin.name:
// other.js 。
import {admin, sayHi} from './admin.js'; 。
。
alert(admin.name); // Pete 。
。
sayHi(); // Ready to serve, Pete! 。
6、import.meta 。
import.meta 对象包含关于当前模块的信息.
它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL.
<script type="module"> 。
alert(import.meta.url); // 脚本的 URL(对于内嵌脚本来说,则是当前 HTML 页面的 URL) 。
</script> 。
7、在一个模块中,“this” 是 undefined 。
这是一个小功能,但为了完整性,我们应该提到它.
在一个模块中,顶级 this 是 undefined.
将其与非模块脚本进行比较会发现,非模块脚本的顶级 this 是全局对象:
<script> 。
alert(this); // window 。
</script> 。
。
<script type="module"> 。
alert(this); // undefined 。
</script> 。
8、浏览器特定功能 。
与常规脚本相比,拥有 type="module" 标识的脚本有一些特定于浏览器的差异.
如果你是第一次阅读或者你不打算在浏览器中使用 JavaScript,那么你可以跳过本节内容.
9、模块脚本是延迟的 。
模块脚本 总是 被延迟的,与 defer 特性(在 脚本:async,defer 一章中描述的)对外部脚本和内联脚本(inline script)的影响相同.
也就是说:
它的一个副作用是,模块脚本总是会“看到”已完全加载的 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.
这是因为模块脚本是被延迟的,所以要等到 HTML 文档被处理完成才会执行它。而常规脚本则会立即运行,所以我们会先看到常规脚本的输出.
当使用模块脚本时,我们应该知道 HTML 页面在加载时就会显示出来,在 HTML 页面加载完成后才会执行 JavaScript 模块,因此用户可能会在 JavaScript 应用程序准备好之前看到该页面。某些功能那时可能还无法正使用。我们应该放置“加载指示器(loading indicator)”,否则,请确保不会使用户感到困惑.
10、Async 适用于内联脚本(inline script) 。
对于非模块脚本,async 特性(attribute)仅适用于外部脚本。异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档.
对于模块脚本,它也适用于内联脚本.
例如,下面的内联脚本具有 async 特性,因此它不会等待任何东西.
它执行导入(fetch ./analytics.js),并在准备导入完成时运行,即使 HTML 文档还未完成,或者其他脚本仍在等待处理中.
这对于不依赖任何其他东西的功能来说是非常棒的,例如计数器,广告,文档级事件监听器.
<!-- 所有依赖都获取完成(analytics.js)然后脚本开始运行 --> 。
<!-- 不会等待 HTML 文档或者其他 <script> 标签 --> 。
<script async type="module"> 。
import {counter} from './analytics.js'; 。
。
counter.count(); 。
</script> 。
11、外部脚本 。
具有 type="module" 的外部脚本(external script)在两个方面有所不同:
12、不允许裸模块(“bare” module) 。
在浏览器中,import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为“裸(bare)”模块。在 import 中不允许这种模块.
例如,下面这个 import 是无效的:
import {sayHi} from 'sayHi'; // Error,“裸”模块 。
// 模块必须有一个路径,例如 './sayHi.js' 或者其他任何路径 。
某些环境,像 Node.js 或者打包工具(bundle tool)允许没有任何路径的裸模块,因为它们有自己的查找模块的方法和钩子(hook)来对它们进行微调。但是浏览器尚不支持裸模块.
十3、兼容性,“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> 。
14、构建工具 。
在实际开发中,浏览器模块很少被以“原始”形式进行使用。通常,我们会使用一些特殊工具,例如 Webpack,将它们打包在一起,然后部署到生产环境的服务器.
使用打包工具的一个好处是 —— 它们可以更好地控制模块的解析方式,允许我们使用裸模块和更多的功能,例如 CSS/HTML 模块等.
构建工具做以下这些事儿:
如果我们使用打包工具,那么脚本会被打包进一个单一文件(或者几个文件),在这些脚本中的 import/export 语句会被替换成特殊的打包函数(bundler function)。因此,最终打包好的脚本中不包含任何 import/export,它也不需要 type="module",我们可以将其放入常规的 <script>:
<!-- 假设我们从诸如 Webpack 这类的打包工具中获得了 "bundle.js" 脚本 --> 。
<script src="bundle.js"></script> 。
也就是说,原生模块也是可以使用的。所以,我们在这儿将不会使用 Webpack:你可以稍后再配置它.
十5、总结 。
下面总结一下模块的核心概念:
当我们使用模块时,每个模块都会实现特定功能并将其导出。然后我们使用 import 将其直接导入到需要的地方即可。浏览器会自动加载并解析脚本.
在生产环境中,出于性能和其他原因,开发者经常使用诸如 Webpack 之类的打包工具将模块打包到一起.
最后此篇关于JavaScript引入模块的历史简介的文章就讲到这里了,如果你想了解更多关于JavaScript引入模块的历史简介的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
背景 之前陆续写过一些和 OpenTelemetry 相关的文章: 实战:如何优雅的从 Skywalking 切换到 OpenTelemetry 实战:如何编写一个 OpenTeleme
我很困惑PSReadLine历史在 Powershell 中跨 session 工作。我可以在 PS 版本 5.1 中看到我以前的命令历史记录自动存储在 %userprofile%\AppData\R
我有一个实体,我正在从面板中保存我们的数据库,您可以在其中执行常规操作(编辑、添加等)。不是很大,大多数时候大约有 1k 行,而且这个数字可能总是在这个左右。该实体有一些与其他实体相关的字段(例如:位
有时有人想直接在环境中更改 crx 中的内容。 这通常是环境不工作状态的原因。而且往往很难找到问题的原因。而且我认为如果 cq5 crx 有审计日志会很有帮助。像这样。 12.12.12 21:03
这个问题与可以在其他问题之一中找到的模式有关here.基本上在数据库中,我存储用户,位置,传感器等。所有这些内容都可以由用户在系统中编辑,并且可以删除。 但是-在编辑或删除项目时,我需要存储旧数据;我
我需要随时跟踪许多项目及其状态。 例子 ItemId Location DateTime State 1 Mall A 2010-02-03 07:00 on
我有这个方法来添加 fragment : public void addFragmentOnTop(Fragment fragment) { getSupportFragmentManager()
我想了解 HTML5 历史对象。这是我开始的一个简单示例。 function addDialog(){ document.getElementById('d').style.
我如何使用 HTML5 history api。我确实通过了https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history
我正在尝试找出在关系数据库中保存表的历史记录/修订的最佳方法。 我进行了一些研究和阅读,但不确定跟踪更改的最佳方式是什么。对于我的主表,我很确定我已经确定了一个修订表,以保持跟踪(见图),但我不确定是
这个问题在这里已经有了答案: Git: discover which commits ever touched a range of lines (6 个答案) 关闭 9 年前。 我一直在研究 gi
我有一个相当复杂的程序(带有 SWIG'ed C++ 代码的 Python,长期运行的服务器),它显示了不断增长的常驻内存使用量。我一直在使用常用的泄漏工具(valgrind、Pythons gc 模
我的 Git 存储库中有一行包含单词“Foo”的数百次提交。 是否有任何方法可以在上次的位置找到它的修订号? 最佳答案 这可以通过 -S 的镐 ( gitlog ) 选项来解决。 git log -
我不小心删除了一个文件(我不是他的创建者)并提交并将其推送到远程。现在我想让 git 取消删除此更改,但是当我使用 git revert #mistaken commit 时,它可以工作,但指责信息指
我使用 spyder 历史 Pane 查看我过去尝试过的命令,但最近我注意到它不会在我键入命令时更新。屏幕截图 1 显示了控制台和历史记录 Pane ,因为您可以看到历史记录中没有显示任何控制台条目。
我的应用程序使用 Camunda 7.7 运行。到目前为止,所有数据都保存在 Camunda 表 (ACT_XXX) 中——它们变得很大。所以现在我想清理表格并配置 Camunda,以便在 14 天后
我在 SVN 上有一个这样组织的旧项目: /一些/子目录/a/trunk/foo /一些/子目录/b/trunk/foo /一些/子目录/c/trunk/foo 我使用GitHub工具git-impo
我有一个通用的工作功能,为此我将使用 GNU Radio 的历史记录功能。在 block 的构造函数中,我调用了 set_history( m )。我以标准方式转换输入缓冲区: const flo
当我加载 php 页面时,我会附加一些数据。例如 MyPage.php?value=something。正如预期的那样,当我使用后退按钮来回移动时,它总是会加载附加的相同数据。我不想那样。我希望在页面
我们有一个相当大的库,我们需要定期将其导入(然后修补)到我们的代码库中。 SVN Book 似乎推荐了一个“vendor branch”方案,我们保留了“vendor drops”的补丁版本。这会起作
我是一名优秀的程序员,十分优秀!