JavaScript模块化-快速入门
前言
模块化是指根据用途或者逻辑将js代码分为多个js文件,且各个文件之间的数据相互隔离,互不影响。模块与模块之间可以通过导出和导入操作来共享或获取模块中想要的数据和功能。
- 导出(暴露):模块公开其内部的变量和函数,通过导入和导出进行数据和功能的共享。
- 导入(引入):模块引用和使用其他模块导出的内容,以重用代码和功能。
为什么需要模块化开发呢?因为js早期是不存在模块化这个概念的,在实际开发过程中出现过如下问题:
- 全局类型污染(相同的变量名会相互覆盖)
- 依赖混乱(典型的jQuery和BootStrap的引入顺序问题)
- 数据安全问题(完整的数据被导出)
因此迫切需要使用模块化技术来解决上述问题。
模块化规范
出现过的模块化规范
- CommonJs(常用)
- AMD
- CMD
- ES6(常用)
随着 Node.js 的出现,JavaScript 不再局限于浏览器环境,而是开始进入服务器端编程领域。开发者们开始需要一个更好的方式来组织和重用代码。在浏览器环境中,通常使用 <script>
标签来加载脚本文件,这种方式对于服务器端环境来说并不适用,服务器端环境需要更灵活和强大的模块化支持。为了满足这一需求,由Mozilla工程师提出ServerJs,因此Nodejs社区制定了 CommonJS 规范。这个规范定义了一种标准的模块系统,允许开发者以文件的形式组织代码,并且可以在这些文件之间共享代码。因此CommonJs广泛用于服务端,也就是Nodejs环境(浏览器不直接支持这一规范)
后来ECMA官方推出了ES6模块化规范,同时可以兼容服务端和浏览器。
CommonJs规范
快速上手
新建student.js
、teacher.js
和index.js
三个js文件
1 | // student.js |
1 | // teacher.js |
1 | // index.js |
从上述代码中可以看出,导出使用的是exports
,导入使用的是require
导出数据
导出的方式有两种
- 使用
exports.name = value
- 使用
module.exports = value
注意点:
只要其他地方导入了当前的js文件,即使js内部没有导出,也会默认导出
{}
1
2
3
4
5
6// 假设student.js没有导出数据
// index.js
const student = require("./student.js");
const teacher = require("./teacher.js");
console.log(student) // 打印实际为{}module.exports
、this
、exports
都指向同一个{}
,最终导出以module.exports
为准导出的例子以如下代码为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// student.js
const name = "John";
const age = 25;
function getEmail() {
return "123456789@gmail.com";
}
function getSkills() {
return ["Python", "JavaScript", "Golang"];
}
exports = { a: 1 };
exports.b = 2;
module.exports.a = 3;
module.exports = { name, getEmail };
this.getSkills = getSkills;在index.js导入并打印
student
,得到的结果为1
{ name: 'John', getEmail: [Function: getEmail] }
exports
是module.exports
的一个引用,主要用于给导出对象添加属性值exports.name = value
导入数据
导入数据使用require
,可以将其作为一个对象接收(见快速上手),也可以使用解构的方式接收
1 | // student.js |
1 | // teacher.js |
1 | const {name,getEmail,getSkills} = require("./student.js"); |
上述代码展示了解构导入的命名冲突问题,有两种解决方法:
第二个对象不使用结构的方法,使用
const teacher = require("./teacher.js");
进行导入使用重命名的语法
1
2
3
4
5
6
7
8
9
10const {name:stuName,getEmail:stuEmail,getSkills} = require("./student.js");
const {name:teaName,getEmail:teaEmail,getSubjects} = require("./teacher.js");
console.log(stuName);
console.log(stuEmail());
console.log(getSkills());
console.log(teaName);
console.log(teaEmail());
console.log(getSubjects());
扩展理解
为什么我们能在js中直接使用非关键字module
、exports
?
因为在本质上来说,当前js文件是被包裹在另一个function
函数中的,示意如下图。
可以打印看到函数的全貌
1 | // teacher.js |
得到的打印结果
1 | function (exports, require, module, __filename, __dirname) { |
浏览器运行
问题展示
因为在诞生之初,CommonJs就是为服务端进行制定的(因此还有另一个称呼叫做ServerJs),所以浏览器并不支持这个规范,我们新建一个index.html
,尝试导入index.js
1 |
|
打开浏览器得到的报错提示如下
这也说明了浏览器并不支持CommonJs规范,需要使用额外的工具进行转化
转化工具
可以使用Browserify进行转化,将CommonJs的部分转化为浏览器支持的其他代码
安装Browerify
1 | npm i browserify -g |
编译
1 | browserify index.js -o build.js |
index.js是输入文件,build.js是输出文件
编译得到的结果为
1 | // build.js |
在html中引入build.js
,之后打开浏览器即可正常打印
ES6规范
这是ECMAScript官方制定的JavaScript模块化规范
快速上手
和之前一样创建index.js
、student.js
、teacher.js
三个js文件
1 | // student.js |
1 | // teacher.js |
1 | // index.js |
打印得到的结果为
1 | [Module: null prototype] { |
Node中运行ES6模块
Nodejs是无法直接运行ES6的模块化文件的,如果直接运行会出现报错。
d:\Web\module_test\index.js:1
import * as student from “./student.js”;
^^^^^^SyntaxError: Cannot use import statement outside a module
这时候有三种办法:
在
index.html
中引入,将script
标签的type
属性设置为module
1
2
3
4
5
6
7
8
9
10
11
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="./index.js"></script>
</body>
</html>使用
live Server
创建本地服务器,打开控制台可以看到如下输出将所有文件的后缀名改为
.mjs
,之后可直接在Nodejs中运行创建
package.json
文件,加上"type":"module"
,之后可直接在Nodejs中运行1
2
3{
"type": "module"
}
导出数据
ES6模块导出有三种方法:
- 分别导出
- 统一导出
- 默认导出
为了方便演示,导入模块使用的是万能写法
1
2
3
4
5 import * as student from "./student.js";
import * as teacher from "./teacher.js";
console.log(student);
console.log(teacher);
分别导出
在每个想要导出的变量前使用export关键字
1 | // student.js |
分别导出仅仅适用于要导出的数据相对较少的情况,而且分别导出也不能直观展示导出的所有数据和方法。
统一导出
统一导出能够一次性导出多个数据,和分别导出没有本质上的差别
1 | // teacher.js |
导出打印的结果为
1 | [Module: null prototype] { |
export后跟{},并不是说明导出的是一个对象,这从上述的打印结果中也可以看出,导出的数据和方法被包裹在默认的导出对象中。
默认导出
从分别导出和统一导出的打印结果可以看出,导出的数据都是统一放在一个对象中。
默认导出相比之前的两种导出方法得到的输出有所不同
1 | // student.js |
打印的输出为
1 | [Module: null prototype] { |
观察输出可以发现,使用export default
导出后得到的数据被包裹在输出对象的default
对象中,如果要进一步访问,则需要使用student.default.属性名
进行获取
混合使用
上述的三种方法可以混合使用
1 | // teacher.js |
得到的打印结果为
1 | [Module: null prototype] { |
从打印的结果来看,完全符合上述不同导出方法对应的规则。
导入数据
导入有很多中方法,但是每种方法都有对应的导出方式,要注意相互的配对。
导入全部
即万能导入
1 | // index.js |
这种方法是通用的,但是如果有模块使用的是默认导出,那么得到导出数据的路径会变长(比如student.default.name
)
命名导入
如果导出模块使用的是分别导出或统一导出(也可以两者混用),那么可以使用命名导入的方式
1 | // student.js |
1 | // index.js |
如果想要更换变量命名,使用name as newName
的方式
1 | // index.js |
默认导入
如果导出模块使用的是默认导出,那么可以使用默认导入的形式,直接给导入的模块命一个模块名即可
1 | // teacher.js |
1 | // index.js |
命名和默认混用
如果出现导出方式混用,可以使用对应的导入方法进行一次性的导入
1 | // teacher.js |
1 | // index.js |
动态导入
动态导入即在需要的情况下才导入模块
1 | // index.js |
当count达到要求后,导入student.js模块并打印,打印的结果如下
1 | [Module: null prototype] { |
导入不接收任何数据
如果不需要接收任何数据,那么可以直接导入模块地址,形如import "模块地址"
1 | // teacher.js |
1 | // index.js |
上面的例子,每次运行index.js都会打印出一个随机数
关于问题解决的理解
那么通过模块化是否解决了开头提出的三个问题呢?
- 全局类型污染(相同的变量名会相互覆盖)
- 依赖混乱(典型的jQuery和BootStrap的引入顺序问题)
- 数据安全问题(完整的数据被导出)
个人的理解如下:
全局污染的问题已经可以通过在导入时重命名的方法防止同名变量的覆盖
1
2
3
4
5
6
7
8
9
10import { name as stuName, age as stuAge, getEmail as stuGetEmail } from "./student.js";
import {name as teaName,age as teaAge,getEmail as teaGetEmail} from "./teacher.js";
console.log(stuName); // John
console.log(stuAge); // 25
console.log(stuGetEmail()); // 123456789@gmail.com
console.log(teaName); // Tom
console.log(teaAge); // 35
console.log(teaGetEmail()); // 79898989898@gmail.com依赖混乱问题的解决可以通过下图来论证(假设存在如下图的相互依赖关系)
可以看到,各个模块不再需要考虑有关导入顺序的问题,当模块需要其他模块作为依赖时,只需关注如何导入该依赖模块,而不需要注意导入的顺序。
数据安全问题
以
student.js
、index.js
为例1
2
3
4
5
6
7
8
9
10
11
12// student.js
const name = "John";
const age = 25;
export function getEmail() {
return "123456789@gmail.com";
}
function getSkills() {
return ["Python", "JavaScript", "Golang"];
}
export { name, age };1
2
3
4
5
6
7// index.js
import { name, age, getEmail } from "./student.js";
// import teacher, { getEmail } from "./teacher.js";
console.log(name);
console.log(age);
console.log(getEmail());在index.html中引入index.js
1
2
3
4
5
6
7
8
9
10
11
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="./index.js"></script>
</body>
</html>打开浏览器,尝试获取
name
、age
、getEmail
,除了name
,发现返回的都是xxx is not defined
,这已经说明解决了数据安全问题,在没有导出数据的情况下,是无法获取到数据的。(实际上,这就是module方式带来的便利之处)name可以返回一个”“的原因是window默认自带name属性,且默认值就为”“
数据引用问题
commonjs和es6处理导出数据的隔离方式有所不同,还要从下面的例子说起
1 | function count() { |
可以看出解构出的sum和函数内的sum值地址已经完全不同,相当于将函数内的sum复制了一份给解构的sum
CommonJs也符合上述的规则
1 | // teacher.js |
1 | // index.js |
但是ES6的模块化规范却截然相反
1 | // teacher.js |
1 | // index.js |
最终打印得到的sum
值为3,这说明导出模块与倒入模块使用的sum
是同一个内存地址,修改一次则处处变化,这可能会导致一些意想不到的问题。
因此在使用ES6的模块化规范下,最好是将导出的数据设置为常量(const
)