0%

模块规范

使用分析

项目变大时需要把不同的业务分割成多个文件,这就是模块的思想。模块是比对象与函数更大的单元,使用模块组织程序便于维护与扩展。

生产环境中一般使用打包工具如 webpack 构建,他提供更多的功能。但学习完本章节后会再学习打包工具会变得简单。

  • 模块就是一个独立的文件,里面是函数或者类库
  • 虽然JS没有命名空间的概念,使用模块可以解决全局变量冲突
  • 模块需要隐藏内部实现,只对外开发接口
  • 模块可以避免滥用全局变量,造成代码不可控
  • 模块可以被不同的应用使用,提高编码效率

实现原理

在过去JS不支持模块时我们使用AMD/CMD(浏览器端使用)CommonJS(Node.js使用)UMD(两者都支持)等形式定义模块。

AMD代表性的是 require.js,CMD 代表是淘宝的 seaJS 框架。

下面通过定义一个类似 require.jsAMD 模块管理引擎,来体验模块的工作原理。

仿写 AMD 规范构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
let module = (function () {
//模块列表集合
const moduleLists = {};
function define(name, modules, action) {
modules.map((m, i) => {
modules[i] = moduleLists[m];
});
//执行并保存模块
moduleLists[name] = action.apply(null, modules);
}

return { define };
})();

//声明模块时不依赖其它模块
module.define("as", [], () => {
return {
getName() {
return this.name;
},
};
});
//声明模块时依赖其它模块
module.define("shun", ["as"], (modules) => {
let curr = {
getAge() {
return this.age;
},
name: "shun",
};
console.log(modules.getName.call(curr));

return curr;
});

module.define("SHUN", ["as", "shun"], (...modules) => {
let curr = {
show() {
console.log(this.name, this.age);
},
name: "Ashun",
age: 18,
};
let merge = Object.assign(...modules, curr);
console.log(merge);

console.log(merge.getName());
return merge;
});

基础知识

标签使用

在浏览器中使用以下语法 靠Javascript脚本提供的模块化规范 管理各个js模块,这样就可以在js文件中使用模块代码了。

在html文件中导入模块,需要定义属性 type="module"

1
<script type="module"></script>

在浏览器中运行js模块化规范管理的模块 在引入时,必须填写正确的路径 如./xxx.js

测试的 as.js 的模块内容如下

1
2
3
export let as = {
name: "阿顺"
};

下面没有指定路径将发生错误

1
2
3
<script type="module">
import { as } from "as.js";
</script>

正确使用需要添加上路径

1
2
3
<script type="module">
import { as } from "./as.js";
</script>

延迟解析

模块总是会在所有html解析后才执行,下面的模块代码可以看到后加载的 button 按钮元素。

  • 建议为用户提供加载动画提示,当模块运行时再去掉动画
1
2
3
4
5
6
7
8
9
<body>
<script type="module">
console.log(document.querySelector("button")); //Button
</script>
<script>
console.log(document.querySelector("button")); //undefined
</script>
<button>阿顺特烦恼</button>
</body>

默认严格

模块默认运行在严格模式,以下代码没有使用声明语句将报错

1
2
3
<script type="module">
as = "Ashuntefannao"; // Error
</script>

下面的 this 也会是 undefined

1
2
3
4
5
6
<script>
console.log(this); //Window
</script>
<script type="module">
console.log(this); //undefiend
</script>

独立作用域

模块都有独立的顶级作用域,下面的模块不能互相访问

1
2
3
4
5
6
7
<script type="module">
let as = "Ashuntefannao";
</script>

<script type="module">
alert(as); // Error
</script>

引入单独文件,作用域也是独立的,下面的模块 1.2.js 不能访问模块 1.1.js 中的数据

1
2
3
4
5
6
7
8
9
<script type="module" src="1.1.js"></script>
<script type="module" src="1.2.js"></script>

