一篇文章图文并茂地带你学习 JavaScript 模块

JavaScript 模块

一个模块仅仅就是一个文件。模块之间可以通过一些特殊的库等来引用或导出。

为何要模块化?

由于大部分的代码是关于修改变量的,如何组织变量会影响编码的质量,以及维护他们的成本。

如果只有少数变量,完全可以通过函数作用域解决。但是这也让我们很难在各个函数之间共享变量。

例如在曾经的 jQuery 时代,在加载任何 jQuery 插件之前,我们必须先保证引入了 jQuery,这个时候所有的 <script> 标签必须以正确的顺序排列,否则将会抛出错误。

当依赖越来越多的时候,<script> 标签的引入成了令人头疼的事,根本原因是代码之间的依赖是不透明的,而且由于变量存在于全局作用域,任何代码都可以改变它,导致程序出现 BUG

模块化方式

<script> 变得更加的复杂的时候,社区创建了许多方式去组织代码。

  1. AMD

    最古老的模块化系统之一,最初由 require.js 库实现

  2. CommonJS

    用于 Node.js 模块化

  3. UMD

    与 AMD 和 CommonJS 兼容的模块系统。

上述的这些模块化方式正在逐渐变为历史,ES6 后出现了标准的模块化语法 importexport 被称之为 ES modules,目前已经被大多数主流浏览器支持。本篇文章只讨论 ES modules

ES module

简单使用

count.jsindex.html 在同个目录下

// count.js
let count = 1;
export { count };
<!-- index.html -->
<script type="module">
	import { count } from './count.js'
	console.log(count);
</script>

import 指令用相对路径加载一个文件 count.jscount 取出并赋值给当前模块的变量 count

注: 模块只在 http(s) 下有效,而 file:// 协议无效,因此不能直接本地打开,如果你用的是 vscode 可以使用 live server 插件开启本地服务运行上述代码。

核心模块特性
  1. 模块总是采用 “use strict” 严格模式
<!-- index.html -->
<script type="module">
	a = 5; // a is not defined
</script>
  1. 模块具有作用域

注: 下列三个文件在一个目录下

// 1.js
let count = 1;
// 2.js
count = 2; // count is not defined
<!-- index.html -->
<script type="module" src="./1.js"></script>
<script type="module" src="./2.js"></script>

上述代码对 count 进行赋值的时候将会报错,正确的做法如下

// 1.js
let count = 1;
export { count };
// 2.js
import { count } from './1.js'
count = 2;
<!-- index.html -->
<script type="module" src="./2.js"></script>

如果一定要共享变量,可以对 window 进行赋值 window.count = 1但是需要合理的理由。

  1. 当一个模块被导出多次时,只会执行一次
// alert.js
alert("huro");
// index.js
import './alert.js'; // huro
import './alert.js'; // (什么也不显示)

这意味着模块导出的变量,也是只初始化一次,会被共享

// user.js
const user = {
    name: null;
};
export {
	user
};

// 1.js
import { user } from './user.js'
user.name = "huro";

// 2.js
import { user } from './user.js'
console.log(user.name); // huro

也就是说,第一个文件 1.js 对变量的修改在 2.js 中会生效。这意味着我们可以单独创建一个文件去初始化一些变量

// init.js
import { user } from './user.js'
user.name = "huro";

// 1.js
import { user } from './user.js'
if (user.name === "huro") {
    console.log("great!")
}
  1. import.meta
// index.html
<script type="module">
	alert(import.mata.url); // 文件的地址 例如 localhost:3000/index.html
</script>
  1. this is undefined
<script>
	alert(this); // window
</script>
<script type="module">
	alert(this); // undefined
</script>
  1. module script 总是被延迟执行
<script type="module">
    alert(typeof button); // object
</script>
<script>
	alert(typeof button); // undefined
</script>
<button id="button">
    Button
</button>

总的来说

  1. 模块脚本会直到 html 已经完全准备好了,才会执行
  2. 各个模块脚本还是会以相对顺序执行
  3. 对于 <script type="module" src="..."> 的脚本,将不会阻塞 html 的渲染,而是和其他资源一同被加载。

由于 html 文档会被先渲染好,这个时候展现给用户的可能是没有经过 script 脚本的 html 文档,因此应该给予 loading 提示给用户。

  1. 异步工作

异步属性 async 只适用于

  1. <script src="./index.js" async></script>
  2. <script src="./index.js" type="module" async></script>
  3. <script type="module" async> xxx </script>

而不适用于非模块的内嵌 script

一旦所需资源加载完毕,代码就会立刻运行。

