什么是模块化
-
什么是模块化
-
模块化开发最终的目的是将程序划分为一个个小的结构
-
在这个结构中编写属于自己的逻辑代码,有自己的作用域,同时不会影响其他的结构
-
这个结构可以将自己希望暴露的变量、函数、对象等导出给其他结构使用
-
也可以通过某种方式,导入另外结构中的变量,函数,对象等
-
-
按照结构划分开发程序的过程,就是模块化开发的过程
-
js的缺陷
-
例如var定义的变量作用域问题
-
js的面向对象并不能像常规面向对象语言一样使用class
-
js没有模块化的问题
-
-
在网页开发早期,js仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等
-
这个时候只需将js代码写到
-
并没有必要放到多个文件中来编写,甚至流行,通常来说js程序的长度只有一行
-
-
随着前端和js的快速发展,js代码变得越来越复杂了
-
ajax的出现,前后端开发分离,意味着后端返回数据后,需要通过js进行前端页面的渲染
-
SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过js来实现
-
包括Node的实现,js编写复杂的后端程序,没有模块化是致命的硬伤
-
-
模块化是js一个非常迫切的需求
-
js本身,直到ES6才推出了模块化方案
-
在此之前,为了让js支持模块化,涌现出了很多不同的模块化规范:AMD,CMD,CommonJS
-
早期没有模块化带来了很多问题
-
可以使用立即函数调用表达式IIFE
-
但是带来了新的问题
-
第一,必须记得每一个模块中返回对象的命名,才能在其他模块中正确的使用
-
第二,代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写
-
第三,在没有合适的规范情况下,每个人,每个公司都可能会任意命名,甚至出现模块名称相同的情况
-
-
虽然实现了模块化,但是由于实现的过于简单,并且没有规范
-
所以需要制定一定的规范约束每个人都按照这个规范去编写模块化的代码
-
这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性
-
js社区为了解决上面的问题,涌现出一系列好用的规范
-
index.js文件
var moduleKobe = (function () {
var name = "kobe12";
var isFlag = true; //自定义实现模块化,返回一个对象
return {
name,
isFlag,
};
})();
kobe.js文件
(function () {
if (moduleKobe.isFlag) {
console.log(moduleKobe.name);
}
})();
上述自定义模块,moduleKobe需要手动赋值,使用非常麻烦
CommonJS基本使用
why.js文件
const name = "why";
const age = 18;
function sum(sum1, sum2) {
return sum1 + sum2;
}
//导出方案1 module.exports module是why模块本身的一个对象,exports也是一个对象
module.exports = {
name,
age,
sum,
};
main.js文件
const { name, age, sum } = require("./why");
console.log(name);
console.log(age);
console.log(sum(1, 2));
why.js文件
const name = "why";
const age = 18;
function sum(num1, num2) {
return num1 + num2;
}
//导出方案2 通过exports导出
exports.name = name;
exports.age = age;
exports.sum = sum;
main.js文件
const why = require("./why");
console.log(why.name);
console.log(why.age);
console.log(why.sum(22, 33));
CommonJS原理
如上图中,why.js文件中module.exports将info这个对象导出,此时module.exports为一个对象,并且指向info。在外部的文件中,使用require函数引入时,require函数会有一个返回值,返回的就是module.exports导出的对象。
在第二种导出方式中,
module.exports = {};
exports = module.exports;
将module.exports赋值给exports,此后在exports中增加属性时,相当与在module.exports中增加属性,文件最终导出的一定是module.exports。严格意义上来说,commonjs在导出时,都是用exports去进行导出,所以严格意义上 通过module.exports导出是不符合规范的。但是原理上,导出时都是导出module.exports这一对象,所以类似exports这样去导出方式,慢慢被抛弃了。
//require函数中,根据后面传入的地址值,类似一个id值,可以在内存中找到对应id值暴露出去的对象
function require(id){
return module.exports
}
//why接受赋值后,即why也指向info这个对象
const why =require('./why')
require函数细节
require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象,导入格式如下 require(x)
-
情况1:X是一个Node的核心模块,比如path、http。 则会直接返回核心模块,并且停止查找
-
情况2:X是以./或../或/(根目录)开头的
第一步,先将x当作一个文件在对应的目录下查找
1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序
1.直接查找文件X
2.查找X.js文件
3.查找X.json文件
4.查找X.node文件
第二步,没有找到对应的文件,将X作为一个目录
查找目录下面的index文件
1.查找X/index文件
2.查找X/index.json文件
3.查找X/index.node文件
-
情况3:直接是一个X(没有路径),并且X不是一个核心模块
这时会沿着上图中的路径,去查找node_modules包中的依赖包,直到全局还没有找到化,则会报错。
模块的加载细节
//foo.js
const name = "why";
const age = 18;
console.log("foo:", name);
console.log("foo中的代码被运行");
module.exports = {
name,
age,
};
//main.js
console.log("main.js代码开始运行");
require("./foo");
require("./foo");
require("./foo");
console.log("main.js代码后续运行");
引入时,main.js最终的输出为
main.js代码开始运行
foo: why
foo中的代码被运行
main.js代码后续运行
代码在执行过程中,遇到require会先执行引入的代码,此时主文件中的代码暂停,等引入的代码执行完毕,且多次引入也只会执行一次,后主文件后续代码再次运行。
模块的加载过程
-
模块在被第一次引入时,模块中的js代码会被执行一次
-
模块被多次引入时,会缓存,最终只加载一次
- 一个js文件,对应一个Module实例,这个Module实例中有loaded属性,表示这个文件有没有被加载过,为false表示还没有被加载,为true表示已经加载了
-
如果出现循环引入,加载顺序。有深度优先搜索和广度优先搜索。Node采用的是深度优先算法,执行顺序为
main->aaa->ccc->ddd->eee->bbb
CommonJS规范缺点
-
CommonJS加载模块是同步的
-
同步意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行
-
这个在服务器上不会有问题,因为服务器加载的js文件都是本地的,加载速度非常快
-
-
应用于浏览器
-
浏览器加载js文件需要先从服务器上将文件下载下来,之后再加载运行
-
采用同步就意味着后续js代码都无法正常运行,即使是一些简单的DOM操作
-
-
在浏览器中,通常不采用CommonJS规范
-
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD
- 目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换
认识ES Module
ES Module模块采用export和import关键字来实现模块化
export负责将模块内的内容导出
import负责从其他模块导入内容
ES Module 基本使用
//foo.js文件
//第一种导出方式
export const name = "why";
export const age = 18;
//第二种导出方式 导出和声明分开
const name = "why";
const age = 22;
function foo() {
console.log("foo");
}
//这种导出方式称为命名导出
export { name, age, foo };
//第三种方式,导出时起别名
export { name as fName, age as fAge, foo as fFoo };
//导入方式1,普通导入
import { name, age } from "./foo.js";
//导入方式2 起别名
import { name as fName, age as fAge, foo as fFoo } from "./foo.js";
//导入方式3 将导出所有内容放到一个标识符中
//导出的所有东西都放到foo里面了
import * as foo from "./foo.js";
console.log(foo.name, foo.age);
const name = "why";
const age = 21;
const foo = "foo value";
//默认导出foo
export default foo;
//导入语句:导入的是默认导出
import why from "./foo.js";
console.log(why); //输出 foo value
import函数
//import { name, age, foo } from "./foo.js";
//console.log("后续的代码需要等import引入完成之后,才会执行");
//import 函数返回的结果是一个promise,这个引入是异步的
import("./foo.js").then((res) => {
console.log("res", res);
});
console.log(11);
//写成import函数后,引入文件不会阻塞后续代码运行
//ES11 新增属性 meta属性本身也是对象,{url:"当前模块所在的路径"}
console.log(import.meta); //{url: 'http://127.0.0.1:5500/03_ES%20Module/05_import%E5%87%BD%E6%95%B0/main.js', resolve: ƒ}
ES Module解析流程
解析过程可划分为三个阶段
-
构建,根据地址查找js文件,并且下载,将js文件解析成模块记录(Module Record)。要使用live-server开启一个本地服务器,不能用file://,但是ES Module需要对这个内容做一个请求和下载。
-
实例化,对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。此时内部的代码没有执行,只分析import和export语句.
-
运行,运行代码,计算值,并且将值填充到内存地址中。导出的模块可以更新变量的值,导入的模块不能更新变量的值。
构建阶段,主要作用是下载依赖的js文件。从服务器里面把当前依赖的main.js下载下来,对js文件做一个解析,解析过程是静态分析的过程,不会运行里面的代码。解析后生成一个Module Record数据结构,这个数据结构有一个专门的属性(RequestedModules)用来记录,当前文件依赖其他那些文件。再到服务器中去请求counter.js 和display.js这两个文件,把这两个文件下载下来后,再对他们做一个解析,解析完以后又生成两个Module Record数据结构。如果多个文件依赖同一个文件,这个文件不会被重复下载,内部有一个映射MODULE MAP来记录当前那些已经在下载,那些文件已经被下载了。
Module Record 经过实例化会生成一个Module Environment Record模块环境记录,里面有一个Bindings绑定,绑定count,在实例化分配的内存空间当中会专门记录count这个值,在记录过程中刚开始是一个undefined。另外一个模块环境记录里面绑定了render函数,刚开始导出的时候也是一个undefined。
main.js有做一个导入,在main.js构建出来的Module Record有ImportEntries,实例化后生成的Module Environment Record模块环境记录中可以绑定值,count和render,通过导入绑定这两个值。这个时候如果去用count和render时候,他们都是undefined
经过求值阶段,运行里面的代码,将保存的undefined值赋值为5,将函数的地址保存到内存空间里。
在main.js导入的时候,是不允许修改值,只能在文件内部导入时修改值
在webpack环境中,es module和commonjs可以相互引用的