文件内容如下
# 1.1.js
let as = "Ashuntefannao";

# 1.2.js
console.log(as)

一次解析

模块在导入时只执行一次解析,之后的导入不会再执行模块代码,而使用第一次解析结果,并共享数据。

  • 可以在首次导入时完成一些初始化工作
  • 如果模块内有后台请求,也只执行一次

引入多入as.js 脚本时只执行一次

1
2
3
4
5
<script type="module" src="./as.js"></script>
<script type="module" src="./as.js"></script>

# as.js内容如下
console.log("阿顺特烦恼");

下面在导入多次 as.js 时只解析一次

1
2
3
4
5
6
7
<script type="module">
import "./as.js";
import "./as.js";
</script>

# as.js内容如下
console.log("阿顺特烦恼");

导入导出

ES6使用基于文件的模块,即一个文件一个模块。

  • 使用export 将开发的接口导出
  • 使用import 导入模块接口

具名导出

下面定义模块 modules/as.js ,使用 export 具名导出模块接口,没有导出的变量都是模块私有的。

下面是对定义的 as.js 模块,分别导出内容

1
2
3
4
5
6
7
8
9
10
let title="阿顺特烦恼";
export const site = "ashuntefannao";
export const func = function() {
return "is a module function";
};
export class User {
show() {
console.log("user.show");
}
}
批量导出

也可在定义被导出变量后,使用批量导出一次性导出多个变量接口,export{…options}

1
2
3
4
5
6
7
8
9
10
11
let title="阿顺特烦恼";
const site = "ashuntefannao";
const func = function() {
return "is a module function";
};
class User {
show() {
console.log("user.show");
}
}
export { site, func, User };

具名导入

具名导入具名导出相对应,具名导出的模块接口,需要具名导入。

下面导入上面定义的 as.js 模块,分别导入模块导出的内容

1
2
3
4
5
<script type="module">
import { User, site, func } from "./as.js";
console.log(site);
console.log(User);
</script>

也可只导入部分变量接口,需要哪些导入哪些,当然了 所导入的变量接口,必须被所引入的模块导出。

1
2
3
4
<script type="module">
import { User } from "./as.js";
console.log(User);
</script>

像下面这样在 {} 中导入是错误的,模块默认是在顶层静态导入,这是为了分析使用的模块,方便打包

1
2
3
if (true) {
import { site, func } from "./as.js"; // Error
}
批量导入

也可使用批量导入语法import * as alias ,一次性将所有暴露的接口导入,并为批量导入的接口起一个别名,之后通过别名来访问批量导出的对象。

  • *整体为一个Object,为其设置别名后通过alias.propName访问导出的具体变量
1
2
3
4
5
<script type="module">
import * as api from "./hd.js";
console.log(api.site);
console.log(api.User);
</script>

导入建议

因为以下几点,我们更建议使用明确导入方式

  • 使用webpack 构建工具时,没有导入的功能会删除节省文件大小
  • 可以更清晰知道都使用了其他模块的哪些功能

别名使用

别名导入

可以为导入的模块重新命名,语法: import { oldName as newName …}

应用场景

  • 有些导出的模块命名过长,起别名可以更加简洁
  • 本模块与导入模块 变量重名时,可以通过起别名防止冲突
1
2
3
4
5
6
7
8
9
10
11
let title="阿顺特烦恼";
const site = "ashuntefannao";
const func = function() {
return "is a module function";
};
class User {
show() {
console.log("user.show");
}
}
export { site, func, User };

模块导入使用 as 对接口重命名,本模块中已经存在 func 变量,需要对导入的模块重命名防止命名错误。

1
2
3
4
5
6
7
<script type="module">
import { User as user, func as method, site as name } from "./as.js";
let func = "ashunMethod";
console.log(name);
console.log(user);
console.log(method);
</script>

别名导出

模块可以对 暴露给外部的接口 起别名,下面是as.js 模块对暴露的接口起别名

