模块化开发
说明
工程化、ES Module、CommonJS、CMD、AMD笔记总结
# 模块化开发
- 模块化开发最终的目的是将程序划分成一个个小的结构;
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构;
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等;
# require加载搜索顺序
1 缓存
模块在第一次加载后会被缓存。 这也意味着(类似其他缓存机制)如果每次调用 require(‘foo’) 都解析到同一文件,则返回相同的对象。
2 核心模块
核心模块定义在 Node.js 源代码的 lib/ 目录下。require() 总是会优先加载核心模块。 例如, require(‘http’) 始终返回内置的 HTTP 模块,即使有同名文件。
3 文件模块
如果按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js、 .json 或 .node 拓展名再加载。
当没有以/ ./ 或 ../
开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录。
4 目录作为模块
可以把程序和库放到一个单独的目录,然后提供一个单一的入口来指向它。 把目录递给 require() 作为一个参数,有三种方式。
第一种方式是在根目录下创建一个 package.json 文件,并指定一个 main 模块。 例子, package.json 文件类似:
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
2
3
4
如果这是在 ./some-library 目录中,则 require(’./some-library’) 会试图加载 ./some-library/lib/some-library.js。
这就是 Node.js 处理 package.json 文件的方式。
如果目录里没有 package.json 文件,或者 ‘main’ 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 或 index.node 文件。 例如,如果上面的例子中没有 package.json 文件,则 require(’./some-library’) 会试图加载:
./some-library/index.js
./some-library/index.node
2
如果这些尝试失败,则 Node.js 将会使用默认错误报告整个模块的缺失:
5 node_modules 目录加载
如果传递给 require() 的模块标识符不是一个核心模块,也没有以 '/' 、 '../' 或 './' 开头,则 Node.js 会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。 Node.js 不会附加 node_modules 到一个已经以 node_modules 结尾的路径上。 如果还是没有找到,则移动到再上一层父目录,直到文件系统的根目录。
例子,如果在 ‘/home/ry/projects/foo.js’ 文件里调用了 require(‘bar.js’),则 Node.js 会按以下顺序查找:
/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
2
3
4
6 从全局目录加载
如果 NODE_PATH 环境变量被设为一个以冒号分割的绝对路径列表,则当在其他地方找不到模块时 Node.js 会搜索这些路径。
windows:
set NODE_PATH=c:\
UNIX/Linux
export NODE_PATH=.
2
虽然NODE_PATH可以指定特殊路径(例如SET NODE_PATH=C:\,这样会检索C盘),但是一般不建议使用。
7 其他全局目录
Node.js 还会搜索以下的全局目录列表:
1: $HOME/.node_modules
2: $HOME/.node_libraries
3: $PREFIX/lib/node
2
3
其中 $HOME 是用户的主目录, $PREFIX 是 Node.js 里配置的 node_prefix。
# 模块加载
模块在第一次被引入时,模块中的js代码会被运行一次,模块被多次引入时,会缓存,最终只加载(运行)一次
需要统一规范
------------------------------
<script src="./demo.js"></script>
<script>
console.log(moduleA);
</script>
-------------------------
// demo.js
const moduleA = (function () {
let name = 'rio'
let age = 20
let eat = ['a','b','c']
return {
name,
age,
eat
}
}())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# CommonJS
CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS
CommonJS加载模块是同步的,只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
exports是一个对象,我们可以在这个对象中添加很多个属性, 添加的属性会被导出
// demo.js
const demo = require("./demo1.js")
console.log(demo.name); // rio
console.log(demo.age); // 20
----------------------------------
// demo1.js
const name = 'rio'
const age = 20
exports.name = name
exports.age = age
2
3
4
5
6
7
8
9
10
demo变量等于exports对象(引用赋值):
- require通过各种查找方式, 最终找到了exports这个对象
- 并且将这个exports对象赋值给了demo变量;
每个模块对象module都有一个属性: loaded
;0为false表示还没有加载,为true表示已经加载;
出现循环引入时遵循深度优先搜索DFS
main -> aaa -> ccc -> ddd -> eee -> bbb
# module.exports
CommonJS中是没有module.exports的概念的;但是为了实现模块的导出module对象的exports属性的引用指针指向CommonJS中exports对象(即exports === module.exports)
Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;所以在Node中真正用于导出的是module.exports
对象
module.exports={}创建了一个新的空间,原来module对象中的exports指向新开创的空间,此时exports === module.exports不成立
CommonJS规范中,通过module.exports
导出的是一个内存引用,而不是值的副本。每个模块访问到的都是同一个内存引用,因此对于这个变量的修改会影响到所有使用它的代码。
// demo1.js
const name = 'rio'
module.exports.name = name
exports.name = '444';
// demo.js
const obj = require("./demo1")
console.log(obj); // { name: '444' }
------------------------------------------------
// demo1.js
const name = 'rio'
const age = 20
module.exports = {
name,
age
}
exports.name = '444';
-------------------------
// demo.js
const demo = require("./demo1.js")
console.log(demo); // { name: 'rio', age: 20 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ES Module
使用了import和export关键字;export负责将模块内的内容导出;import负责从其他模块导入内容;采用编译期的静态分析,并且地加入了动态引用的方式;
//demo.js
// 导入整个模块
import * as f from "./demo1.js"
import { name, age } from "./demo1.js"
console.log(name);
console.log(f.age);
-----------------------------
// demo1.js
const name = 'rio'
const age = 20
export {
name,
age
}
-------------------------
//demo.html
<script src="./demo1.js" type="module"></script>
<script src="./demo.js" type="module"></script>
// rio 20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
默认导出(default export)
默认导出export时可以不需要指定名字;
在导入时不需要使用{},并且可以自己来指定名字;
一个模块中只能有一个默认导出
// demo.js
import def from "./demo1.js"
import { name } from "./demo1.js"
console.log(name);
console.log(def());
// console.log(import.meta);
------------------------------
// demo1.js
const name = 'rio'
const age = 20
export {
name,
age
}
export default function () {
return 'default'
};
-----------------------------
// demo.html
<script src="./demo1.js" type="module"></script>
<script src="./demo.js" type="module"></script>
// rio default
-----------------------------------
// 满足条件才引入
let flag = true
if (flag) {
// import函数返回Promise
import('./demo1.js').then(res=>{
console.log(res.name);
})
}
// rio
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
# ES Module解析流程
- 构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)
- 根据入口创建依赖关系的AST;
- 下载module文件,用于解析;
- 解析每个module文件,生成 Module Record(包含当前module的AST、变量等);
- 将Module Record 映射到 Module Map中,保持每个module文件的唯一性;这意味着无论何时从现在开始请求它,加载器都可以从该映射中获取它。
- 实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
- 生成模每个Module Record的块环境记录(
Module Enviroment Record
),用来管理Module Record
的变量等; - 在内存中写入每个Module的数据,同时 Module文件的导出export和引用文件的 import指向该地址;
- 确定了
export和import
内存中的指向,同时该内存空间中定义了Module文件的变量(但是还未赋值);
- 运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;
- 执行对应Module文件中顶层作用域的代码,确定实例化阶段中定义变量的值,放入内存中
- 求值阶段确定了Module文件中变量的值,由于 ES Module使用的是动态绑定(指向内存地址),export中修改数据会映射到内存*中,import数据相应也会改变。559o20
# EsModule 和 CommonJs 的区别
本质区别,EsModule 是语法层,Js引擎做的事,而 CommonJs 是 node 自己实现的模块机制。
CommonJs 从文件系统加载文件,加载时可阻塞主流程,在加载时便进行模块的实例化和执行,意味着在返回模块实例之前,会遍历整个树,加载、实例化和执行任何依赖项。
浏览器中ES Module
是异步加载,不会堵塞浏览器,即等到整个页面渲染完,再执行模块脚本。如果网页有多个ESM
,它们会按照在页面出现的顺序依次执行。
CommonJs 可以在模块说明符中使用变量,EsModule 需要在执行前构建整个模块图,不可在模块说明符中使用变量,因为这些变量还没有值。
CommonJS模块依赖关系是发生在代码运行时;而ES6 Module模块的依赖关系在编译时已经可以确立
ES6 Module对比CommonJS有以下优势:
tree shaking
。通过静态分析工具在编译时候检测哪些import
进来的模块没有被实际使用过,以及模块中哪些变量、函数没有被使用,都可以在打包前先移除,减少打包体积。
模块变量检查。JavaScript属于动态语言,不会在代码执行前检查类型错误。ES6 Module的静态模块结构有助于结合其他工具在开发或编译过程中去检查值类型是否正确。
# 值拷贝与动态引用
require 是浅拷贝,也就是说你可以修改对象第二层的属性并影响原数据
import 是引用,基本数据类型,修改不会影响原数据,但是对象修改属性会。import 必须写在文件的顶部,这是不正确的,import 可以写在页面最下面,因为它具有置顶性
// tom.js
var name = 'Tom';
module.exports = {
name: name,
setName: function(otherName){
name = otherName;
}
};
// index.js
var name = require('./tom.js').name;
var setName = require('./tom.js').setName;
console.log(name); // 打印出 'Tom',这里是tom.js执行结果name的拷贝,name的值为'Tom'
setName('Jeremy');
console.log(name); // 打印出 'Tom', tom.js中name的改变不会对这里造成影响
name = 'Tim';
console.log(name); // 打印出 'Tim',当前模块name的值是可以重新赋值的
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ES6 Module中导入的变量其实是对原有模块值的动态引用
// tom.js
var name = 'Tom';
export {
name: name,
setName: function(otherName){
name = otherName;
}
};
// index.js
import { name, setName } from './tom.js';
console.log(name); // 打印出 'Tom',这里是tom.js中name变量的引用
setName('Jeremy');
console.log(name); // 打印出 'Jeremy', 引用反应出 tom.js中 name 值的变化
name = 'Tim'; // 会抛出 SyntaxError: "name" is read-only
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 习题
// module.js
export let thing = 'initial';
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import { thing as importedThing } from './module.js';
const module = await import('./module.js');
let { thing } = await import('./module.js');
setTimeout(() => {
console.log(importedThing); // changed
console.log(module.thing); // changed
console.log(thing); // initial
// 通过import解构生成的变量,不会因原内容改变而改变,因为解构将import时刻thing当前值(而不是引用)赋值给了一个新变量。
}, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// module.js
let thing = 'initial';
export { thing };
export default thing;
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
console.log(thing); // changed
console.log(defaultThing); // initial
console.log(anotherDefaultThing); // initial
}, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// './module.js'
let thing = 'initial';
export { thing, thing as default };
setTimeout(() => {
thing = 'changed';
}, 500);
// main.js
import { thing, default as defaultThing } from './module.js';
import anotherDefaultThing from './module.js';
setTimeout(() => {
console.log(thing); // changed
console.log(defaultThing); // changed
console.log(anotherDefaultThing); // changed
}, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 总结
// 这些会返回给你一个原export变量的动态引用:
import { thing } from './module.js';
import { thing as otherName } from './module.js';
import * as module from './module.js';
const module = await import('./module.js');
// 这会将原export变量的当前值,赋值给一个新变量:(因为发生了解构赋值)
let { thing } = await import('./module.js');
// 些export会导出原变量的动态引用:
export { thing };
export { thing as otherName };
export { thing as default };
export default function thing() {}
// 这些export导出的是变量的当前值:
export default thing;
export default 'hello!';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# AMD和CMD
AMD是Asynchronous Module Definition (异步模块定义)主要用于浏览器的一种模块化规范
CMD是Common Module Definition (通用模块定义)主要用于浏览器的一种模块化规范
# 参考
ES modules: A cartoon deep-dive - Mozilla Hacks - the Web developer blog (opens new window)
Es Module - 深度解析 - 掘金 (juejin.cn) (opens new window)
export default A
与 export { A as default }
是不同的 - 掘金 (juejin.cn) (opens new window)
CommonJS 与 ES6 Module 之一 - 掘金 (juejin.cn) (opens new window)
nodejs的require加载模块的路径搜索顺序 - 掘金 (juejin.cn) (opens new window)