Daxia Blog
Uncategorized | Rust | WebUI | FHIR | Javascript | KB

Javascript Module

回顾

随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块(module)”。一个模块可以包含用于特定目的的类或函数库。

很长一段时间,JavaScript 都没有语言级(language-level)的模块语法。这不是一个问题,因为最初的脚本又小又简单,所以没必要将其模块化。

但是最终脚本变得越来越复杂,因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块。

  • AMD: 最古老的模块系统之一,最初由require.js库实现。
  • CommonJS: 为Node.js服务器创建的模块系统。
  • UMD: 另外一个模块系统,建议作为通用的模块系统,它与AMDCommonJS都兼容。

现在,它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们。语言级的模块系统在2015年的时候出现在了ES6标准中,此后逐渐发展,现在已经得到了所有主流浏览器和Node.js的支持。

什么是模块?

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

每个模块都可以使用指令exportimport导入导出一些变量和函数,这样两个模块之间就可以互相调用:

  • export关键字标记了可以从当前模块外部访问的变量、函数和类定义。
  • import关键字允许从其他模块导入变量、函数和类定义。

假设模块helloworld.js导出如下函数:

export function helloWorld() {
  alert(`Hello World!`);
}

这样在其它模块中可以如下所示将其导入:

import { helloWorld } from './helloworld.js';

helloWorld();

在浏览器中由于模块支持特殊的关键字和功能,因此我们必须通过使用<script type="module">来声明此脚本应该被当作模块使用。

<script type="module">
    import { helloWorld } from './helloworld.js';
    helloWorld();
</script>

模块间的导出与导入

这是import的几种常见用法:

import { export1 } from "module-name";
import defaultExport from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。

再看看export的几种常见用法:

export { name1, name2, ...};
export { variable1 as name1, variable2 as name2, ...};
export function FunctionName() {...}
export class ClassName {...}

export default expression;
export default function(){}
export default function name(){}
export { name as default};

导入导出的内容可以是变量、函数,还可以是类定义。同时,导入的内容需要使用{}包裹起来,当然export default除外。这时import命令后面不需要使用大括号,而且在导入时可以为该缺省导入指定任意名字。

另外,export后面的名字和import后面的名字严格一致。同时我们可以在导入或导出时使用的as关键字,相当于起了个别名。

export default可以为模块指定默认导出,这样在import的时候就可以为这个缺省的导出名称另起别名。但是,一个模块只能有一个默认导出,也就是说一个模块只能用一次export default

模块级作用域

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

模块应该export它们想要被外部访问的内容,并import它们所需要的内容。在设计时,对于模块我们应该尽量使用导入导出方式,而不应依赖全局变量。

如下示例展示了在浏览器中,每个<script type="module">都存在着自己的作用域,它们之间的变量是不可互通的:

<script type="module">
    // 变量仅在当前模块中可见
    let name = "张三";
</script>

<script type="module">
    // 抛出错误:变量未定义
    alert(name); 
</script>

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

如果同一个模块被其它多个模块导入,那么它的代码只会执行一次,即在第一次被导入时。例如:

// mod1.js
alert("改代码仅会被执行一次!");
// mod2.js
import './mod1.js';
// mod3.js
import './mod1.js';

顶层模块代码可以用于初始化,初始化模块或者初始化整个系统,设置系统参数等等。如果我们需要多次调用某些内容,我们应该将其以函数的形式封装并导出。

模块脚本是延迟的

模块脚本会被延迟加载,也就是说,模块脚本会等到HTML文档完全准备就绪(即使它们很小并且比HTML加载速度更快),然后才会运行。当然,下载外部模块脚本<script type="module" src="...">不会阻塞HTML的处理,它们会与其他资源并行加载。这就导致一个结论,模块脚本总是会“看到”已完全加载的HTML页面。

当然,也会有特殊情况出现,那就是在内联脚本中。下面给出两种类型脚本的示例:

<!-- 内联脚本 -->
<script type="module">
  import {counter} from './helloworld.js';
  helloworld();
</script>
<!-- 外部脚本 -->
<script type="module" src="./helloworld.js"></script>

对于内联模块脚本,可以通过添加async异步特性,可以让其不会等待任何东西。它执行导入,fetch ./helloworld.js,并在导入完成后立即执行,即使这时候HTML文档还未全部加载完成,或者其他脚本仍在等待处理中。

<script async type="module">
  import {counter} from './helloworld.js';
  helloworld();
</script>

About Daxia
我是一名独立开发者,国家工信部认证高级系统架构设计师,在健康信息化领域与许多组织合作。具备大型卫生信息化平台产品架构、设计和开发的能力,从事软件研发、服务咨询、解决方案、行业标准编著相关工作。
我对健康信息化非常感兴趣,尤其是与HL7和FHIR标准的健康互操作性。我是HL7中国委员会成员,从事FHIR培训讲师和FHIR测评现场指导。
我还是FHIR Chi的作者,这是一款用于FHIR测评的工具。