1
2
3
4
5
6
7
8
9
10
11
let title="阿顺特烦恼";
const site = "ashuntefannao";
const func = function() {
return "is a module function";
};
class User {
show() {
console.log("user.show");
}
}
export { site, func as method, User as user };

由于模块暴露时,为其接口起了别名,所以在具名导入时,应导入对应的别名。

1
2
3
4
<script type="module">
import { user, method } from "./as.js";
method();
</script>

默认导出

很多时候模块只是一个类,也就是说只需要导入一个内容,这地可以使用默认导入。

使用default关键字 定义默认导出的接口,导入时不需要使用 {}

  • 只能有一个默认导出

  • 默认导出可以没有命名

  • 在导入时可以任意命名

单一导出

下面是as.js 模块内容,默认只导出一个类。并且没有对类命名,这是可以的

1
2
3
4
5
export default class {
static show() {
console.log("User.method");
}
}

如果将一个导出的接口 分配别名为 default 也算默认导出

1
2
3
4
5
6
class User {
static show() {
console.log("User.method");
}
}
export { User as default };

导入时就不需要使用 {} 来导入了

1
2
3
4
<script type="module">
import User from "./hd.js";
User.show();
</script>

默认导出的接口 在引入时 可以随意命名

1
2
3
4
<script type="module">
import as from "./hd.js";
as.show();
</script>

混合导出

模块可以存在默认导出与命名导出。

使用export default 导出默认接口,使用 export {} 导入普通接口

1
2
3
4
5
6
7
8
9
10
11
let title="阿顺特烦恼";
const site = "ashuntefannao";
const func = function() {
return "is a module function";
};
export default class {
show() {
console.log("user.show");
}
}
export { site, func };

也可以结合别名oldName as default综合导出各个接口

1
2
3
4
5
6
7
8
9
10
11
let title="阿顺特烦恼";
const site = "ashuntefannao";
const func = function() {
return "is a module function";
};
class User {
show() {
console.log("user.show");
}
}
export { site, func, User as default };

导入默认接口时不需要使用 {} ,普通接口还用 {} 导入

1
2
3
4
5
6
7
<script type="module">
//可以将 as 替换为任何变量
import as from "./as.js";
import { site } from "./as.js";
console.log(site);
as.show();
</script>

可以使用一条语句导入默认接口与常规接口,使用,隔开

1
import show, { name } from "/modules/ashun.js";

也可以使用别名导入 默认导出的接口

  • default as 自定义名称,之后通过 自定义的变量 访问默认导出接口
1
2
3
import { site, default as as } from "./as.js";
console.log(site);
as.show();

如果是批量导入时,使用 alias.default 获得默认导出

1
2
3
4
5
<script type="module">
import * as api from "./as.js";
console.log(api.site);
api.default.show();
</script>

使用建议

对于默认导出和命名导出有以下建议

  • 不建议使用默认导出,会让开发者导入时随意命名

    1
    2
    import as from "./as.js";
    import shun from "./as.js";
  • 如果使用默认导入,自定义的名称最好和 模块的文件名 有关联,会使用代码更易阅读

    1
    import as from "./as.js";

导出合并

解决问题

可以将导入的模块重新导出使用,比如项目模块比较多如下所示,这时可以将所有模块合并到一个入口文件中。

这样只需要使用一个模块入口文件,而不用关注多个模块文件

1
2
3
4
|--test1.js
|--test2.js
|--test3.js
...

实际使用

下面是 test1.js 模块内容

1
2
3
4
5
const site = "阿顺特烦恼";
const func = function () {
console.log("is a module function");
};
export { site, func };

下面是 test2.js 模块内容

1
2
3
4
5
export default class {
static get() {
console.log("ashun.js.get");
}
}

下面是 test3.js 模块内容

1
2
3
export function method() {
console.log("test3 method");
}