<script async type="module">
	import { counter } from './count.js' // 这个加载完毕,马上运行代码
    counter.count();
</script>

异步模块和 html 与其他 script 是独立的。

  1. 外部模块脚本只会执行一次
<script type="module" src="./index.js"></script>
<script type="module" src="./index.js"></script>
  1. 只能用相对路径或者绝对路径
import { count } from 'count.js'; 		// no
import { count } from './count.js'; 	// yes

总结:

  1. 非异步模块会被延迟执行
  2. 异步标签可以用在除了非模块的行内 <script>
  3. 模块只会被执行一次,但可以多处导出
  4. 模块具有自己的作用域
  5. 模块总是采用严格模式

大多数情况下我们并不会采用这种单纯的模块引入方式,而是为了更好的性能采用 webpack 等模块打包工具将各个模块打包成一个或多个文件。

详细使用方法
  1. 普通导出和导入
// count.js
export let count = 1;
export function fn() {
    alert("huro");
}
// 上述是导出后再声明变量,也可以先声明再导出
// count1.js
let count = 1;
let fn = () => alert("huro");
export {
	fn,
    count,
}

// index.js
import { count, fn } from './count.js'
  1. 导入全部

    // count.js
    export let count = 1;
    export function fn() {
        alert("huro");
    }
    
    // index.js
    import * as counter from './count.js'
    counter.count; // 1
    

    这种方法可以一次性将导出文件的东西获得,看起来很便捷,但是不推荐使用

    1. 更长的名字 count 将会书写为 counter.count

    2. 不利于打包工具进行优化

      // fn.js
      export function fn1(){}
      export function fn2(){}
      
      // index.js
      import { fn1 } from './fn.js'
      

      如果 fn1fn2 并没有依赖,借助打包工具的 tree-shaking 可能在打包的最后,就不会出现 fn2 函数,减少了没有必要的代码。

    3. 显式引入可以使模块之间的关系更为明确。

  2. 导入导出 as

可以对导入和导出的变量起别名

// import.js
import { count as c } from './export.js';
console.log(c);

// export.js
let c = 1;
export { c as count };
  1. 默认导出

一个模块只能有一个默认导出

// user.js
export default class User{
    constructor(name) {
        this.name = name;
    }
}

// index.js
import User from './user.js';

实际上导入的时候可以任意取变量名

// index.js
import U from './user.js';

另外默认导出可以不用名称

如:

export default [1, 2];

但是普通导出就不可以

export [1, 2];

因为这样当引入的时候,不知道名称,无法引入。

  1. default 关键字

导出时

// user.js
class User{
    constructor(name) {
        this.name = name;
    }
}

export default User;
// 与下面一致
export {
	User as default;
}

导入时

import { default as User } from './user.js'

// or
import * as user from './user.js'
const User = user.default;
  1. 重复导出

在实际项目中可能需要重复导出让引入路径变短

如在 Components 目录下有许多的组件

这些组件都类似于

class Component {}
export default Component;

这个时候可以在 Components 下创建一个 index.js

注: 路径如果没有明确指定文件,则默认是该目录下的 index.js 文件

如: import xxx from './components'import xxx from './components/index.js' 相同

// index.js
export { default as NavBar } from './NavBar';
export { default as Menu } from './Menu';

// 注: 该文件下无法使用 NavBar 以及 Menu

这样在页面引入的时候就可以

import { NavBar, Menu } from './Components'

路径就会更加简短一些,而且引入的时候也更加明确,由于在 index.js 内设置了名字,这样在多个地方导入的时候,名字也会保持一致。

  1. 只能使用在全局作用域中
import * as User from './user.js' // ok

if (true) {
    import * as User from './user.js' // Error
}

function fn() {
    import * as User from './user.js' // Error
}
动态引入

exportimport 旨在为了代码结构提供支柱,不支持动态引入

import * from getModuleName();

由于设计的比较简单,代码将会更好被分析,也容易做 tree-shaking,是一个优点。

但是如果我们一定要使用动态引入呢?

可以采用 import() 表达式

import() 表达式返回一个 Promise

if (true) {
    const data = await import('./data.js')
}

可以根据需要动态引入。

需要注意的是

  1. 动态引入并不是一个执行函数,他与 super 类似

例如 super.name 表示父类的 name 属性而 super() 表示执行父类的构造函数,

由于不是一个函数,因此不能用函数的方法,例如 call, apply

  1. 动态引入表达式不需要 <script type="module"></script>
参考文献
  1. https://javascript.info/modules-intro
  2. https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值