模块化

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

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

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

实现原理

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

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

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

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
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("ha", [], function () {
return {
show() {
console.log("ha module show")
}
}
})

//声明模块时依赖其它模块
module.define("xj", ["ha"], function (ha) {
ha.show()
})

基础知识

标签使用

在浏览器中使用以下语法靠之脚本做为模块使用,这样就可以在里面使用模块的代码了

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

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

模块路径

在浏览器中引用模块必须添加路径如./ ,但在打包工具如webpack中则不需要,因为他们有自己的存放方式

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

1
2
3
export function two (){
alert("恭喜成功")
}

使用需要添加上路径

1
2
3
<script type="module">
import {two} from "./demo"
</script>

严格模式

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

1
2
3
<script type="module">
demo = "你好" // 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 he = "hahhah";
</script>

<script type="module">
alert(he); // 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 he = "hahhah";

# 1.2.js
console.log(he)

预解析

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

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

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

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

#hd.js内容如下
console.log("DE");

在导入多次 hd.js 时只解析一次

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

# hd.js内容如下
console.log("eeeeee");

导入导出

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

  • 使用export 将开发的接口导出
  • 使用import 导入模块接口
  • 使用*可以导入全部模块接口
  • 导出是以引用方式导出,无论是变量还是对象,即模块内部变量发生变化将影响已经导入的变量

导出模块

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

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

1
2
3
4
5
6
7
8
9
export const site = "陈若";
export const func = function() {
return "is a module function";
};
export class User {
show() {
console.log("user.show");
}
}

下面定义了two.js 模块,并使用指量导出

1
2
3
4
5
6
7
8
9
10
const site = "陈若"
const func = function () {
return "is a module function"
}
class User {
show() {
console.log("user.show")
}
}
export { site, func, User }

具名导入

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

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

批量导入

如果要导入的内容比较多,可以使用 * 来批量导入

1
2
3
4
5
<script type="module">
import * as app from "./demo.js"
console.log(app.site)
console.log(app.func)
</script>

导入建议

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

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

别名使用

导入别名

可以为导入的模块重新命名,下面是为了测试定义的 two.js 模块内容。

  • 有些导出的模块命名过长,起别名可以理简洁
  • 本模块与导入模块重名时,可以通过起别名防止错误

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

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

导出别名

模块可以对导出给外部的功能起别名,下面是two.js 模块对导出给外部的模块功能起了别名

1
2
3
4
5
6
7
8
9
10
const site = "陈若"
const func = function () {
return "is a module function"
}
class User {
show() {
console.log("user.show")
}
}
export { site as name, func as hahah, User }

这时就要使用新的别名导入了

1
2
3
4
<script type="module">
import { name, hahah } from "./hd.js";
action();
</script>

默认导出

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

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

  • 可以为默认导出自定义别名
  • 只能有一个默认导出
  • 默认导出可以没有命名

单一导出

下面是two.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 "./two.js";
User.show();
</script>

默认导出的功能可以使用任意变量接收

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

混合导出

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

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

1
2
3
4
5
6
7
8
9
10
const site = "陈若"
const func = function () {
return "is a module function"
}
export default class {
show() {
console.log("user.show")
}
}
export { site as name, func as hahah }

可以使用以下方式导出模块

1
2
3
4
5
6
7
8
9
10
const site = "陈若"
const func = function () {
return "is a module function"
}
class User {
show() {
console.log("user.show")
}
}
export { site as name, func as hahah, User as default}

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

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

使用建议

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

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

    1
    2
    import two from "./two.js";
    import xj from "./two.js";
  • 如果使用默认导入最好以模块的文件名有关联,会使用代码更易阅读

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

导出合并

解决问题

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//two.js
const site = "陈若"
let demo = function(){
console.log("你好呀")
}
export {site , demo}
//one.js
export default class {
static get (){
console.log("哈哈哈哈")
}
}
//index.js
export * as two from "./two.js"
// 默认模块需要单独导出
export {default as demo1} from "./one.js"

<script type="module">
import * as app from "./index.js"
console.log(app)
app.demo1.get()
console.log(app.two.site)
</script>

动态加载

使用 import 必须在顶层静态导入模块,而使用import() 函数可以动态导入模块,它返回一个 promise 对象

静态导入

使用 import 顶层静态导入,像下面这样在 {} 中导入是错误的,这是为了分析使用的模块方便打包,所以系统禁止这种行为

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

动态使用

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

1
2
3
4
5
let site = "陈若"
let demo = function(){
console.log("你好呀")
}
export {site , demo}

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

1
2
3
4
5
6
7
<script>
if(true){
let demo = import("./one.js").then(module => {
console.log(module.site)
})
}
</script>

在点击事件发生后按需要加载模块,案例

1
2
3
4
5
6
7
8
9
10
<body>
<button>点击一下试试</button>
<script>
document.querySelector("button").addEventListener("click", () => {
let hd = import("./one.js").then(module => {
console.log(module.site);
})
})
</script>
</body>

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

1
2
3
4
5
6
7
8
9
10
<body>
<button>点击一下试试</button>
<script>
document.querySelector("button").addEventListener("click", () => {
let hd = import("./one.js").then(({ site, func }) => {
console.log(site)
})
})
</script>
</body>

指令总结

表达式说明
export function show(){}导出函数
export const name=’xxx’导出变量
export class User{}导出类
export default show默认导出
const name = ‘xxx’ export {name}导出已经存在变量
export {name as chen_name}别名导出
import defaultVar from ‘one.js’导入默认导出
import {name,show} from ‘demo.j’导入命名导出
Import {name as haName,show} from ‘two.js’别名导入
Import * as api from ‘demo.js’导入全部接口