下面是 index.js 模块内容,由于我们想让index.js为入口文件,所有的模块都存放其中,暴露给外部。ES模块化规范提供了对应的语法,在index.js导入其他模块的同时也将其导出

  • importexport合并使用,在导入的同时进行导出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export * as as from "./test1.js";
//相当于以下语句
// import * as as from "./test1.js"
// export { as }

export { default as shunClass } from "./test2.js";
// 以上导入test2的语句相当于下两句
// import { default as shunClass } from "./test2.js";
// export { shunClass };
// 也等价于
// import shunClass from "./test2.js";
// export { shunClass };

// 但以下方式导出 test2默认导出的模块 是错误的
// export shunClass from "./test3.js";

// 在index.js中,默认导出test3中的method。
export { method as default } from "./test3.js";
// 相当于以下语句
// import {method} from "./test3.js"
// export {method as default}

使用方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script type="module">
import * as api from "./index.js";
console.log(api.shunClass);
console.log(api.as.site);
api.as.func();
api.default();
</script>
//等价于
<script type="module">
import test3Method, { as, shunClass } from "./index.js";
console.log(shunClass);
console.log(as.site);
test3Method();
</script>
//等价于
<script type="module">
import { as, shunClass, default as test3Method } from "./index.js";
console.log(shunClass);
console.log(as.site);
test3Method();
</script>

动态加载

使用 import 关键字 必须在顶层静态导入模块,而使用import("path") 函数可以按需动态的导入模块,它返回一个 promise 对象。

静态导入

必须在顶层静态导入,否则会报错

1
2
3
if (true) {
import { site, func } from "./as.js"; // Error
}

按需动态导入

测试用的 as.js 模块内容如下

1
2
3
4
5
const site = "阿顺特烦恼";
const func = function() {
console.log("is a module function");
};
export { site, func };

使用 import("path") 函数可以动态导入,实现按需加载

  • import()返回promise,可以使用promise方法在导入模块后进行操作
  • import("path").then(module=>{}).catch(err=>{})
1
2
3
4
5
6
7
<script>
if (true) {
let as = import("./as.js").then(module => {
console.log(module.site);
});
}
</script>

下面是在点击事件发生后按需要加载模块

1
2
3
4
5
6
7
8
<button>阿顺特烦恼</button>
<script>
document.querySelector("button").addEventListener("click", () => {
let as = import("./as.js").then(module => {
console.log(module.site);
});
});
</script>

因为是返回的对象可以使用解构语法

1
2
3
4
5
6
7
8
<button>后盾人</button>
<script>
document.querySelector("button").addEventListener("click", () => {
let hd = import("./hd.js").then(({ site, func }) => {
console.log(site);
});
});
</script>

指令总结

表达式 说明
export function show(){} 具名导出函数
export const name=’阿顺’ 具名导出变量
export class User{} 具名导出类
export { show , name , User } 批量具名导出
export default show 默认导出
export default show …… export{ name , User } 混合导出
export { show as default , name , User } 批量混合导出
export {name as shun_name} 别名导出
import {name,show} from ‘as.js’ 具名导入
import defaultVar from ‘ashun.js’ 导入默认导出
import defaultVar,{name,show} from 'as.js' 混合导入
import {name , show , default as defaultVar} from 'as.js' 混合导入
Import { name as asName , show } from ‘ashun.js’ 别名导入
Import * as api from ‘ashun.js’ 导入全部接口

导入并导出

表达式 说明
export { name, site } from “./as.js” 将as.js中的name、site具名导入并导出
export { name as asName , site } from “./as.js” 将as.js中的name别名导入导出;site具名导入并导出
export { name as default } from “./as.js” 将as.js中的name具名导入再默认导出
export { default as defaultVar } from “./as.js” 将as.js中的默认导出进行导入,再以defaultVar具名导出
export * as asAPI from “./as.js”; 将as.js中暴露的接口合并导入,再以asAPI具名导出