javascript模块化
名词:
文件划分 命名空间 IIFE(Immediately Invokable Function Expressions)私有作用域 AMD UMD CommonJS ES Modules CMD
模块化规范:
CommonJS
定义和引用
CommonJS 规定每个文件就是一个模块,有独立的作用域,这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;每个模块内部,都有一个 module 对象,代表当前模块。通过它来导出 API,它有以下属性:
- id 模块的识别符,通常是带有绝对路径的模块文件名;
- filename 模块的文件名,带有绝对路径;
- loaded 返回一个布尔值,表示模块是否已经完成加载;
- parent 返回一个对象,表示调用该模块的模块;
- children 返回一个数组,表示该模块要用到的其他模块;
- exports 表示模块对外输出的值;
- module.exports是模块最终对外输出的值,最初它和exports指向同一个对象,exports是较晚提出的简单替代写法。
引用模块则需要通过 require 函数,它的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。
特性
CommonJS ,它采用的是值拷贝和动态声明。值拷贝和值引用相反,一旦输出一个值,模块内部的变化就影响不到这个值了,可以简单地理解为变量浅拷贝。
动态声明,就是消除了静态声明的限制,可以“自由”地在表达式语句中引用模块。
require文件查找规则
导入格式如下:require(X)
情况一:X是一个Node核心模块,比如path、http
直接返回核心模块,并且停止查找
情况二:X是以./ 或…/ 或/(根目录)开头的
第一步:将X当做一个文件在对应的目录下查找;
1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序:
- 直接查找文件X
- 查找X.js文件
- 查找X.json文件
- 查找X.node文件
第二步:没有找到对应的文件,将X作为一个目录查找目录下面的index文件
-
- 查找X/index.js文件
- 查找X/index.json文件
- 查找X/index.node文件
如果没有找到,那么报错:not found
模块的加载过程
一:模块在被第一次引入时,模块中的js代码会被运行一次
二:模块被多次引入时,会缓存,最终只加载(运行)一次
只会加载运行一次是因为每个模块对象module都有一个属性:loaded,loaded为false表示还没有加载,为true表示已经加载;
三:如果有循环引入,那么加载顺序是什么?
如图:
这个其实是一种数据结构:图结构;
图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS规范缺点
-
- 它的模块加载器由 Node.js 提供,依赖了 Node.js 本身的功能实现。如果 CommonJS 模块直接放到浏览器中无法执行。
- CommonJS 约定以同步的方式进行模块加载,这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。即,模块请求会造成浏览器 JS 解析过程的阻塞,导致页面加载速度缓慢。
- CommonJS 的这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络 IO,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。
- 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD,但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ESModule代码的转换,所以AMD和CMD已经使用非常少了。
AMD(Asynchromous Module Definition - 异步模块定义)
AMD
全称为Asynchronous Module Definition
,即异步模块定义规范,是开源社区早期为浏览器提供的,它采用同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。
AMD 实现的比较常用的库是 require.js 和 curl.js
AMD 规范只定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数:define(id?, dependencies?, factory);
- id 为模块的名称,该参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
- dependencies 是个数组,它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂函数中。
- factory 为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。
举例:
//foo.js define(function() { const name = "pjy"; const age = 19; function sum(num1,num2) { return num1 + num2 } return { name, age, sum } }) //main.js require.config({ baseUrl: './src', paths: { foo: "./foo", bar: "./bar" } }) require(["foo","bar"],function(foo) { console.log("main:",foo); })
使用如下:
// main.js define(["./print"], function (printModule) { printModule.print("main"); }); // print.js define(function () { return { print: function (msg) { console.log("print " + msg); }, }; });
在 AMD 规范当中,我们可以通过 define 去定义或加载一个模块,比如上面的main
模块和print
模块,如果模块需要导出一些成员需要通过在定义模块的函数中 return 出去(参考print
模块),如果当前模块依赖了一些其它的模块则可以通过 define 的第一个参数来声明依赖(参考main
模块),这样模块的代码执行之前浏览器会先加载依赖模块。可以使用 require 关键字来加载一个模块,如:
// module-a.js require(["./print.js"], function (printModule) { printModule.print("module-a"); });
不过 require 与 define 的区别在于前者只能加载模块,不能定义一个模块。
由于没有得到浏览器的原生支持,AMD 规范仍然需要由第三方的 loader 来实现。不过 AMD 规范使用起来稍显复杂,代码阅读和书写都比较困难。因此,关于新的模块化规范的探索,业界从仍未停止脚步。
CMD(Common Module Definition - 公共模块定义)
CMD 是 SeaJS 在推广过程中对模块定义的规范,具有CommonJS 特性和 AMD 特性
定义模块 define(factory)
加载模块 require(id)
UMD(AMD和CommonJS的糅合)
它让 CommonJS 和 AMD 模块跨端运行
UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。
在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
ES Modules(又称ESM)
是由 ECMAScript 官方提出的模块化规范,它已经得到了现代浏览器的内置支持。
在现代浏览器中,如果在 HTML 中加入含有type="module"
属性的 script 标签,那么浏览器会按照 ES Module 规范来进行依赖加载和模块解析,这也是 Vite 在开发阶段实现 no-bundle 的原因。
由于模块加载的任务交给了浏览器,即使不打包也可以顺利运行模块代码。
除了浏览器端,一直以 CommonJS 作为模块标准的 Node.js 也从12.20
版本开始正式支持原生 ES Module。
模块化发展历程:
ES6模块化理解:
在ES6中每一个模块即是一个文件,在文件中定义的变量,函数,对象在外部是无法获取的。如果你希望外部可以读取模块当中的内容,就必须使用export来对其进行暴露(输出)
b.js import { aa } from "./a" a.js export const aa = '123'
输出文件(test.js)
export let myName="itlong";
引入文件(index.js)
import {myName} from './test.js'; console.log(myName); //itlong
输出/导出/暴露
一、默认暴露
注意:一个模块只能有一个默认导出,对于默认导出,导入的名称可以和导出的名称不一致;
/************导出 test.js *************/ export default function(){ return "默认导出一个方法" } /************引入***********/ import myFn from "./test.js";//注意这里默认导出不需要用{}。 console.log(myFn());//默认导出一个方法
也可以将所有需要导出的变量放入一个对象中,然后通过默认暴露进行导出;
/************导出 test.js ****************/ export default { myFn(){ return "默认导出一个方法" }, myName:"itlong" } /*************引入 test.js *****************/ import myObj from "./test.js"; console.log(myObj.myFn(),myObj.myName);//默认导出一个方法 itlong
二、统一暴露
如果想要导出多个变量,可以将这些变量包装成对象进行模块化输出:
/***************导出 test.js*************/
let myName="itlong"; let myAge=18; let myfn=function(){ return "我是"+myName+"!今年"+myAge+"岁了" } export { myName, myAge, myfn }
/***************引入 test.js *************/
import {myfn,myAge,myName} from "./test.js"; console.log(myfn());//我是itlong!今年18岁了 console.log(myAge);//18 console.log(myName);//itlong
如果不想暴露模块当中的变量名字,可以通过as来进行操作:
/***************导出 test.js*************/
let myName="itlong"; let myAge=18; let myfn=function(){ return "我是"+myName+"!今年"+myAge+"岁了" } export { myName as name, myAge as age, myfn as fn }
/*****************接收的代码调整为***********/ import {fn,age,Name} from "./test.js"; console.log(fn());//我是itlong!今年18岁了 console.log(age);//18 console.log(name);//itlong
三、分别暴露
export let school = 'gc'; export function teach() { console.log("m1--我们可以教给你很多东西!"); };
重命名export和import:
理解:
如果导入的多个文件中,变量名字相同,即会产生命名冲突的问题,为了解决该问题,ES6为提供了重命名的方法,当你在导入名称时可以这样做:
/*******************test1.js****************/ export let myName="我来自test1.js"; /*******************test2.js***************/ export let myName="我来自test2.js"; /*************index.js*****************/ import {myName as name1} from "./test1.js"; import {myName as name2} from "./test2.js"; console.log(name1);//我来自test1.js console.log(name2);//我来自test1.js
导入
通用的导入方式
直接导入整个模块:
/***************导出 test.js **************/
let myName="itlong";
let myAge=18;
let myfn=function(){
return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
myName as name,
myAge as age,
myfn as fn
}
/************* 导入 test.js **************/
import * as info from "./test.js"; //通过*来批量接收,用as来指定接收的名字 console.log(info.fn());//我是itlong!今年18岁了 console.log(info.age);//18 console.log(info.name);//itlong
当在一个文件中使用import导入多个模块时,不论import语句与其它代码的顺序如何,最终都会最先使用import导入并运行。
导出语法深入理解
用法注意
分别暴露 export
统一暴露 export {}
导出的到底是啥?
导出语法总结
导入语法深入理解
用法注意
具名导入
默认导入
整体导入
导入的到底是啥
导入语法总结
导入导出复合写法
Node.js
是JavaScript的服务器运行环境,对ES6的支持度更高除了那些默认打开的功能,还有一些语法功能已经实现了,但默认没打开
Babel
可以将ES6代码转为ES5代码,从而在老版本的浏览器执行,Babel的配置文件是.babelrc,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件