什么是模块化?
进化史
说明
1、命名冲突
以前没有let、const的时候,只能用var定义全局变量。
多个人开发一个项目,只能通过上图的方式,引用自己的js文件。
而不同的人可能会有相同的变量名,这些变量名会同时起作用,导致某个人的覆盖了自己的,产生问题,而且及其难以调试!
解决方法,定义函数,把变量名写在函数里面,这样就用了函数的作用域,而不是全局作用域,然后立即执行。
2、命名空间
根据上面“命名冲突”的解决方案,当我想把一些东西共享出去的时候,就返回一个对象,把想暴露给外面的变量,放在这个对象里面。
然后把对象赋值给一个变量,这种模式就是命名空间。
但是这个仍然有问题:
1、假如你的变量名moduleA,在别人文件里也有,就被覆盖了;
2、而且名字也不好记,里面还有不同的变量名要去找;
3、没有一个统一的规范,你是这样操作,但是别的公司别的程序员可能又不是这样返回的
模块化目的
以前是所有js代码都写在1个文件里面,问题很多。
模块化,可以把js代码分成多个文件组合而成。
优势:
1)防止命名冲突
2)代码复用
3)高维护性
4)按需加载
===============
CommonJS 规范(CJS) *
规范说明
CommonJS 只是一个规范(标准),并没有具体的实现。
而NodeJS,是CommonJS这个规范的代表性的实现。
(比如说,国家某个标准规范了螺丝钉,某个型号的螺丝钉,多长,直径多少毫米,但是不负责生产这个螺丝;而其他工厂,会按照这个标准,来生产螺丝)
Browserify 也是CommonJS的一种实现,让CommonJS代码能在浏览器中运行,但现在用的很少很少,基本都是通过ES6模块化。
在github上,某些库的说明里面,也有说到 Browserify ,意思是你想在浏览器中使用它这个库,需要用 Browserify 这个工具才能用。
webpack 就是运行在NodeJS下的,因此编写的配置文件,用的都是CommonJS 的方式。
导出
格式
// ============= 第一种方式 =============
// 原则上一个.js 文件里面,module.exports 只能有一个,exports是一个对象,如果有多个,相当于把最后一个赋值给了这个对象,前面的就没意义了
module.exports = {
key1:value1,
key2:value2,
...
}
// ============= 第二种方式 =============
// 原则上,第二种方式可以有多个,因为exports是一个对象,第二种方式只是给这个对象,增加对象的属性而已,只要key名不一样就行
exports.key1 = value1
exports.key2 = value2
// 不能 exports = {key1:value1} ,因为本质上,导出用的是module.exports,它的内存地址赋值给了exports 这个变量,你这样会修改了exports 的引用,因此导不出任何东西(原理见下)
解析说明
NodeJS里面,每个.js 文件,都是一个 module 类的实例。
exports只是这个类的一个属性。
那为什么要多第二种方式导出?因为严格意义上,CommonJS规范是没有 module.exports 这个语法的,只有 exports。
因此 module.exports 是NodeJS特有的,第二种方式是为了实现 CommonJS规范,加上去的。
NodeJS 源码
导入
格式
let a = require(xxx)
//第三方模块:xxx为模块名
//自定义模块:xxx为模块文件的路径
a.key1 // value1
a.key2 // value2
...
导入文件的查找方式
require 路径查找规则官方文档: https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_all_together
情况一:X是一个Node核心模块,比如path、http,直接返回核心模块,并且停止查找
NodeJS官网API文档,这里这列的都是核心模块API
情况二:X是以 ./ 或 ../ 或 /(根目录)开头的
./ 是当前目录
../ 是上一层目录
强烈建议加上完整的后缀名,因为这个查找规则是NodeJS环境下的,浏览器环境是没有的
情况三:直接是一个X(没有路径),并且X不是一个核心模块
可以打印module查看一下,这个对象就是当前编写的文件对象,里面有个path属性
console.log(module)
console.log(module.path)
可以看到这个path属性是个数组,里面有一堆路径,第一个就是文件所在的路径后面加了一个/node_modules;
其他就是上一层文件夹,后加一个/node_modules;
第三种情况,就会从这里一个一个找,如果上面的路径中都没有找到,那么报错:not found
这种情况就是我们用Vue 脚手架或者webpack等工具,引用第三方模块时,的查找方式。
实现原理
导出的对象,在内存里面是用一个内存地址储存的。
而导入,相当于下面的函数,把对应文件导出的对象的内存地址,赋值给这个文件的某个变量,因此就能直接使用(而且他们用的是同一个对象)
function require(id){
return module.exports
}
模块加载规则
(foo.js)
(main.js)
打印结果
(main.js)
打印结果
(模块间循环引用)
(就是沿着一条线走到底,走完再走其他线)
缺点
===============
AMD(了解)
规范说明
使用
导出
===============
CMD(了解)
规范说明
阿里写的,但是后面出售给了其他公司
使用
导出
导入
===============
ES Module (ESM)*
说明
使用要求
1、脚本标签
要加上type=”module”,才表示加载的是js模块,否则只是一个普通的js文件,无法使用导入导出
2、需要开启服务器
如果本地文件直接运行JS 模块文件,会报一个跨域错误,说的是文件的前缀 file 被一个浏览器上的CORS协议阻止了,因为 Javascript 模块安全性需要。
你需要通过一个服务器来运行JS模块文件,比如 Tomcat、Apache、Nginx等服务器软件,或者PHPStudy、宝塔等集成服务器和数据库的软件,或者 VSCode的一些插件如:Live Server。
这样让浏览器通过http 前缀访问到你模拟出来的服务器里面的本地的JS文件,通过http请求的方式获取这个文件,这样才不会被浏览器的CORS协议拦截。
(完整的应该是http://127.0.0.1 : 端口号/文件路径;127.0.0.1这个是你电脑本机地址,指向的就是你电脑;端口号不同服务器软件不同)
导出 export
(同一个文件,可以多个同时导出)
这是固定的语句格式,同一个文件只能有1个这样的格式
(用的比较少)
这是固定的语句格式,同一个文件只能有1个这样的格式
方式四:默认导出
导出某个东西,它是最重要的时候,可以用默认导出
注意:在一个模块中,只能有一个默认导出(default export);
或者
导入 import
*表示所有,然后把所有导出的放入一个命名为foo 的对象里面
方式四:默认导入
(默认导出的变量名)
(使用这个导出文件的默认导出,并且自己命名)
导入 / 导出结合 *
有时候项目里面,各种工具函数,可以给它设置统一的导出出口,这样外部引用会更方便
(/utils/index.js)
可以简化成:上下两种是一样的
(/utils/index.js)
再简化(阅读性差点):
(/utils/index.js)
(外部引用,更方便一点)
动态导入(ES11)
比如第一行代码是 import 导入文件,需要等这个文件导入完后,才会开始运行第二行代码,造成阻塞后面代码运行。
动态导入,加载完后,返回的是一个Promise,可以通过 then方法获得结果(结果就是这个文件所有导出的东西,都放在1个对象里面)
而且这样不会阻塞其他代码运行
下面的是你可能会需要动态导入的场景:
1、当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
2、当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
3、当被导入的模块,在加载时并不存在,需要异步获取
4、当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
5、当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)
请不要滥用动态导入(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和tree shaking发挥作用
文件所在路径 import.meta
ESM 模块解析
https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
阶段一:构建阶段
这个阶段,仅仅是下载需要的文件,解析成特定类型的数据,以及它们的依赖关系,不执行里面的代码,所以动态引入 import( ) 这个阶段是不运行的。
此时所以exports 的变量的值,都是undefined。
那会不会有几个模块import 同一个模块,这个模块重复下载多个?不会的,浏览器中用一个类似map结构储存,哪些是已经下载过,哪些没有下载。
阶段二:实例化
查找 exports 和 import,然后给这些变量分配内存地址,以及绑定对应的内存地址,此时所以的变量都是undefined
阶段三:运行求值
开始运行所有模块里面的代码,获取这些变量的值(值或者函数),然后更新到对应的内存里面,其他import的因为上一个阶段已经绑定过了对应的内存地址,因此能直接使用这些计算后的值或函数。
但是有个注意事项,同一个变量,exports这个变量的模块,可以去修改这个的值。但是import 这个变量的,没办法去修改这个值。
===============
CJS 和 ESM 混合使用
CJS的导出,用ESM能否导入使用?
相反
ESM的导出,用CJS能否导入使用?
要区分开发环境。
1、浏览器:不能,因为浏览器默认就是不支持CJS
2、NodeJS:要看版本和配置,低版本的NodeJS不支持ESM
3、Webpack等脚手架工具:支持混合使用,因为内部的一些模块会对CJS 和 ESM进行解析,转换成ES5的代码执行。
一般开发尽可能用ESM,不要混用。