一个模块仅仅就是一个文件。模块之间可以通过一些特殊的库等来引用或导出。
由于大部分的代码是关于修改变量的,如何组织变量会影响编码的质量,以及维护他们的成本。
如果只有少数变量,完全可以通过函数作用域解决。但是这也让我们很难在各个函数之间共享变量。
例如在曾经的 jQuery
时代,在加载任何 jQuery
插件之前,我们必须先保证引入了 jQuery
,这个时候所有的 标签必须以正确的顺序排列,否则将会抛出错误。
当依赖越来越多的时候, 标签的引入成了令人头疼的事,根本原因是代码之间的依赖是不透明的,而且由于变量存在于全局作用域,任何代码都可以改变它,导致程序出现
BUG
当 变得更加的复杂的时候,社区创建了许多方式去组织代码。
AMD
最古老的模块化系统之一,最初由 require.js
库实现
CommonJS
用于 Node.js
模块化
UMD
与 AMD 和 CommonJS 兼容的模块系统。
上述的这些模块化方式正在逐渐变为历史,ES6
后出现了标准的模块化语法 import
和 export
被称之为 ES modules
,目前已经被大多数主流浏览器支持。本篇文章只讨论 ES modules
count.js
与 index.html
在同个目录下
// count.js
let count = 1;
export {
count };
<script type="module">
import {
count } from './count.js'
console.log(count);
script>
import
指令用相对路径加载一个文件 count.js
将 count
取出并赋值给当前模块的变量 count
注: 模块只在 http(s)
下有效,而 file://
协议无效,因此不能直接本地打开,如果你用的是 vscode
可以使用 live server
插件开启本地服务运行上述代码。
<script type="module">
a = 5; // a is not defined
script>
注: 下列三个文件在一个目录下
// 1.js
let count = 1;
// 2.js
count = 2; // count is not defined
<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;
<script type="module" src="./2.js">script>
如果一定要共享变量,可以对 window
进行赋值 window.count = 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!")
}
import.meta
// index.html
<script type="module">
alert(import.mata.url); // 文件的地址 例如 localhost:3000/index.html
script>
this
is undefined
<script>
alert(this); // window
script>
<script type="module">
alert(this); // undefined
script>
module
script
总是被延迟执行<script type="module">
alert(typeof button); // object
script>
<script>
alert(typeof button); // undefined
script>
<button id="button">
Button
button>
总的来说
html
已经完全准备好了,才会执行
的脚本,将不会阻塞 html
的渲染,而是和其他资源一同被加载。由于 html
文档会被先渲染好,这个时候展现给用户的可能是没有经过 script
脚本的 html
文档,因此应该给予 loading
提示给用户。
异步属性 async
只适用于
而不适用于非模块的内嵌 script
一旦所需资源加载完毕,代码就会立刻运行。
<script async type="module">
import {
counter } from './count.js' // 这个加载完毕,马上运行代码
counter.count();
script>
异步模块和 html
与其他 script
是独立的。
<script type="module" src="./index.js">script>
<script type="module" src="./index.js">script>
import {
count } from 'count.js'; // no
import {
count } from './count.js'; // yes
总结:
内大多数情况下我们并不会采用这种单纯的模块引入方式,而是为了更好的性能采用 webpack
等模块打包工具将各个模块打包成一个或多个文件。
// 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'
导入全部
// count.js
export let count = 1;
export function fn() {
alert("huro");
}
// index.js
import * as counter from './count.js'
counter.count; // 1
这种方法可以一次性将导出文件的东西获得,看起来很便捷,但是不推荐使用
更长的名字 count
将会书写为 counter.count
不利于打包工具进行优化
// fn.js
export function fn1(){
}
export function fn2(){
}
// index.js
import {
fn1 } from './fn.js'
如果 fn1
对 fn2
并没有依赖,借助打包工具的 tree-shaking
可能在打包的最后,就不会出现 fn2
函数,减少了没有必要的代码。
显式引入可以使模块之间的关系更为明确。
导入导出 as
可以对导入和导出的变量起别名
// import.js
import {
count as c } from './export.js';
console.log(c);
// export.js
let c = 1;
export {
c as count };
一个模块只能有一个默认导出
// 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];
因为这样当引入的时候,不知道名称,无法引入。
导出时
// 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;
在实际项目中可能需要重复导出让引入路径变短
如在 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
内设置了名字,这样在多个地方导入的时候,名字也会保持一致。
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
}
export
和 import
旨在为了代码结构提供支柱,不支持动态引入
import * from getModuleName();
由于设计的比较简单,代码将会更好被分析,也容易做 tree-shaking
,是一个优点。
但是如果我们一定要使用动态引入呢?
可以采用 import()
表达式
import()
表达式返回一个 Promise
if (true) {
const data = await import('./data.js')
}
可以根据需要动态引入。
需要注意的是
super
类似例如 super.name
表示父类的 name
属性而 super()
表示执行父类的构造函数,
由于不是一个函数,因此不能用函数的方法,例如 call
, apply