模块化
模块化方案现在常用的有两种,一种是CommonJS规范,一种是2015年ES6推出的ES Modules。在node当中每一个js文件都是一个单独的模块。
node默认使用CommonJS规范,但是可以配置使用ES6的模块化规范。或者将文件后缀改成
mjs也可以时使用ES6规范。文件后缀cjs是CommonJS规范
CommonJS模块化
CommonJS模块化使用module.exports或者exports(值拷贝)导出模块,使用require()导入模块。require()加载模块时,加载的是module.exports属性。
exports导出模块
exports是一个对象,直接在js文件中打印console.log(exports);打印出来是个空对象。我们可以在这个对象中添加很多个属性,添加的属性会导出。
hello.js文件模块导出
var nickname = "123"
function sayHello(){
console.log("打招呼");
}
exports.nickname = nickname
exports.sayHello = sayHello
setTimeout(() => {
exports.nickname = "456"
}, 1000);2
3
4
5
6
7
8
9
导入模块
let command = require("./hello.js")
console.log(command.sayHello);
console.log(command.nickname);
setTimeout(() => {
console.log(command.hello); //直接使用exports导出模块是一种浅拷贝,hello.js中nickname的值发生变化时,导入模块的值也会变化
}, 2000);2
3
4
5
6
解构的方式导入模块
const { sayHello, nickname} = require("./hello.js")
console.log(sayHello);
console.log(nickname);2
3
导出的command和exports是同一个对象,command对象是exports对象的浅拷贝,浅拷贝的本质就是一种引用的赋值而已
使用module.exports的方式导出
Node中我们经常通过module.exports导出模块,每一个js文件都是Module的一个实例,也就是module。在Node中真正用于导出的其实根本不是exports,而是module.exports;
module对象的exports属性是exports对象的一个引用,也就是说 module.exports = exports。验证方式使用module.exports直接修改属性。
使用module.exports修改属性
var nickname = "123"
exports.nickname = nickname
setTimeout(() => {
module.exports.nickname = "456"
}, 1000);2
3
4
5
导入模块
let command = require("./hello.js")
setTimeout(() => {
console.log(command.hello); //直接使用exports导出模块是一种浅拷贝,hello.js中nickname的值发生变化时,导入模块的值也会变化
}, 2000);2
3
4
经过验证module.exports直接修改属性,导入的模块也会发生变化。也就是说module.exports = exports,既然这样为什么还要使用module.exports导出呢?
因为使用module.exports导出时,以 modeule.exports 指向的对象为准,比如下面的代码,导入后只 sayHello 和 nickname,并没有age 和 hello属性,这是因为module.exports导出的是一个新对象,指向一块新的内存区域,即使修改原来模块中的值也不会影响导入的模块的使用。
模块导出
var hello = "124"
module.exports.hello = hello
module.exports.age = 10
function sayHello(){
console.log("打招呼");
}
module.exports = {
sayHello,
nickname: '小黑'
}2
3
4
5
6
7
8
9
10
11
导入模块
let command = require("./hello.js")
console.log(command.sayHello);
console.log(command.nickname);2
3
module打印结果
每个 .js 自定义模块中都有一个module对象,存储了和当前模块有关的信息,在js文件中打印console.log(module)结果
{
id: '.',
path: '/Users/xxx/Desktop/projects/module',
exports: {},
filename: '/Users/xxx/Desktop/projects/module/234.js',
loaded: false,
children: [
{
id: '/Users/xxx/Desktop/projects/module/123.js',
path: '/Users/xxx/Desktop/projects/module',
exports: {},
filename: '/Users/xxx/Desktop/projects/module/123.js',
loaded: true,
children: [],
paths: [Array]
}
],
paths: [
'/Users/xxx/Desktop/projects/module/node_modules',
'/Users/xxx/Desktop/projects/node_modules',
'/Users/xxx/Desktop/node_modules',
'/Users/xxx/node_modules',
'/Users/node_modules',
'/node_modules'
]
}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
require(x)导入模块规则
require(x)函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内,它的导入规则
- 情况1: X 是一个核心模块,比如path、http等,直接返回核心模块
- 情况2: X 是以
./ 或 ../ 或 /(根目录)开头的- 将 X 作为一个文件在对应的目录下查找
- 如果有后缀名,按照后缀名的格式查找对应的文件
- 如果没有后缀名,会按照如下顺序:
- 直接查找文件x
- 查找
x.js文件 - 查找
x.json文件 - 查找
x.node文件
- 没有找到对应的文件,将X作为一个目录,查找目录下面的index文件
- 查找
X/index.js文件 - 查找
X/index.json文件 - 查找
X/index.node文件
- 查找
- 将 X 作为一个文件在对应的目录下查找
- 情况3:直接是一个X(没有路径),并且X不是一个核心模块,比如我在桌面
/Users/mlive/Desktop/hello.js中导入require("axios"),如果在下面的路径中都没有找到axios就会报错javascriptpaths: [ '/Users/xxx/Desktop/node_modules', '/Users/xxx/node_modules', '/Users/node_modules', '/node_modules' ]1
2
3
4
5
6
使用
npm install axios安装第三方时,会在文件下生成node_modules文件夹,文件夹内包含第三方文件夹axios, axios 下有index.js文件。所以在其他模块引入时可以找到对应的模块
模块的加载过程
- 结论一:模块在被第一次引入时,模块中的js代码会被运行一次
- 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
- 每个模块对象module都有一个属性:loaded。为false表示还没有加载,为true表示已经加载;
CommonJS加载模块时是同步的,在浏览器中,通常不使用CommonJS规范,
- 同步意味着只有等待对应的模块加载完毕,当前模块的内容才能运行。node 使用的是 CommonJS 模块化方案,
webpack打包工具具备对CommonJS的支持和转换,它会将我们的代码转成浏览器可以直接执行的代码.
ES Modules模块化
ES Module 模块采用export和import关键字来实现模块化,export负责将模块内的内容导出,import负责从其他模块导入内容。
采用 ES Module 将自动采用严格模式:use strict。
ES Modules 使用
浏览器中使用ES6的模块化开发,导入时需要设置type="module",直接在浏览器中运行代码,会报如 CORS 错误,需要通过服务器测试,可以使用VSCode插件:Live Server。MDN关于模块化文档
HTML 文件
<script src="./foo.js" type="module"></script>
<script src="./use.js" type="module"></script>2
foo.js导出
let name = "foo"
function sayhello(){
console.log("sayhello");
}
export let age = 20
export{
name,
sayhello
}
/**
* 导出的方式
* export let age = 20 //直接导出变量
* export{name,sayhello} //一次导出多个变量
*/2
3
4
5
6
7
8
9
10
11
12
13
14
use.js导入
// 导入时可以使用 as 给变量起别名
import {name as nickname,age,sayhello} from "./foo.js"
console.log(nickname, age, sayhello);2
3
4
如果导入模块后有变量名冲突,可以给导入变量起别名或者对整个模块起别名
// 也可以对导入的模块起别名
import * as Person from "./foo.js"
console.log(Person.name,Person.age);2
3
export和import可以结合使用,第三方的做法往往是这样,常见多个js文件,然后再在文件夹下创建index.js文件 将其他js文件统一导出到index.js文件中。这样我们在使用第三方时,只需要找到index.js文件,然后导入即可。
// 导入之后直接导出
export { formatCount,formatDate } from "./format.js";
// 全部导出
export * from "./format.js"2
3
4
default 默认导出
当一个模块使用了默认导出,在导入是不需要使用{},比如``。当一个模块使用了命名导出时,需要使用大括号。一个模块只能有一个默认导出。
默认导出
// 默认导出`export`时可以不需要指定名字,在导入时不需要使用 {},并且可以自己来指定名字
export default function formatCount() {
}
// default默认导出,导入时不需要使用大括号{}
import formatCount from "./format.js";2
3
4
5
6
7
有名字的导出
export function formatCount() {
}
// 非默认导出导入时需要使用大括号{}
import {formatCount} from "./format.js";2
3
4
5
6
import函数动态加载模块
通过import加载一个模块,是不可以在其放到逻辑代码中的,这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系。由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况。
如果根据不懂的条件,需要动态来选择加载模块的路径,可以使用 import() 函数来动态加载,import函数返回一个 Promise,可以通过 then 获取结果
let flag = true
if (flag) {
import("./utils/index.js").then( aaa => {
console.log(aaa);
})
}2
3
4
5
6
CommonJS和ES Module交互
- 结论一:通常情况下,
CommonJS不能加载ES Module- 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码;
- 但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持;
- Node当中是不支持的;
- 结论二:多数情况下,
ES Module可以加载CommonJS- ES Module在加载CommonJS时,会将其
module.exports导出的内容作为default导出方式来使用; - 这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的;
- ES Module在加载CommonJS时,会将其
import meta
import.meta 是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。在ES11(ES2020)中新增的特性。