一、原始模块化的问题
- 早期的网页中,没有实质的模块化规范,当时实现模块化的方式,就是原始地通过script标签引入多个js文件
- 例如:引入jquery是直接引入一个js文件<script src="./jquery.js"></script>
,$中有jquery中所有的 功能
- 问题:
- 无法选择要引入这个模块的哪些内容(一引全引)
- 在复杂的模块常见下容易出错(引入顺序)
- 解决方法:第三方的模块化规范CommonJS、ES官方的模块化规范(2015年)
二、CommonJS
- 2009年,Nodejs采用CommonJS模块化
- 直到2015年,ES官方才推出模块化规范
- Node.js支持两种模块化,默认支持的模块化规范是CommonJS,在CommonJS中,一个JS文件就是一个模块
1. 定义一个模块:
- 定义一个模块时,模块中的内容默认不能被外部看到(CommonJS就是一个闭包),但是可以通过
exports
设置要暴露出去的内容,exports其实就是一个对象
- 访问exports的方式有两种
console.log(exports === module.exports)
- 当我们在其他模块引入当前模块时,require(“”)函数的返回值就是exports对象,因此可以将要暴露的数据设置为exports的属性
exports.a = "孙悟空"
exports.b = {
name:"猪八戒",
age:28,
gender:"男"
}
exports.c = function fn(){
console.log("哈哈")
}
- 可以通过
module.exports = {}
给exports对象赋值
修改值:此后修改暴露的内容时,相当于在修改modul对象的exports属性,那么所有指向exports对象的都会受到影响;exports变量存储的地址不变,该地址指向的对象的内容变化了,因此其他引入exports的地方(它们的地址永远不变),能看到修改后的内容
- 不可以通过
exports = {}
给exports对象赋值
修改地址:此后修改暴露的内容时,相当于重新给exports变量赋值,改变量,只会影响该变量本身;此时exports存储的地址变了,而其他引入exports的地方,它们存储的地址不变,因此它们看不到任何修改后的内容
// 修改module对象的exports属性
// 所有引入该模块(module.exports)的地方都会受到影响
module.exports = {
a : "孙悟空",
b : {
name:"猪八戒",
age:28,
gender:"男"
},
c : function fn(){
console.log("哈哈")
}
}
// 这是在给exports重新赋值
// 修改exports变量,只会影响exports变量自身,其他引入了该模块的模块不会受到影响
exports = {
a:xxx,
b:xxx,
c:xxx
}
2. 引入一个模块:
- 使用
require("模块的路径")
函数来引入模块
- 引入自定义模块时,模块名要以
../
或者./
开头(即模块的路径是相对路径) - 可以省略扩展名
.js
(node会自动为文件补全扩展名,先补全.js,再.json,再.node)
const m1 = require("./m1")
// 完整写法是"m1.js"
- 引入核心模块(第三方模块)时,
- 直接写核心模块的名字即可,例如
const path = require("path") //引入node内置的path模块
- 可以在核心模块名字前面加
node:
,例如const path = require("node:path")
- Node.js会将以下内容视为CommonJS模块:
- 扩展名为
.cjs
时,遵循CommonJS的模块化标准;m1.js文件
m1.cjs文件
- package.json文件的
"type":"commonjs"
时,扩展名为.js的文件也遵循CommonJS模块化- 一个名为hello文件夹为模块时,
require("./hello")
相当于require("./hello/index.js")
,hello文件夹的默认入口文件是index.js- 文件扩展名为mjs、cjs、json、node、js以外的值时(type属性不是module时)
-
require()
是同步加载模块的方法,所以无法用来加载ES6的模块(加载ES模块,需要用import()
方法来加载) -
默认情况下,Node使用CommonJS模块化标准,若想使用ES的模块化标准,有两个方法:
- 把文件扩展名改为.mjs
- 修改package.json文件,修改属性type为
"type":"module"
console.log(module)
在CommonJS标准下module才有值
3. CommonJS运行原理
- Node将所有的CommonJS模块封装到一个立即执行函数中,该函数有五个参数
(function(exports, require, module, __filename, __dirname){
// 模块代码会被放在这里
let a = 10;
const b = { }
/*
console.log(arguments)
得到exports, require, module, __filename, __dirname
*/
})
- 怎么证明这一点?找到一个只有函数里有的东西——arguments实参,一个类数组
_ _filename
表示当前模块的绝对路径_ _dirname
表示当前模块所在目录的绝对路径module
是一个对象,里面有exports属性
require
是一个函数
4. 总结
CommonJS记住 怎么导出module.exports 和 怎么导入require 就行了
-
不涉及别名,因为导入时是用变量接收的
const a = require("./xxx")
-
如果只想导入其中某个变量呢
- 要么按需引入
const a = require("./m3").name
- 要么解构赋值
const {name,gender} = require("./m3")
(将一个对象的属性拆解给对应的变量,左边的name、gender是变量,因为key和value一致,省略了value)
- 要么按需引入
三、ES模块化
让Node遵循ES模块化标准:
1. 模块的导出export和引入import
注意:
-
自定义模块中,无论文件的扩展名为
.mjs
或.js
,ES模块都不能省略扩展名(官方标准)webpack是打包工具,会自动帮助补充扩展名
-
核心模块中,
1.1命名导出export以及引入{}:
- 使用 export 关键字将模块中的变量、函数或类进行命名导出
- 引入时
{}
里的名字必须 === 导出时的名字,或者import {a as hello} from "./m1.mjs"
- 引入全部变量、函数、类:
import * as m4 from "./m4.mjs"
(全部引入的话,项目会臃肿)
import {a,b,c,d……} from "./m4.mjs"
//从模块里导入a,b,c,d(名字必须跟定义时一致)
// 相当于把这个模块导入成一个对象,对这个对象进行解构赋值
import {a as hello, b, c} from "./m4.mjs"
// 利用as可以起别名
import * as m from "./m4.mjs"
// 把m4.mjs模块的所有东西引进来,统一放在m这个对象中
// 开发时尽量避免这种情况
- 导出单个变量或常量
// 导出单个变量
export const myVariable = 42;
// 也可以在声明时直接导出
const myVariable = 42;
export { myVariable };
import { myVariable } from './moduleA';
- 导出多个变量或常量
export const var1 = 'foo';
export const var2 = 'bar';
import { var1, var2 } from './moduleB';
- 导出函数或类:
// 导出单个函数
export function myFunction() { }
// 或者
function myFunction() {}
export { myFunction };
// 导出单个类
export class MyClass { }
// 或者
class MyClass { }
export { MyClass };
import { myFunction, MyClass } from './moduleC';
1.2 默认导出(export default)以及引入
-
一个模块只有一个默认导出(export default只有一个)
可以将一个值、函数、类或对象作为默认导出
-
引入其他模块的"默认导出"时,可以随意命名
import xxx from "./m1.mjs"
-
对于 ES6 模块中的默认导出(Default Export),通过 export default 语法将要导出的内容赋值给 default。这样在导入模块时,可以使用 import 语句指定一个名称,它会引用被赋值给 default 的内容。
-
通过 ES模块化 导入的内容都是常量,不能直接修改常量(即不能直接修改常量存储的地址值),但是常量并不影响对象的修改
import {c} from "./m4.mjs"
c.name = "猪八戒" // c存储的地址没有变,但是地址指向的对象里的属性name,变了
console.log(c)
// 发现之前模块里定义的对象的name由"孙悟空"变成了"猪八戒"
export default function sum(a,b){}
// import sum,{a,b,c} from "./m4.mjs" 默认导出不要写在中括号里
// 默认导出后面需要一个值,而不是语句(export default let d = 20)
// 这样的默认导出才是正确的
let d
export default d = 20
- ES模块都是运行在严格模式下(即,只要启动ES模块,就默认开启了严格模式)
- 浏览器中也支持ES模块化,但是我们通常不会直接使用(要考虑兼容性问题),通常都会结合打包工具